diff --git a/plugins/base/imap.go b/plugins/base/imap.go index 0c39ca6..a9de217 100755 --- a/plugins/base/imap.go +++ b/plugins/base/imap.go @@ -2,12 +2,15 @@ package koushinbase import ( "bufio" + "bytes" "fmt" "sort" "strconv" "strings" + "time" "github.com/emersion/go-imap" + imapspecialuse "github.com/emersion/go-imap-specialuse" imapclient "github.com/emersion/go-imap/client" "github.com/emersion/go-message" "github.com/emersion/go-message/textproto" @@ -375,3 +378,45 @@ func markMessageAnswered(conn *imapclient.Client, mboxName string, uid uint32) e flags := []interface{}{imap.AnsweredFlag} return conn.UidStore(seqSet, item, flags, nil) } + +type mailboxType int + +const ( + mailboxSent mailboxType = iota + mailboxDrafts +) + +func appendMessage(c *imapclient.Client, msg *OutgoingMessage, mboxType mailboxType) (saved bool, err error) { + var mboxAttr string + switch mboxType { + case mailboxSent: + mboxAttr = imapspecialuse.Sent + case mailboxDrafts: + mboxAttr = imapspecialuse.Drafts + } + + mbox, err := getMailboxByAttribute(c, mboxAttr) + if err != nil { + return false, err + } + if mbox == nil { + return false, nil + } + + // IMAP needs to know in advance the final size of the message, so + // there's no way around storing it in a buffer here. + var buf bytes.Buffer + if err := msg.WriteTo(&buf); err != nil { + return false, err + } + + flags := []string{imap.SeenFlag} + if mboxType == mailboxDrafts { + flags = append(flags, imap.DraftFlag) + } + if err := c.Append(mbox.Name, flags, time.Now(), &buf); err != nil { + return false, err + } + + return true, nil +} diff --git a/plugins/base/public/compose.html b/plugins/base/public/compose.html index 0e16acf..4d69c81 100644 --- a/plugins/base/public/compose.html +++ b/plugins/base/public/compose.html @@ -26,6 +26,7 @@

+ diff --git a/plugins/base/routes.go b/plugins/base/routes.go index 961f25a..9eaf18a 100644 --- a/plugins/base/routes.go +++ b/plugins/base/routes.go @@ -1,7 +1,6 @@ package koushinbase import ( - "bytes" "fmt" "io/ioutil" "mime" @@ -9,12 +8,10 @@ import ( "net/url" "strconv" "strings" - "time" "git.sr.ht/~emersion/koushin" "github.com/emersion/go-imap" imapmove "github.com/emersion/go-imap-move" - imapspecialuse "github.com/emersion/go-imap-specialuse" imapclient "github.com/emersion/go-imap/client" "github.com/emersion/go-message" "github.com/emersion/go-smtp" @@ -281,6 +278,39 @@ type ComposeRenderData struct { Message *OutgoingMessage } +// 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 { + err := ctx.Session.DoSMTP(func(c *smtp.Client) error { + return sendMessage(c, msg) + }) + if err != nil { + if _, ok := err.(koushin.AuthError); ok { + return echo.NewHTTPError(http.StatusForbidden, err) + } + return fmt.Errorf("failed to send message: %v", err) + } + + if inReplyToUid != 0 { + err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { + return markMessageAnswered(c, inReplyToMboxName, inReplyToUid) + }) + if err != nil { + return fmt.Errorf("failed to mark original message as answered: %v", err) + } + } + + err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { + _, err := appendMessage(c, msg, mailboxSent) + return err + }) + if err != nil { + return fmt.Errorf("failed to save message to Sent mailbox: %v", err) + } + + return ctx.Redirect(http.StatusFound, "/mailbox/INBOX") +} + func handleCompose(ctx *koushin.Context) error { var msg OutgoingMessage if strings.ContainsRune(ctx.Session.Username(), '@') { @@ -355,6 +385,12 @@ func handleCompose(ctx *koushin.Context) error { } 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"] + msg.From = ctx.FormValue("from") msg.To = parseAddressList(ctx.FormValue("to")) msg.Subject = ctx.FormValue("subject") @@ -367,52 +403,23 @@ func handleCompose(ctx *koushin.Context) error { } msg.Attachments = form.File["attachments"] - err = ctx.Session.DoSMTP(func(c *smtp.Client) error { - return sendMessage(c, &msg) - }) - if err != nil { - if _, ok := err.(koushin.AuthError); ok { - return echo.NewHTTPError(http.StatusForbidden, err) - } - return fmt.Errorf("failed to send message: %v", err) - } - - if inReplyToUid != 0 { + if saveAsDraft { err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { - return markMessageAnswered(c, inReplyToMboxName, inReplyToUid) + 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 mark original message as answered: %v", err) - } - } - - err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { - mbox, err := getMailboxByAttribute(c, imapspecialuse.Sent) - if err != nil { - return err + return fmt.Errorf("failed to save message to Draft mailbox: %v", err) } - if mbox == nil { - return nil - } - - // IMAP needs to know in advance the final size of the message, so - // there's no way around storing it in a buffer here. - var buf bytes.Buffer - if err := msg.WriteTo(&buf); err != nil { - return err - } - - flags := []string{imap.SeenFlag} - return c.Append(mbox.Name, flags, time.Now(), &buf) - }) - if err != nil { - return fmt.Errorf("failed to save message to Sent mailbox: %v", err) + } else { + return submitCompose(ctx, &msg, inReplyToMboxName, inReplyToUid) } - - // TODO: append to IMAP Sent mailbox - // TODO: add \Answered flag to original IMAP message - - return ctx.Redirect(http.StatusFound, "/mailbox/INBOX") } return ctx.Render(http.StatusOK, "compose.html", &ComposeRenderData{ diff --git a/plugins/lua/lua.go b/plugins/lua/lua.go index bbc5c39..d236702 100644 --- a/plugins/lua/lua.go +++ b/plugins/lua/lua.go @@ -5,10 +5,10 @@ import ( "html/template" "path/filepath" + "git.sr.ht/~emersion/koushin" "github.com/labstack/echo/v4" "github.com/yuin/gopher-lua" "layeh.com/gopher-luar" - "git.sr.ht/~emersion/koushin" ) type luaRoute struct {