Browse Source

plugins/base: edit drafts

Note that attachments will be lost. This is a TODO.
master
Simon Ser 4 years ago
parent
commit
d31c56ec98
No known key found for this signature in database GPG Key ID: FDE7BE0E88F5E48
3 changed files with 145 additions and 63 deletions
  1. +7
    -1
      plugins/base/public/message.html
  2. +137
    -62
      plugins/base/routes.go
  3. +1
    -0
      plugins/base/smtp.go

+ 7
- 1
plugins/base/public/message.html View File

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


+ 137
- 62
plugins/base/routes.go View File

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


+ 1
- 0
plugins/base/smtp.go View File

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


Loading…
Cancel
Save