Bläddra i källkod

plugins/base: support attachments in drafts

References: https://todo.sr.ht/~sircmpwn/koushin/16
master
Simon Ser 4 år sedan
förälder
incheckning
85c01b87a9
Ingen känd nyckel hittad för denna signaturen i databasen GPG-nyckel ID: FDE7BE0E88F5E48
4 ändrade filer med 143 tillägg och 6 borttagningar
  1. +22
    -0
      plugins/base/imap.go
  2. +7
    -0
      plugins/base/public/compose.html
  3. +60
    -2
      plugins/base/routes.go
  4. +54
    -4
      plugins/base/smtp.go

+ 22
- 0
plugins/base/imap.go Visa fil

@@ -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


+ 7
- 0
plugins/base/public/compose.html Visa fil

@@ -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">


+ 60
- 2
plugins/base/routes.go Visa fil

@@ -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)


+ 54
- 4
plugins/base/smtp.go Visa fil

@@ -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 {


Laddar…
Avbryt
Spara