Browse Source

Display & download any message part

master
Simon Ser 4 years ago
parent
commit
be14524c33
No known key found for this signature in database GPG Key ID: FDE7BE0E88F5E48
5 changed files with 142 additions and 55 deletions
  1. +39
    -25
      imap.go
  2. +29
    -8
      public/message.html
  3. +65
    -21
      server.go
  4. +4
    -0
      strconv.go
  5. +5
    -1
      template.go

+ 39
- 25
imap.go View File

@@ -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
}

+ 29
- 8
public/message.html View File

@@ -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"}}

+ 65
- 21
server.go View File

@@ -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)


+ 4
- 0
strconv.go View File

@@ -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 {


+ 5
- 1
template.go View File

@@ -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
}

Loading…
Cancel
Save