From d72ced943e0fe2f942a9810ba73a233f03841b82 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Thu, 30 Apr 2020 01:37:40 -0400 Subject: [PATCH] Change the send-as functionality to use a string in the Subject. Using the RCPT TO for BCCs does not work if mailpopbox is not the original MTA to accept the message. The originating MTA creates two envelopes with 1 RCPT TO each, rather than 1 envelope with 2 RCPT TO. Editing the subject breaks Gmail threading on the send-side, but it works. An alternative was to search the body of the message for a string, but that gets more complex with complex message Content-Types. --- README.md | 8 +-- smtp/conn.go | 135 +++++++++++++++++++++------------------------- smtp/conn_test.go | 35 +++++------- smtp/server.go | 3 +- 4 files changed, 82 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index da6ae6c..05a0e51 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ Since mailpopbox is designed as a catch-all mail server, it would be impractical accounts to enable replying from any address handled by the server. The SMTP server instead provides a way to send messages from arbitrary addresses by authenticating as the mailbox@DOMAIN user. Any valid SMTP MAIL FROM is supported after authentication, but mail clients will typically -use the mailbox@DOMAIN user or the From header. The SMTP server's feature is that if the message has -a SMTP RCPT TO an address sendas+ANYTHING@DOMAIN, the server will alter the From message header to -be ANYTHING@DOMAIN. +use the mailbox@DOMAIN user or the From header. The SMTP server's feature is that if the message's +Subject header has a special "[sendas:ANYTHING]" string, the server will alter the From message +header to be from ANYTHING@DOMAIN. Practically, this means configuring an outbound mail client to send mail as mailbox@DOMAIN and authenticate to the SMTP server as such. And in order to change the sending address as perceived by -the recipient, BCC sendas+ANYTHING@DOMAIN, so the resulting message is from ANYTHING@DOMAIN. +the recipient, edit the subject with [sendas:ADDRESS]. ## RFCs diff --git a/smtp/conn.go b/smtp/conn.go index f1c195d..eb868e4 100644 --- a/smtp/conn.go +++ b/smtp/conn.go @@ -330,11 +330,7 @@ func (conn *connection) doMAIL() { } if conn.server.VerifyAddress(*conn.mailFrom) == ReplyOK { - // Message is being sent from a domain that this is an MTA for. Ultimate - // handling of the outbound message requires knowing the recipient. - domain := DomainForAddress(*conn.mailFrom) - // TODO: better way to authenticate this? - if !strings.HasSuffix(conn.authc, "@"+domain) { + if DomainForAddress(*conn.mailFrom) != DomainForAddressString(conn.authc) { conn.writeReply(550, "not authenticated") return } @@ -367,47 +363,12 @@ func (conn *connection) doRCPT() { return } - if reply := conn.server.VerifyAddress(*address); reply == ReplyOK { - // Message is addressed to this server. If it's outbound, only support - // the special send-as addressing. - if conn.delivery == deliverOutbound { - if !strings.HasPrefix(address.Address, SendAsAddress) { - conn.log.Error("internal relay addressing not supported", - zap.String("address", address.Address)) - conn.reply(ReplyBadMailbox) - return - } - address.Address = strings.TrimPrefix(address.Address, SendAsAddress) - if DomainForAddress(*address) != DomainForAddressString(conn.authc) { - conn.log.Error("not authenticated for send-as", - zap.String("address", address.Address), - zap.String("authc", conn.authc)) - conn.reply(ReplyBadMailbox) - return - } - if conn.sendAs != nil { - conn.log.Error("sendAs already specified", - zap.String("address", address.Address), - zap.String("sendAs", conn.sendAs.Address)) - conn.reply(ReplyMailboxUnallowed) - return - } - conn.log.Info("doRCPT()", - zap.String("sendAs", address.Address)) - conn.sendAs = address - conn.state = stateRecipient - conn.reply(ReplyOK) - return - } - } else { - // Message is not addressed to this server, so the delivery must be outbound. - if conn.delivery == deliverInbound { - conn.log.Warn("invalid address", - zap.String("address", address.Address), - zap.Stringer("reply", reply)) - conn.reply(reply) - return - } + if reply := conn.server.VerifyAddress(*address); reply != ReplyOK && conn.delivery == deliverInbound { + conn.log.Warn("invalid address", + zap.String("address", address.Address), + zap.Stringer("reply", reply)) + conn.reply(reply) + return } conn.log.Info("doRCPT()", @@ -438,8 +399,6 @@ func (conn *connection) doDATA() { return } - conn.handleSendAs(&data) - received := time.Now() env := Envelope{ RemoteAddr: conn.remoteAddr, @@ -448,8 +407,11 @@ func (conn *connection) doDATA() { RcptTo: conn.rcptTo, Received: received, ID: conn.envelopeID(received), + Data: data, } + conn.handleSendAs(&env) + conn.log.Info("received message", zap.Int("bytes", len(data)), zap.Time("date", received), @@ -458,7 +420,7 @@ func (conn *connection) doDATA() { trace := conn.getReceivedInfo(env) - env.Data = append(trace, data...) + env.Data = append(trace, env.Data...) if conn.delivery == deliverInbound { if reply := conn.server.OnMessageDelivered(env); reply != nil { @@ -475,46 +437,71 @@ func (conn *connection) doDATA() { conn.reply(ReplyOK) } -func (conn *connection) handleSendAs(data *[]byte) { - if conn.delivery != deliverOutbound || conn.sendAs == nil { +func (conn *connection) handleSendAs(env *Envelope) { + if conn.delivery != deliverOutbound { return } - conn.mailFrom = conn.sendAs - // Find the separator between the message header and body. - headerIdx := bytes.Index(*data, []byte("\n\n")) + headerIdx := bytes.Index(env.Data, []byte("\n\n")) if headerIdx == -1 { conn.log.Error("send-as: could not find headers index") return } - fromPrefix := []byte("From: ") - fromIdx := bytes.Index(*data, fromPrefix) - if fromIdx == -1 || fromIdx >= headerIdx { - conn.log.Error("send-as: could not find From header") - return - } - if fromIdx != 0 { - if (*data)[fromIdx-1] != '\n' { - conn.log.Error("send-as: could not find From header") - return + var buf bytes.Buffer + + headers := bytes.SplitAfter(env.Data[:headerIdx], []byte("\n")) + + var fromIdx, subjectIdx int + for i, header := range headers { + if bytes.HasPrefix(header, []byte("From:")) { + fromIdx = i + continue + } + if bytes.HasPrefix(header, []byte("Subject:")) { + subjectIdx = i + continue } } - fromEndIdx := bytes.IndexByte((*data)[fromIdx:], '\n') + if subjectIdx == -1 { + conn.log.Error("send-as: could not find Subject header") + return + } if fromIdx == -1 { - conn.log.Error("send-as: could not find end of From header") + conn.log.Error("send-as: could not find From header") return } - fromEndIdx += fromIdx - newData := (*data)[:fromIdx] - newData = append(newData, fromPrefix...) - newData = append(newData, []byte(conn.sendAs.String())...) - newData = append(newData, (*data)[fromEndIdx:]...) + sendAs := SendAsSubject.FindSubmatchIndex(headers[subjectIdx]) + if sendAs == nil { + // No send-as modification. + return + } - *data = newData + // Submatch 0 is the whole sendas magic. Submatch 1 is the address prefix. + sendAsUser := headers[subjectIdx][sendAs[2]:sendAs[3]] + sendAsAddress := string(sendAsUser) + "@" + DomainForAddressString(conn.authc) + + for i, header := range headers { + if i == subjectIdx { + buf.Write(header[:sendAs[0]]) + buf.Write(header[sendAs[1]:]) + } else if i == fromIdx { + addressStart := bytes.LastIndexByte(header, byte('<')) + buf.Write(header[:addressStart+1]) + buf.WriteString(sendAsAddress) + buf.WriteString(">\n") + } else { + buf.Write(header) + } + } + + buf.Write(env.Data[headerIdx:]) + + env.Data = buf.Bytes() + env.MailFrom.Address = sendAsAddress } func (conn *connection) envelopeID(t time.Time) string { @@ -545,7 +532,9 @@ func (conn *connection) getReceivedInfo(envelope Envelope) []byte { } base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn.server.Name(), with, envelope.ID) - base += fmt.Sprintf("for <%s>\r\n ", envelope.RcptTo[0].Address) + if len(envelope.RcptTo) > 0 { + base += fmt.Sprintf("for <%s>\r\n ", envelope.RcptTo[0].Address) + } transport := conn.getTransportString() date := envelope.Received.Format(time.RFC1123Z) // Same as RFC 5322 § 3.3 diff --git a/smtp/conn_test.go b/smtp/conn_test.go index fd4fef3..72b5592 100644 --- a/smtp/conn_test.go +++ b/smtp/conn_test.go @@ -28,9 +28,9 @@ func ok(t testing.TB, err error) { } func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string { - _, message, err := conn.ReadCodeLine(code) + actual, message, err := conn.ReadCodeLine(code) if err != nil { - t.Errorf("%s ReadCodeLine error: %v", _fl(1), err) + t.Errorf("%s ReadCodeLine error, expected %d, got %d: %v", _fl(1), code, actual, err) } return message } @@ -496,18 +496,6 @@ func TestBasicRelay(t *testing.T) { } } -func TestNoInternalRelays(t *testing.T) { - _, l, conn := setupRelayTest(t) - defer l.Close() - - runTableTest(t, conn, []requestResponse{ - {"MAIL FROM:", 250, nil}, - {"RCPT TO:", 250, nil}, - {"RCPT TO:", 550, nil}, - {"RCPT TO:", 550, nil}, - }) -} - func TestSendAsRelay(t *testing.T) { server, l, conn := setupRelayTest(t) defer l.Close() @@ -515,14 +503,12 @@ func TestSendAsRelay(t *testing.T) { runTableTest(t, conn, []requestResponse{ {"MAIL FROM:", 250, nil}, {"RCPT TO:", 250, nil}, - {"RCPT TO:", 250, nil}, - {"RCPT TO:", 550, nil}, {"DATA", 354, func(t testing.TB, conn *textproto.Conn) { readCodeLine(t, conn, 354) ok(t, conn.PrintfLine("From: ")) ok(t, conn.PrintfLine("To: ")) - ok(t, conn.PrintfLine("Subject: Send-as relay\n")) + ok(t, conn.PrintfLine("Subject: Send-as relay [sendas:source]\n")) ok(t, conn.PrintfLine("We've switched the senders!")) ok(t, conn.PrintfLine(".")) readCodeLine(t, conn, 250) @@ -557,6 +543,10 @@ func TestSendAsRelay(t *testing.T) { if strings.Index(msg, "\nFrom: \n") == -1 { t.Errorf("Could not find From: header in message %q", msg) } + + if strings.Index(msg, "\nSubject: Send-as relay \n") == -1 { + t.Errorf("Could not find modified Subject: header in message %q", msg) + } } func TestSendMultipleRelay(t *testing.T) { @@ -566,14 +556,13 @@ func TestSendMultipleRelay(t *testing.T) { runTableTest(t, conn, []requestResponse{ {"MAIL FROM:", 250, nil}, {"RCPT TO:", 250, nil}, - {"RCPT TO:", 250, nil}, {"RCPT TO:", 250, nil}, {"DATA", 354, func(t testing.TB, conn *textproto.Conn) { readCodeLine(t, conn, 354) ok(t, conn.PrintfLine("To: Cindy , Sam ")) - ok(t, conn.PrintfLine("From: ")) - ok(t, conn.PrintfLine("Subject: Two destinations\n")) + ok(t, conn.PrintfLine("From: Finn ")) + ok(t, conn.PrintfLine("Subject: Two destinations [sendas:source]\n")) ok(t, conn.PrintfLine("And we've switched the senders!")) ok(t, conn.PrintfLine(".")) readCodeLine(t, conn, 250) @@ -605,7 +594,11 @@ func TestSendMultipleRelay(t *testing.T) { t.Errorf("Should not find %q in message %q", original, msg) } - if strings.Index(msg, "\nFrom: \n") == -1 { + if strings.Index(msg, "\nFrom: Finn \n") == -1 { t.Errorf("Could not find From: header in message %q", msg) } + + if strings.Index(msg, "\nSubject: Two destinations \n") == -1 { + t.Errorf("Could not find modified Subject: header in message %q", msg) + } } diff --git a/smtp/server.go b/smtp/server.go index a83f582..abbd4f7 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -2,6 +2,7 @@ package smtp import ( "crypto/tls" + "regexp" "fmt" "io" "net" @@ -19,7 +20,7 @@ func (l ReplyLine) String() string { return fmt.Sprintf("%d %s", l.Code, l.Message) } -const SendAsAddress = "sendas+" +var SendAsSubject = regexp.MustCompile(`(?i)\[sendas:\s*([a-zA-Z0-9\.\-_]+)\]`) var ( ReplyOK = ReplyLine{250, "OK"} -- 2.43.5