1 package mail
2
3 import (
4 "bytes"
5 "crypto/rand"
6 "crypto/tls"
7 "crypto/x509"
8 "errors"
9 "fmt"
10 "io"
11 "math"
12 "math/big"
13 "mime/quotedprintable"
14 "net"
15 "net/mail"
16 "net/smtp"
17 "net/textproto"
18 "strconv"
19 "strings"
20 "syscall"
21 "time"
22
23 "github.com/jmhodges/clock"
24 "github.com/prometheus/client_golang/prometheus"
25
26 "github.com/letsencrypt/boulder/core"
27 blog "github.com/letsencrypt/boulder/log"
28 )
29
30 type idGenerator interface {
31 generate() *big.Int
32 }
33
34 var maxBigInt = big.NewInt(math.MaxInt64)
35
36 type realSource struct{}
37
38 func (s realSource) generate() *big.Int {
39 randInt, err := rand.Int(rand.Reader, maxBigInt)
40 if err != nil {
41 panic(err)
42 }
43 return randInt
44 }
45
46
47
48 type Mailer interface {
49 Connect() (Conn, error)
50 }
51
52
53
54
55 type Conn interface {
56 SendMail([]string, string, string) error
57 Close() error
58 }
59
60
61
62 type connImpl struct {
63 config
64 client smtpClient
65 }
66
67
68
69 type mailerImpl struct {
70 config
71 }
72
73 type config struct {
74 log blog.Logger
75 dialer dialer
76 from mail.Address
77 clk clock.Clock
78 csprgSource idGenerator
79 reconnectBase time.Duration
80 reconnectMax time.Duration
81 sendMailAttempts *prometheus.CounterVec
82 }
83
84 type dialer interface {
85 Dial() (smtpClient, error)
86 }
87
88 type smtpClient interface {
89 Mail(string) error
90 Rcpt(string) error
91 Data() (io.WriteCloser, error)
92 Reset() error
93 Close() error
94 }
95
96 type dryRunClient struct {
97 log blog.Logger
98 }
99
100 func (d dryRunClient) Dial() (smtpClient, error) {
101 return d, nil
102 }
103
104 func (d dryRunClient) Mail(from string) error {
105 d.log.Debugf("MAIL FROM:<%s>", from)
106 return nil
107 }
108
109 func (d dryRunClient) Rcpt(to string) error {
110 d.log.Debugf("RCPT TO:<%s>", to)
111 return nil
112 }
113
114 func (d dryRunClient) Close() error {
115 return nil
116 }
117
118 func (d dryRunClient) Data() (io.WriteCloser, error) {
119 return d, nil
120 }
121
122 func (d dryRunClient) Write(p []byte) (n int, err error) {
123 for _, line := range strings.Split(string(p), "\n") {
124 d.log.Debugf("data: %s", line)
125 }
126 return len(p), nil
127 }
128
129 func (d dryRunClient) Reset() (err error) {
130 d.log.Debugf("RESET")
131 return nil
132 }
133
134
135
136 func New(
137 server,
138 port,
139 username,
140 password string,
141 rootCAs *x509.CertPool,
142 from mail.Address,
143 logger blog.Logger,
144 stats prometheus.Registerer,
145 reconnectBase time.Duration,
146 reconnectMax time.Duration) *mailerImpl {
147
148 sendMailAttempts := prometheus.NewCounterVec(prometheus.CounterOpts{
149 Name: "send_mail_attempts",
150 Help: "A counter of send mail attempts labelled by result",
151 }, []string{"result", "error"})
152 stats.MustRegister(sendMailAttempts)
153
154 return &mailerImpl{
155 config: config{
156 dialer: &dialerImpl{
157 username: username,
158 password: password,
159 server: server,
160 port: port,
161 rootCAs: rootCAs,
162 },
163 log: logger,
164 from: from,
165 clk: clock.New(),
166 csprgSource: realSource{},
167 reconnectBase: reconnectBase,
168 reconnectMax: reconnectMax,
169 sendMailAttempts: sendMailAttempts,
170 },
171 }
172 }
173
174
175
176 func NewDryRun(from mail.Address, logger blog.Logger) *mailerImpl {
177 return &mailerImpl{
178 config: config{
179 dialer: dryRunClient{logger},
180 from: from,
181 clk: clock.New(),
182 csprgSource: realSource{},
183 sendMailAttempts: prometheus.NewCounterVec(prometheus.CounterOpts{
184 Name: "send_mail_attempts",
185 Help: "A counter of send mail attempts labelled by result",
186 }, []string{"result", "error"}),
187 },
188 }
189 }
190
191 func (c config) generateMessage(to []string, subject, body string) ([]byte, error) {
192 mid := c.csprgSource.generate()
193 now := c.clk.Now().UTC()
194 addrs := []string{}
195 for _, a := range to {
196 if !core.IsASCII(a) {
197 return nil, fmt.Errorf("Non-ASCII email address")
198 }
199 addrs = append(addrs, strconv.Quote(a))
200 }
201 headers := []string{
202 fmt.Sprintf("To: %s", strings.Join(addrs, ", ")),
203 fmt.Sprintf("From: %s", c.from.String()),
204 fmt.Sprintf("Subject: %s", subject),
205 fmt.Sprintf("Date: %s", now.Format(time.RFC822)),
206 fmt.Sprintf("Message-Id: <%s.%s.%s>", now.Format("20060102T150405"), mid.String(), c.from.Address),
207 "MIME-Version: 1.0",
208 "Content-Type: text/plain; charset=UTF-8",
209 "Content-Transfer-Encoding: quoted-printable",
210 }
211 for i := range headers[1:] {
212
213 headers[i] = strings.Replace(headers[i], "\n", "", -1)
214 }
215 bodyBuf := new(bytes.Buffer)
216 mimeWriter := quotedprintable.NewWriter(bodyBuf)
217 _, err := mimeWriter.Write([]byte(body))
218 if err != nil {
219 return nil, err
220 }
221 err = mimeWriter.Close()
222 if err != nil {
223 return nil, err
224 }
225 return []byte(fmt.Sprintf(
226 "%s\r\n\r\n%s\r\n",
227 strings.Join(headers, "\r\n"),
228 bodyBuf.String(),
229 )), nil
230 }
231
232 func (c *connImpl) reconnect() {
233 for i := 0; ; i++ {
234 sleepDuration := core.RetryBackoff(i, c.reconnectBase, c.reconnectMax, 2)
235 c.log.Infof("sleeping for %s before reconnecting mailer", sleepDuration)
236 c.clk.Sleep(sleepDuration)
237 c.log.Info("attempting to reconnect mailer")
238 client, err := c.dialer.Dial()
239 if err != nil {
240 c.log.Warningf("reconnect error: %s", err)
241 continue
242 }
243 c.client = client
244 break
245 }
246 c.log.Info("reconnected successfully")
247 }
248
249
250
251 func (m *mailerImpl) Connect() (Conn, error) {
252 client, err := m.dialer.Dial()
253 if err != nil {
254 return nil, err
255 }
256 return &connImpl{m.config, client}, nil
257 }
258
259 type dialerImpl struct {
260 username, password, server, port string
261 rootCAs *x509.CertPool
262 }
263
264 func (di *dialerImpl) Dial() (smtpClient, error) {
265 hostport := net.JoinHostPort(di.server, di.port)
266 var conn net.Conn
267 var err error
268 conn, err = tls.Dial("tcp", hostport, &tls.Config{
269 RootCAs: di.rootCAs,
270 })
271 if err != nil {
272 return nil, err
273 }
274 client, err := smtp.NewClient(conn, di.server)
275 if err != nil {
276 return nil, err
277 }
278 auth := smtp.PlainAuth("", di.username, di.password, di.server)
279 if err = client.Auth(auth); err != nil {
280 return nil, err
281 }
282 return client, nil
283 }
284
285
286
287
288
289 func (c *connImpl) resetAndError(err error) error {
290 if err == io.EOF {
291 return err
292 }
293 if err2 := c.client.Reset(); err2 != nil {
294 return fmt.Errorf("%s (also, on sending RSET: %s)", err, err2)
295 }
296 return err
297 }
298
299 func (c *connImpl) sendOne(to []string, subject, msg string) error {
300 if c.client == nil {
301 return errors.New("call Connect before SendMail")
302 }
303 body, err := c.generateMessage(to, subject, msg)
304 if err != nil {
305 return err
306 }
307 if err = c.client.Mail(c.from.String()); err != nil {
308 return err
309 }
310 for _, t := range to {
311 if err = c.client.Rcpt(t); err != nil {
312 return c.resetAndError(err)
313 }
314 }
315 w, err := c.client.Data()
316 if err != nil {
317 return c.resetAndError(err)
318 }
319 _, err = w.Write(body)
320 if err != nil {
321 return c.resetAndError(err)
322 }
323 err = w.Close()
324 if err != nil {
325 return c.resetAndError(err)
326 }
327 return nil
328 }
329
330
331
332
333
334 type BadAddressSMTPError struct {
335 Message string
336 }
337
338 func (e BadAddressSMTPError) Error() string {
339 return e.Message
340 }
341
342
343
344
345 var badAddressErrorCodes = map[int]bool{
346 401: true,
347 422: true,
348 441: true,
349 450: true,
350 501: true,
351 510: true,
352 511: true,
353 513: true,
354 541: true,
355 550: true,
356 553: true,
357 }
358
359
360
361 func (c *connImpl) SendMail(to []string, subject, msg string) error {
362 var protoErr *textproto.Error
363 for {
364 err := c.sendOne(to, subject, msg)
365 if err == nil {
366
367 break
368 } else if err == io.EOF {
369 c.sendMailAttempts.WithLabelValues("failure", "EOF").Inc()
370
371
372 c.reconnect()
373
374 continue
375 } else if errors.Is(err, syscall.ECONNRESET) {
376 c.sendMailAttempts.WithLabelValues("failure", "TCP RST").Inc()
377
378
379 c.reconnect()
380
381 continue
382 } else if errors.Is(err, syscall.EPIPE) {
383
384 c.sendMailAttempts.WithLabelValues("failure", "EPIPE").Inc()
385 c.reconnect()
386 continue
387 } else if errors.As(err, &protoErr) && protoErr.Code == 421 {
388 c.sendMailAttempts.WithLabelValues("failure", "SMTP 421").Inc()
389
404 c.reconnect()
405
406 continue
407 } else if errors.As(err, &protoErr) && badAddressErrorCodes[protoErr.Code] {
408 c.sendMailAttempts.WithLabelValues("failure", fmt.Sprintf("SMTP %d", protoErr.Code)).Inc()
409 return BadAddressSMTPError{fmt.Sprintf("%d: %s", protoErr.Code, protoErr.Msg)}
410 } else {
411
412
413 c.sendMailAttempts.WithLabelValues("failure", "unexpected").Inc()
414 return err
415 }
416 }
417
418 c.sendMailAttempts.WithLabelValues("success", "").Inc()
419 return nil
420 }
421
422
423 func (c *connImpl) Close() error {
424 err := c.client.Close()
425 if err != nil {
426 return err
427 }
428 c.client = nil
429 return nil
430 }
431
View as plain text