From 5eede9ae4ed0ac112570ec8b5a72f1e912816b91 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 12 Jun 2022 16:29:25 -0400 Subject: [PATCH] Move send-as handling out of the smtp package and into the core server. --- smtp.go | 81 ++++++++++++++++++++++++++++++++++++-- smtp/conn.go | 74 +---------------------------------- smtp/conn_test.go | 72 +++------------------------------- smtp/server.go | 7 +--- smtp_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+), 148 deletions(-) diff --git a/smtp.go b/smtp.go index 5b6952d..4173d9b 100644 --- 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 +} diff --git a/smtp/conn.go b/smtp/conn.go index 939c900..47b202f 100644 --- a/smtp/conn.go +++ b/smtp/conn.go @@ -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)) diff --git a/smtp/conn_test.go b/smtp/conn_test.go index 6ae0be7..ab34bfd 100644 --- a/smtp/conn_test.go +++ b/smtp/conn_test.go @@ -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:", 250, nil}, - {"RCPT TO:", 250, 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 [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: \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 , Sam ")) ok(t, conn.PrintfLine("From: Finn ")) - 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 \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) + if strings.Index(msg, "\nSubject: Two destinations\n") == -1 { + t.Errorf("Could not find Subject: header in message %q", msg) } } diff --git a/smtp/server.go b/smtp/server.go index d14c2b1..746b675 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -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 diff --git a/smtp_test.go b/smtp_test.go index 40d0dca..ba8c254 100644 --- a/smtp_test.go +++ b/smtp_test.go @@ -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: \r") + fmt.Fprintln(buf, "To: \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: ") + fmt.Fprintln(buf, "To: ") + 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: \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) + } +} -- 2.22.5