aboutsummaryrefslogtreecommitdiff
path: root/plugins/base
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/base')
-rw-r--r--plugins/base/public/assets/script.js18
-rw-r--r--plugins/base/public/assets/style.css5
-rw-r--r--plugins/base/public/foot.html1
-rw-r--r--plugins/base/public/head.html1
-rw-r--r--plugins/base/public/message.html10
-rw-r--r--plugins/base/routes.go24
-rw-r--r--plugins/base/sanitize_html.go193
-rw-r--r--plugins/base/viewer.go38
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
+}