aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Ser <contact@emersion.fr>2020-01-28 12:30:07 +0100
committerSimon Ser <contact@emersion.fr>2020-01-28 12:30:07 +0100
commit85c01b87a99233fcc273e810b4ad48bb0d33096f (patch)
tree18c2bf279602f684db34ae5a0180f1f6b07a94b0
parent50046b62ac61a985f82bfc22b4e7b39b334d030c (diff)
downloadalps-85c01b87a99233fcc273e810b4ad48bb0d33096f.tar.gz
alps-85c01b87a99233fcc273e810b4ad48bb0d33096f.zip
plugins/base: support attachments in drafts
References: https://todo.sr.ht/~sircmpwn/koushin/16
-rwxr-xr-xplugins/base/imap.go22
-rw-r--r--plugins/base/public/compose.html7
-rw-r--r--plugins/base/routes.go62
-rw-r--r--plugins/base/smtp.go58
4 files changed, 143 insertions, 6 deletions
diff --git a/plugins/base/imap.go b/plugins/base/imap.go
index 1b41265..0c36baa 100755
--- a/plugins/base/imap.go
+++ b/plugins/base/imap.go
@@ -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
diff --git a/plugins/base/public/compose.html b/plugins/base/public/compose.html
index 0db84c0..dd3c7aa 100644
--- a/plugins/base/public/compose.html
+++ b/plugins/base/public/compose.html
@@ -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">
diff --git a/plugins/base/routes.go b/plugins/base/routes.go
index bf0bc5a..f7b85e6 100644
--- a/plugins/base/routes.go
+++ b/plugins/base/routes.go
@@ -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)
diff --git a/plugins/base/smtp.go b/plugins/base/smtp.go
index 663283d..489a6a1 100644
--- a/plugins/base/smtp.go
+++ b/plugins/base/smtp.go
@@ -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 {