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,
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 {
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
+}
package smtp
import (
- "bytes"
"crypto/tls"
"encoding/base64"
"fmt"
Data: data,
}
- conn.handleSendAs(&env)
-
conn.log.Info("received message",
zap.Int("bytes", len(data)),
zap.Time("date", received),
return
}
} else if conn.delivery == deliverOutbound {
- conn.server.RelayMessage(env)
+ conn.server.RelayMessage(env, conn.authc)
}
conn.state = stateInitial
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))
s.userAuth.passwd == passwd
}
-func (s *testServer) RelayMessage(en Envelope) {
+func (s *testServer) RelayMessage(en Envelope, authc string) {
s.relayed = append(s.relayed, en)
}
}
}
-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()
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)
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)
}
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)
}
}
"io"
"net"
"net/mail"
- "regexp"
"strings"
"time"
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"}
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
import (
"bytes"
+ "fmt"
"io/ioutil"
"net/mail"
"os"
"path/filepath"
+ "strings"
"testing"
"go.uber.org/zap"
}
}
}
+
+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)
+ }
+}