diff --git a/public/compose.html b/public/compose.html index 933083c..e35b0fd 100644 --- a/public/compose.html +++ b/public/compose.html @@ -8,11 +8,13 @@

Compose new message

-
+ + +

From:

To:

- +

Subject:

Body:

diff --git a/public/message.html b/public/message.html index fc97bf4..2e1a308 100644 --- a/public/message.html +++ b/public/message.html @@ -40,6 +40,7 @@
{{if .Body}} +

Reply

{{.Body}}
{{else}}

Can't preview this message part.

diff --git a/server.go b/server.go index 1de4eae..af84d4f 100644 --- a/server.go +++ b/server.go @@ -142,11 +142,7 @@ func handleLogin(ectx echo.Context) error { } func handleGetPart(ctx *context, raw bool) error { - mboxName, err := url.PathUnescape(ctx.Param("mbox")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err) - } - uid, err := parseUid(ctx.Param("uid")) + mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err) } @@ -219,6 +215,61 @@ func handleCompose(ectx echo.Context) error { msg.From = ctx.session.username } + if ctx.Request().Method == http.MethodGet && ctx.Param("uid") != "" { + // This is a reply + mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + partPath, err := parsePartPath(ctx.QueryParam("part")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + + var inReplyTo *imapMessage + var part *message.Entity + err = ctx.session.Do(func(c *imapclient.Client) error { + var err error + inReplyTo, part, err = getMessagePart(c, mboxName, 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.HasPrefix(strings.ToLower(mimeType), "text/") { + err := fmt.Errorf("cannot reply to \"%v\" part", mimeType) + return echo.NewHTTPError(http.StatusBadRequest, err) + } + + msg.Text, err = quote(part.Body) + if err != nil { + return err + } + + msg.InReplyTo = inReplyTo.Envelope.MessageId + // TODO: populate From from known user addresses and inReplyTo.Envelope.To + replyTo := inReplyTo.Envelope.ReplyTo + 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.MailboxName + "@" + to.HostName + } + } + msg.Subject = inReplyTo.Envelope.Subject + if !strings.HasPrefix(strings.ToLower(msg.Subject), "re:") { + msg.Subject = "Re: " + msg.Subject + } + } + if ctx.Request().Method == http.MethodPost { // TODO: parse address lists from := ctx.FormValue("from") @@ -374,6 +425,9 @@ func New(imapURL, smtpURL string) *echo.Echo { e.GET("/compose", handleCompose) e.POST("/compose", handleCompose) + e.GET("/message/:mbox/:uid/reply", handleCompose) + e.POST("/message/:mbox/:uid/reply", handleCompose) + e.Static("/assets", "public/assets") return e diff --git a/smtp.go b/smtp.go index 0ec1a92..288f332 100644 --- a/smtp.go +++ b/smtp.go @@ -1,14 +1,30 @@ package koushin import ( + "bufio" "fmt" - "io" "time" + "io" + "strings" "github.com/emersion/go-message/mail" "github.com/emersion/go-smtp" ) +func quote(r io.Reader) (string, error) { + scanner := bufio.NewScanner(r) + var builder strings.Builder + for scanner.Scan() { + builder.WriteString("> ") + builder.Write(scanner.Bytes()) + builder.WriteString("\n") + } + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("quote: failed to read original message: %s", err) + } + return builder.String(), nil +} + func (s *Server) connectSMTP() (*smtp.Client, error) { var c *smtp.Client var err error @@ -34,10 +50,15 @@ func (s *Server) connectSMTP() (*smtp.Client, error) { } type OutgoingMessage struct { - From string - To []string + From string + To []string Subject string - Text string + InReplyTo string + Text string +} + +func (msg *OutgoingMessage) ToString() string { + return strings.Join(msg.To, ", ") } func (msg *OutgoingMessage) WriteTo(w io.Writer) error { @@ -55,6 +76,9 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error { if msg.Subject != "" { h.SetText("Subject", msg.Subject) } + if msg.InReplyTo != "" { + h.Set("In-Reply-To", msg.InReplyTo) + } mw, err := mail.CreateWriter(w, h) if err != nil { diff --git a/strconv.go b/strconv.go index 0879aac..b34241d 100644 --- a/strconv.go +++ b/strconv.go @@ -4,12 +4,13 @@ import ( "fmt" "strconv" "strings" + "net/url" ) func parseUid(s string) (uint32, error) { uid, err := strconv.ParseUint(s, 10, 32) if err != nil { - return 0, err + return 0, fmt.Errorf("invalid UID: %v", err) } if uid == 0 { return 0, fmt.Errorf("UID must be non-zero") @@ -17,6 +18,15 @@ func parseUid(s string) (uint32, error) { return uint32(uid), nil } +func parseMboxAndUid(mboxString, uidString string) (string, uint32, error) { + mboxName, err := url.PathUnescape(mboxString) + if err != nil { + return "", 0, fmt.Errorf("invalid mailbox name: %v", err) + } + uid, err := parseUid(uidString) + return mboxName, uid, err +} + func parsePartPath(s string) ([]int, error) { if s == "" { return nil, nil