From 8299617ebc24a4a5bd1dc03070e17713be7e1e1b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 12 Feb 2020 14:42:51 +0100 Subject: Turn message part viewers into plugins --- .gitignore | 2 + cmd/koushin/main.go | 2 + plugins/base/public/assets/script.js | 18 --- plugins/base/public/assets/style.css | 5 - plugins/base/public/foot.html | 1 - plugins/base/public/head.html | 1 - plugins/base/public/message.html | 10 +- plugins/base/routes.go | 24 +--- plugins/base/sanitize_html.go | 193 ------------------------------- plugins/base/viewer.go | 38 ++++++ plugins/viewhtml/plugin.go | 10 ++ plugins/viewhtml/public/assets/script.js | 18 +++ plugins/viewhtml/public/assets/style.css | 5 + plugins/viewhtml/sanitize.go | 193 +++++++++++++++++++++++++++++++ plugins/viewhtml/viewer.go | 57 +++++++++ plugins/viewtext/plugin.go | 10 ++ plugins/viewtext/viewer.go | 49 ++++++++ themes/sourcehut/message.html | 12 +- 18 files changed, 393 insertions(+), 255 deletions(-) delete mode 100644 plugins/base/public/assets/script.js delete mode 100644 plugins/base/public/assets/style.css delete mode 100644 plugins/base/sanitize_html.go create mode 100644 plugins/base/viewer.go create mode 100644 plugins/viewhtml/plugin.go create mode 100644 plugins/viewhtml/public/assets/script.js create mode 100644 plugins/viewhtml/public/assets/style.css create mode 100644 plugins/viewhtml/sanitize.go create mode 100644 plugins/viewhtml/viewer.go create mode 100644 plugins/viewtext/plugin.go create mode 100644 plugins/viewtext/viewer.go diff --git a/.gitignore b/.gitignore index 6221491..c36e863 100644 --- a/.gitignore +++ b/.gitignore @@ -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/assets/script.js b/plugins/base/public/assets/script.js deleted file mode 100644 index fd7a1d5..0000000 --- a/plugins/base/public/assets/script.js +++ /dev/null @@ -1,18 +0,0 @@ -var emailFrame = document.getElementById("email-frame"); -if (emailFrame) { - // Resize the frame with its content - var resizeFrame = function() { - emailFrame.style.height = emailFrame.contentWindow.document.documentElement.scrollHeight + "px"; - }; - emailFrame.addEventListener("load", resizeFrame); - emailFrame.contentWindow.addEventListener("resize", resizeFrame); - - // Polyfill in case the srcdoc attribute isn't supported - if (!emailFrame.srcdoc) { - var srcdoc = emailFrame.getAttribute("srcdoc"); - var doc = emailFrame.contentWindow.document; - doc.open(); - doc.write(srcdoc); - doc.close(); - } -} diff --git a/plugins/base/public/assets/style.css b/plugins/base/public/assets/style.css deleted file mode 100644 index 4f91f63..0000000 --- a/plugins/base/public/assets/style.css +++ /dev/null @@ -1,5 +0,0 @@ -iframe { - width: 100%; - height: 400px; - border: 0; -} 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 @@ - 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 @@ koushin - 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 @@
-{{if .Body}} +{{if .View}}

{{if .Message.HasFlag "\\Draft"}} Edit draft @@ -118,13 +118,7 @@ Reply {{end}}

- {{if .IsHTML}} - - - - {{else}} -
{{.Body}}
- {{end}} + {{.View}} {{else}}

Can't preview this message part.

Download 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/sanitize_html.go b/plugins/base/sanitize_html.go deleted file mode 100644 index a82c852..0000000 --- a/plugins/base/sanitize_html.go +++ /dev/null @@ -1,193 +0,0 @@ -package koushinbase - -import ( - "bytes" - "fmt" - "regexp" - "strings" - - "github.com/aymerick/douceur/css" - cssparser "github.com/chris-ramon/douceur/parser" - "github.com/microcosm-cc/bluemonday" - "golang.org/x/net/html" -) - -// TODO: this doesn't accomodate for quoting -var ( - cssURLRegexp = regexp.MustCompile(`url\([^)]*\)`) - cssExprRegexp = regexp.MustCompile(`expression\([^)]*\)`) -) - -var allowedStyles = map[string]bool{ - "direction": true, - "font": true, - "font-family": true, - "font-style": true, - "font-variant": true, - "font-size": true, - "font-weight": true, - "letter-spacing": true, - "line-height": true, - "text-align": true, - "text-decoration": true, - "text-indent": true, - "text-overflow": true, - "text-shadow": true, - "text-transform": true, - "white-space": true, - "word-spacing": true, - "word-wrap": true, - "vertical-align": true, - - "color": true, - "background": true, - "background-color": true, - "background-image": true, - "background-repeat": true, - - "border": true, - "border-color": true, - "border-radius": true, - "height": true, - "margin": true, - "padding": true, - "width": true, - "max-width": true, - "min-width": true, - - "clear": true, - "float": true, - - "border-collapse": true, - "border-spacing": true, - "caption-side": true, - "empty-cells": true, - "table-layout": true, - - "list-style-type": true, - "list-style-position": true, -} - -func sanitizeCSSDecls(decls []*css.Declaration) []*css.Declaration { - sanitized := make([]*css.Declaration, 0, len(decls)) - for _, decl := range decls { - if !allowedStyles[decl.Property] { - continue - } - if cssExprRegexp.FindStringIndex(decl.Value) != nil { - continue - } - - // TODO: more robust CSS declaration parsing - decl.Value = cssURLRegexp.ReplaceAllString(decl.Value, "url(about:blank)") - - sanitized = append(sanitized, decl) - } - return sanitized -} - -func sanitizeCSSRule(rule *css.Rule) { - // Disallow @import - if rule.Kind == css.AtRule && strings.EqualFold(rule.Name, "@import") { - rule.Prelude = "url(about:blank)" - } - - rule.Declarations = sanitizeCSSDecls(rule.Declarations) - - for _, child := range rule.Rules { - sanitizeCSSRule(child) - } -} - -func sanitizeNode(n *html.Node) { - if n.Type == html.ElementNode { - if strings.EqualFold(n.Data, "img") { - for i := range n.Attr { - attr := &n.Attr[i] - if strings.EqualFold(attr.Key, "src") { - attr.Val = "about:blank" - } - } - } else if strings.EqualFold(n.Data, "style") { - var s string - c := n.FirstChild - for c != nil { - if c.Type == html.TextNode { - s += c.Data - } - - next := c.NextSibling - n.RemoveChild(c) - c = next - } - - stylesheet, err := cssparser.Parse(s) - if err != nil { - s = "" - } else { - for _, rule := range stylesheet.Rules { - sanitizeCSSRule(rule) - } - - s = stylesheet.String() - } - - n.AppendChild(&html.Node{ - Type: html.TextNode, - Data: s, - }) - } - - for i := range n.Attr { - // Don't use `i, attr := range n.Attr` since `attr` would be a copy - attr := &n.Attr[i] - - if strings.EqualFold(attr.Key, "style") { - decls, err := cssparser.ParseDeclarations(attr.Val) - if err != nil { - attr.Val = "" - continue - } - - decls = sanitizeCSSDecls(decls) - - attr.Val = "" - for _, d := range decls { - attr.Val += d.String() - } - } - } - } - - for c := n.FirstChild; c != nil; c = c.NextSibling { - sanitizeNode(c) - } -} - -func sanitizeHTML(b []byte) ([]byte, error) { - doc, err := html.Parse(bytes.NewReader(b)) - if err != nil { - return nil, fmt.Errorf("failed to parse HTML: %v", err) - } - - sanitizeNode(doc) - - var buf bytes.Buffer - if err := html.Render(&buf, doc); err != nil { - return nil, fmt.Errorf("failed to render HTML: %v", err) - } - b = buf.Bytes() - - // bluemonday must always be run last - p := bluemonday.UGCPolicy() - - // TODO: use bluemonday's AllowStyles once it's released and - // supports