...

Source file src/github.com/letsencrypt/boulder/mail/mailer.go

Documentation: github.com/letsencrypt/boulder/mail

     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  // Mailer is an interface that allows creating Conns. Implementations must
    47  // be safe for concurrent use.
    48  type Mailer interface {
    49  	Connect() (Conn, error)
    50  }
    51  
    52  // Conn is an interface that allows sending mail. When you are done with a
    53  // Conn, call Close(). Implementations are not required to be safe for
    54  // concurrent use.
    55  type Conn interface {
    56  	SendMail([]string, string, string) error
    57  	Close() error
    58  }
    59  
    60  // connImpl represents a single connection to a mail server. It is not safe
    61  // for concurrent use.
    62  type connImpl struct {
    63  	config
    64  	client smtpClient
    65  }
    66  
    67  // mailerImpl defines a mail transfer agent to use for sending mail. It is
    68  // safe for concurrent us.
    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  // New constructs a Mailer to represent an account on a particular mail
   135  // transfer agent.
   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  // NewDryRun constructs a Mailer suitable for doing a dry run. It simply logs
   175  // each command that would have been run, at debug level.
   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  		// strip LFs
   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  // Connect opens a connection to the specified mail server. It must be called
   250  // before SendMail.
   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  // resetAndError resets the current mail transaction and then returns its
   286  // argument as an error. If the reset command also errors, it combines both
   287  // errors and returns them. Without this we would get `nested MAIL command`.
   288  // https://github.com/letsencrypt/boulder/issues/3191
   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  // BadAddressSMTPError is returned by SendMail when the server rejects a message
   331  // but for a reason that doesn't prevent us from continuing to send mail. The
   332  // error message contains the error code and the error message returned from the
   333  // server.
   334  type BadAddressSMTPError struct {
   335  	Message string
   336  }
   337  
   338  func (e BadAddressSMTPError) Error() string {
   339  	return e.Message
   340  }
   341  
   342  // Based on reading of various SMTP documents these are a handful
   343  // of errors we are likely to be able to continue sending mail after
   344  // receiving. The majority of these errors boil down to 'bad address'.
   345  var badAddressErrorCodes = map[int]bool{
   346  	401: true, // Invalid recipient
   347  	422: true, // Recipient mailbox is full
   348  	441: true, // Recipient server is not responding
   349  	450: true, // User's mailbox is not available
   350  	501: true, // Bad recipient address syntax
   351  	510: true, // Invalid recipient
   352  	511: true, // Invalid recipient
   353  	513: true, // Address type invalid
   354  	541: true, // Recipient rejected message
   355  	550: true, // Non-existent address
   356  	553: true, // Non-existent address
   357  }
   358  
   359  // SendMail sends an email to the provided list of recipients. The email body
   360  // is simple text.
   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  			// If the error is nil, we sent the mail without issue. nice!
   367  			break
   368  		} else if err == io.EOF {
   369  			c.sendMailAttempts.WithLabelValues("failure", "EOF").Inc()
   370  			// If the error is an EOF, we should try to reconnect on a backoff
   371  			// schedule, sleeping between attempts.
   372  			c.reconnect()
   373  			// After reconnecting, loop around and try `sendOne` again.
   374  			continue
   375  		} else if errors.Is(err, syscall.ECONNRESET) {
   376  			c.sendMailAttempts.WithLabelValues("failure", "TCP RST").Inc()
   377  			// If the error is `syscall.ECONNRESET`, we should try to reconnect on a backoff
   378  			// schedule, sleeping between attempts.
   379  			c.reconnect()
   380  			// After reconnecting, loop around and try `sendOne` again.
   381  			continue
   382  		} else if errors.Is(err, syscall.EPIPE) {
   383  			// EPIPE also seems to be a common way to signal TCP RST.
   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  			/*
   390  			 *  If the error is an instance of `textproto.Error` with a SMTP error code,
   391  			 *  and that error code is 421 then treat this as a reconnect-able event.
   392  			 *
   393  			 *  The SMTP RFC defines this error code as:
   394  			 *   421 <domain> Service not available, closing transmission channel
   395  			 *   (This may be a reply to any command if the service knows it
   396  			 *   must shut down)
   397  			 *
   398  			 * In practice we see this code being used by our production SMTP server
   399  			 * when the connection has gone idle for too long. For more information
   400  			 * see issue #2249[0].
   401  			 *
   402  			 * [0] - https://github.com/letsencrypt/boulder/issues/2249
   403  			 */
   404  			c.reconnect()
   405  			// After reconnecting, loop around and try `sendOne` again.
   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  			// If it wasn't an EOF error or a recoverable SMTP error it is unexpected and we
   412  			// return from SendMail() with the error
   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  // Close closes the connection.
   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