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) {
--- /dev/null
+// 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))
+ }
+}