plugins/base: support attachments in drafts

References: https://todo.sr.ht/~sircmpwn/koushin/16
This commit is contained in:
Simon Ser 2020-01-28 12:30:07 +01:00
parent 50046b62ac
commit 85c01b87a9
No known key found for this signature in database
GPG key ID: 0FDE7BE0E88F5E48
4 changed files with 143 additions and 6 deletions

View file

@ -144,6 +144,28 @@ func (msg *IMAPMessage) TextPartName() string {
return strings.Join(l, ".")
}
func (msg *IMAPMessage) Attachments() []IMAPPartNode {
if msg.BodyStructure == nil {
return nil
}
var attachments []IMAPPartNode
msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
if !strings.EqualFold(part.Disposition, "attachment") {
return true
}
filename, _ := part.Filename()
attachments = append(attachments, IMAPPartNode{
Path: path,
MIMEType: strings.ToLower(part.MIMEType + "/" + part.MIMESubType),
Filename: filename,
})
return true
})
return attachments
}
type IMAPPartNode struct {
Path []int
MIMEType string

View file

@ -25,6 +25,13 @@
<br><br>
<label for="attachments">Attachments:</label>
<input type="file" name="attachments" id="attachments" multiple>
{{range .Message.Attachments}}
<br>
<label>
<input type="checkbox" name="prev_attachments" value="{{.Node.PathString}}" checked>
{{.Node}}
</label>
{{end}}
<br><br>
<input type="submit" name="save_as_draft" value="Save as draft">
<input type="submit" value="Send">

View file

@ -1,7 +1,9 @@
package koushinbase
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
@ -14,6 +16,7 @@ import (
imapmove "github.com/emersion/go-imap-move"
imapclient "github.com/emersion/go-imap/client"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-smtp"
"github.com/labstack/echo/v4"
)
@ -348,7 +351,51 @@ func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
if err != nil {
return fmt.Errorf("failed to get multipart form: %v", err)
}
msg.Attachments = form.File["attachments"]
// Fetch previous attachments from draft
if draft != nil {
for _, s := range form.Value["prev_attachments"] {
path, err := parsePartPath(s)
if err != nil {
return fmt.Errorf("failed to parse draft attachment path: %v", err)
}
var part *message.Entity
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
var err error
_, part, err = getMessagePart(c, draft.Mailbox, draft.Uid, path)
return err
})
if err != nil {
return fmt.Errorf("failed to fetch attachment from draft: %v", err)
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, part.Body); err != nil {
return fmt.Errorf("failed to copy attachment from draft: %v", err)
}
h := mail.AttachmentHeader{part.Header}
mimeType, _, _ := h.ContentType()
filename, _ := h.Filename()
msg.Attachments = append(msg.Attachments, &imapAttachment{
Mailbox: draft.Mailbox,
Uid: draft.Uid,
Node: &IMAPPartNode{
Path: path,
MIMEType: mimeType,
Filename: filename,
},
Body: buf.Bytes(),
})
}
} else if len(form.Value["prev_attachments"]) > 0 {
return fmt.Errorf("previous attachments specified but no draft available")
}
for _, fh := range form.File["attachments"] {
msg.Attachments = append(msg.Attachments, &formAttachment{fh})
}
if saveAsDraft {
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
@ -510,7 +557,18 @@ func handleEdit(ctx *koushin.Context) error {
msg.Subject = source.Envelope.Subject
msg.InReplyTo = source.Envelope.InReplyTo
// TODO: preserve Message-Id
// TODO: preserve attachments
attachments := source.Attachments()
for i := range attachments {
att := &attachments[i]
// No need to populate attachment body here, we just need the
// metadata
msg.Attachments = append(msg.Attachments, &imapAttachment{
Mailbox: sourcePath.Mailbox,
Uid: sourcePath.Uid,
Node: att,
})
}
}
return handleCompose(ctx, &msg, &sourcePath, nil)

View file

@ -2,8 +2,11 @@ package koushinbase
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"strings"
"time"
@ -26,23 +29,70 @@ func quote(r io.Reader) (string, error) {
return builder.String(), nil
}
type Attachment interface {
MIMEType() string
Filename() string
Open() (io.ReadCloser, error)
}
type formAttachment struct {
*multipart.FileHeader
}
func (att *formAttachment) Open() (io.ReadCloser, error) {
return att.FileHeader.Open()
}
func (att *formAttachment) MIMEType() string {
// TODO: retain params, e.g. "charset"?
t, _, _ := mime.ParseMediaType(att.FileHeader.Header.Get("Content-Type"))
return t
}
func (att *formAttachment) Filename() string {
return att.FileHeader.Filename
}
type imapAttachment struct {
Mailbox string
Uid uint32
Node *IMAPPartNode
Body []byte
}
func (att *imapAttachment) Open() (io.ReadCloser, error) {
if att.Body == nil {
return nil, fmt.Errorf("IMAP attachment has not been pre-fetched")
}
return ioutil.NopCloser(bytes.NewReader(att.Body)), nil
}
func (att *imapAttachment) MIMEType() string {
return att.Node.MIMEType
}
func (att *imapAttachment) Filename() string {
return att.Node.Filename
}
type OutgoingMessage struct {
From string
To []string
Subject string
InReplyTo string
Text string
Attachments []*multipart.FileHeader
Attachments []Attachment
}
func (msg *OutgoingMessage) ToString() string {
return strings.Join(msg.To, ", ")
}
func writeAttachment(mw *mail.Writer, att *multipart.FileHeader) error {
func writeAttachment(mw *mail.Writer, att Attachment) error {
var h mail.AttachmentHeader
h.Set("Content-Type", att.Header.Get("Content-Type"))
h.SetFilename(att.Filename)
h.SetContentType(att.MIMEType(), nil)
h.SetFilename(att.Filename())
aw, err := mw.CreateAttachment(h)
if err != nil {