Add support for replying to a message

This commit is contained in:
Simon Ser 2019-12-03 18:41:23 +01:00
parent b386d1c2bb
commit a103309935
No known key found for this signature in database
GPG key ID: 0FDE7BE0E88F5E48
5 changed files with 103 additions and 12 deletions

View file

@ -8,11 +8,13 @@
<h2>Compose new message</h2> <h2>Compose new message</h2>
<form method="post" action="/compose"> <form method="post" action="">
<input type="hidden" name="in_reply_to" value="{{.Message.InReplyTo}}">
<p>From:</p> <p>From:</p>
<input type="email" name="from" value="{{.Message.From}}"> <input type="email" name="from" value="{{.Message.From}}">
<p>To:</p> <p>To:</p>
<input type="email" name="to" multiple> <input type="email" name="to" multiple value="{{.Message.ToString}}">
<p>Subject:</p> <p>Subject:</p>
<input type="text" name="subject" value="{{.Message.Subject}}"> <input type="text" name="subject" value="{{.Message.Subject}}">
<p>Body:</p> <p>Body:</p>

View file

@ -40,6 +40,7 @@
<hr> <hr>
{{if .Body}} {{if .Body}}
<p><a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a></p>
<pre>{{.Body}}</pre> <pre>{{.Body}}</pre>
{{else}} {{else}}
<p>Can't preview this message part.</p> <p>Can't preview this message part.</p>

View file

@ -142,11 +142,7 @@ func handleLogin(ectx echo.Context) error {
} }
func handleGetPart(ctx *context, raw bool) error { func handleGetPart(ctx *context, raw bool) error {
mboxName, err := url.PathUnescape(ctx.Param("mbox")) mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
uid, err := parseUid(ctx.Param("uid"))
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err) return echo.NewHTTPError(http.StatusBadRequest, err)
} }
@ -219,6 +215,61 @@ func handleCompose(ectx echo.Context) error {
msg.From = ctx.session.username 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 { if ctx.Request().Method == http.MethodPost {
// TODO: parse address lists // TODO: parse address lists
from := ctx.FormValue("from") from := ctx.FormValue("from")
@ -374,6 +425,9 @@ func New(imapURL, smtpURL string) *echo.Echo {
e.GET("/compose", handleCompose) e.GET("/compose", handleCompose)
e.POST("/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") e.Static("/assets", "public/assets")
return e return e

32
smtp.go
View file

@ -1,14 +1,30 @@
package koushin package koushin
import ( import (
"bufio"
"fmt" "fmt"
"io"
"time" "time"
"io"
"strings"
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/emersion/go-smtp" "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) { func (s *Server) connectSMTP() (*smtp.Client, error) {
var c *smtp.Client var c *smtp.Client
var err error var err error
@ -34,10 +50,15 @@ func (s *Server) connectSMTP() (*smtp.Client, error) {
} }
type OutgoingMessage struct { type OutgoingMessage struct {
From string From string
To []string To []string
Subject 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 { func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
@ -55,6 +76,9 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
if msg.Subject != "" { if msg.Subject != "" {
h.SetText("Subject", msg.Subject) h.SetText("Subject", msg.Subject)
} }
if msg.InReplyTo != "" {
h.Set("In-Reply-To", msg.InReplyTo)
}
mw, err := mail.CreateWriter(w, h) mw, err := mail.CreateWriter(w, h)
if err != nil { if err != nil {

View file

@ -4,12 +4,13 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"net/url"
) )
func parseUid(s string) (uint32, error) { func parseUid(s string) (uint32, error) {
uid, err := strconv.ParseUint(s, 10, 32) uid, err := strconv.ParseUint(s, 10, 32)
if err != nil { if err != nil {
return 0, err return 0, fmt.Errorf("invalid UID: %v", err)
} }
if uid == 0 { if uid == 0 {
return 0, fmt.Errorf("UID must be non-zero") return 0, fmt.Errorf("UID must be non-zero")
@ -17,6 +18,15 @@ func parseUid(s string) (uint32, error) {
return uint32(uid), nil 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) { func parsePartPath(s string) ([]int, error) {
if s == "" { if s == "" {
return nil, nil return nil, nil