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 + return nil, nil, err } - text, err := message.New(message.Header{h}, msg.GetBody(&textBodySection)) + part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection)) if err != nil { - return nil, "", err + return nil, nil, err } - b, err := ioutil.ReadAll(text.Body) - if err != nil { - return 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 @@

{{.Message.Envelope.Subject}}

-{{define "message-part"}} - {{.MIMEType}} - {{if gt (len .Children) 0}} - {{end}} {{end}} -{{template "message-part" .Message.PartTree}} +

Parts:

+ +{{template "message-part-tree" (tuple $ .Message.PartTree)}} + +
{{if .Body}}
{{.Body}}
+{{else}} +

Can't preview this message part.

+ Download {{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 }