From 60e4177c5b6d47540ce35164bd8f8f022eb43315 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 11 May 2024 15:25:43 -0400 Subject: [PATCH] Refactor the mta implementation into a base and derived smtpMTA. --- smtp.go | 2 +- smtp/relay.go | 84 +++++++-------------------------------- smtp/relay_smtp.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++ smtp/relay_test.go | 12 ++---- smtp/server.go | 24 ----------- 5 files changed, 117 insertions(+), 104 deletions(-) create mode 100644 smtp/relay_smtp.go diff --git a/smtp.go b/smtp.go index c22728e..5823eeb 100644 --- a/smtp.go +++ b/smtp.go @@ -29,7 +29,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) + server.mta = smtp.NewSMTPMTA(&server, server.log) go server.run() return server.controlChan } diff --git a/smtp/relay.go b/smtp/relay.go index ac637f3..b99230c 100644 --- a/smtp/relay.go +++ b/smtp/relay.go @@ -8,92 +8,36 @@ package smtp import ( "bytes" - "crypto/tls" "fmt" "mime/multipart" - "net" "net/mail" - "net/smtp" "net/textproto" "time" "go.uber.org/zap" ) -func (m *mta) RelayMessage(env Envelope) { - for _, rcptTo := range env.RcptTo { - 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 { - m.deliverRelayFailure(env, sendLog, rcptTo.Address, "failed to lookup MX records", err) - return - } - m.relayMessageToHost(env, sendLog, rcptTo.Address, mx[0].Host, "25") - } +// 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 (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)) - - c, err := smtp.Dial(hostPort) - if err != nil { - // TODO - retry, or look at other MX records - m.deliverRelayFailure(env, log, to, "failed to dial host", err) - return - } - defer c.Quit() - - 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 { - m.deliverRelayFailure(env, log, to, "failed to STARTTLS", err) - return - } - } - - if err = c.Mail(from); err != nil { - m.deliverRelayFailure(env, log, to, "failed MAIL FROM", err) - return - } - - if err = c.Rcpt(to); err != nil { - m.deliverRelayFailure(env, log, to, "failed to RCPT TO", err) - return - } - - wc, err := c.Data() - if err != nil { - m.deliverRelayFailure(env, log, to, "failed to DATA", err) - return - } - - _, err = wc.Write(env.Data) - if err != nil { - wc.Close() - m.deliverRelayFailure(env, log, to, "failed to write DATA", err) - return - } - - if err = wc.Close(); err != nil { - m.deliverRelayFailure(env, log, to, "failed to close DATA", err) - return - } +// mta is a base implementation of the MTA interface. +type mta struct { + server Server + log *zap.Logger } -// deliverRelayFailure logs and generates a delivery status notification. It +// RelayFailure logs and generates a delivery status notification. It // 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 (m *mta) deliverRelayFailure(env Envelope, log *zap.Logger, to, errorStr string, sendErr error) { +func (m *mta) RelayFailure(env Envelope, log *zap.Logger, to, errorStr string, sendErr error) { log.Error(errorStr, zap.Error(sendErr)) buf := &bytes.Buffer{} diff --git a/smtp/relay_smtp.go b/smtp/relay_smtp.go new file mode 100644 index 0000000..9bee431 --- /dev/null +++ b/smtp/relay_smtp.go @@ -0,0 +1,99 @@ +// mailpopbox +// Copyright 2024 Blue Static +// This program is free software licensed under the GNU General Public License, +// version 3.0. The full text of the license can be found in LICENSE.txt. +// SPDX-License-Identifier: GPL-3.0-only + +package smtp + +import ( + "crypto/tls" + "net" + "net/smtp" + + "go.uber.org/zap" +) + +type smtpMTA struct { + mta +} + +// NewSMTPMTA creates an MTA that directly delivers messages to the recipient's +// mail exchanger. +func NewSMTPMTA(server Server, log *zap.Logger) MTA { + return &smtpMTA{ + mta: mta{ + server: server, + log: log, + }, + } +} + +func (m *smtpMTA) RelayMessage(env Envelope) { + for _, rcptTo := range env.RcptTo { + 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 { + m.RelayFailure(env, sendLog, rcptTo.Address, "failed to lookup MX records", err) + return + } + m.relayMessageToHost(env, sendLog, rcptTo.Address, mx[0].Host, "25") + } +} + +func (m *smtpMTA) 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)) + + c, err := smtp.Dial(hostPort) + if err != nil { + // TODO - retry, or look at other MX records + m.RelayFailure(env, log, to, "failed to dial host", err) + return + } + defer c.Quit() + + if err = c.Hello(m.server.Name()); err != nil { + m.RelayFailure(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 { + m.RelayFailure(env, log, to, "failed to STARTTLS", err) + return + } + } + + if err = c.Mail(from); err != nil { + m.RelayFailure(env, log, to, "failed MAIL FROM", err) + return + } + + if err = c.Rcpt(to); err != nil { + m.RelayFailure(env, log, to, "failed to RCPT TO", err) + return + } + + wc, err := c.Data() + if err != nil { + m.RelayFailure(env, log, to, "failed to DATA", err) + return + } + + _, err = wc.Write(env.Data) + if err != nil { + wc.Close() + m.RelayFailure(env, log, to, "failed to write DATA", err) + return + } + + if err = wc.Close(); err != nil { + m.RelayFailure(env, log, to, "failed to close DATA", err) + return + } +} diff --git a/smtp/relay_test.go b/smtp/relay_test.go index c6a116b..8395717 100644 --- a/smtp/relay_test.go +++ b/smtp/relay_test.go @@ -45,10 +45,7 @@ func TestRelayRoundTrip(t *testing.T) { } host, port, _ := net.SplitHostPort(l.Addr().String()) - mta := mta{ - server: s, - log: zap.NewNop(), - } + mta := NewSMTPMTA(s, zap.NewNop()).(*smtpMTA) mta.relayMessageToHost(env, zap.NewNop(), env.RcptTo[0].Address, host, port) if want, got := 1, len(s.messages); want != got { @@ -88,11 +85,8 @@ func TestDeliveryFailureMessage(t *testing.T) { errorStr1 := "internal message" errorStr2 := "general error 122" - mta := mta{ - server: s, - log: zap.NewNop(), - } - mta.deliverRelayFailure(env, zap.NewNop(), env.RcptTo[0].Address, errorStr1, fmt.Errorf(errorStr2)) + mta := NewSMTPMTA(s, zap.NewNop()).(*smtpMTA) + mta.RelayFailure(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 746b675..b8285a7 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -15,8 +15,6 @@ import ( "net/mail" "strings" "time" - - "go.uber.org/zap" ) type ReplyLine struct { @@ -115,28 +113,6 @@ type Server interface { RelayMessage(en Envelope, authc string) } -// 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.52.0