Fix TLS connection in relayMessageToHost.
[mailpopbox.git] / smtp / relay.go
1 // mailpopbox
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
6
7 package smtp
8
9 import (
10 "bytes"
11 "crypto/tls"
12 "fmt"
13 "mime/multipart"
14 "net"
15 "net/mail"
16 "net/smtp"
17 "net/textproto"
18 "time"
19
20 "go.uber.org/zap"
21 )
22
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))
26
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)
31 return
32 }
33 relayMessageToHost(server, env, sendLog, rcptTo.Address, mx[0].Host, "25")
34 }
35 }
36
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))
41
42 c, err := smtp.Dial(hostPort)
43 if err != nil {
44 // TODO - retry, or look at other MX records
45 deliverRelayFailure(server, env, log, to, "failed to dial host", err)
46 return
47 }
48 defer c.Quit()
49
50 if err = c.Hello(server.Name()); err != nil {
51 deliverRelayFailure(server, env, log, to, "failed to HELO", err)
52 return
53 }
54
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)
59 return
60 }
61 }
62
63 if err = c.Mail(from); err != nil {
64 deliverRelayFailure(server, env, log, to, "failed MAIL FROM", err)
65 return
66 }
67
68 if err = c.Rcpt(to); err != nil {
69 deliverRelayFailure(server, env, log, to, "failed to RCPT TO", err)
70 return
71 }
72
73 wc, err := c.Data()
74 if err != nil {
75 deliverRelayFailure(server, env, log, to, "failed to DATA", err)
76 return
77 }
78
79 _, err = wc.Write(env.Data)
80 if err != nil {
81 wc.Close()
82 deliverRelayFailure(server, env, log, to, "failed to write DATA", err)
83 return
84 }
85
86 if err = wc.Close(); err != nil {
87 deliverRelayFailure(server, env, log, to, "failed to close DATA", err)
88 return
89 }
90 }
91
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))
98
99 buf := &bytes.Buffer{}
100 mw := multipart.NewWriter(buf)
101
102 now := time.Now()
103
104 failure := Envelope{
105 MailFrom: mail.Address{"mailpopbox", "mailbox@" + DomainForAddress(env.MailFrom)},
106 RcptTo: []mail.Address{env.MailFrom},
107 ID: generateEnvelopeId("f", now),
108 Received: now,
109 }
110
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())
118
119 tw, err := mw.CreatePart(textproto.MIMEHeader{
120 "Content-Type": []string{"text/plain; charset=UTF-8"},
121 })
122 if err != nil {
123 log.Error("failed to create multipart 0", zap.Error(err))
124 return
125 }
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())
128
129 sw, err := mw.CreatePart(textproto.MIMEHeader{
130 "Content-Type": []string{"message/delivery-status"},
131 })
132 if err != nil {
133 log.Error("failed to create multipart 1", zap.Error(err))
134 return
135 }
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))
140 }
141 fmt.Fprintf(sw, "Date: %s\n", env.Received.Format(time.RFC1123Z))
142
143 ocw, err := mw.CreatePart(textproto.MIMEHeader{
144 "Content-Type": []string{"message/rfc822"},
145 })
146 if err != nil {
147 log.Error("failed to create multipart 2", zap.Error(err))
148 return
149 }
150
151 ocw.Write(env.Data)
152
153 mw.Close()
154
155 failure.Data = buf.Bytes()
156 server.OnMessageDelivered(failure)
157 }