@@ -6,6 +6,7 @@ require ( | |||||
git.sr.ht/~sircmpwn/dowork v0.0.0-20201013174036-53a05612a082 | git.sr.ht/~sircmpwn/dowork v0.0.0-20201013174036-53a05612a082 | ||||
github.com/aymerick/douceur v0.2.0 | github.com/aymerick/douceur v0.2.0 | ||||
github.com/chris-ramon/douceur v0.2.0 | github.com/chris-ramon/douceur v0.2.0 | ||||
github.com/dustin/go-humanize v1.0.0 | |||||
github.com/emersion/go-ical v0.0.0-20200225233454-26ef720b8bf1 | github.com/emersion/go-ical v0.0.0-20200225233454-26ef720b8bf1 | ||||
github.com/emersion/go-imap v1.0.4 | github.com/emersion/go-imap v1.0.4 | ||||
github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915 | github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915 | ||||
@@ -22,6 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | |||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | ||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= | |||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= | |||||
github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e h1:YGM1sI7edZOt8KAfX9Miq/X99d2QXdgjkJ7vN4HjxAA= | github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e h1:YGM1sI7edZOt8KAfX9Miq/X99d2QXdgjkJ7vN4HjxAA= | ||||
github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e/go.mod h1:4xVTBPcT43a1pp3vdaa+FuRdX5XhKCZPpWv7m0z9ByM= | github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e/go.mod h1:4xVTBPcT43a1pp3vdaa+FuRdX5XhKCZPpWv7m0z9ByM= | ||||
github.com/emersion/go-ical v0.0.0-20200225233454-26ef720b8bf1 h1:v0W797seT60q3pzrphQUKh22Nt8uS7rgZyD6lqYgM0E= | github.com/emersion/go-ical v0.0.0-20200225233454-26ef720b8bf1 h1:v0W797seT60q3pzrphQUKh22Nt8uS7rgZyD6lqYgM0E= | ||||
@@ -10,11 +10,12 @@ import ( | |||||
"strings" | "strings" | ||||
"time" | "time" | ||||
"github.com/dustin/go-humanize" | |||||
"github.com/emersion/go-imap" | "github.com/emersion/go-imap" | ||||
imapspecialuse "github.com/emersion/go-imap-specialuse" | |||||
imapclient "github.com/emersion/go-imap/client" | |||||
"github.com/emersion/go-message" | "github.com/emersion/go-message" | ||||
"github.com/emersion/go-message/textproto" | "github.com/emersion/go-message/textproto" | ||||
imapclient "github.com/emersion/go-imap/client" | |||||
imapspecialuse "github.com/emersion/go-imap-specialuse" | |||||
) | ) | ||||
type MailboxInfo struct { | type MailboxInfo struct { | ||||
@@ -177,6 +178,7 @@ func newIMAPPartNode(msg *IMAPMessage, path []int, part *imap.BodyStructure) *IM | |||||
MIMEType: strings.ToLower(part.MIMEType + "/" + part.MIMESubType), | MIMEType: strings.ToLower(part.MIMEType + "/" + part.MIMESubType), | ||||
Filename: filename, | Filename: filename, | ||||
Message: msg, | Message: msg, | ||||
Size: part.Size, | |||||
} | } | ||||
} | } | ||||
@@ -301,6 +303,7 @@ type IMAPPartNode struct { | |||||
Filename string | Filename string | ||||
Children []IMAPPartNode | Children []IMAPPartNode | ||||
Message *IMAPMessage | Message *IMAPMessage | ||||
Size uint32 | |||||
} | } | ||||
func (node IMAPPartNode) PathString() string { | func (node IMAPPartNode) PathString() string { | ||||
@@ -308,10 +311,13 @@ func (node IMAPPartNode) PathString() string { | |||||
for i, partNum := range node.Path { | for i, partNum := range node.Path { | ||||
l[i] = strconv.Itoa(partNum) | l[i] = strconv.Itoa(partNum) | ||||
} | } | ||||
return strings.Join(l, ".") | return strings.Join(l, ".") | ||||
} | } | ||||
func (node IMAPPartNode) SizeString() string { | |||||
return humanize.IBytes(uint64(node.Size)) | |||||
} | |||||
func (node IMAPPartNode) URL(raw bool) *url.URL { | func (node IMAPPartNode) URL(raw bool) *url.URL { | ||||
u := node.Message.URL() | u := node.Message.URL() | ||||
if raw { | if raw { | ||||
@@ -348,6 +354,7 @@ func imapPartTree(msg *IMAPMessage, bs *imap.BodyStructure, path []int) *IMAPPar | |||||
Filename: filename, | Filename: filename, | ||||
Children: make([]IMAPPartNode, len(bs.Parts)), | Children: make([]IMAPPartNode, len(bs.Parts)), | ||||
Message: msg, | Message: msg, | ||||
Size: bs.Size, | |||||
} | } | ||||
for i, part := range bs.Parts { | for i, part := range bs.Parts { | ||||
@@ -396,7 +403,12 @@ func listMessages(conn *imapclient.Client, mbox *MailboxStatus, page, messagesPe | |||||
var seqSet imap.SeqSet | var seqSet imap.SeqSet | ||||
seqSet.AddRange(uint32(from), uint32(to)) | seqSet.AddRange(uint32(from), uint32(to)) | ||||
fetch := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure} | |||||
fetch := []imap.FetchItem{ | |||||
imap.FetchFlags, | |||||
imap.FetchEnvelope, | |||||
imap.FetchUid, | |||||
imap.FetchBodyStructure, | |||||
} | |||||
ch := make(chan *imap.Message, 10) | ch := make(chan *imap.Message, 10) | ||||
done := make(chan error, 1) | done := make(chan error, 1) | ||||
@@ -452,7 +464,12 @@ func searchMessages(conn *imapclient.Client, mboxName, query string, page, messa | |||||
var seqSet imap.SeqSet | var seqSet imap.SeqSet | ||||
seqSet.AddNum(nums...) | seqSet.AddNum(nums...) | ||||
fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid, imap.FetchBodyStructure} | |||||
fetch := []imap.FetchItem{ | |||||
imap.FetchEnvelope, | |||||
imap.FetchFlags, | |||||
imap.FetchUid, | |||||
imap.FetchBodyStructure, | |||||
} | |||||
ch := make(chan *imap.Message, 10) | ch := make(chan *imap.Message, 10) | ||||
done := make(chan error, 1) | done := make(chan error, 1) | ||||
@@ -506,6 +523,7 @@ func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPa | |||||
imap.FetchUid, | imap.FetchUid, | ||||
imap.FetchBodyStructure, | imap.FetchBodyStructure, | ||||
imap.FetchFlags, | imap.FetchFlags, | ||||
imap.FetchRFC822Size, | |||||
partHeaderSection.FetchItem(), | partHeaderSection.FetchItem(), | ||||
partBodySection.FetchItem(), | partBodySection.FetchItem(), | ||||
} | } | ||||
@@ -261,14 +261,21 @@ main.message .message-header { | |||||
} | } | ||||
main.message .message-header .parts { | main.message .message-header .parts { | ||||
width: 30%; | |||||
margin-left: 0.3rem; | margin-left: 0.3rem; | ||||
padding: 0.3rem 1rem; | |||||
padding: 0.3rem 0.5rem; | |||||
background: white; | background: white; | ||||
border-top: 1px solid #f2f2f2; | border-top: 1px solid #f2f2f2; | ||||
} | } | ||||
main.message .message-header .parts ul { | main.message .message-header .parts ul { | ||||
margin-left: 1rem; | |||||
margin: 0; | |||||
padding: 0; | |||||
list-style: none; | |||||
} | |||||
main.message .message-header .parts li { | |||||
margin-left: 0; | |||||
} | } | ||||
main.contact dl { | main.contact dl { | ||||
@@ -2,32 +2,6 @@ | |||||
{{template "nav.html" .}} | {{template "nav.html" .}} | ||||
{{template "util.html" .}} | {{template "util.html" .}} | ||||
{{define "message-part-tree"}} | |||||
{{/* nested templates can't access the parent's context */}} | |||||
{{$ = index . 0}} | |||||
{{with index . 1}} | |||||
<a | |||||
class="nav-link" | |||||
{{if .IsText}} | |||||
href="?part={{.PathString}}" | |||||
{{else}} | |||||
href="{{$.Message.URL}}raw?part={{.PathString}}" | |||||
{{end}} | |||||
> | |||||
{{if eq $.Part.PathString .PathString}}<strong>{{end}} | |||||
{{.String}} | |||||
{{if eq $.Part.PathString .PathString}}</strong>{{end}} | |||||
</a> | |||||
{{if gt (len .Children) 0}} | |||||
<ul class="nav flex-column"> | |||||
{{range .Children}} | |||||
<li class="nav-item">{{template "message-part-tree" (tuple $ .)}}</li> | |||||
{{end}} | |||||
</ul> | |||||
{{end}} | |||||
{{end}} | |||||
{{end}} | |||||
<div class="page-wrap"> | <div class="page-wrap"> | ||||
{{ $current := .Mailbox }} | {{ $current := .Mailbox }} | ||||
{{ template "aside" . }} | {{ template "aside" . }} | ||||
@@ -162,9 +136,32 @@ | |||||
</tr> | </tr> | ||||
{{end}} | {{end}} | ||||
</table> | </table> | ||||
{{ $attachments := .Message.Attachments }} | |||||
{{ if $attachments }} | |||||
<section class="parts"> | <section class="parts"> | ||||
{{template "message-part-tree" (tuple $ .Message.PartTree)}} | |||||
<h3>Attachments</h3> | |||||
<ul> | |||||
{{ range .Message.Attachments }} | |||||
<li> | |||||
<a | |||||
class="nav-link" | |||||
{{if .IsText}} | |||||
href="?part={{.PathString}}" | |||||
{{else}} | |||||
href="{{$.Message.URL}}/raw?part={{.PathString}}" | |||||
{{end}} | |||||
> | |||||
{{- if .Filename -}} | |||||
{{.Filename}} | |||||
{{- else -}} | |||||
(no filename) | |||||
{{- end -}} | |||||
</a> ({{.MIMEType}}, {{.SizeString}}) | |||||
</li> | |||||
{{ end }} | |||||
</ul> | |||||
</section> | </section> | ||||
{{ end }} | |||||
</div> | </div> | ||||
{{define "addr-list"}} | {{define "addr-list"}} | ||||
@@ -197,6 +194,7 @@ | |||||
{{ end }} | {{ end }} | ||||
<a href="{{.Message.URL}}/raw?plain=1">Raw email</a> | <a href="{{.Message.URL}}/raw?plain=1">Raw email</a> | ||||
</div> | </div> | ||||
{{if .View}} | {{if .View}} | ||||
{{.View}} | {{.View}} | ||||
{{else}} | {{else}} | ||||