aboutsummaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/base/public/message.html3
-rw-r--r--plugins/base/routes.go118
2 files changed, 100 insertions, 21 deletions
diff --git a/plugins/base/public/message.html b/plugins/base/public/message.html
index f019acf..b537ff0 100644
--- a/plugins/base/public/message.html
+++ b/plugins/base/public/message.html
@@ -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> &middot;
+ <a href="{{.Message.URL}}/forward?part={{.Part.PathString}}">Forward</a>
{{end}}
</p>
{{.View}}
diff --git a/plugins/base/routes.go b/plugins/base/routes.go
index 4503fb9..85034de 100644
--- a/plugins/base/routes.go
+++ b/plugins/base/routes.go
@@ -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 {