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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -140,10 +139,11 @@ func (msg *imapMessage) TextPartName() string {
|
||||||
type IMAPPartNode struct {
|
type IMAPPartNode struct {
|
||||||
Path []int
|
Path []int
|
||||||
MIMEType string
|
MIMEType string
|
||||||
|
Filename string
|
||||||
Children []IMAPPartNode
|
Children []IMAPPartNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *IMAPPartNode) PathString() string {
|
func (node IMAPPartNode) PathString() string {
|
||||||
l := make([]string, len(node.Path))
|
l := make([]string, len(node.Path))
|
||||||
for i, partNum := range node.Path {
|
for i, partNum := range node.Path {
|
||||||
l[i] = strconv.Itoa(partNum)
|
l[i] = strconv.Itoa(partNum)
|
||||||
|
@ -152,14 +152,32 @@ func (node *IMAPPartNode) PathString() string {
|
||||||
return strings.Join(l, ".")
|
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 {
|
func imapPartTree(bs *imap.BodyStructure, path []int) *IMAPPartNode {
|
||||||
if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
|
if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
|
||||||
path = []int{1}
|
path = []int{1}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var filename string
|
||||||
|
if strings.EqualFold(bs.Disposition, "attachment") {
|
||||||
|
filename = bs.DispositionParams["filename"]
|
||||||
|
}
|
||||||
|
|
||||||
node := &IMAPPartNode{
|
node := &IMAPPartNode{
|
||||||
Path: path,
|
Path: path,
|
||||||
MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
|
MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
|
||||||
|
Filename: filename,
|
||||||
Children: make([]IMAPPartNode, len(bs.Parts)),
|
Children: make([]IMAPPartNode, len(bs.Parts)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,56 +243,52 @@ func listMessages(conn *imapclient.Client, mboxName string) ([]imapMessage, erro
|
||||||
return msgs, nil
|
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 {
|
if err := ensureMailboxSelected(conn, mboxName); err != nil {
|
||||||
return nil, "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
seqSet := new(imap.SeqSet)
|
seqSet := new(imap.SeqSet)
|
||||||
seqSet.AddNum(uid)
|
seqSet.AddNum(uid)
|
||||||
|
|
||||||
var textHeaderSection imap.BodySectionName
|
var partHeaderSection imap.BodySectionName
|
||||||
textHeaderSection.Peek = true
|
partHeaderSection.Peek = true
|
||||||
textHeaderSection.Specifier = imap.HeaderSpecifier
|
partHeaderSection.Specifier = imap.HeaderSpecifier
|
||||||
textHeaderSection.Path = partPath
|
partHeaderSection.Path = partPath
|
||||||
|
|
||||||
var textBodySection imap.BodySectionName
|
var partBodySection imap.BodySectionName
|
||||||
textBodySection.Peek = true
|
partBodySection.Peek = true
|
||||||
textBodySection.Path = partPath
|
partBodySection.Specifier = imap.TextSpecifier
|
||||||
|
partBodySection.Path = partPath
|
||||||
|
|
||||||
fetch := []imap.FetchItem{
|
fetch := []imap.FetchItem{
|
||||||
imap.FetchEnvelope,
|
imap.FetchEnvelope,
|
||||||
imap.FetchUid,
|
imap.FetchUid,
|
||||||
imap.FetchBodyStructure,
|
imap.FetchBodyStructure,
|
||||||
textHeaderSection.FetchItem(),
|
partHeaderSection.FetchItem(),
|
||||||
textBodySection.FetchItem(),
|
partBodySection.FetchItem(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := make(chan *imap.Message, 1)
|
ch := make(chan *imap.Message, 1)
|
||||||
if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
|
if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
|
||||||
return nil, "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := <-ch
|
msg := <-ch
|
||||||
if msg == nil {
|
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)
|
h, err := textproto.ReadHeader(headerReader)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(text.Body)
|
return &imapMessage{msg}, part, nil
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &imapMessage{msg}, string(b), nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,21 +6,42 @@
|
||||||
|
|
||||||
<h2>{{.Message.Envelope.Subject}}</h2>
|
<h2>{{.Message.Envelope.Subject}}</h2>
|
||||||
|
|
||||||
{{define "message-part"}}
|
{{define "message-part-tree"}}
|
||||||
<a href="?part={{.PathString}}">{{.MIMEType}}</a>
|
{{/* nested templates can't access the parent's context */}}
|
||||||
{{if gt (len .Children) 0}}
|
{{$ = index . 0}}
|
||||||
<ul>
|
{{with index . 1}}
|
||||||
{{range .Children}}
|
<a
|
||||||
<li>{{template "message-part" .}}</li>
|
{{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}}
|
{{end}}
|
||||||
</ul>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "message-part" .Message.PartTree}}
|
<p>Parts:</p>
|
||||||
|
|
||||||
|
{{template "message-part-tree" (tuple $ .Message.PartTree)}}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
{{if .Body}}
|
{{if .Body}}
|
||||||
<pre>{{.Body}}</pre>
|
<pre>{{.Body}}</pre>
|
||||||
|
{{else}}
|
||||||
|
<p>Can't preview this message part.</p>
|
||||||
|
<a href="{{.Message.Uid}}/raw?part={{.PartPath}}">Download</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "foot"}}
|
{{template "foot"}}
|
||||||
|
|
86
server.go
86
server.go
|
@ -2,8 +2,11 @@ package koushin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
imapclient "github.com/emersion/go-imap/client"
|
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)
|
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 {
|
func New(imapURL string) *echo.Echo {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
|
@ -157,27 +217,11 @@ func New(imapURL string) *echo.Echo {
|
||||||
|
|
||||||
e.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
|
e.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
|
||||||
ctx := ectx.(*context)
|
ctx := ectx.(*context)
|
||||||
|
return handleGetPart(ctx, false)
|
||||||
uid, err := parseUid(ctx.Param("uid"))
|
})
|
||||||
if err != nil {
|
e.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
ctx := ectx.(*context)
|
||||||
}
|
return handleGetPart(ctx, true)
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
e.GET("/login", handleLogin)
|
e.GET("/login", handleLogin)
|
||||||
|
|
|
@ -18,6 +18,10 @@ func parseUid(s string) (uint32, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePartPath(s string) ([]int, error) {
|
func parsePartPath(s string) ([]int, error) {
|
||||||
|
if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
l := strings.Split(s, ".")
|
l := strings.Split(s, ".")
|
||||||
path := make([]int, len(l))
|
path := make([]int, len(l))
|
||||||
for i, s := range 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) {
|
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
|
return &tmpl{t}, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue