diff options
Diffstat (limited to 'plugins/base')
-rw-r--r-- | plugins/base/public/assets/script.js | 18 | ||||
-rw-r--r-- | plugins/base/public/assets/style.css | 5 | ||||
-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/sanitize_html.go | 193 | ||||
-rw-r--r-- | plugins/base/viewer.go | 38 |
8 files changed, 45 insertions, 245 deletions
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 @@ - <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/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 <style> - p.AllowElements("style") - p.AllowAttrs("style").Globally() - - p.AddTargetBlankToFullyQualifiedLinks(true) - p.RequireNoFollowOnLinks(true) - - return p.SanitizeBytes(b), nil -} 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 +} |