aboutsummaryrefslogtreecommitdiff
path: root/plugins/base
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/base')
-rw-r--r--plugins/base/public/message.html8
-rw-r--r--plugins/base/routes.go199
-rw-r--r--plugins/base/smtp.go1
3 files changed, 145 insertions, 63 deletions
diff --git a/plugins/base/public/message.html b/plugins/base/public/message.html
index 9544d0a..3dc7f62 100644
--- a/plugins/base/public/message.html
+++ b/plugins/base/public/message.html
@@ -111,7 +111,13 @@
<hr>
{{if .Body}}
- <p><a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a></p>
+ <p>
+ {{if .Message.HasFlag "\\Draft"}}
+ <a href="{{.Message.Uid}}/edit?part={{.PartPath}}">Edit draft</a>
+ {{else}}
+ <a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a>
+ {{end}}
+ </p>
{{if .IsHTML}}
<!-- allow-same-origin is required to resize the frame with its content -->
<!-- allow-popups is required for target="_blank" links -->
diff --git a/plugins/base/routes.go b/plugins/base/routes.go
index 9eaf18a..49f7674 100644
--- a/plugins/base/routes.go
+++ b/plugins/base/routes.go
@@ -38,11 +38,14 @@ func registerRoutes(p *koushin.GoPlugin) {
p.GET("/logout", handleLogout)
- p.GET("/compose", handleCompose)
- p.POST("/compose", handleCompose)
+ p.GET("/compose", handleComposeNew)
+ p.POST("/compose", handleComposeNew)
- p.GET("/message/:mbox/:uid/reply", handleCompose)
- p.POST("/message/:mbox/:uid/reply", handleCompose)
+ p.GET("/message/:mbox/:uid/reply", handleReply)
+ p.POST("/message/:mbox/:uid/reply", handleReply)
+
+ p.GET("/message/:mbox/:uid/edit", handleEdit)
+ p.POST("/message/:mbox/:uid/edit", handleEdit)
p.POST("/message/:mbox/:uid/move", handleMove)
@@ -278,9 +281,14 @@ type ComposeRenderData struct {
Message *OutgoingMessage
}
+type messagePath struct {
+ Mailbox string
+ Uid uint32
+}
+
// Send message, append it to the Sent mailbox, mark the original message as
// answered
-func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, inReplyToMboxName string, inReplyToUid uint32) error {
+func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, inReplyTo *messagePath) error {
err := ctx.Session.DoSMTP(func(c *smtp.Client) error {
return sendMessage(c, msg)
})
@@ -291,9 +299,9 @@ func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, inReplyToMboxName
return fmt.Errorf("failed to send message: %v", err)
}
- if inReplyToUid != 0 {
+ if inReplyTo != nil {
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
- return markMessageAnswered(c, inReplyToMboxName, inReplyToUid)
+ return markMessageAnswered(c, inReplyTo.Mailbox, inReplyTo.Uid)
})
if err != nil {
return fmt.Errorf("failed to mark original message as answered: %v", err)
@@ -311,29 +319,83 @@ func submitCompose(ctx *koushin.Context, msg *OutgoingMessage, inReplyToMboxName
return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
}
-func handleCompose(ctx *koushin.Context) error {
- var msg OutgoingMessage
- if strings.ContainsRune(ctx.Session.Username(), '@') {
+func handleCompose(ctx *koushin.Context, msg *OutgoingMessage, source *messagePath, inReplyTo *messagePath) error {
+ if msg.From == "" && strings.ContainsRune(ctx.Session.Username(), '@') {
msg.From = ctx.Session.Username()
}
- msg.To = strings.Split(ctx.QueryParam("to"), ",")
- msg.Subject = ctx.QueryParam("subject")
- msg.Text = ctx.QueryParam("body")
- msg.InReplyTo = ctx.QueryParam("in-reply-to")
+ if ctx.Request().Method == http.MethodPost {
+ formParams, err := ctx.FormParams()
+ if err != nil {
+ return fmt.Errorf("failed to parse form: %v", err)
+ }
+ _, saveAsDraft := formParams["save_as_draft"]
- var inReplyToMboxName string
- var inReplyToUid uint32
- if ctx.Param("uid") != "" {
- // This is a reply
- var err error
- inReplyToMboxName, inReplyToUid, err = parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
+ msg.From = ctx.FormValue("from")
+ msg.To = parseAddressList(ctx.FormValue("to"))
+ msg.Subject = ctx.FormValue("subject")
+ msg.Text = ctx.FormValue("text")
+ msg.InReplyTo = ctx.FormValue("in_reply_to")
+
+ form, err := ctx.MultipartForm()
if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err)
+ return fmt.Errorf("failed to get multipart form: %v", err)
+ }
+ msg.Attachments = form.File["attachments"]
+
+ if saveAsDraft {
+ err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
+ copied, err := appendMessage(c, msg, mailboxDrafts)
+ if err != nil {
+ return err
+ }
+ if !copied {
+ return fmt.Errorf("no Draft mailbox found")
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("failed to save message to Draft mailbox: %v", err)
+ }
+ } else {
+ return submitCompose(ctx, msg, inReplyTo)
}
}
- if ctx.Request().Method == http.MethodGet && inReplyToUid != 0 {
+ return ctx.Render(http.StatusOK, "compose.html", &ComposeRenderData{
+ BaseRenderData: *koushin.NewBaseRenderData(ctx),
+ Message: msg,
+ })
+}
+
+func handleComposeNew(ctx *koushin.Context) error {
+ // These are common mailto URL query parameters
+ return handleCompose(ctx, &OutgoingMessage{
+ To: strings.Split(ctx.QueryParam("to"), ","),
+ Subject: ctx.QueryParam("subject"),
+ Text: ctx.QueryParam("body"),
+ InReplyTo: ctx.QueryParam("in-reply-to"),
+ }, nil, nil)
+}
+
+func unwrapIMAPAddressList(addrs []*imap.Address) []string {
+ l := make([]string, len(addrs))
+ for i, addr := range addrs {
+ l[i] = addr.Address()
+ }
+ return l
+}
+
+func handleReply(ctx *koushin.Context) error {
+ var inReplyToPath messagePath
+ var err error
+ inReplyToPath.Mailbox, inReplyToPath.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 {
@@ -344,7 +406,7 @@ func handleCompose(ctx *koushin.Context) error {
var part *message.Entity
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
var err error
- inReplyTo, part, err = getMessagePart(c, inReplyToMboxName, inReplyToUid, partPath)
+ inReplyTo, part, err = getMessagePart(c, inReplyToPath.Mailbox, inReplyToPath.Uid, partPath)
return err
})
if err != nil {
@@ -357,10 +419,11 @@ func handleCompose(ctx *koushin.Context) error {
}
if !strings.HasPrefix(strings.ToLower(mimeType), "text/") {
- err := fmt.Errorf("cannot reply to \"%v\" part", mimeType)
+ err := fmt.Errorf("cannot reply to %q part", mimeType)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
+ // TODO: strip HTML tags if text/html
msg.Text, err = quote(part.Body)
if err != nil {
return err
@@ -372,60 +435,72 @@ func handleCompose(ctx *koushin.Context) error {
if len(replyTo) == 0 {
replyTo = inReplyTo.Envelope.From
}
- if len(replyTo) > 0 {
- msg.To = make([]string, len(replyTo))
- for i, to := range replyTo {
- msg.To[i] = to.Address()
- }
- }
+ msg.To = unwrapIMAPAddressList(replyTo)
msg.Subject = inReplyTo.Envelope.Subject
if !strings.HasPrefix(strings.ToLower(msg.Subject), "re:") {
msg.Subject = "Re: " + msg.Subject
}
}
- if ctx.Request().Method == http.MethodPost {
- formParams, err := ctx.FormParams()
+ return handleCompose(ctx, &msg, nil, &inReplyToPath)
+}
+
+func handleEdit(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)
+ }
+
+ // TODO: somehow get the path to the In-Reply-To message (with a search?)
+
+ var msg OutgoingMessage
+ if ctx.Request().Method == http.MethodGet {
+ // Populate fields from source message
+ partPath, err := parsePartPath(ctx.QueryParam("part"))
if err != nil {
- return fmt.Errorf("failed to parse form: %v", err)
+ return echo.NewHTTPError(http.StatusBadRequest, err)
}
- _, saveAsDraft := formParams["save_as_draft"]
- msg.From = ctx.FormValue("from")
- msg.To = parseAddressList(ctx.FormValue("to"))
- msg.Subject = ctx.FormValue("subject")
- msg.Text = ctx.FormValue("text")
- msg.InReplyTo = ctx.FormValue("in_reply_to")
+ 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
+ }
- form, err := ctx.MultipartForm()
+ mimeType, _, err := part.Header.ContentType()
if err != nil {
- return fmt.Errorf("failed to get multipart form: %v", err)
+ return fmt.Errorf("failed to parse part Content-Type: %v", err)
}
- msg.Attachments = form.File["attachments"]
- if saveAsDraft {
- err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
- copied, err := appendMessage(c, &msg, mailboxDrafts)
- if err != nil {
- return err
- }
- if !copied {
- return fmt.Errorf("no Draft mailbox found")
- }
- return nil
- })
- if err != nil {
- return fmt.Errorf("failed to save message to Draft mailbox: %v", err)
- }
- } else {
- return submitCompose(ctx, &msg, inReplyToMboxName, inReplyToUid)
+ if !strings.EqualFold(mimeType, "text/plain") {
+ err := fmt.Errorf("cannot edit %q part", mimeType)
+ return echo.NewHTTPError(http.StatusBadRequest, err)
}
+
+ b, err := ioutil.ReadAll(part.Body)
+ if err != nil {
+ return fmt.Errorf("failed to read part body: %v", err)
+ }
+ msg.Text = string(b)
+
+ if len(source.Envelope.From) > 0 {
+ msg.From = source.Envelope.From[0].Address()
+ }
+ msg.To = unwrapIMAPAddressList(source.Envelope.To)
+ msg.Subject = source.Envelope.Subject
+ msg.InReplyTo = source.Envelope.InReplyTo
+ // TODO: preserve Message-Id
+ // TODO: preserve attachments
}
- return ctx.Render(http.StatusOK, "compose.html", &ComposeRenderData{
- BaseRenderData: *koushin.NewBaseRenderData(ctx),
- Message: &msg,
- })
+ return handleCompose(ctx, &msg, &sourcePath, nil)
}
func handleMove(ctx *koushin.Context) error {
diff --git a/plugins/base/smtp.go b/plugins/base/smtp.go
index 81da6ef..663283d 100644
--- a/plugins/base/smtp.go
+++ b/plugins/base/smtp.go
@@ -88,6 +88,7 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
if msg.InReplyTo != "" {
h.Set("In-Reply-To", msg.InReplyTo)
}
+ // TODO: set Message-ID
mw, err := mail.CreateWriter(w, h)
if err != nil {