plugins/base: support attachments in drafts
References: https://todo.sr.ht/~sircmpwn/koushin/16
This commit is contained in:
parent
50046b62ac
commit
85c01b87a9
4 changed files with 143 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue