Move send-as handling out of the smtp package and into the core server.
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 12 Jun 2022 20:29:25 +0000 (16:29 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 12 Jun 2022 20:29:25 +0000 (16:29 -0400)
smtp.go
smtp/conn.go
smtp/conn_test.go
smtp/server.go
smtp_test.go

diff --git a/smtp.go b/smtp.go
index 5b6952d8a6928597f4615da320bbd04f34557961..4173d9b81a44a31cf64cce58c2b273e5f70f5007 100644 (file)
--- a/smtp.go
+++ b/smtp.go
@@ -7,18 +7,22 @@
 package main
 
 import (
+       "bytes"
        "crypto/tls"
        "fmt"
        "net"
        "net/mail"
        "os"
        "path"
+       "regexp"
 
        "go.uber.org/zap"
 
        "src.bluestatic.org/mailpopbox/smtp"
 )
 
+var sendAsSubject = regexp.MustCompile(`(?i)\[sendas:\s*([a-zA-Z0-9\.\-_]+)\]`)
+
 func runSMTPServer(config Config, log *zap.Logger) <-chan ServerControlMessage {
        server := smtpServer{
                config:      config,
@@ -146,10 +150,6 @@ func (server *smtpServer) DeliverMessage(en smtp.Envelope) *smtp.ReplyLine {
        return nil
 }
 
-func (server *smtpServer) RelayMessage(en smtp.Envelope) {
-       go server.mta.RelayMessage(en)
-}
-
 func (server *smtpServer) maildropForAddress(addr mail.Address) string {
        domain := smtp.DomainForAddress(addr)
        for _, s := range server.config.Servers {
@@ -160,3 +160,76 @@ func (server *smtpServer) maildropForAddress(addr mail.Address) string {
 
        return ""
 }
+
+func (server *smtpServer) RelayMessage(en smtp.Envelope, authc string) {
+       go func() {
+               log := server.log.With(zap.String("id", en.ID))
+               server.handleSendAs(log, &en, authc)
+               server.mta.RelayMessage(en)
+       }()
+}
+
+func (server *smtpServer) handleSendAs(log *zap.Logger, en *smtp.Envelope, authc string) {
+       // Find the separator between the message header and body.
+       headerIdx := bytes.Index(en.Data, []byte("\n\n"))
+       if headerIdx == -1 {
+               log.Error("send-as: could not find headers index")
+               return
+       }
+
+       var buf bytes.Buffer
+
+       headers := bytes.SplitAfter(en.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
+               }
+       }
+
+       if subjectIdx == -1 {
+               log.Error("send-as: could not find Subject header")
+               return
+       }
+       if fromIdx == -1 {
+               log.Error("send-as: could not find From header")
+               return
+       }
+
+       sendAs := sendAsSubject.FindSubmatchIndex(headers[subjectIdx])
+       if sendAs == nil {
+               // No send-as modification.
+               return
+       }
+
+       // Submatch 0 is the whole sendas magic. Submatch 1 is the address prefix.
+       sendAsUser := headers[subjectIdx][sendAs[2]:sendAs[3]]
+       sendAsAddress := string(sendAsUser) + "@" + smtp.DomainForAddressString(authc)
+
+       log.Info("handling send-as", zap.String("address", sendAsAddress))
+
+       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(en.Data[headerIdx:])
+
+       en.Data = buf.Bytes()
+       en.MailFrom.Address = sendAsAddress
+}
index 939c90052bab5713e384fcd50bbb24fc1223d802..47b202fbbf0a8c04293ba9b5c9b355025cc33619 100644 (file)
@@ -7,7 +7,6 @@
 package smtp
 
 import (
-       "bytes"
        "crypto/tls"
        "encoding/base64"
        "fmt"
@@ -415,8 +414,6 @@ func (conn *connection) doDATA() {
                Data:       data,
        }
 
-       conn.handleSendAs(&env)
-
        conn.log.Info("received message",
                zap.Int("bytes", len(data)),
                zap.Time("date", received),
@@ -434,7 +431,7 @@ func (conn *connection) doDATA() {
                        return
                }
        } else if conn.delivery == deliverOutbound {
-               conn.server.RelayMessage(env)
+               conn.server.RelayMessage(env, conn.authc)
        }
 
        conn.state = stateInitial
@@ -442,75 +439,6 @@ func (conn *connection) doDATA() {
        conn.reply(ReplyOK)
 }
 
-func (conn *connection) handleSendAs(env *Envelope) {
-       if conn.delivery != deliverOutbound {
-               return
-       }
-
-       // Find the separator between the message header and body.
-       headerIdx := bytes.Index(env.Data, []byte("\n\n"))
-       if headerIdx == -1 {
-               conn.log.Error("send-as: could not find headers index")
-               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
-               }
-       }
-
-       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 From header")
-               return
-       }
-
-       sendAs := SendAsSubject.FindSubmatchIndex(headers[subjectIdx])
-       if sendAs == nil {
-               // No send-as modification.
-               return
-       }
-
-       // 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)
-
-       conn.log.Info("handling send-as", zap.String("address", sendAsAddress))
-
-       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) getReceivedInfo(envelope Envelope) []byte {
        base := fmt.Sprintf("Received: from %s (%s)\r\n        ", conn.ehlo, lookupRemoteHost(conn.remoteAddr))
 
index 6ae0be78a58c30eb6eb451559fe39d6a42ac36df..ab34bfde5b6c404400e6068e90d3111d4647ed92 100644 (file)
@@ -102,7 +102,7 @@ func (s *testServer) Authenticate(authz, authc, passwd string) bool {
                s.userAuth.passwd == passwd
 }
 
-func (s *testServer) RelayMessage(en Envelope) {
+func (s *testServer) RelayMessage(en Envelope, authc string) {
        s.relayed = append(s.relayed, en)
 }
 
@@ -501,59 +501,6 @@ func TestBasicRelay(t *testing.T) {
        }
 }
 
-func TestSendAsRelay(t *testing.T) {
-       server, l, conn := setupRelayTest(t)
-       defer l.Close()
-
-       runTableTest(t, conn, []requestResponse{
-               {"MAIL FROM:<mailbox@example.com>", 250, nil},
-               {"RCPT TO:<valid@dest.xyz>", 250, nil},
-               {"DATA", 354, func(t testing.TB, conn *textproto.Conn) {
-                       readCodeLine(t, conn, 354)
-
-                       ok(t, conn.PrintfLine("From: <mailbox@example.com>"))
-                       ok(t, conn.PrintfLine("To: <valid@dest.xyz>"))
-                       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)
-               }},
-       })
-
-       if want, got := 1, len(server.relayed); want != got {
-               t.Fatalf("Want %d relayed message, got %d", want, got)
-       }
-
-       replaced := "source@example.com"
-       original := "mailbox@example.com"
-
-       en := server.relayed[0]
-       if want, got := replaced, en.MailFrom.Address; want != got {
-               t.Errorf("Want mail to be from %q, got %q", want, got)
-       }
-
-       if want, got := 1, len(en.RcptTo); want != got {
-               t.Errorf("Want %d recipient, got %d", want, got)
-       }
-       if want, got := "valid@dest.xyz", en.RcptTo[0].Address; want != got {
-               t.Errorf("Unexpected RcptTo %q", got)
-       }
-
-       msg := string(en.Data)
-
-       if strings.Index(msg, original) != -1 {
-               t.Errorf("Should not find %q in message %q", original, msg)
-       }
-
-       if strings.Index(msg, "\nFrom: <source@example.com>\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) {
        server, l, conn := setupRelayTest(t)
        defer l.Close()
@@ -567,7 +514,7 @@ func TestSendMultipleRelay(t *testing.T) {
 
                        ok(t, conn.PrintfLine("To: Cindy <valid@dest.xyz>, Sam <another@dest.org>"))
                        ok(t, conn.PrintfLine("From: Finn <mailbox@example.com>"))
-                       ok(t, conn.PrintfLine("Subject: Two destinations [sendas:source]\n"))
+                       ok(t, conn.PrintfLine("Subject: Two destinations\n"))
                        ok(t, conn.PrintfLine("And we've switched the senders!"))
                        ok(t, conn.PrintfLine("."))
                        readCodeLine(t, conn, 250)
@@ -578,11 +525,8 @@ func TestSendMultipleRelay(t *testing.T) {
                t.Fatalf("Expected 1 relayed message, got %d", len(server.relayed))
        }
 
-       replaced := "source@example.com"
-       original := "mailbox@example.com"
-
        en := server.relayed[0]
-       if want, got := replaced, en.MailFrom.Address; want != got {
+       if want, got := "mailbox@example.com", en.MailFrom.Address; want != got {
                t.Errorf("Want mail to be from %q, got %q", want, got)
        }
 
@@ -595,15 +539,11 @@ func TestSendMultipleRelay(t *testing.T) {
 
        msg := string(en.Data)
 
-       if strings.Index(msg, original) != -1 {
-               t.Errorf("Should not find %q in message %q", original, msg)
-       }
-
-       if strings.Index(msg, "\nFrom: Finn <source@example.com>\n") == -1 {
+       if strings.Index(msg, "\nFrom: Finn <mailbox@example.com>\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)
+       if strings.Index(msg, "\nSubject: Two destinations\n") == -1 {
+               t.Errorf("Could not find Subject: header in message %q", msg)
        }
 }
index d14c2b1ef9b7a49adb3b837c8164f3b17190c24c..746b675bdd99c54afca0233a2739421589a608af 100644 (file)
@@ -13,7 +13,6 @@ import (
        "io"
        "net"
        "net/mail"
-       "regexp"
        "strings"
        "time"
 
@@ -29,8 +28,6 @@ func (l ReplyLine) String() string {
        return fmt.Sprintf("%d %s", l.Code, l.Message)
 }
 
-var SendAsSubject = regexp.MustCompile(`(?i)\[sendas:\s*([a-zA-Z0-9\.\-_]+)\]`)
-
 var (
        ReplyOK               = ReplyLine{250, "OK"}
        ReplyAuthOK           = ReplyLine{235, "auth success"}
@@ -114,8 +111,8 @@ type Server interface {
        DeliverMessage(Envelope) *ReplyLine
 
        // RelayMessage instructs the server to send the Envelope to another
-       // MTA for outbound delivery.
-       RelayMessage(Envelope)
+       // MTA for outbound delivery. `authc` reports the authenticated username.
+       RelayMessage(en Envelope, authc string)
 }
 
 // MTA (Mail Transport Agent) allows a Server to interface with other SMTP
index 40d0dca7c2087da6bde04d0e1c4bcac4fbe83ba4..ba8c254e8cad160aeac540cb9630c6b734aa354a 100644 (file)
@@ -8,10 +8,12 @@ package main
 
 import (
        "bytes"
+       "fmt"
        "io/ioutil"
        "net/mail"
        "os"
        "path/filepath"
+       "strings"
        "testing"
 
        "go.uber.org/zap"
@@ -142,3 +144,100 @@ func TestAuthenticate(t *testing.T) {
                }
        }
 }
+
+type testMTA struct {
+       relayed chan smtp.Envelope
+}
+
+func (m *testMTA) RelayMessage(en smtp.Envelope) {
+       m.relayed <- en
+}
+
+func newTestMTA() *testMTA {
+       return &testMTA{
+               relayed: make(chan smtp.Envelope),
+       }
+}
+
+func TestBasicRelay(t *testing.T) {
+       mta := newTestMTA()
+       server := smtpServer{
+               mta: mta,
+               log: zap.NewNop(),
+       }
+
+       buf := new(bytes.Buffer)
+       fmt.Fprintln(buf, "From: <mailbox@example.com>\r")
+       fmt.Fprintln(buf, "To: <dest@another.net>\r")
+       fmt.Fprintf(buf, "Subject: Basic relay\n\n")
+       fmt.Fprintln(buf, "This is a basic relay message")
+
+       en := smtp.Envelope{
+               MailFrom: mail.Address{Address: "mailbox@example.com"},
+               RcptTo:   []mail.Address{{Address: "dest@another.com"}},
+               Data:     buf.Bytes(),
+               ID:       "id1",
+       }
+
+       server.RelayMessage(en, en.MailFrom.Address)
+
+       relayed := <-mta.relayed
+
+       if !bytes.Equal(relayed.Data, en.Data) {
+               t.Errorf("Relayed message data does not match")
+       }
+}
+
+func TestSendAsRelay(t *testing.T) {
+       mta := newTestMTA()
+       server := smtpServer{
+               mta: mta,
+               log: zap.NewNop(),
+       }
+
+       buf := new(bytes.Buffer)
+       fmt.Fprintln(buf, "Received: msg from wherever")
+       fmt.Fprintln(buf, "From: <mailbox@example.com>")
+       fmt.Fprintln(buf, "To: <valid@dest.xyz>")
+       fmt.Fprintf(buf, "Subject: Send-as relay [sendas:source]\n\n")
+       fmt.Fprintln(buf, "We've switched the senders!")
+
+       en := smtp.Envelope{
+               MailFrom: mail.Address{Address: "mailbox@example.com"},
+               RcptTo:   []mail.Address{{Address: "valid@dest.xyz"}},
+               Data:     buf.Bytes(),
+               ID:       "id1",
+       }
+
+       server.RelayMessage(en, en.MailFrom.Address)
+
+       relayed := <-mta.relayed
+
+       replaced := "source@example.com"
+       original := "mailbox@example.com"
+
+       if want, got := replaced, relayed.MailFrom.Address; want != got {
+               t.Errorf("Want mail to be from %q, got %q", want, got)
+       }
+
+       if want, got := 1, len(relayed.RcptTo); want != got {
+               t.Errorf("Want %d recipient, got %d", want, got)
+       }
+       if want, got := "valid@dest.xyz", relayed.RcptTo[0].Address; want != got {
+               t.Errorf("Unexpected RcptTo %q", got)
+       }
+
+       msg := string(relayed.Data)
+
+       if strings.Index(msg, original) != -1 {
+               t.Errorf("Should not find %q in message %q", original, msg)
+       }
+
+       if strings.Index(msg, "\nFrom: <source@example.com>\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)
+       }
+}