aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--imap.go64
-rw-r--r--public/message.html37
-rw-r--r--server.go86
-rw-r--r--strconv.go4
-rw-r--r--template.go6
5 files changed, 142 insertions, 55 deletions
diff --git a/imap.go b/imap.go
index 78e2f88..539a6bf 100644
--- a/imap.go
+++ b/imap.go
@@ -3,7 +3,6 @@ package koushin
import (
"bufio"
"fmt"
- "io/ioutil"
"sort"
"strconv"
"strings"
@@ -140,10 +139,11 @@ func (msg *imapMessage) TextPartName() string {
type IMAPPartNode struct {
Path []int
MIMEType string
+ Filename string
Children []IMAPPartNode
}
-func (node *IMAPPartNode) PathString() string {
+func (node IMAPPartNode) PathString() string {
l := make([]string, len(node.Path))
for i, partNum := range node.Path {
l[i] = strconv.Itoa(partNum)
@@ -152,14 +152,32 @@ func (node *IMAPPartNode) PathString() string {
return strings.Join(l, ".")
}
+func (node IMAPPartNode) IsText() bool {
+ return strings.HasPrefix(strings.ToLower(node.MIMEType), "text/")
+}
+
+func (node IMAPPartNode) String() string {
+ if node.Filename != "" {
+ return fmt.Sprintf("%s (%s)", node.Filename, node.MIMEType)
+ } else {
+ return node.MIMEType
+ }
+}
+
func imapPartTree(bs *imap.BodyStructure, path []int) *IMAPPartNode {
if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
path = []int{1}
}
+ var filename string
+ if strings.EqualFold(bs.Disposition, "attachment") {
+ filename = bs.DispositionParams["filename"]
+ }
+
node := &IMAPPartNode{
Path: path,
MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
+ Filename: filename,
Children: make([]IMAPPartNode, len(bs.Parts)),
}
@@ -225,56 +243,52 @@ func listMessages(conn *imapclient.Client, mboxName string) ([]imapMessage, erro
return msgs, nil
}
-func getMessage(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*imapMessage, string, error) {
+func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*imapMessage, *message.Entity, error) {
if err := ensureMailboxSelected(conn, mboxName); err != nil {
- return nil, "", err
+ return nil, nil, err
}
seqSet := new(imap.SeqSet)
seqSet.AddNum(uid)
- var textHeaderSection imap.BodySectionName
- textHeaderSection.Peek = true
- textHeaderSection.Specifier = imap.HeaderSpecifier
- textHeaderSection.Path = partPath
+ var partHeaderSection imap.BodySectionName
+ partHeaderSection.Peek = true
+ partHeaderSection.Specifier = imap.HeaderSpecifier
+ partHeaderSection.Path = partPath
- var textBodySection imap.BodySectionName
- textBodySection.Peek = true
- textBodySection.Path = partPath
+ var partBodySection imap.BodySectionName
+ partBodySection.Peek = true
+ partBodySection.Specifier = imap.TextSpecifier
+ partBodySection.Path = partPath
fetch := []imap.FetchItem{
imap.FetchEnvelope,
imap.FetchUid,
imap.FetchBodyStructure,
- textHeaderSection.FetchItem(),
- textBodySection.FetchItem(),
+ partHeaderSection.FetchItem(),
+ partBodySection.FetchItem(),
}
ch := make(chan *imap.Message, 1)
if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
- return nil, "", err
+ return nil, nil, err
}
msg := <-ch
if msg == nil {
- return nil, "", fmt.Errorf("server didn't return message")
+ return nil, nil, fmt.Errorf("server didn't return message")
}
- headerReader := bufio.NewReader(msg.GetBody(&textHeaderSection))
+ headerReader := bufio.NewReader(msg.GetBody(&partHeaderSection))
h, err := textproto.ReadHeader(headerReader)
if err != nil {
- return nil, "", err
- }
-
- text, err := message.New(message.Header{h}, msg.GetBody(&textBodySection))
- if err != nil {
- return nil, "", err
+ return nil, nil, err
}
- b, err := ioutil.ReadAll(text.Body)
+ part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection))
if err != nil {
- return nil, "", err
+ return nil, nil, err
}
- return &imapMessage{msg}, string(b), nil
+ return &imapMessage{msg}, part, nil
}
diff --git a/public/message.html b/public/message.html
index 18646f0..09e56ac 100644
--- a/public/message.html
+++ b/public/message.html
@@ -6,21 +6,42 @@
<h2>{{.Message.Envelope.Subject}}</h2>
-{{define "message-part"}}
- <a href="?part={{.PathString}}">{{.MIMEType}}</a>
- {{if gt (len .Children) 0}}
- <ul>
- {{range .Children}}
- <li>{{template "message-part" .}}</li>
+{{define "message-part-tree"}}
+ {{/* nested templates can't access the parent's context */}}
+ {{$ = index . 0}}
+ {{with index . 1}}
+ <a
+ {{if .IsText}}
+ href="{{$.Message.Uid}}?part={{.PathString}}"
+ {{else}}
+ href="{{$.Message.Uid}}/raw?part={{.PathString}}"
+ {{end}}
+ >
+ {{if eq $.PartPath .PathString}}<strong>{{end}}
+ {{.String}}
+ {{if eq $.PartPath .PathString}}</strong>{{end}}
+ </a>
+ {{if gt (len .Children) 0}}
+ <ul>
+ {{range .Children}}
+ <li>{{template "message-part-tree" (tuple $ .)}}</li>
+ {{end}}
+ </ul>
{{end}}
- </ul>
{{end}}
{{end}}
-{{template "message-part" .Message.PartTree}}
+<p>Parts:</p>
+
+{{template "message-part-tree" (tuple $ .Message.PartTree)}}
+
+<hr>
{{if .Body}}
<pre>{{.Body}}</pre>
+{{else}}
+ <p>Can't preview this message part.</p>
+ <a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a>
{{end}}
{{template "foot"}}
diff --git a/server.go b/server.go
index a66bbe1..a853687 100644
--- a/server.go
+++ b/server.go
@@ -2,8 +2,11 @@ package koushin
import (
"fmt"
+ "io/ioutil"
+ "mime"
"net/http"
"net/url"
+ "strings"
"time"
imapclient "github.com/emersion/go-imap/client"
@@ -94,6 +97,63 @@ func handleLogin(ectx echo.Context) error {
return ctx.Render(http.StatusOK, "login.html", nil)
}
+func handleGetPart(ctx *context, raw bool) error {
+ mboxName := ctx.Param("mbox")
+ uid, err := parseUid(ctx.Param("uid"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err)
+ }
+ partPathString := ctx.QueryParam("part")
+ partPath, err := parsePartPath(partPathString)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err)
+ }
+
+ msg, part, err := getMessagePart(ctx.conn, mboxName, uid, partPath)
+ if err != nil {
+ return err
+ }
+
+ mimeType, _, err := part.Header.ContentType()
+ if err != nil {
+ return err
+ }
+ if len(partPath) == 0 {
+ mimeType = "message/rfc822"
+ }
+
+ if raw {
+ disp, dispParams, _ := part.Header.ContentDisposition()
+ filename := dispParams["filename"]
+
+ if !strings.EqualFold(mimeType, "text/plain") || strings.EqualFold(disp, "attachment") {
+ dispParams := make(map[string]string)
+ if filename != "" {
+ dispParams["filename"] = filename
+ }
+ disp := mime.FormatMediaType("attachment", dispParams)
+ ctx.Response().Header().Set("Content-Disposition", disp)
+ }
+ return ctx.Stream(http.StatusOK, mimeType, part.Body)
+ }
+
+ var body string
+ if strings.HasPrefix(strings.ToLower(mimeType), "text/") {
+ b, err := ioutil.ReadAll(part.Body)
+ if err != nil {
+ return err
+ }
+ body = string(b)
+ }
+
+ return ctx.Render(http.StatusOK, "message.html", map[string]interface{}{
+ "Mailbox": ctx.conn.Mailbox(),
+ "Message": msg,
+ "Body": body,
+ "PartPath": partPathString,
+ })
+}
+
func New(imapURL string) *echo.Echo {
e := echo.New()
@@ -157,27 +217,11 @@ func New(imapURL string) *echo.Echo {
e.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
ctx := ectx.(*context)
-
- uid, err := parseUid(ctx.Param("uid"))
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err)
- }
- // TODO: handle messages without a text part
- part, err := parsePartPath(ctx.QueryParam("part"))
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err)
- }
-
- msg, body, err := getMessage(ctx.conn, ctx.Param("mbox"), uid, part)
- if err != nil {
- return err
- }
-
- return ctx.Render(http.StatusOK, "message.html", map[string]interface{}{
- "Mailbox": ctx.conn.Mailbox(),
- "Message": msg,
- "Body": body,
- })
+ return handleGetPart(ctx, false)
+ })
+ e.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
+ ctx := ectx.(*context)
+ return handleGetPart(ctx, true)
})
e.GET("/login", handleLogin)
diff --git a/strconv.go b/strconv.go
index 2fb9d73..0879aac 100644
--- a/strconv.go
+++ b/strconv.go
@@ -18,6 +18,10 @@ func parseUid(s string) (uint32, error) {
}
func parsePartPath(s string) ([]int, error) {
+ if s == "" {
+ return nil, nil
+ }
+
l := strings.Split(s, ".")
path := make([]int, len(l))
for i, s := range l {
diff --git a/template.go b/template.go
index c7db0ae..5d0d28b 100644
--- a/template.go
+++ b/template.go
@@ -16,6 +16,10 @@ func (t *tmpl) Render(w io.Writer, name string, data interface{}, c echo.Context
}
func loadTemplates() (*tmpl, error) {
- t, err := template.New("drmdb").ParseGlob("public/*.html")
+ t, err := template.New("drmdb").Funcs(template.FuncMap{
+ "tuple": func(values ...interface{}) []interface{} {
+ return values
+ },
+ }).ParseGlob("public/*.html")
return &tmpl{t}, err
}