@@ -6,3 +6,5 @@ | |||||
!/plugins/caldav | !/plugins/caldav | ||||
!/plugins/carddav | !/plugins/carddav | ||||
!/plugins/lua | !/plugins/lua | ||||
!/plugins/viewhtml | |||||
!/plugins/viewtext |
@@ -16,6 +16,8 @@ import ( | |||||
_ "git.sr.ht/~emersion/koushin/plugins/caldav" | _ "git.sr.ht/~emersion/koushin/plugins/caldav" | ||||
_ "git.sr.ht/~emersion/koushin/plugins/carddav" | _ "git.sr.ht/~emersion/koushin/plugins/carddav" | ||||
_ "git.sr.ht/~emersion/koushin/plugins/lua" | _ "git.sr.ht/~emersion/koushin/plugins/lua" | ||||
_ "git.sr.ht/~emersion/koushin/plugins/viewhtml" | |||||
_ "git.sr.ht/~emersion/koushin/plugins/viewtext" | |||||
) | ) | ||||
func main() { | func main() { | ||||
@@ -1,3 +1,2 @@ | |||||
<script src="/plugins/base/assets/script.js"></script> | |||||
</body> | </body> | ||||
</html> | </html> |
@@ -3,6 +3,5 @@ | |||||
<head> | <head> | ||||
<meta charset="utf-8"> | <meta charset="utf-8"> | ||||
<title>koushin</title> | <title>koushin</title> | ||||
<link rel="stylesheet" href="/plugins/base/assets/style.css"> | |||||
</head> | </head> | ||||
<body> | <body> |
@@ -110,7 +110,7 @@ | |||||
<hr> | <hr> | ||||
{{if .Body}} | |||||
{{if .View}} | |||||
<p> | <p> | ||||
{{if .Message.HasFlag "\\Draft"}} | {{if .Message.HasFlag "\\Draft"}} | ||||
<a href="{{.Message.Uid}}/edit?part={{.PartPath}}">Edit draft</a> | <a href="{{.Message.Uid}}/edit?part={{.PartPath}}">Edit draft</a> | ||||
@@ -118,13 +118,7 @@ | |||||
<a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a> | <a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a> | ||||
{{end}} | {{end}} | ||||
</p> | </p> | ||||
{{if .IsHTML}} | |||||
<!-- allow-same-origin is required to resize the frame with its content --> | |||||
<!-- allow-popups is required for target="_blank" links --> | |||||
<iframe id="email-frame" srcdoc="{{.Body}}" sandbox="allow-same-origin allow-popups"></iframe> | |||||
{{else}} | |||||
<pre>{{.Body}}</pre> | |||||
{{end}} | |||||
{{.View}} | |||||
{{else}} | {{else}} | ||||
<p>Can't preview this message part.</p> | <p>Can't preview this message part.</p> | ||||
<a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a> | <a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a> | ||||
@@ -176,8 +176,7 @@ type MessageRenderData struct { | |||||
Mailboxes []*imap.MailboxInfo | Mailboxes []*imap.MailboxInfo | ||||
Mailbox *imap.MailboxStatus | Mailbox *imap.MailboxStatus | ||||
Message *IMAPMessage | Message *IMAPMessage | ||||
Body string | |||||
IsHTML bool | |||||
View interface{} | |||||
PartPath string | PartPath string | ||||
MailboxPage int | MailboxPage int | ||||
Flags map[string]bool | Flags map[string]bool | ||||
@@ -255,21 +254,9 @@ func handleGetPart(ctx *koushin.Context, raw bool) error { | |||||
} | } | ||||
} | } | ||||
var body []byte | |||||
if strings.HasPrefix(strings.ToLower(mimeType), "text/") { | |||||
body, err = ioutil.ReadAll(part.Body) | |||||
if err != nil { | |||||
return fmt.Errorf("failed to read part body: %v", err) | |||||
} | |||||
} | |||||
isHTML := false | |||||
if strings.EqualFold(mimeType, "text/html") { | |||||
body, err = sanitizeHTML(body) | |||||
if err != nil { | |||||
return fmt.Errorf("failed to sanitize HTML part: %v", err) | |||||
} | |||||
isHTML = true | |||||
view, err := viewMessagePart(ctx, msg, part) | |||||
if err == ErrViewUnsupported { | |||||
view = nil | |||||
} | } | ||||
flags := make(map[string]bool) | flags := make(map[string]bool) | ||||
@@ -286,8 +273,7 @@ func handleGetPart(ctx *koushin.Context, raw bool) error { | |||||
Mailboxes: mailboxes, | Mailboxes: mailboxes, | ||||
Mailbox: mbox, | Mailbox: mbox, | ||||
Message: msg, | Message: msg, | ||||
Body: string(body), | |||||
IsHTML: isHTML, | |||||
View: view, | |||||
PartPath: partPathString, | PartPath: partPathString, | ||||
MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage, | MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage, | ||||
Flags: flags, | Flags: flags, | ||||
@@ -0,0 +1,38 @@ | |||||
package koushinbase | |||||
import ( | |||||
"fmt" | |||||
"git.sr.ht/~emersion/koushin" | |||||
"github.com/emersion/go-message" | |||||
) | |||||
// ErrViewUnsupported is returned by Viewer.ViewMessagePart when the message | |||||
// part isn't supported. | |||||
var ErrViewUnsupported = fmt.Errorf("cannot generate message view: unsupported part") | |||||
// Viewer is a message part viewer. | |||||
type Viewer interface { | |||||
// ViewMessagePart renders a message part. The returned value is displayed | |||||
// in a template. ErrViewUnsupported is returned if the message part isn't | |||||
// supported. | |||||
ViewMessagePart(*koushin.Context, *IMAPMessage, *message.Entity) (interface{}, error) | |||||
} | |||||
var viewers []Viewer | |||||
// RegisterViewer registers a message part viewer. | |||||
func RegisterViewer(viewer Viewer) { | |||||
viewers = append(viewers, viewer) | |||||
} | |||||
func viewMessagePart(ctx *koushin.Context, msg *IMAPMessage, part *message.Entity) (interface{}, error) { | |||||
for _, viewer := range viewers { | |||||
v, err := viewer.ViewMessagePart(ctx, msg, part) | |||||
if err == ErrViewUnsupported { | |||||
continue | |||||
} | |||||
return v, err | |||||
} | |||||
return nil, ErrViewUnsupported | |||||
} |
@@ -0,0 +1,10 @@ | |||||
package koushinviewhtml | |||||
import ( | |||||
"git.sr.ht/~emersion/koushin" | |||||
) | |||||
func init() { | |||||
p := koushin.GoPlugin{Name: "viewhtml"} | |||||
koushin.RegisterPluginLoader(p.Loader()) | |||||
} |
@@ -1,4 +1,4 @@ | |||||
package koushinbase | |||||
package koushinviewhtml | |||||
import ( | import ( | ||||
"bytes" | "bytes" |
@@ -0,0 +1,57 @@ | |||||
package koushinviewhtml | |||||
import ( | |||||
"bytes" | |||||
"fmt" | |||||
"html/template" | |||||
"io/ioutil" | |||||
"strings" | |||||
"git.sr.ht/~emersion/koushin" | |||||
koushinbase "git.sr.ht/~emersion/koushin/plugins/base" | |||||
"github.com/emersion/go-message" | |||||
) | |||||
const tpl = ` | |||||
<!-- allow-same-origin is required to resize the frame with its content --> | |||||
<!-- allow-popups is required for target="_blank" links --> | |||||
<iframe id="email-frame" srcdoc="{{.}}" sandbox="allow-same-origin allow-popups"></iframe> | |||||
<script src="/plugins/viewhtml/assets/script.js"></script> | |||||
<link rel="stylesheet" href="/plugins/viewhtml/assets/style.css"> | |||||
` | |||||
type viewer struct{} | |||||
func (viewer) ViewMessagePart(ctx *koushin.Context, msg *koushinbase.IMAPMessage, part *message.Entity) (interface{}, error) { | |||||
mimeType, _, err := part.Header.ContentType() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if !strings.EqualFold(mimeType, "text/html") { | |||||
return nil, koushinbase.ErrViewUnsupported | |||||
} | |||||
body, err := ioutil.ReadAll(part.Body) | |||||
if err != nil { | |||||
return nil, fmt.Errorf("failed to read part body: %v", err) | |||||
} | |||||
body, err = sanitizeHTML(body) | |||||
if err != nil { | |||||
return nil, fmt.Errorf("failed to sanitize HTML part: %v", err) | |||||
} | |||||
t := template.Must(template.New("view-html.html").Parse(tpl)) | |||||
var buf bytes.Buffer | |||||
err = t.Execute(&buf, string(body)) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return template.HTML(buf.String()), nil | |||||
} | |||||
func init() { | |||||
koushinbase.RegisterViewer(viewer{}) | |||||
} |
@@ -0,0 +1,10 @@ | |||||
package koushinviewtext | |||||
import ( | |||||
"git.sr.ht/~emersion/koushin" | |||||
) | |||||
func init() { | |||||
p := koushin.GoPlugin{Name: "viewtext"} | |||||
koushin.RegisterPluginLoader(p.Loader()) | |||||
} |
@@ -0,0 +1,49 @@ | |||||
package koushinviewtext | |||||
import ( | |||||
"bytes" | |||||
"fmt" | |||||
"html/template" | |||||
"io/ioutil" | |||||
"strings" | |||||
"git.sr.ht/~emersion/koushin" | |||||
koushinbase "git.sr.ht/~emersion/koushin/plugins/base" | |||||
"github.com/emersion/go-message" | |||||
) | |||||
// TODO: dim quotes and "On xxx, xxx wrote:" lines | |||||
// TODO: turn URLs into links | |||||
const tpl = `<pre>{{.}}</pre>` | |||||
type viewer struct{} | |||||
func (viewer) ViewMessagePart(ctx *koushin.Context, msg *koushinbase.IMAPMessage, part *message.Entity) (interface{}, error) { | |||||
mimeType, _, err := part.Header.ContentType() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if !strings.EqualFold(mimeType, "text/plain") { | |||||
return nil, koushinbase.ErrViewUnsupported | |||||
} | |||||
body, err := ioutil.ReadAll(part.Body) | |||||
if err != nil { | |||||
return nil, fmt.Errorf("failed to read part body: %v", err) | |||||
} | |||||
t := template.Must(template.New("view-text.html").Parse(tpl)) | |||||
var buf bytes.Buffer | |||||
err = t.Execute(&buf, string(body)) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return template.HTML(buf.String()), nil | |||||
} | |||||
func init() { | |||||
koushinbase.RegisterViewer(viewer{}) | |||||
} |
@@ -99,16 +99,8 @@ | |||||
{{end}} | {{end}} | ||||
</ul> | </ul> | ||||
{{if .Body}} | |||||
{{if .IsHTML}} | |||||
<!-- allow-same-origin is required to resize the frame with its content --> | |||||
<!-- allow-popups is required for target="_blank" links --> | |||||
<iframe id="email-frame" | |||||
srcdoc="{{.Body}}" | |||||
sandbox="allow-same-origin allow-popups"></iframe> | |||||
{{else}} | |||||
<pre>{{.Body}}</pre> | |||||
{{end}} | |||||
{{if .View}} | |||||
{{.View}} | |||||
{{else}} | {{else}} | ||||
<p>Can't preview this message part.</p> | <p>Can't preview this message part.</p> | ||||
<a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a> | <a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a> | ||||