Display & download any message part
This commit is contained in:
parent
33b8679f1c
commit
be14524c33
5 changed files with 142 additions and 55 deletions
64
imap.go
64
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
|
||||
}
|
||||
|
|
|
@ -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"}}
|
||||
|
|
86
server.go
86
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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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…
Reference in a new issue