Turn message part viewers into plugins
This commit is contained in:
parent
892f1fa581
commit
8299617ebc
15 changed files with 178 additions and 40 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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}}
|
{{.View}}
|
||||||
<!-- 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}}
|
|
||||||
{{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
|
View interface{}
|
||||||
IsHTML bool
|
|
||||||
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
|
view, err := viewMessagePart(ctx, msg, part)
|
||||||
if strings.HasPrefix(strings.ToLower(mimeType), "text/") {
|
if err == ErrViewUnsupported {
|
||||||
body, err = ioutil.ReadAll(part.Body)
|
view = nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
View: view,
|
||||||
IsHTML: isHTML,
|
|
||||||
PartPath: partPathString,
|
PartPath: partPathString,
|
||||||
MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage,
|
MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
|
|
38
plugins/base/viewer.go
Normal file
38
plugins/base/viewer.go
Normal file
|
@ -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
|
||||||
|
}
|
10
plugins/viewhtml/plugin.go
Normal file
10
plugins/viewhtml/plugin.go
Normal file
|
@ -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"
|
57
plugins/viewhtml/viewer.go
Normal file
57
plugins/viewhtml/viewer.go
Normal file
|
@ -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{})
|
||||||
|
}
|
10
plugins/viewtext/plugin.go
Normal file
10
plugins/viewtext/plugin.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package koushinviewtext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.sr.ht/~emersion/koushin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
p := koushin.GoPlugin{Name: "viewtext"}
|
||||||
|
koushin.RegisterPluginLoader(p.Loader())
|
||||||
|
}
|
49
plugins/viewtext/viewer.go
Normal file
49
plugins/viewtext/viewer.go
Normal file
|
@ -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 .View}}
|
||||||
{{if .IsHTML}}
|
{{.View}}
|
||||||
<!-- 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}}
|
|
||||||
{{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>
|
||||||
|
|
Loading…
Reference in a new issue