Stop using smtp.SendMail to relay messages.
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 24 May 2020 04:51:30 +0000 (00:51 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 24 May 2020 04:51:30 +0000 (00:51 -0400)
Instead, dial the SMTP server and manage the protocol manually.

smtp.go
smtp/relay.go
smtp/relay_test.go [new file with mode: 0644]

diff --git a/smtp.go b/smtp.go
index 2bbb15460f28c98fc9e2bbdfce93028a7e5bf2fb..fe0c4b5f47356075986b47d877887b26a209b578 100644 (file)
--- 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 {
index 23333e5dd08d7855545452b2e82187b9ac9aa76c..f709ce18b8f0122ccf89120aaed20b4b8bfd8f82 100644 (file)
@@ -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 (file)
index 0000000..bfd3724
--- /dev/null
@@ -0,0 +1,64 @@
+// mailpopbox
+// Copyright 2020 Blue Static <https://www.bluestatic.org>
+// 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))
+       }
+}