plugins/base: add route to forward messages
Only inline forwarding is supported for now. References: https://todo.sr.ht/~sircmpwn/koushin/37
This commit is contained in:
parent
4b887f5771
commit
ae8658f468
3 changed files with 106 additions and 21 deletions
|
@ -115,7 +115,8 @@
|
|||
{{if .Message.HasFlag "\\Draft"}}
|
||||
<a href="{{.Message.URL}}/edit?part={{.Part.PathString}}">Edit draft</a>
|
||||
{{else}}
|
||||
<a href="{{.Message.URL}}/reply?part={{.Part.PathString}}">Reply</a>
|
||||
<a href="{{.Message.URL}}/reply?part={{.Part.PathString}}">Reply</a> ·
|
||||
<a href="{{.Message.URL}}/forward?part={{.Part.PathString}}">Forward</a>
|
||||
{{end}}
|
||||
</p>
|
||||
{{.View}}
|
||||
|
|
|
@ -47,6 +47,9 @@ func registerRoutes(p *koushin.GoPlugin) {
|
|||
p.GET("/message/:mbox/:uid/reply", handleReply)
|
||||
p.POST("/message/:mbox/:uid/reply", handleReply)
|
||||
|
||||
p.GET("/message/:mbox/:uid/forward", handleForward)
|
||||
p.POST("/message/:mbox/:uid/forward", handleForward)
|
||||
|
||||
p.GET("/message/:mbox/:uid/edit", handleEdit)
|
||||
p.POST("/message/:mbox/:uid/edit", handleEdit)
|
||||
|
||||
|
@ -289,9 +292,15 @@ type messagePath struct {
|
|||
Uid uint32
|
||||
}
|
||||
|
||||
type composeOptions struct {
|
||||
Draft *messagePath
|
||||
Forward *messagePath
|
||||
InReplyTo *messagePath
|
||||
}
|
||||
|
||||
// Send message, append it to the Sent mailbox, mark the original message as
|
||||
// answered
|
||||
func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePath, inReplyTo *messagePath) error {
|
||||
func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, options *composeOptions) error {
|
||||
err := ctx.Session.DoSMTP(func(c *smtp.Client) error {
|
||||
return sendMessage(c, msg)
|
||||
})
|
||||
|
@ -302,7 +311,7 @@ func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
return fmt.Errorf("failed to send message: %v", err)
|
||||
}
|
||||
|
||||
if inReplyTo != nil {
|
||||
if inReplyTo := options.InReplyTo; inReplyTo != nil {
|
||||
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||
return markMessageAnswered(c, inReplyTo.Mailbox, inReplyTo.Uid)
|
||||
})
|
||||
|
@ -315,7 +324,7 @@ func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
if _, err := appendMessage(c, msg, mailboxSent); err != nil {
|
||||
return err
|
||||
}
|
||||
if draft != nil {
|
||||
if draft := options.Draft; draft != nil {
|
||||
if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -329,7 +338,7 @@ func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
|
||||
}
|
||||
|
||||
func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePath, inReplyTo *messagePath) error {
|
||||
func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, options *composeOptions) error {
|
||||
if msg.From == "" && strings.ContainsRune(ctx.Session.Username(), '@') {
|
||||
msg.From = ctx.Session.Username()
|
||||
}
|
||||
|
@ -352,35 +361,41 @@ func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
return fmt.Errorf("failed to get multipart form: %v", err)
|
||||
}
|
||||
|
||||
// Fetch previous attachments from draft
|
||||
if draft != nil {
|
||||
// Fetch previous attachments from original message
|
||||
var original *messagePath
|
||||
if options.Draft != nil {
|
||||
original = options.Draft
|
||||
} else if options.Forward != nil {
|
||||
original = options.Forward
|
||||
}
|
||||
if original != 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)
|
||||
return fmt.Errorf("failed to parse original 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)
|
||||
_, part, err = getMessagePart(c, original.Mailbox, original.Uid, path)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch attachment from draft: %v", err)
|
||||
return fmt.Errorf("failed to fetch attachment from original message: %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)
|
||||
return fmt.Errorf("failed to copy attachment from original message: %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,
|
||||
Mailbox: original.Mailbox,
|
||||
Uid: original.Uid,
|
||||
Node: &IMAPPartNode{
|
||||
Path: path,
|
||||
MIMEType: mimeType,
|
||||
|
@ -390,7 +405,7 @@ func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
})
|
||||
}
|
||||
} else if len(form.Value["prev_attachments"]) > 0 {
|
||||
return fmt.Errorf("previous attachments specified but no draft available")
|
||||
return fmt.Errorf("previous attachments specified but no original message available")
|
||||
}
|
||||
|
||||
for _, fh := range form.File["attachments"] {
|
||||
|
@ -406,7 +421,7 @@ func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
if !copied {
|
||||
return fmt.Errorf("no Draft mailbox found")
|
||||
}
|
||||
if draft != nil {
|
||||
if draft := options.Draft; draft != nil {
|
||||
if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -418,7 +433,7 @@ func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, draft *messagePat
|
|||
}
|
||||
return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
|
||||
} else {
|
||||
return submitCompose(ctx, msg, draft, inReplyTo)
|
||||
return submitCompose(ctx, msg, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,7 +451,7 @@ func handleComposeNew(ctx *koushin.Context) error {
|
|||
Subject: ctx.QueryParam("subject"),
|
||||
Text: ctx.QueryParam("body"),
|
||||
InReplyTo: ctx.QueryParam("in-reply-to"),
|
||||
}, nil, nil)
|
||||
}, &composeOptions{})
|
||||
}
|
||||
|
||||
func unwrapIMAPAddressList(addrs []*imap.Address) []string {
|
||||
|
@ -503,7 +518,71 @@ func handleReply(ctx *koushin.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
return handleCompose(ctx, &msg, nil, &inReplyToPath)
|
||||
return handleCompose(ctx, &msg, &composeOptions{InReplyTo: &inReplyToPath})
|
||||
}
|
||||
|
||||
func handleForward(ctx *koushin.Context) error {
|
||||
var sourcePath messagePath
|
||||
var err error
|
||||
sourcePath.Mailbox, sourcePath.Uid, err = parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
var msg OutgoingMessage
|
||||
if ctx.Request().Method == http.MethodGet {
|
||||
// Populate fields from original message
|
||||
partPath, err := parsePartPath(ctx.QueryParam("part"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
var source *IMAPMessage
|
||||
var part *message.Entity
|
||||
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||
var err error
|
||||
source, part, err = getMessagePart(c, sourcePath.Mailbox, sourcePath.Uid, partPath)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mimeType, _, err := part.Header.ContentType()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse part Content-Type: %v", err)
|
||||
}
|
||||
|
||||
if !strings.EqualFold(mimeType, "text/plain") {
|
||||
err := fmt.Errorf("cannot forward %q part", mimeType)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
msg.Text, err = quote(part.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Subject = source.Envelope.Subject
|
||||
if !strings.HasPrefix(strings.ToLower(msg.Subject), "fwd:") &&
|
||||
!strings.HasPrefix(strings.ToLower(msg.Subject), "fw:") {
|
||||
msg.Subject = "Fwd: " + msg.Subject
|
||||
}
|
||||
msg.InReplyTo = source.Envelope.InReplyTo
|
||||
|
||||
attachments := source.Attachments()
|
||||
for i := range attachments {
|
||||
// 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: &attachments[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return handleCompose(ctx, &msg, &composeOptions{Forward: &sourcePath})
|
||||
}
|
||||
|
||||
func handleEdit(ctx *koushin.Context) error {
|
||||
|
@ -561,18 +640,17 @@ func handleEdit(ctx *koushin.Context) error {
|
|||
|
||||
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,
|
||||
Node: &attachments[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return handleCompose(ctx, &msg, &sourcePath, nil)
|
||||
return handleCompose(ctx, &msg, &composeOptions{Draft: &sourcePath})
|
||||
}
|
||||
|
||||
func handleMove(ctx *koushin.Context) error {
|
||||
|
|
|
@ -22,6 +22,12 @@
|
|||
href="{{.Message.URL}}/reply?part={{.Part.PathString}}"
|
||||
>Reply</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="{{.Message.URL}}/forward?part={{.Part.PathString}}"
|
||||
>Forward</a>
|
||||
</li>
|
||||
<li class="mr-auto d-none d-sm-flex"></li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
|
|
Loading…
Reference in a new issue