2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
23 func RelayMessage(server Server, env Envelope, log *zap.Logger) {
24 for _, rcptTo := range env.RcptTo {
25 sendLog := log.With(zap.String("address", rcptTo.Address))
27 domain := DomainForAddress(rcptTo)
28 mx, err := net.LookupMX(domain)
29 if err != nil || len(mx) < 1 {
30 deliverRelayFailure(server, env, log, rcptTo.Address, "failed to lookup MX records", err)
33 relayMessageToHost(server, env, sendLog, rcptTo.Address, mx[0].Host, "25")
37 func relayMessageToHost(server Server, env Envelope, log *zap.Logger, to, host, port string) {
38 from := env.MailFrom.Address
39 hostPort := net.JoinHostPort(host, port)
40 log = log.With(zap.String("host", hostPort))
42 c, err := smtp.Dial(hostPort)
44 // TODO - retry, or look at other MX records
45 deliverRelayFailure(server, env, log, to, "failed to dial host", err)
50 if err = c.Hello(server.Name()); err != nil {
51 deliverRelayFailure(server, env, log, to, "failed to HELO", err)
55 if hasTls, _ := c.Extension("STARTTLS"); hasTls {
56 config := &tls.Config{ServerName: host}
57 if err = c.StartTLS(config); err != nil {
58 deliverRelayFailure(server, env, log, to, "failed to STARTTLS", err)
63 if err = c.Mail(from); err != nil {
64 deliverRelayFailure(server, env, log, to, "failed MAIL FROM", err)
68 if err = c.Rcpt(to); err != nil {
69 deliverRelayFailure(server, env, log, to, "failed to RCPT TO", err)
75 deliverRelayFailure(server, env, log, to, "failed to DATA", err)
79 _, err = wc.Write(env.Data)
82 deliverRelayFailure(server, env, log, to, "failed to write DATA", err)
86 if err = wc.Close(); err != nil {
87 deliverRelayFailure(server, env, log, to, "failed to close DATA", err)
92 // deliverRelayFailure logs and generates a delivery status notification. It
93 // writes to |log| the |errorStr| and |sendErr|, as well as preparing a new
94 // message, based of |env|, delivered to |server| that reports error
95 // information about the attempted delivery.
96 func deliverRelayFailure(server Server, env Envelope, log *zap.Logger, to, errorStr string, sendErr error) {
97 log.Error(errorStr, zap.Error(sendErr))
99 buf := &bytes.Buffer{}
100 mw := multipart.NewWriter(buf)
105 MailFrom: mail.Address{"mailpopbox", "mailbox@" + DomainForAddress(env.MailFrom)},
106 RcptTo: []mail.Address{env.MailFrom},
107 ID: generateEnvelopeId("f", now),
111 fmt.Fprintf(buf, "From: %s\n", failure.MailFrom.String())
112 fmt.Fprintf(buf, "To: %s\n", failure.RcptTo[0].String())
113 fmt.Fprintf(buf, "Subject: Delivery Status Notification (Failure)\n")
114 fmt.Fprintf(buf, "X-Failed-Recipients: %s\n", to)
115 fmt.Fprintf(buf, "Message-ID: %s\n", failure.ID)
116 fmt.Fprintf(buf, "Date: %s\n", now.Format(time.RFC1123Z))
117 fmt.Fprintf(buf, "Content-Type: multipart/report; boundary=%s; report-type=delivery-status\n\n", mw.Boundary())
119 tw, err := mw.CreatePart(textproto.MIMEHeader{
120 "Content-Type": []string{"text/plain; charset=UTF-8"},
123 log.Error("failed to create multipart 0", zap.Error(err))
126 fmt.Fprintf(tw, "* * * Delivery Failure * * *\n\n")
127 fmt.Fprintf(tw, "The server failed to relay the message:\n\n%s:\n%s\n", errorStr, sendErr.Error())
129 sw, err := mw.CreatePart(textproto.MIMEHeader{
130 "Content-Type": []string{"message/delivery-status"},
133 log.Error("failed to create multipart 1", zap.Error(err))
136 fmt.Fprintf(sw, "Original-Envelope-ID: %s\n", env.ID)
137 fmt.Fprintf(sw, "Reporting-UA: %s\n", env.EHLO)
138 if env.RemoteAddr != nil {
139 fmt.Fprintf(sw, "Reporting-MTA: dns; %s\n", lookupRemoteHost(env.RemoteAddr))
141 fmt.Fprintf(sw, "Date: %s\n", env.Received.Format(time.RFC1123Z))
143 ocw, err := mw.CreatePart(textproto.MIMEHeader{
144 "Content-Type": []string{"message/rfc822"},
147 log.Error("failed to create multipart 2", zap.Error(err))
155 failure.Data = buf.Bytes()
156 server.OnMessageDelivered(failure)