diff --git a/go.mod b/go.mod index 2f63a62..47852cb 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/chris-ramon/douceur v0.2.0 github.com/emersion/go-imap v1.0.3 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 + github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 github.com/emersion/go-message v0.11.1 github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b github.com/emersion/go-smtp v0.12.1 diff --git a/go.sum b/go.sum index 3fb32fe..a056692 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/emersion/go-imap v1.0.3 h1:5eEee8/DTSIPfliiWqwfvjPGkU8bBtvOy/Wx+eeXzO github.com/emersion/go-imap v1.0.3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= +github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk= +github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= diff --git a/plugins/base/imap.go b/plugins/base/imap.go index 09ae305..0c39ca6 100755 --- a/plugins/base/imap.go +++ b/plugins/base/imap.go @@ -35,6 +35,30 @@ func listMailboxes(conn *imapclient.Client) ([]*imap.MailboxInfo, error) { return mailboxes, nil } +func getMailboxByAttribute(conn *imapclient.Client, attr string) (*imap.MailboxInfo, error) { + ch := make(chan *imap.MailboxInfo, 10) + done := make(chan error, 1) + go func() { + done <- conn.List("", "%", ch) + }() + + var mailbox *imap.MailboxInfo + for mbox := range ch { + for _, a := range mbox.Attributes { + if attr == a { + mailbox = mbox + break + } + } + } + + if err := <-done; err != nil { + return nil, fmt.Errorf("failed to get mailbox with attribute %q: %v", attr, err) + } + + return mailbox, nil +} + func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error { mbox := conn.Mailbox() if mbox == nil || mbox.Name != mboxName { @@ -339,3 +363,15 @@ func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPa return &IMAPMessage{msg}, part, nil } + +func markMessageAnswered(conn *imapclient.Client, mboxName string, uid uint32) error { + if err := ensureMailboxSelected(conn, mboxName); err != nil { + return err + } + + seqSet := new(imap.SeqSet) + seqSet.AddNum(uid) + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.AnsweredFlag} + return conn.UidStore(seqSet, item, flags, nil) +} diff --git a/plugins/base/routes.go b/plugins/base/routes.go index ad93c6f..961f25a 100644 --- a/plugins/base/routes.go +++ b/plugins/base/routes.go @@ -1,6 +1,7 @@ package koushinbase import ( + "bytes" "fmt" "io/ioutil" "mime" @@ -8,10 +9,12 @@ 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" @@ -289,12 +292,19 @@ func handleCompose(ctx *koushin.Context) error { msg.Text = ctx.QueryParam("body") msg.InReplyTo = ctx.QueryParam("in-reply-to") - if ctx.Request().Method == http.MethodGet && ctx.Param("uid") != "" { + var inReplyToMboxName string + var inReplyToUid uint32 + if ctx.Param("uid") != "" { // This is a reply - mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid")) + var err error + inReplyToMboxName, inReplyToUid, err = parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err) } + } + + if ctx.Request().Method == http.MethodGet && inReplyToUid != 0 { + // Populate fields from original message partPath, err := parsePartPath(ctx.QueryParam("part")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err) @@ -304,7 +314,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, mboxName, uid, partPath) + inReplyTo, part, err = getMessagePart(c, inReplyToMboxName, inReplyToUid, partPath) return err }) if err != nil { @@ -364,7 +374,39 @@ func handleCompose(ctx *koushin.Context) error { if _, ok := err.(koushin.AuthError); ok { return echo.NewHTTPError(http.StatusForbidden, err) } - return 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 { + mbox, err := getMailboxByAttribute(c, imapspecialuse.Sent) + if err != nil { + return 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) } // TODO: append to IMAP Sent mailbox