Add support for replying to a message
This commit is contained in:
parent
b386d1c2bb
commit
a103309935
5 changed files with 103 additions and 12 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
64
server.go
64
server.go
|
@ -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
32
smtp.go
|
@ -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 {
|
||||||
|
|
12
strconv.go
12
strconv.go
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue