aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatarLibravatar Simon Ser <[email protected]> 2019-12-03 18:41:23 +0100
committerLibravatarLibravatar Simon Ser <[email protected]> 2019-12-03 18:41:23 +0100
commita103309935c4a2c72770343542493d1c285d94dd (patch)
tree8d030950cf75f4b10f1cc657b56a0079b027b7b5
parentb386d1c2bb0efacea2484b4aa2088c769e048a9c (diff)
downloadalps-a103309935c4a2c72770343542493d1c285d94dd.tar.gz
alps-a103309935c4a2c72770343542493d1c285d94dd.tar.bz2
alps-a103309935c4a2c72770343542493d1c285d94dd.zip
Add support for replying to a message
-rw-r--r--public/compose.html6
-rw-r--r--public/message.html1
-rw-r--r--server.go64
-rw-r--r--smtp.go32
-rw-r--r--strconv.go12
5 files changed, 103 insertions, 12 deletions
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 @@
<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>
<input type="email" name="from" value="{{.Message.From}}">
<p>To:</p>
- <input type="email" name="to" multiple>
+ <input type="email" name="to" multiple value="{{.Message.ToString}}">
<p>Subject:</p>
<input type="text" name="subject" value="{{.Message.Subject}}">
<p>Body:</p>
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 @@
<hr>
{{if .Body}}
+ <p><a href="{{.Message.Uid}}/reply?part={{.PartPath}}">Reply</a></p>
<pre>{{.Body}}</pre>
{{else}}
<p>Can't preview this message part.</p>
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