diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | cmd/koushin/main.go | 2 | ||||
-rw-r--r-- | plugins/base/public/foot.html | 1 | ||||
-rw-r--r-- | plugins/base/public/head.html | 1 | ||||
-rw-r--r-- | plugins/base/public/message.html | 10 | ||||
-rw-r--r-- | plugins/base/routes.go | 24 | ||||
-rw-r--r-- | plugins/base/viewer.go | 38 | ||||
-rw-r--r-- | plugins/viewhtml/plugin.go | 10 | ||||
-rw-r--r-- | plugins/viewhtml/public/assets/script.js (renamed from plugins/base/public/assets/script.js) | 0 | ||||
-rw-r--r-- | plugins/viewhtml/public/assets/style.css (renamed from plugins/base/public/assets/style.css) | 0 | ||||
-rw-r--r-- | plugins/viewhtml/sanitize.go (renamed from plugins/base/sanitize_html.go) | 2 | ||||
-rw-r--r-- | plugins/viewhtml/viewer.go | 57 | ||||
-rw-r--r-- | plugins/viewtext/plugin.go | 10 | ||||
-rw-r--r-- | plugins/viewtext/viewer.go | 49 | ||||
-rw-r--r-- | themes/sourcehut/message.html | 12 |
15 files changed, 178 insertions, 40 deletions
@@ -6,3 +6,5 @@ !/plugins/caldav !/plugins/carddav !/plugins/lua +!/plugins/viewhtml +!/plugins/viewtext diff --git a/cmd/koushin/main.go b/cmd/koushin/main.go index 392150f..d260d99 100644 --- a/cmd/koushin/main.go +++ b/cmd/koushin/main.go @@ -16,6 +16,8 @@ import ( _ "git.sr.ht/~emersion/koushin/plugins/caldav" _ "git.sr.ht/~emersion/koushin/plugins/carddav" _ "git.sr.ht/~emersion/koushin/plugins/lua" + _ "git.sr.ht/~emersion/koushin/plugins/viewhtml" + _ "git.sr.ht/~emersion/koushin/plugins/viewtext" ) func main() { diff --git a/plugins/base/public/foot.html b/plugins/base/public/foot.html index 284d779..b605728 100644 --- a/plugins/base/public/foot.html +++ b/plugins/base/public/foot.html @@ -1,3 +1,2 @@ - <script src="/plugins/base/assets/script.js"></script> </body> </html> diff --git a/plugins/base/public/head.html b/plugins/base/public/head.html index bed1bb3..bb47cfc 100644 --- a/plugins/base/public/head.html +++ b/plugins/base/public/head.html @@ -3,6 +3,5 @@ <head> <meta charset="utf-8"> <title>koushin</title> - <link rel="stylesheet" href="/plugins/base/assets/style.css"> </head> <body> diff --git a/plugins/base/public/message.html b/plugins/base/public/message.html index a973881..5457627 100644 --- a/plugins/base/public/message.html +++ b/plugins/base/public/message.html @@ -110,7 +110,7 @@ <hr> -{{if .Body}} +{{if .View}} <p> {{if .Message.HasFlag "\\Draft"}} <a href="{{.Message.Uid}}/edit?part={{.PartPath}}">Edit draft</a> @@ -118,13 +118,7 @@ <a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a> {{end}} </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}} <p>Can't preview this message part.</p> <a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a> diff --git a/plugins/base/routes.go b/plugins/base/routes.go index ec22d1b..ad4d121 100644 --- a/plugins/base/routes.go +++ b/plugins/base/routes.go @@ -176,8 +176,7 @@ type MessageRenderData struct { Mailboxes []*imap.MailboxInfo Mailbox *imap.MailboxStatus Message *IMAPMessage - Body string - IsHTML bool + View interface{} PartPath string MailboxPage int 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) @@ -286,8 +273,7 @@ func handleGetPart(ctx *koushin.Context, raw bool) error { Mailboxes: mailboxes, Mailbox: mbox, Message: msg, - Body: string(body), - IsHTML: isHTML, + View: view, PartPath: partPathString, MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage, Flags: flags, diff --git a/plugins/base/viewer.go b/plugins/base/viewer.go new file mode 100644 index 0000000..a76ecf9 --- /dev/null +++ b/plugins/base/viewer.go @@ -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 +} diff --git a/plugins/viewhtml/plugin.go b/plugins/viewhtml/plugin.go new file mode 100644 index 0000000..f22364e --- /dev/null +++ b/plugins/viewhtml/plugin.go @@ -0,0 +1,10 @@ +package koushinviewhtml + +import ( + "git.sr.ht/~emersion/koushin" +) + +func init() { + p := koushin.GoPlugin{Name: "viewhtml"} + koushin.RegisterPluginLoader(p.Loader()) +} diff --git a/plugins/base/public/assets/script.js b/plugins/viewhtml/public/assets/script.js index fd7a1d5..fd7a1d5 100644 --- a/plugins/base/public/assets/script.js +++ b/plugins/viewhtml/public/assets/script.js diff --git a/plugins/base/public/assets/style.css b/plugins/viewhtml/public/assets/style.css index 4f91f63..4f91f63 100644 --- a/plugins/base/public/assets/style.css +++ b/plugins/viewhtml/public/assets/style.css diff --git a/plugins/base/sanitize_html.go b/plugins/viewhtml/sanitize.go index a82c852..8cee481 100644 --- a/plugins/base/sanitize_html.go +++ b/plugins/viewhtml/sanitize.go @@ -1,4 +1,4 @@ -package koushinbase +package koushinviewhtml import ( "bytes" diff --git a/plugins/viewhtml/viewer.go b/plugins/viewhtml/viewer.go new file mode 100644 index 0000000..9734c37 --- /dev/null +++ b/plugins/viewhtml/viewer.go @@ -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{}) +} diff --git a/plugins/viewtext/plugin.go b/plugins/viewtext/plugin.go new file mode 100644 index 0000000..c7a2bcc --- /dev/null +++ b/plugins/viewtext/plugin.go @@ -0,0 +1,10 @@ +package koushinviewtext + +import ( + "git.sr.ht/~emersion/koushin" +) + +func init() { + p := koushin.GoPlugin{Name: "viewtext"} + koushin.RegisterPluginLoader(p.Loader()) +} diff --git a/plugins/viewtext/viewer.go b/plugins/viewtext/viewer.go new file mode 100644 index 0000000..cca38a8 --- /dev/null +++ b/plugins/viewtext/viewer.go @@ -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{}) +} diff --git a/themes/sourcehut/message.html b/themes/sourcehut/message.html index 4e1ab44..fb38932 100644 --- a/themes/sourcehut/message.html +++ b/themes/sourcehut/message.html @@ -99,16 +99,8 @@ {{end}} </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}} <p>Can't preview this message part.</p> <a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a> |