mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
change the message composing code from webmail over to message.Composer too
This commit is contained in:
parent
96faf4b5ec
commit
42f6f9cbb3
4 changed files with 132 additions and 197 deletions
|
@ -6,6 +6,7 @@ import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
@ -779,9 +780,9 @@ Period: %s - %s UTC
|
||||||
// The attached file follows the naming convention from the RFC. ../rfc/7489:1812
|
// The attached file follows the naming convention from the RFC. ../rfc/7489:1812
|
||||||
reportFilename := fmt.Sprintf("%s!%s!%d!%d.xml.gz", mox.Conf.Static.HostnameDomain.ASCII, dom.ASCII, beginTime.Unix(), endTime.Add(-time.Second).Unix())
|
reportFilename := fmt.Sprintf("%s!%s!%d!%d.xml.gz", mox.Conf.Static.HostnameDomain.ASCII, dom.ASCII, beginTime.Unix(), endTime.Add(-time.Second).Unix())
|
||||||
|
|
||||||
var addrs []smtp.Address
|
var addrs []message.NameAddress
|
||||||
for _, rcpt := range recipients {
|
for _, rcpt := range recipients {
|
||||||
addrs = append(addrs, rcpt.address)
|
addrs = append(addrs, message.NameAddress{Address: rcpt.address})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose the message.
|
// Compose the message.
|
||||||
|
@ -841,19 +842,29 @@ Period: %s - %s UTC
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeAggregateReport(ctx context.Context, log *mlog.Log, mf *os.File, fromAddr smtp.Address, recipients []smtp.Address, subject, text, filename string, reportXMLGzipFile *os.File) (msgPrefix string, has8bit, smtputf8 bool, messageID string, rerr error) {
|
func composeAggregateReport(ctx context.Context, log *mlog.Log, mf *os.File, fromAddr smtp.Address, recipients []message.NameAddress, subject, text, filename string, reportXMLGzipFile *os.File) (msgPrefix string, has8bit, smtputf8 bool, messageID string, rerr error) {
|
||||||
xc := message.NewComposer(mf)
|
xc := message.NewComposer(mf, 100*1024*1024)
|
||||||
defer xc.Recover(&rerr)
|
defer func() {
|
||||||
|
x := recover()
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err, ok := x.(error); ok && errors.Is(err, message.ErrCompose) {
|
||||||
|
rerr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(x)
|
||||||
|
}()
|
||||||
|
|
||||||
// We only use smtputf8 if we have to, with a utf-8 localpart. For IDNA, we use ASCII domains.
|
// We only use smtputf8 if we have to, with a utf-8 localpart. For IDNA, we use ASCII domains.
|
||||||
for _, a := range recipients {
|
for _, a := range recipients {
|
||||||
if a.Localpart.IsInternational() {
|
if a.Address.Localpart.IsInternational() {
|
||||||
xc.SMTPUTF8 = true
|
xc.SMTPUTF8 = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xc.HeaderAddrs("From", []smtp.Address{fromAddr})
|
xc.HeaderAddrs("From", []message.NameAddress{{Address: fromAddr}})
|
||||||
xc.HeaderAddrs("To", recipients)
|
xc.HeaderAddrs("To", recipients)
|
||||||
xc.Subject(subject)
|
xc.Subject(subject)
|
||||||
messageID = fmt.Sprintf("<%s>", mox.MessageIDGen(xc.SMTPUTF8))
|
messageID = fmt.Sprintf("<%s>", mox.MessageIDGen(xc.SMTPUTF8))
|
||||||
|
@ -904,7 +915,7 @@ func composeAggregateReport(ctx context.Context, log *mlog.Log, mf *os.File, fro
|
||||||
// Though this functionality is quite underspecified, we'll do our best to send our
|
// Though this functionality is quite underspecified, we'll do our best to send our
|
||||||
// an error report in case our report is too large for all recipients.
|
// an error report in case our report is too large for all recipients.
|
||||||
// ../rfc/7489:1918
|
// ../rfc/7489:1918
|
||||||
func sendErrorReport(ctx context.Context, log *mlog.Log, fromAddr smtp.Address, recipients []smtp.Address, reportDomain dns.Domain, reportID string, reportMsgSize int64) error {
|
func sendErrorReport(ctx context.Context, log *mlog.Log, fromAddr smtp.Address, recipients []message.NameAddress, reportDomain dns.Domain, reportID string, reportMsgSize int64) error {
|
||||||
log.Debug("no reporting addresses willing to accept report given size, queuing short error message")
|
log.Debug("no reporting addresses willing to accept report given size, queuing short error message")
|
||||||
|
|
||||||
msgf, err := store.CreateMessageTemp("dmarcreportmsg-out")
|
msgf, err := store.CreateMessageTemp("dmarcreportmsg-out")
|
||||||
|
@ -915,7 +926,7 @@ func sendErrorReport(ctx context.Context, log *mlog.Log, fromAddr smtp.Address,
|
||||||
|
|
||||||
var recipientStrs []string
|
var recipientStrs []string
|
||||||
for _, rcpt := range recipients {
|
for _, rcpt := range recipients {
|
||||||
recipientStrs = append(recipientStrs, rcpt.String())
|
recipientStrs = append(recipientStrs, rcpt.Address.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := fmt.Sprintf("DMARC aggregate reporting error report for %s", reportDomain.ASCII)
|
subject := fmt.Sprintf("DMARC aggregate reporting error report for %s", reportDomain.ASCII)
|
||||||
|
@ -941,7 +952,7 @@ Submitting-URI: %s
|
||||||
msgSize := int64(len(msgPrefix)) + msgInfo.Size()
|
msgSize := int64(len(msgPrefix)) + msgInfo.Size()
|
||||||
|
|
||||||
for _, rcpt := range recipients {
|
for _, rcpt := range recipients {
|
||||||
qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, fromAddr.Path(), rcpt.Path(), has8bit, smtputf8, msgSize, messageID, []byte(msgPrefix), nil)
|
qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, fromAddr.Path(), rcpt.Address.Path(), has8bit, smtputf8, msgSize, messageID, []byte(msgPrefix), nil)
|
||||||
// Don't try as long as regular deliveries, and stop before we would send the
|
// Don't try as long as regular deliveries, and stop before we would send the
|
||||||
// delayed DSN. Though we also won't send that due to IsDMARCReport.
|
// delayed DSN. Though we also won't send that due to IsDMARCReport.
|
||||||
qm.MaxAttempts = 5
|
qm.MaxAttempts = 5
|
||||||
|
@ -958,19 +969,29 @@ Submitting-URI: %s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeErrorReport(ctx context.Context, log *mlog.Log, mf *os.File, fromAddr smtp.Address, recipients []smtp.Address, subject, text string) (msgPrefix string, has8bit, smtputf8 bool, messageID string, rerr error) {
|
func composeErrorReport(ctx context.Context, log *mlog.Log, mf *os.File, fromAddr smtp.Address, recipients []message.NameAddress, subject, text string) (msgPrefix string, has8bit, smtputf8 bool, messageID string, rerr error) {
|
||||||
xc := message.NewComposer(mf)
|
xc := message.NewComposer(mf, 100*1024*1024)
|
||||||
defer xc.Recover(&rerr)
|
defer func() {
|
||||||
|
x := recover()
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err, ok := x.(error); ok && errors.Is(err, message.ErrCompose) {
|
||||||
|
rerr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(x)
|
||||||
|
}()
|
||||||
|
|
||||||
// We only use smtputf8 if we have to, with a utf-8 localpart. For IDNA, we use ASCII domains.
|
// We only use smtputf8 if we have to, with a utf-8 localpart. For IDNA, we use ASCII domains.
|
||||||
for _, a := range recipients {
|
for _, a := range recipients {
|
||||||
if a.Localpart.IsInternational() {
|
if a.Address.Localpart.IsInternational() {
|
||||||
xc.SMTPUTF8 = true
|
xc.SMTPUTF8 = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xc.HeaderAddrs("From", []smtp.Address{fromAddr})
|
xc.HeaderAddrs("From", []message.NameAddress{{Address: fromAddr}})
|
||||||
xc.HeaderAddrs("To", recipients)
|
xc.HeaderAddrs("To", recipients)
|
||||||
xc.Header("Subject", subject)
|
xc.Header("Subject", subject)
|
||||||
messageID = fmt.Sprintf("<%s>", mox.MessageIDGen(xc.SMTPUTF8))
|
messageID = fmt.Sprintf("<%s>", mox.MessageIDGen(xc.SMTPUTF8))
|
||||||
|
|
|
@ -13,46 +13,50 @@ import (
|
||||||
"github.com/mjl-/mox/smtp"
|
"github.com/mjl-/mox/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errCompose = errors.New("compose")
|
var (
|
||||||
|
ErrMessageSize = errors.New("message too large")
|
||||||
|
ErrCompose = errors.New("compose")
|
||||||
|
)
|
||||||
|
|
||||||
// Composer helps compose a message. Operations that fail call panic, which can be
|
// Composer helps compose a message. Operations that fail call panic, which should
|
||||||
// caught with Composer.Recover. Writes are buffered.
|
// be caught with recover(), checking for ErrCompose and optionally ErrMessageSize.
|
||||||
|
// Writes are buffered.
|
||||||
type Composer struct {
|
type Composer struct {
|
||||||
Has8bit bool // Whether message contains 8bit data.
|
Has8bit bool // Whether message contains 8bit data.
|
||||||
SMTPUTF8 bool // Whether message needs to be sent with SMTPUTF8 extension.
|
SMTPUTF8 bool // Whether message needs to be sent with SMTPUTF8 extension.
|
||||||
|
Size int64 // Total bytes written.
|
||||||
|
|
||||||
bw *bufio.Writer
|
bw *bufio.Writer
|
||||||
|
maxSize int64 // If greater than zero, writes beyond maximum size raise ErrMessageSize.
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewComposer(w io.Writer) *Composer {
|
// NewComposer initializes a new composer with a buffered writer around w, and
|
||||||
return &Composer{bw: bufio.NewWriter(w)}
|
// with a maximum message size if maxSize is greater than zero.
|
||||||
|
// Operations on a Composer do not return an error. Caller must use recover() to
|
||||||
|
// catch ErrCompose and optionally ErrMessageSize errors.
|
||||||
|
func NewComposer(w io.Writer, maxSize int64) *Composer {
|
||||||
|
return &Composer{bw: bufio.NewWriter(w), maxSize: maxSize}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements io.Writer, but calls panic (that is handled higher up) on
|
// Write implements io.Writer, but calls panic (that is handled higher up) on
|
||||||
// i/o errors.
|
// i/o errors.
|
||||||
func (c *Composer) Write(buf []byte) (int, error) {
|
func (c *Composer) Write(buf []byte) (int, error) {
|
||||||
|
if c.maxSize > 0 && c.Size+int64(len(buf)) > c.maxSize {
|
||||||
|
c.Checkf(ErrMessageSize, "writing message")
|
||||||
|
}
|
||||||
n, err := c.bw.Write(buf)
|
n, err := c.bw.Write(buf)
|
||||||
|
if n > 0 {
|
||||||
|
c.Size += int64(n)
|
||||||
|
}
|
||||||
c.Checkf(err, "write")
|
c.Checkf(err, "write")
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recover recovers the sentinel panic error value, storing it into rerr.
|
|
||||||
func (c *Composer) Recover(rerr *error) {
|
|
||||||
x := recover()
|
|
||||||
if x == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err, ok := x.(error); ok && errors.Is(err, errCompose) {
|
|
||||||
*rerr = err
|
|
||||||
} else {
|
|
||||||
panic(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checkf checks err, panicing with sentinel error value.
|
// Checkf checks err, panicing with sentinel error value.
|
||||||
func (c *Composer) Checkf(err error, format string, args ...any) {
|
func (c *Composer) Checkf(err error, format string, args ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("%w: %s: %v", errCompose, err, fmt.Sprintf(format, args...)))
|
// We expose the original error too, needed at least for ErrMessageSize.
|
||||||
|
panic(fmt.Errorf("%w: %w: %v", ErrCompose, err, fmt.Sprintf(format, args...)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +71,14 @@ func (c *Composer) Header(k, v string) {
|
||||||
fmt.Fprintf(c, "%s: %s\r\n", k, v)
|
fmt.Fprintf(c, "%s: %s\r\n", k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NameAddress holds both an address display name, and an SMTP path address.
|
||||||
|
type NameAddress struct {
|
||||||
|
DisplayName string
|
||||||
|
Address smtp.Address
|
||||||
|
}
|
||||||
|
|
||||||
// HeaderAddrs writes a message header with addresses.
|
// HeaderAddrs writes a message header with addresses.
|
||||||
func (c *Composer) HeaderAddrs(k string, l []smtp.Address) {
|
func (c *Composer) HeaderAddrs(k string, l []NameAddress) {
|
||||||
if len(l) == 0 {
|
if len(l) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -79,7 +89,7 @@ func (c *Composer) HeaderAddrs(k string, l []smtp.Address) {
|
||||||
v += ","
|
v += ","
|
||||||
linelen++
|
linelen++
|
||||||
}
|
}
|
||||||
addr := mail.Address{Address: a.Pack(c.SMTPUTF8)}
|
addr := mail.Address{Name: a.DisplayName, Address: a.Address.Pack(c.SMTPUTF8)}
|
||||||
s := addr.String()
|
s := addr.String()
|
||||||
if v != "" && linelen+1+len(s) > 77 {
|
if v != "" && linelen+1+len(s) > 77 {
|
||||||
v += "\r\n\t"
|
v += "\r\n\t"
|
||||||
|
|
|
@ -276,7 +276,7 @@ func sendReportDomain(ctx context.Context, log *mlog.Log, resolver dns.Resolver,
|
||||||
return cleanup, fmt.Errorf("looking up current tlsrpt record for reporting addresses: %v", err)
|
return cleanup, fmt.Errorf("looking up current tlsrpt record for reporting addresses: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var recipients []smtp.Address
|
var recipients []message.NameAddress
|
||||||
|
|
||||||
for _, l := range record.RUAs {
|
for _, l := range record.RUAs {
|
||||||
for _, s := range l {
|
for _, s := range l {
|
||||||
|
@ -292,7 +292,7 @@ func sendReportDomain(ctx context.Context, log *mlog.Log, resolver dns.Resolver,
|
||||||
log.Debugx("parsing mailto uri in tlsrpt record rua value, ignoring", err, mlog.Field("rua", s))
|
log.Debugx("parsing mailto uri in tlsrpt record rua value, ignoring", err, mlog.Field("rua", s))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
recipients = append(recipients, addr)
|
recipients = append(recipients, message.NameAddress{Address: addr})
|
||||||
} else if u.Scheme == "https" {
|
} else if u.Scheme == "https" {
|
||||||
// Although "report" is ambiguous and could mean both only the JSON data or an
|
// Although "report" is ambiguous and could mean both only the JSON data or an
|
||||||
// entire message (including DKIM-Signature) with the JSON data, it appears the
|
// entire message (including DKIM-Signature) with the JSON data, it appears the
|
||||||
|
@ -414,7 +414,7 @@ Period: %s - %s UTC
|
||||||
msgSize := int64(len(msgPrefix)) + msgInfo.Size()
|
msgSize := int64(len(msgPrefix)) + msgInfo.Size()
|
||||||
|
|
||||||
for _, rcpt := range recipients {
|
for _, rcpt := range recipients {
|
||||||
qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, from.Path(), rcpt.Path(), has8bit, smtputf8, msgSize, messageID, []byte(msgPrefix), nil)
|
qm := queue.MakeMsg(mox.Conf.Static.Postmaster.Account, from.Path(), rcpt.Address.Path(), has8bit, smtputf8, msgSize, messageID, []byte(msgPrefix), nil)
|
||||||
// Don't try as long as regular deliveries, and stop before we would send the
|
// Don't try as long as regular deliveries, and stop before we would send the
|
||||||
// delayed DSN. Though we also won't send that due to IsTLSReport.
|
// delayed DSN. Though we also won't send that due to IsTLSReport.
|
||||||
// ../rfc/8460:1077
|
// ../rfc/8460:1077
|
||||||
|
@ -442,19 +442,29 @@ Period: %s - %s UTC
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeMessage(ctx context.Context, log *mlog.Log, mf *os.File, policyDomain dns.Domain, fromAddr smtp.Address, recipients []smtp.Address, subject, text, filename string, reportFile *os.File) (msgPrefix string, has8bit, smtputf8 bool, messageID string, rerr error) {
|
func composeMessage(ctx context.Context, log *mlog.Log, mf *os.File, policyDomain dns.Domain, fromAddr smtp.Address, recipients []message.NameAddress, subject, text, filename string, reportFile *os.File) (msgPrefix string, has8bit, smtputf8 bool, messageID string, rerr error) {
|
||||||
xc := message.NewComposer(mf)
|
xc := message.NewComposer(mf, 100*1024*1024)
|
||||||
defer xc.Recover(&rerr)
|
defer func() {
|
||||||
|
x := recover()
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err, ok := x.(error); ok && errors.Is(err, message.ErrCompose) {
|
||||||
|
rerr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(x)
|
||||||
|
}()
|
||||||
|
|
||||||
// We only use smtputf8 if we have to, with a utf-8 localpart. For IDNA, we use ASCII domains.
|
// We only use smtputf8 if we have to, with a utf-8 localpart. For IDNA, we use ASCII domains.
|
||||||
for _, a := range recipients {
|
for _, a := range recipients {
|
||||||
if a.Localpart.IsInternational() {
|
if a.Address.Localpart.IsInternational() {
|
||||||
xc.SMTPUTF8 = true
|
xc.SMTPUTF8 = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xc.HeaderAddrs("From", []smtp.Address{fromAddr})
|
xc.HeaderAddrs("From", []message.NameAddress{{Address: fromAddr}})
|
||||||
xc.HeaderAddrs("To", recipients)
|
xc.HeaderAddrs("To", recipients)
|
||||||
xc.Subject(subject)
|
xc.Subject(subject)
|
||||||
// ../rfc/8460:926
|
// ../rfc/8460:926
|
||||||
|
|
194
webmail/api.go
194
webmail/api.go
|
@ -1,7 +1,6 @@
|
||||||
package webmail
|
package webmail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -10,7 +9,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
@ -180,48 +178,20 @@ type File struct {
|
||||||
DataURI string // Full data of the attachment, with base64 encoding and including content-type.
|
DataURI string // Full data of the attachment, with base64 encoding and including content-type.
|
||||||
}
|
}
|
||||||
|
|
||||||
// xerrWriter is an io.Writer that panics with a *sherpa.Error when Write
|
|
||||||
// returns an error.
|
|
||||||
type xerrWriter struct {
|
|
||||||
ctx context.Context
|
|
||||||
w *bufio.Writer
|
|
||||||
size int64
|
|
||||||
max int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements io.Writer, but calls panic (that is handled higher up) on
|
|
||||||
// i/o errors.
|
|
||||||
func (w *xerrWriter) Write(buf []byte) (int, error) {
|
|
||||||
n, err := w.w.Write(buf)
|
|
||||||
xcheckf(w.ctx, err, "writing message file")
|
|
||||||
if n > 0 {
|
|
||||||
w.size += int64(n)
|
|
||||||
if w.size > w.max {
|
|
||||||
xcheckuserf(w.ctx, errors.New("max message size reached"), "writing message file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type nameAddress struct {
|
|
||||||
Name string
|
|
||||||
Address smtp.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAddress expects either a plain email address like "user@domain", or a
|
// parseAddress expects either a plain email address like "user@domain", or a
|
||||||
// single address as used in a message header, like "name <user@domain>".
|
// single address as used in a message header, like "name <user@domain>".
|
||||||
func parseAddress(msghdr string) (nameAddress, error) {
|
func parseAddress(msghdr string) (message.NameAddress, error) {
|
||||||
a, err := mail.ParseAddress(msghdr)
|
a, err := mail.ParseAddress(msghdr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nameAddress{}, nil
|
return message.NameAddress{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: parse more fully according to ../rfc/5322:959
|
// todo: parse more fully according to ../rfc/5322:959
|
||||||
path, err := smtp.ParseAddress(a.Address)
|
path, err := smtp.ParseAddress(a.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nameAddress{}, err
|
return message.NameAddress{}, err
|
||||||
}
|
}
|
||||||
return nameAddress{a.Name, path}, nil
|
return message.NameAddress{DisplayName: a.Name, Address: path}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func xmailboxID(ctx context.Context, tx *bstore.Tx, mailboxID int64) store.Mailbox {
|
func xmailboxID(ctx context.Context, tx *bstore.Tx, mailboxID int64) store.Mailbox {
|
||||||
|
@ -278,7 +248,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
fromAddr, err := parseAddress(m.From)
|
fromAddr, err := parseAddress(m.From)
|
||||||
xcheckuserf(ctx, err, "parsing From address")
|
xcheckuserf(ctx, err, "parsing From address")
|
||||||
|
|
||||||
var replyTo *nameAddress
|
var replyTo *message.NameAddress
|
||||||
if m.ReplyTo != "" {
|
if m.ReplyTo != "" {
|
||||||
a, err := parseAddress(m.ReplyTo)
|
a, err := parseAddress(m.ReplyTo)
|
||||||
xcheckuserf(ctx, err, "parsing Reply-To address")
|
xcheckuserf(ctx, err, "parsing Reply-To address")
|
||||||
|
@ -287,7 +257,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
|
|
||||||
var recipients []smtp.Address
|
var recipients []smtp.Address
|
||||||
|
|
||||||
var toAddrs []nameAddress
|
var toAddrs []message.NameAddress
|
||||||
for _, s := range m.To {
|
for _, s := range m.To {
|
||||||
addr, err := parseAddress(s)
|
addr, err := parseAddress(s)
|
||||||
xcheckuserf(ctx, err, "parsing To address")
|
xcheckuserf(ctx, err, "parsing To address")
|
||||||
|
@ -295,7 +265,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
recipients = append(recipients, addr.Address)
|
recipients = append(recipients, addr.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ccAddrs []nameAddress
|
var ccAddrs []message.NameAddress
|
||||||
for _, s := range m.Cc {
|
for _, s := range m.Cc {
|
||||||
addr, err := parseAddress(s)
|
addr, err := parseAddress(s)
|
||||||
xcheckuserf(ctx, err, "parsing Cc address")
|
xcheckuserf(ctx, err, "parsing Cc address")
|
||||||
|
@ -362,74 +332,19 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
defer store.CloseRemoveTempFile(log, dataFile, "message to submit")
|
defer store.CloseRemoveTempFile(log, dataFile, "message to submit")
|
||||||
|
|
||||||
// If writing to the message file fails, we abort immediately.
|
// If writing to the message file fails, we abort immediately.
|
||||||
xmsgw := &xerrWriter{ctx, bufio.NewWriter(dataFile), 0, w.maxMessageSize}
|
xc := message.NewComposer(dataFile, w.maxMessageSize)
|
||||||
|
defer func() {
|
||||||
isASCII := func(s string) bool {
|
x := recover()
|
||||||
for _, c := range s {
|
if x == nil {
|
||||||
if c >= 0x80 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
header := func(k, v string) {
|
|
||||||
fmt.Fprintf(xmsgw, "%s: %s\r\n", k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
headerAddrs := func(k string, l []nameAddress) {
|
|
||||||
if len(l) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v := ""
|
if err, ok := x.(error); ok && errors.Is(err, message.ErrMessageSize) {
|
||||||
linelen := len(k) + len(": ")
|
xcheckuserf(ctx, err, "making message")
|
||||||
for _, a := range l {
|
} else if ok && errors.Is(err, message.ErrCompose) {
|
||||||
if v != "" {
|
xcheckf(ctx, err, "making message")
|
||||||
v += ","
|
|
||||||
linelen++
|
|
||||||
}
|
|
||||||
addr := mail.Address{Name: a.Name, Address: a.Address.Pack(smtputf8)}
|
|
||||||
s := addr.String()
|
|
||||||
if v != "" && linelen+1+len(s) > 77 {
|
|
||||||
v += "\r\n\t"
|
|
||||||
linelen = 1
|
|
||||||
} else if v != "" {
|
|
||||||
v += " "
|
|
||||||
linelen++
|
|
||||||
}
|
|
||||||
v += s
|
|
||||||
linelen += len(s)
|
|
||||||
}
|
}
|
||||||
fmt.Fprintf(xmsgw, "%s: %s\r\n", k, v)
|
panic(x)
|
||||||
}
|
}()
|
||||||
|
|
||||||
line := func(w io.Writer) {
|
|
||||||
_, _ = w.Write([]byte("\r\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
text := m.TextBody
|
|
||||||
if !strings.HasSuffix(text, "\n") {
|
|
||||||
text += "\n"
|
|
||||||
}
|
|
||||||
text = strings.ReplaceAll(text, "\n", "\r\n")
|
|
||||||
|
|
||||||
charset := "us-ascii"
|
|
||||||
if !isASCII(text) {
|
|
||||||
charset = "utf-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
var cte string
|
|
||||||
if message.NeedsQuotedPrintable(text) {
|
|
||||||
var sb strings.Builder
|
|
||||||
_, err := io.Copy(quotedprintable.NewWriter(&sb), strings.NewReader(text))
|
|
||||||
xcheckf(ctx, err, "converting text to quoted printable")
|
|
||||||
text = sb.String()
|
|
||||||
cte = "quoted-printable"
|
|
||||||
} else if has8bit || charset == "utf-8" {
|
|
||||||
cte = "8bit"
|
|
||||||
} else {
|
|
||||||
cte = "7bit"
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo spec: can we add an Authentication-Results header that indicates this is an authenticated message? the "auth" method is for SMTP AUTH, which this isn't. ../rfc/8601 https://www.iana.org/assignments/email-auth/email-auth.xhtml
|
// todo spec: can we add an Authentication-Results header that indicates this is an authenticated message? the "auth" method is for SMTP AUTH, which this isn't. ../rfc/8601 https://www.iana.org/assignments/email-auth/email-auth.xhtml
|
||||||
|
|
||||||
|
@ -454,39 +369,19 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outer message headers.
|
// Outer message headers.
|
||||||
headerAddrs("From", []nameAddress{fromAddr})
|
xc.HeaderAddrs("From", []message.NameAddress{fromAddr})
|
||||||
if replyTo != nil {
|
if replyTo != nil {
|
||||||
headerAddrs("Reply-To", []nameAddress{*replyTo})
|
xc.HeaderAddrs("Reply-To", []message.NameAddress{*replyTo})
|
||||||
}
|
}
|
||||||
headerAddrs("To", toAddrs)
|
xc.HeaderAddrs("To", toAddrs)
|
||||||
headerAddrs("Cc", ccAddrs)
|
xc.HeaderAddrs("Cc", ccAddrs)
|
||||||
|
if m.Subject != "" {
|
||||||
var subjectValue string
|
xc.Subject(m.Subject)
|
||||||
subjectLineLen := len("Subject: ")
|
|
||||||
subjectWord := false
|
|
||||||
for i, word := range strings.Split(m.Subject, " ") {
|
|
||||||
if !smtputf8 && !isASCII(word) {
|
|
||||||
word = mime.QEncoding.Encode("utf-8", word)
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
subjectValue += " "
|
|
||||||
subjectLineLen++
|
|
||||||
}
|
|
||||||
if subjectWord && subjectLineLen+len(word) > 77 {
|
|
||||||
subjectValue += "\r\n\t"
|
|
||||||
subjectLineLen = 1
|
|
||||||
}
|
|
||||||
subjectValue += word
|
|
||||||
subjectLineLen += len(word)
|
|
||||||
subjectWord = true
|
|
||||||
}
|
|
||||||
if subjectValue != "" {
|
|
||||||
header("Subject", subjectValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messageID := fmt.Sprintf("<%s>", mox.MessageIDGen(smtputf8))
|
messageID := fmt.Sprintf("<%s>", mox.MessageIDGen(smtputf8))
|
||||||
header("Message-Id", messageID)
|
xc.Header("Message-Id", messageID)
|
||||||
header("Date", time.Now().Format(message.RFC5322Z))
|
xc.Header("Date", time.Now().Format(message.RFC5322Z))
|
||||||
// Add In-Reply-To and References headers.
|
// Add In-Reply-To and References headers.
|
||||||
if m.ResponseMessageID > 0 {
|
if m.ResponseMessageID > 0 {
|
||||||
xdbread(ctx, acc, func(tx *bstore.Tx) {
|
xdbread(ctx, acc, func(tx *bstore.Tx) {
|
||||||
|
@ -504,39 +399,39 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
if rp.Envelope == nil {
|
if rp.Envelope == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
header("In-Reply-To", rp.Envelope.MessageID)
|
xc.Header("In-Reply-To", rp.Envelope.MessageID)
|
||||||
ref := h.Get("References")
|
ref := h.Get("References")
|
||||||
if ref == "" {
|
if ref == "" {
|
||||||
ref = h.Get("In-Reply-To")
|
ref = h.Get("In-Reply-To")
|
||||||
}
|
}
|
||||||
if ref != "" {
|
if ref != "" {
|
||||||
header("References", ref+"\r\n\t"+rp.Envelope.MessageID)
|
xc.Header("References", ref+"\r\n\t"+rp.Envelope.MessageID)
|
||||||
} else {
|
} else {
|
||||||
header("References", rp.Envelope.MessageID)
|
xc.Header("References", rp.Envelope.MessageID)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if m.UserAgent != "" {
|
if m.UserAgent != "" {
|
||||||
header("User-Agent", m.UserAgent)
|
xc.Header("User-Agent", m.UserAgent)
|
||||||
}
|
}
|
||||||
if m.RequireTLS != nil && !*m.RequireTLS {
|
if m.RequireTLS != nil && !*m.RequireTLS {
|
||||||
header("TLS-Required", "No")
|
xc.Header("TLS-Required", "No")
|
||||||
}
|
}
|
||||||
header("MIME-Version", "1.0")
|
xc.Header("MIME-Version", "1.0")
|
||||||
|
|
||||||
if len(m.Attachments) > 0 || len(m.ForwardAttachments.Paths) > 0 {
|
if len(m.Attachments) > 0 || len(m.ForwardAttachments.Paths) > 0 {
|
||||||
mp := multipart.NewWriter(xmsgw)
|
mp := multipart.NewWriter(xc)
|
||||||
header("Content-Type", fmt.Sprintf(`multipart/mixed; boundary="%s"`, mp.Boundary()))
|
xc.Header("Content-Type", fmt.Sprintf(`multipart/mixed; boundary="%s"`, mp.Boundary()))
|
||||||
line(xmsgw)
|
xc.Line()
|
||||||
|
|
||||||
ct := mime.FormatMediaType("text/plain", map[string]string{"charset": charset})
|
textBody, ct, cte := xc.TextPart(m.TextBody)
|
||||||
textHdr := textproto.MIMEHeader{}
|
textHdr := textproto.MIMEHeader{}
|
||||||
textHdr.Set("Content-Type", ct)
|
textHdr.Set("Content-Type", ct)
|
||||||
textHdr.Set("Content-Transfer-Encoding", cte)
|
textHdr.Set("Content-Transfer-Encoding", cte)
|
||||||
|
|
||||||
textp, err := mp.CreatePart(textHdr)
|
textp, err := mp.CreatePart(textHdr)
|
||||||
xcheckf(ctx, err, "adding text part to message")
|
xcheckf(ctx, err, "adding text part to message")
|
||||||
_, err = textp.Write([]byte(text))
|
_, err = textp.Write(textBody)
|
||||||
xcheckf(ctx, err, "writing text part")
|
xcheckf(ctx, err, "writing text part")
|
||||||
|
|
||||||
xaddPart := func(ct, filename string) io.Writer {
|
xaddPart := func(ct, filename string) io.Writer {
|
||||||
|
@ -650,15 +545,14 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
err = mp.Close()
|
err = mp.Close()
|
||||||
xcheckf(ctx, err, "writing mime multipart")
|
xcheckf(ctx, err, "writing mime multipart")
|
||||||
} else {
|
} else {
|
||||||
ct := mime.FormatMediaType("text/plain", map[string]string{"charset": charset})
|
textBody, ct, cte := xc.TextPart(m.TextBody)
|
||||||
header("Content-Type", ct)
|
xc.Header("Content-Type", ct)
|
||||||
header("Content-Transfer-Encoding", cte)
|
xc.Header("Content-Transfer-Encoding", cte)
|
||||||
line(xmsgw)
|
xc.Line()
|
||||||
xmsgw.Write([]byte(text))
|
xc.Write([]byte(textBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = xmsgw.w.Flush()
|
xc.Flush()
|
||||||
xcheckf(ctx, err, "writing message")
|
|
||||||
|
|
||||||
// Add DKIM-Signature headers.
|
// Add DKIM-Signature headers.
|
||||||
var msgPrefix string
|
var msgPrefix string
|
||||||
|
@ -680,7 +574,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
}
|
}
|
||||||
for _, rcpt := range recipients {
|
for _, rcpt := range recipients {
|
||||||
rcptMsgPrefix := recvHdrFor(rcpt.Pack(smtputf8)) + msgPrefix
|
rcptMsgPrefix := recvHdrFor(rcpt.Pack(smtputf8)) + msgPrefix
|
||||||
msgSize := int64(len(rcptMsgPrefix)) + xmsgw.size
|
msgSize := int64(len(rcptMsgPrefix)) + xc.Size
|
||||||
toPath := smtp.Path{
|
toPath := smtp.Path{
|
||||||
Localpart: rcpt.Localpart,
|
Localpart: rcpt.Localpart,
|
||||||
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
||||||
|
@ -752,7 +646,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
MailboxID: sentmb.ID,
|
MailboxID: sentmb.ID,
|
||||||
MailboxOrigID: sentmb.ID,
|
MailboxOrigID: sentmb.ID,
|
||||||
Flags: store.Flags{Notjunk: true, Seen: true},
|
Flags: store.Flags{Notjunk: true, Seen: true},
|
||||||
Size: int64(len(msgPrefix)) + xmsgw.size,
|
Size: int64(len(msgPrefix)) + xc.Size,
|
||||||
MsgPrefix: []byte(msgPrefix),
|
MsgPrefix: []byte(msgPrefix),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue