From 2defb3ffa9d1231d5cb9d3b59c8df1870d5385d3 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 24 May 2020 00:51:30 -0400 Subject: [PATCH] Stop using smtp.SendMail to relay messages. Instead, dial the SMTP server and manage the protocol manually. --- smtp.go | 2 +- smtp/relay.go | 82 +++++++++++++++++++++++++++++++++++++--------- smtp/relay_test.go | 64 ++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 smtp/relay_test.go diff --git a/smtp.go b/smtp.go index 2bbb154..fe0c4b5 100644 --- a/smtp.go +++ b/smtp.go @@ -139,7 +139,7 @@ func (server *smtpServer) OnMessageDelivered(en smtp.Envelope) *smtp.ReplyLine { func (server *smtpServer) RelayMessage(en smtp.Envelope) { log := server.log.With(zap.String("id", en.ID)) - go smtp.RelayMessage(en, log) + go smtp.RelayMessage(server, en, log) } func (server *smtpServer) maildropForAddress(addr mail.Address) string { diff --git a/smtp/relay.go b/smtp/relay.go index 23333e5..f709ce1 100644 --- a/smtp/relay.go +++ b/smtp/relay.go @@ -7,41 +7,93 @@ package smtp import ( + "crypto/tls" "net" "net/smtp" "github.com/uber-go/zap" ) -func RelayMessage(env Envelope, log zap.Logger) { +func RelayMessage(server Server, env Envelope, log zap.Logger) { for _, rcptTo := range env.RcptTo { + sendLog := log.With(zap.String("address", rcptTo.Address)) + domain := DomainForAddress(rcptTo) mx, err := net.LookupMX(domain) if err != nil || len(mx) < 1 { - log.Error("failed to lookup MX records", - zap.String("address", rcptTo.Address), + sendLog.Error("failed to lookup MX records", zap.Error(err)) deliverRelayFailure(env, err) return } - - to := []string{rcptTo.Address} - from := env.MailFrom.Address host := mx[0].Host + ":25" + relayMessageToHost(server, env, sendLog, rcptTo.Address, host) + } +} - log.Info("relay message", - zap.String("to", to[0]), - zap.String("from", from), - zap.String("server", host)) - err = smtp.SendMail(host, nil, from, to, env.Data) - if err != nil { - log.Error("failed to relay message", - zap.String("address", rcptTo.Address), - zap.Error(err)) +func relayMessageToHost(server Server, env Envelope, log zap.Logger, to, host string) { + from := env.MailFrom.Address + + c, err := smtp.Dial(host) + if err != nil { + // TODO - retry, or look at other MX records + log.Error("failed to dial host", + zap.String("host", host), + zap.Error(err)) + deliverRelayFailure(env, err) + return + } + defer c.Quit() + + log = log.With(zap.String("host", host)) + + if err = c.Hello(server.Name()); err != nil { + log.Error("failed to HELO", zap.Error(err)) + deliverRelayFailure(env, err) + return + } + + if hasTls, _ := c.Extension("STARTTLS"); hasTls { + config := &tls.Config{ServerName: host} + if err = c.StartTLS(config); err != nil { + log.Error("failed to STARTTLS", zap.Error(err)) deliverRelayFailure(env, err) return } } + + if err = c.Mail(from); err != nil { + log.Error("failed MAIL FROM", zap.Error(err)) + deliverRelayFailure(env, err) + return + } + + if err = c.Rcpt(to); err != nil { + log.Error("failed to RCPT TO", zap.Error(err)) + deliverRelayFailure(env, err) + return + } + + wc, err := c.Data() + if err != nil { + log.Error("failed to DATA", zap.Error(err)) + deliverRelayFailure(env, err) + return + } + + _, err = wc.Write(env.Data) + if err != nil { + wc.Close() + log.Error("failed to write DATA", zap.Error(err)) + deliverRelayFailure(env, err) + return + } + + if err = wc.Close(); err != nil { + log.Error("failed to close DATA", zap.Error(err)) + deliverRelayFailure(env, err) + return + } } func deliverRelayFailure(env Envelope, err error) { diff --git a/smtp/relay_test.go b/smtp/relay_test.go new file mode 100644 index 0000000..bfd3724 --- /dev/null +++ b/smtp/relay_test.go @@ -0,0 +1,64 @@ +// mailpopbox +// Copyright 2020 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 ( + "bytes" + "net/mail" + "testing" + + "github.com/uber-go/zap" +) + +type deliveryServer struct { + testServer + messages []Envelope +} + +func (s *deliveryServer) OnMessageDelivered(env Envelope) *ReplyLine { + s.messages = append(s.messages, env) + return nil +} + +func TestRelayRoundTrip(t *testing.T) { + s := &deliveryServer{ + testServer: testServer{domain: "receive.net"}, + } + l := runServer(t, s) + defer l.Close() + + env := Envelope{ + MailFrom: mail.Address{Address: "from@sender.org"}, + RcptTo: []mail.Address{{Address: "to@receive.net"}}, + Data: []byte("~~~Message~~~\n"), + ID: "ididid", + } + + relayMessageToHost(s, env, zap.New(zap.NullEncoder()), env.RcptTo[0].Address, l.Addr().String()) + + if len(s.messages) != 1 { + t.Errorf("Expected 1 message to be delivered, got %d", len(s.messages)) + return + } + + received := s.messages[0] + + if env.MailFrom.Address != received.MailFrom.Address { + t.Errorf("Expected MailFrom %s, got %s", env.MailFrom.Address, received.MailFrom.Address) + } + if len(received.RcptTo) != 1 { + t.Errorf("Expected 1 RcptTo, got %d", len(received.RcptTo)) + return + } + if env.RcptTo[0].Address != received.RcptTo[0].Address { + t.Errorf("Expected RcptTo %s, got %s", env.RcptTo[0].Address, received.RcptTo[0].Address) + } + + if !bytes.HasSuffix(received.Data, env.Data) { + t.Errorf("Delivered message does not match relayed one. Delivered=%q Relayed=%q", string(env.Data), string(received.Data)) + } +} -- 2.22.5