From 2f4abb65bf5d8d434662e937f4acdc48272bc945 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 29 May 2022 13:09:56 -0400 Subject: [PATCH] Move smtp.RelayMessage into a new smtp.MTA interface. --- smtp.go | 6 ++++-- smtp/relay.go | 32 ++++++++++++++++---------------- smtp/relay_test.go | 12 ++++++++++-- smtp/server.go | 40 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/smtp.go b/smtp.go index 90f0ec5..5b6952d 100644 --- a/smtp.go +++ b/smtp.go @@ -25,6 +25,7 @@ func runSMTPServer(config Config, log *zap.Logger) <-chan ServerControlMessage { controlChan: make(chan ServerControlMessage), log: log.With(zap.String("server", "smtp")), } + server.mta = smtp.NewDefaultMTA(&server, server.log) go server.run() return server.controlChan } @@ -33,6 +34,8 @@ type smtpServer struct { config Config tlsConfig *tls.Config + mta smtp.MTA + log *zap.Logger controlChan chan ServerControlMessage @@ -144,8 +147,7 @@ func (server *smtpServer) DeliverMessage(en smtp.Envelope) *smtp.ReplyLine { } func (server *smtpServer) RelayMessage(en smtp.Envelope) { - log := server.log.With(zap.String("id", en.ID)) - go smtp.RelayMessage(server, en, log) + go server.mta.RelayMessage(en) } func (server *smtpServer) maildropForAddress(addr mail.Address) string { diff --git a/smtp/relay.go b/smtp/relay.go index 5d65d84..ac637f3 100644 --- a/smtp/relay.go +++ b/smtp/relay.go @@ -20,21 +20,21 @@ import ( "go.uber.org/zap" ) -func RelayMessage(server Server, env Envelope, log *zap.Logger) { +func (m *mta) RelayMessage(env Envelope) { for _, rcptTo := range env.RcptTo { - sendLog := log.With(zap.String("address", rcptTo.Address)) + sendLog := m.log.With(zap.String("address", rcptTo.Address), zap.String("id", env.ID)) domain := DomainForAddress(rcptTo) mx, err := net.LookupMX(domain) if err != nil || len(mx) < 1 { - deliverRelayFailure(server, env, log, rcptTo.Address, "failed to lookup MX records", err) + m.deliverRelayFailure(env, sendLog, rcptTo.Address, "failed to lookup MX records", err) return } - relayMessageToHost(server, env, sendLog, rcptTo.Address, mx[0].Host, "25") + m.relayMessageToHost(env, sendLog, rcptTo.Address, mx[0].Host, "25") } } -func relayMessageToHost(server Server, env Envelope, log *zap.Logger, to, host, port string) { +func (m *mta) relayMessageToHost(env Envelope, log *zap.Logger, to, host, port string) { from := env.MailFrom.Address hostPort := net.JoinHostPort(host, port) log = log.With(zap.String("host", hostPort)) @@ -42,49 +42,49 @@ func relayMessageToHost(server Server, env Envelope, log *zap.Logger, to, host, c, err := smtp.Dial(hostPort) if err != nil { // TODO - retry, or look at other MX records - deliverRelayFailure(server, env, log, to, "failed to dial host", err) + m.deliverRelayFailure(env, log, to, "failed to dial host", err) return } defer c.Quit() - if err = c.Hello(server.Name()); err != nil { - deliverRelayFailure(server, env, log, to, "failed to HELO", err) + if err = c.Hello(m.server.Name()); err != nil { + m.deliverRelayFailure(env, log, to, "failed to HELO", err) return } if hasTls, _ := c.Extension("STARTTLS"); hasTls { config := &tls.Config{ServerName: host} if err = c.StartTLS(config); err != nil { - deliverRelayFailure(server, env, log, to, "failed to STARTTLS", err) + m.deliverRelayFailure(env, log, to, "failed to STARTTLS", err) return } } if err = c.Mail(from); err != nil { - deliverRelayFailure(server, env, log, to, "failed MAIL FROM", err) + m.deliverRelayFailure(env, log, to, "failed MAIL FROM", err) return } if err = c.Rcpt(to); err != nil { - deliverRelayFailure(server, env, log, to, "failed to RCPT TO", err) + m.deliverRelayFailure(env, log, to, "failed to RCPT TO", err) return } wc, err := c.Data() if err != nil { - deliverRelayFailure(server, env, log, to, "failed to DATA", err) + m.deliverRelayFailure(env, log, to, "failed to DATA", err) return } _, err = wc.Write(env.Data) if err != nil { wc.Close() - deliverRelayFailure(server, env, log, to, "failed to write DATA", err) + m.deliverRelayFailure(env, log, to, "failed to write DATA", err) return } if err = wc.Close(); err != nil { - deliverRelayFailure(server, env, log, to, "failed to close DATA", err) + m.deliverRelayFailure(env, log, to, "failed to close DATA", err) return } } @@ -93,7 +93,7 @@ func relayMessageToHost(server Server, env Envelope, log *zap.Logger, to, host, // writes to |log| the |errorStr| and |sendErr|, as well as preparing a new // message, based of |env|, delivered to |server| that reports error // information about the attempted delivery. -func deliverRelayFailure(server Server, env Envelope, log *zap.Logger, to, errorStr string, sendErr error) { +func (m *mta) deliverRelayFailure(env Envelope, log *zap.Logger, to, errorStr string, sendErr error) { log.Error(errorStr, zap.Error(sendErr)) buf := &bytes.Buffer{} @@ -153,5 +153,5 @@ func deliverRelayFailure(server Server, env Envelope, log *zap.Logger, to, error mw.Close() failure.Data = buf.Bytes() - server.DeliverMessage(failure) + m.server.DeliverMessage(failure) } diff --git a/smtp/relay_test.go b/smtp/relay_test.go index cbced01..c6a116b 100644 --- a/smtp/relay_test.go +++ b/smtp/relay_test.go @@ -45,7 +45,11 @@ func TestRelayRoundTrip(t *testing.T) { } host, port, _ := net.SplitHostPort(l.Addr().String()) - relayMessageToHost(s, env, zap.NewNop(), env.RcptTo[0].Address, host, port) + mta := mta{ + server: s, + log: zap.NewNop(), + } + mta.relayMessageToHost(env, zap.NewNop(), env.RcptTo[0].Address, host, port) if want, got := 1, len(s.messages); want != got { t.Errorf("Want %d message to be delivered, got %d", want, got) @@ -84,7 +88,11 @@ func TestDeliveryFailureMessage(t *testing.T) { errorStr1 := "internal message" errorStr2 := "general error 122" - deliverRelayFailure(s, env, zap.NewNop(), env.RcptTo[0].Address, errorStr1, fmt.Errorf(errorStr2)) + mta := mta{ + server: s, + log: zap.NewNop(), + } + mta.deliverRelayFailure(env, zap.NewNop(), env.RcptTo[0].Address, errorStr1, fmt.Errorf(errorStr2)) if want, got := 1, len(s.messages); want != got { t.Errorf("Want %d failure notification, got %d", want, got) diff --git a/smtp/server.go b/smtp/server.go index 0f72e32..d14c2b1 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -16,6 +16,8 @@ import ( "regexp" "strings" "time" + + "go.uber.org/zap" ) type ReplyLine struct { @@ -89,12 +91,26 @@ func lookupRemoteHost(addr net.Addr) string { return rhost } +// Server provides an interface for handling incoming SMTP requests via +// AcceptConnection. type Server interface { + // Returns the name of the server, to use in HELO advertisements. Name() string + + // If non-nil, enables STARTTLS support on the SMTP server with the given + // configuration. TLSConfig() *tls.Config + + // Returns an status line indicating whether this server can receive + // mail for the specified email address. VerifyAddress(mail.Address) ReplyLine - // Verify that the authc+passwd identity can send mail as authz. + + // Verify that the authc+passwd identity can send mail as authz on this + // server. Authenticate(authz, authc, passwd string) bool + + // Delivers a valid incoming message to a recipient on this server. The + // addressee has been validated via VerifyAddress. DeliverMessage(Envelope) *ReplyLine // RelayMessage instructs the server to send the Envelope to another @@ -102,6 +118,28 @@ type Server interface { RelayMessage(Envelope) } +// MTA (Mail Transport Agent) allows a Server to interface with other SMTP +// MTAs. +type MTA interface { + // RelayMessage will attempt to send the specified Envelope. It will ask the + // Server to dial the MX servers for the addresses in Envelope.RcptTo for + // delivery. If relaying fails, a failure notice will be sent to the sender + // via Server.DeliverMessage. + RelayMessage(Envelope) +} + +func NewDefaultMTA(server Server, log *zap.Logger) MTA { + return &mta{ + server: server, + log: log, + } +} + +type mta struct { + server Server + log *zap.Logger +} + type EmptyServerCallbacks struct{} func (*EmptyServerCallbacks) TLSConfig() *tls.Config { -- 2.22.5