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, ".")
|
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 {
|
type IMAPPartNode struct {
|
||||||
Path []int
|
Path []int
|
||||||
MIMEType string
|
MIMEType string
|
||||||
|
|
|
@ -25,6 +25,13 @@
|
||||||
<br><br>
|
<br><br>
|
||||||
<label for="attachments">Attachments:</label>
|
<label for="attachments">Attachments:</label>
|
||||||
<input type="file" name="attachments" id="attachments" multiple>
|
<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>
|
<br><br>
|
||||||
<input type="submit" name="save_as_draft" value="Save as draft">
|
<input type="submit" name="save_as_draft" value="Save as draft">
|
||||||
<input type="submit" value="Send">
|
<input type="submit" value="Send">
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package koushinbase
|
package koushinbase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -14,6 +16,7 @@ import (
|
||||||
imapmove "github.com/emersion/go-imap-move"
|
imapmove "github.com/emersion/go-imap-move"
|
||||||
imapclient "github.com/emersion/go-imap/client"
|
imapclient "github.com/emersion/go-imap/client"
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
@ -348,7 +351,51 @@ func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get multipart form: %v", err)
|
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 {
|
if saveAsDraft {
|
||||||
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
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.Subject = source.Envelope.Subject
|
||||||
msg.InReplyTo = source.Envelope.InReplyTo
|
msg.InReplyTo = source.Envelope.InReplyTo
|
||||||
// TODO: preserve Message-Id
|
// 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)
|
return handleCompose(ctx, &msg, &sourcePath, nil)
|
||||||
|
|
|
@ -2,8 +2,11 @@ package koushinbase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -26,23 +29,70 @@ func quote(r io.Reader) (string, error) {
|
||||||
return builder.String(), nil
|
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 {
|
type OutgoingMessage struct {
|
||||||
From string
|
From string
|
||||||
To []string
|
To []string
|
||||||
Subject string
|
Subject string
|
||||||
InReplyTo string
|
InReplyTo string
|
||||||
Text string
|
Text string
|
||||||
Attachments []*multipart.FileHeader
|
Attachments []Attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *OutgoingMessage) ToString() string {
|
func (msg *OutgoingMessage) ToString() string {
|
||||||
return strings.Join(msg.To, ", ")
|
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
|
var h mail.AttachmentHeader
|
||||||
h.Set("Content-Type", att.Header.Get("Content-Type"))
|
h.SetContentType(att.MIMEType(), nil)
|
||||||
h.SetFilename(att.Filename)
|
h.SetFilename(att.Filename())
|
||||||
|
|
||||||
aw, err := mw.CreateAttachment(h)
|
aw, err := mw.CreateAttachment(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue