...

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

Documentation: github.com/letsencrypt/boulder/mail

     1  package mail
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"fmt"
     8  	"math/big"
     9  	"net"
    10  	"net/mail"
    11  	"net/textproto"
    12  	"os"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/jmhodges/clock"
    18  
    19  	blog "github.com/letsencrypt/boulder/log"
    20  	"github.com/letsencrypt/boulder/metrics"
    21  	"github.com/letsencrypt/boulder/test"
    22  )
    23  
    24  type fakeSource struct{}
    25  
    26  func (f fakeSource) generate() *big.Int {
    27  	return big.NewInt(1991)
    28  }
    29  
    30  func TestGenerateMessage(t *testing.T) {
    31  	fc := clock.NewFake()
    32  	fromAddress, _ := mail.ParseAddress("happy sender <send@email.com>")
    33  	log := blog.UseMock()
    34  	m := New("", "", "", "", nil, *fromAddress, log, metrics.NoopRegisterer, 0, 0)
    35  	m.clk = fc
    36  	m.csprgSource = fakeSource{}
    37  	messageBytes, err := m.generateMessage([]string{"recv@email.com"}, "test subject", "this is the body\n")
    38  	test.AssertNotError(t, err, "Failed to generate email body")
    39  	message := string(messageBytes)
    40  	fields := strings.Split(message, "\r\n")
    41  	test.AssertEquals(t, len(fields), 12)
    42  	fmt.Println(message)
    43  	test.AssertEquals(t, fields[0], "To: \"recv@email.com\"")
    44  	test.AssertEquals(t, fields[1], "From: \"happy sender\" <send@email.com>")
    45  	test.AssertEquals(t, fields[2], "Subject: test subject")
    46  	test.AssertEquals(t, fields[3], "Date: 01 Jan 70 00:00 UTC")
    47  	test.AssertEquals(t, fields[4], "Message-Id: <19700101T000000.1991.send@email.com>")
    48  	test.AssertEquals(t, fields[5], "MIME-Version: 1.0")
    49  	test.AssertEquals(t, fields[6], "Content-Type: text/plain; charset=UTF-8")
    50  	test.AssertEquals(t, fields[7], "Content-Transfer-Encoding: quoted-printable")
    51  	test.AssertEquals(t, fields[8], "")
    52  	test.AssertEquals(t, fields[9], "this is the body")
    53  }
    54  
    55  func TestFailNonASCIIAddress(t *testing.T) {
    56  	log := blog.UseMock()
    57  	fromAddress, _ := mail.ParseAddress("send@email.com")
    58  	m := New("", "", "", "", nil, *fromAddress, log, metrics.NoopRegisterer, 0, 0)
    59  	_, err := m.generateMessage([]string{"遗憾@email.com"}, "test subject", "this is the body\n")
    60  	test.AssertError(t, err, "Allowed a non-ASCII to address incorrectly")
    61  }
    62  
    63  func expect(t *testing.T, buf *bufio.Reader, expected string) error {
    64  	line, _, err := buf.ReadLine()
    65  	if err != nil {
    66  		t.Errorf("readline: %s expected: %s\n", err, expected)
    67  		return err
    68  	}
    69  	if string(line) != expected {
    70  		t.Errorf("Expected %s, got %s", expected, line)
    71  		return fmt.Errorf("Expected %s, got %s", expected, line)
    72  	}
    73  	return nil
    74  }
    75  
    76  type connHandler func(int, *testing.T, net.Conn, *net.TCPConn)
    77  
    78  func listenForever(l *net.TCPListener, t *testing.T, handler connHandler) {
    79  	keyPair, err := tls.LoadX509KeyPair("../test/mail-test-srv/localhost/cert.pem", "../test/mail-test-srv/localhost/key.pem")
    80  	if err != nil {
    81  		t.Errorf("loading keypair: %s", err)
    82  
    83  	}
    84  	tlsConf := &tls.Config{
    85  		Certificates: []tls.Certificate{keyPair},
    86  	}
    87  	connID := 0
    88  	for {
    89  		tcpConn, err := l.AcceptTCP()
    90  		if err != nil {
    91  			return
    92  		}
    93  
    94  		tlsConn := tls.Server(tcpConn, tlsConf)
    95  		connID++
    96  		go handler(connID, t, tlsConn, tcpConn)
    97  	}
    98  }
    99  
   100  func authenticateClient(t *testing.T, conn net.Conn) {
   101  	buf := bufio.NewReader(conn)
   102  	// we can ignore write errors because any
   103  	// failures will be caught on the connecting
   104  	// side
   105  	_, _ = conn.Write([]byte("220 smtp.example.com ESMTP\n"))
   106  	err := expect(t, buf, "EHLO localhost")
   107  	if err != nil {
   108  		return
   109  	}
   110  
   111  	_, _ = conn.Write([]byte("250-PIPELINING\n"))
   112  	_, _ = conn.Write([]byte("250-AUTH PLAIN LOGIN\n"))
   113  	_, _ = conn.Write([]byte("250 8BITMIME\n"))
   114  	// Base64 encoding of "\0user@example.com\0passwd"
   115  	err = expect(t, buf, "AUTH PLAIN AHVzZXJAZXhhbXBsZS5jb20AcGFzc3dk")
   116  	if err != nil {
   117  		return
   118  	}
   119  	_, _ = conn.Write([]byte("235 2.7.0 Authentication successful\n"))
   120  }
   121  
   122  // The normal handler authenticates the client and then disconnects without
   123  // further command processing. It is sufficient for TestConnect()
   124  func normalHandler(connID int, t *testing.T, tlsConn net.Conn, tcpConn *net.TCPConn) {
   125  	defer func() {
   126  		err := tlsConn.Close()
   127  		if err != nil {
   128  			t.Errorf("conn.Close: %s", err)
   129  		}
   130  	}()
   131  	authenticateClient(t, tlsConn)
   132  }
   133  
   134  // The disconnectHandler authenticates the client like the normalHandler but
   135  // additionally processes an email flow (e.g. MAIL, RCPT and DATA commands).
   136  // When the `connID` is <= `closeFirst` the connection is closed immediately
   137  // after the MAIL command is received and prior to issuing a 250 response. If
   138  // a `goodbyeMsg` is provided, it is written to the client immediately before
   139  // closing. In this way the first `closeFirst` connections will not complete
   140  // normally and can be tested for reconnection logic.
   141  func disconnectHandler(closeFirst int, goodbyeMsg string) connHandler {
   142  	return func(connID int, t *testing.T, conn net.Conn, _ *net.TCPConn) {
   143  		defer func() {
   144  			err := conn.Close()
   145  			if err != nil {
   146  				t.Errorf("conn.Close: %s", err)
   147  			}
   148  		}()
   149  		authenticateClient(t, conn)
   150  
   151  		buf := bufio.NewReader(conn)
   152  		err := expect(t, buf, "MAIL FROM:<<you-are-a-winner@example.com>> BODY=8BITMIME")
   153  		if err != nil {
   154  			return
   155  		}
   156  
   157  		if connID <= closeFirst {
   158  			// If there was a `goodbyeMsg` specified, write it to the client before
   159  			// closing the connection. This is a good way to deliver a SMTP error
   160  			// before closing
   161  			if goodbyeMsg != "" {
   162  				_, _ = fmt.Fprintf(conn, "%s\r\n", goodbyeMsg)
   163  				t.Logf("Wrote goodbye msg: %s", goodbyeMsg)
   164  			}
   165  			t.Log("Cutting off client early")
   166  			return
   167  		}
   168  		_, _ = conn.Write([]byte("250 Sure. Go on. \r\n"))
   169  
   170  		err = expect(t, buf, "RCPT TO:<hi@bye.com>")
   171  		if err != nil {
   172  			return
   173  		}
   174  		_, _ = conn.Write([]byte("250 Tell Me More \r\n"))
   175  
   176  		err = expect(t, buf, "DATA")
   177  		if err != nil {
   178  			return
   179  		}
   180  		_, _ = conn.Write([]byte("354 Cool Data\r\n"))
   181  		_, _ = conn.Write([]byte("250 Peace Out\r\n"))
   182  	}
   183  }
   184  
   185  func badEmailHandler(messagesToProcess int) connHandler {
   186  	return func(_ int, t *testing.T, conn net.Conn, _ *net.TCPConn) {
   187  		defer func() {
   188  			err := conn.Close()
   189  			if err != nil {
   190  				t.Errorf("conn.Close: %s", err)
   191  			}
   192  		}()
   193  		authenticateClient(t, conn)
   194  
   195  		buf := bufio.NewReader(conn)
   196  		err := expect(t, buf, "MAIL FROM:<<you-are-a-winner@example.com>> BODY=8BITMIME")
   197  		if err != nil {
   198  			return
   199  		}
   200  
   201  		_, _ = conn.Write([]byte("250 Sure. Go on. \r\n"))
   202  
   203  		err = expect(t, buf, "RCPT TO:<hi@bye.com>")
   204  		if err != nil {
   205  			return
   206  		}
   207  		_, _ = conn.Write([]byte("401 4.1.3 Bad recipient address syntax\r\n"))
   208  		err = expect(t, buf, "RSET")
   209  		if err != nil {
   210  			return
   211  		}
   212  		_, _ = conn.Write([]byte("250 Ok yr rset now\r\n"))
   213  	}
   214  }
   215  
   216  // The rstHandler authenticates the client like the normalHandler but
   217  // additionally processes an email flow (e.g. MAIL, RCPT and DATA
   218  // commands). When the `connID` is <= `rstFirst` the socket of the
   219  // listening connection is set to abruptively close (sends TCP RST but
   220  // no FIN). The listening connection is closed immediately after the
   221  // MAIL command is received and prior to issuing a 250 response. In this
   222  // way the first `rstFirst` connections will not complete normally and
   223  // can be tested for reconnection logic.
   224  func rstHandler(rstFirst int) connHandler {
   225  	return func(connID int, t *testing.T, tlsConn net.Conn, tcpConn *net.TCPConn) {
   226  		defer func() {
   227  			err := tcpConn.Close()
   228  			if err != nil {
   229  				t.Errorf("conn.Close: %s", err)
   230  			}
   231  		}()
   232  		authenticateClient(t, tlsConn)
   233  
   234  		buf := bufio.NewReader(tlsConn)
   235  		err := expect(t, buf, "MAIL FROM:<<you-are-a-winner@example.com>> BODY=8BITMIME")
   236  		if err != nil {
   237  			return
   238  		}
   239  		// Set the socket of the listening connection to abruptively
   240  		// close.
   241  		if connID <= rstFirst {
   242  			err := tcpConn.SetLinger(0)
   243  			if err != nil {
   244  				t.Error(err)
   245  				return
   246  			}
   247  			t.Log("Socket set for abruptive close. Cutting off client early")
   248  			return
   249  		}
   250  		_, _ = tlsConn.Write([]byte("250 Sure. Go on. \r\n"))
   251  
   252  		err = expect(t, buf, "RCPT TO:<hi@bye.com>")
   253  		if err != nil {
   254  			return
   255  		}
   256  		_, _ = tlsConn.Write([]byte("250 Tell Me More \r\n"))
   257  
   258  		err = expect(t, buf, "DATA")
   259  		if err != nil {
   260  			return
   261  		}
   262  		_, _ = tlsConn.Write([]byte("354 Cool Data\r\n"))
   263  		_, _ = tlsConn.Write([]byte("250 Peace Out\r\n"))
   264  	}
   265  }
   266  
   267  func setup(t *testing.T) (*mailerImpl, *net.TCPListener, func()) {
   268  	fromAddress, _ := mail.ParseAddress("you-are-a-winner@example.com")
   269  	log := blog.UseMock()
   270  
   271  	// Listen on port 0 to get any free available port
   272  	tcpAddr, err := net.ResolveTCPAddr("tcp", ":0")
   273  	if err != nil {
   274  		t.Fatalf("resolving tcp addr: %s", err)
   275  	}
   276  	tcpl, err := net.ListenTCP("tcp", tcpAddr)
   277  	if err != nil {
   278  		t.Fatalf("listen: %s", err)
   279  	}
   280  
   281  	cleanUp := func() {
   282  		err := tcpl.Close()
   283  		if err != nil {
   284  			t.Errorf("listen.Close: %s", err)
   285  		}
   286  	}
   287  
   288  	pem, err := os.ReadFile("../test/mail-test-srv/minica.pem")
   289  	if err != nil {
   290  		t.Fatalf("loading smtp root: %s", err)
   291  	}
   292  	smtpRoots := x509.NewCertPool()
   293  	ok := smtpRoots.AppendCertsFromPEM(pem)
   294  	if !ok {
   295  		t.Fatal("failed parsing SMTP root")
   296  	}
   297  
   298  	// We can look at the listener Addr() to figure out which free port was
   299  	// assigned by the operating system
   300  
   301  	_, port, err := net.SplitHostPort(tcpl.Addr().String())
   302  	if err != nil {
   303  		t.Fatal("failed parsing port from tcp listen")
   304  	}
   305  
   306  	m := New(
   307  		"localhost",
   308  		port,
   309  		"user@example.com",
   310  		"passwd",
   311  		smtpRoots,
   312  		*fromAddress,
   313  		log,
   314  		metrics.NoopRegisterer,
   315  		time.Second*2, time.Second*10)
   316  
   317  	return m, tcpl, cleanUp
   318  }
   319  
   320  func TestConnect(t *testing.T) {
   321  	m, l, cleanUp := setup(t)
   322  	defer cleanUp()
   323  
   324  	go listenForever(l, t, normalHandler)
   325  	conn, err := m.Connect()
   326  	if err != nil {
   327  		t.Errorf("Failed to connect: %s", err)
   328  	}
   329  	err = conn.Close()
   330  	if err != nil {
   331  		t.Errorf("Failed to clean up: %s", err)
   332  	}
   333  }
   334  
   335  func TestReconnectSuccess(t *testing.T) {
   336  	m, l, cleanUp := setup(t)
   337  	defer cleanUp()
   338  	const closedConns = 5
   339  
   340  	// Configure a test server that will disconnect the first `closedConns`
   341  	// connections after the MAIL cmd
   342  	go listenForever(l, t, disconnectHandler(closedConns, ""))
   343  
   344  	// With a mailer client that has a max attempt > `closedConns` we expect no
   345  	// error. The message should be delivered after `closedConns` reconnect
   346  	// attempts.
   347  	conn, err := m.Connect()
   348  	if err != nil {
   349  		t.Errorf("Failed to connect: %s", err)
   350  	}
   351  	err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
   352  	if err != nil {
   353  		t.Errorf("Expected SendMail() to not fail. Got err: %s", err)
   354  	}
   355  }
   356  
   357  func TestBadEmailError(t *testing.T) {
   358  	m, l, cleanUp := setup(t)
   359  	defer cleanUp()
   360  	const messages = 3
   361  
   362  	go listenForever(l, t, badEmailHandler(messages))
   363  
   364  	conn, err := m.Connect()
   365  	if err != nil {
   366  		t.Errorf("Failed to connect: %s", err)
   367  	}
   368  
   369  	err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
   370  	// We expect there to be an error
   371  	if err == nil {
   372  		t.Errorf("Expected SendMail() to return an BadAddressSMTPError, got nil")
   373  	}
   374  	expected := "401: 4.1.3 Bad recipient address syntax"
   375  	var badAddrErr BadAddressSMTPError
   376  	test.AssertErrorWraps(t, err, &badAddrErr)
   377  	test.AssertEquals(t, badAddrErr.Message, expected)
   378  }
   379  
   380  func TestReconnectSMTP421(t *testing.T) {
   381  	m, l, cleanUp := setup(t)
   382  	defer cleanUp()
   383  	const closedConns = 5
   384  
   385  	// A SMTP 421 can be generated when the server times out an idle connection.
   386  	// For more information see https://github.com/letsencrypt/boulder/issues/2249
   387  	smtp421 := "421 1.2.3 green.eggs.and.spam Error: timeout exceeded"
   388  
   389  	// Configure a test server that will disconnect the first `closedConns`
   390  	// connections after the MAIL cmd with a SMTP 421 error
   391  	go listenForever(l, t, disconnectHandler(closedConns, smtp421))
   392  
   393  	// With a mailer client that has a max attempt > `closedConns` we expect no
   394  	// error. The message should be delivered after `closedConns` reconnect
   395  	// attempts.
   396  	conn, err := m.Connect()
   397  	if err != nil {
   398  		t.Errorf("Failed to connect: %s", err)
   399  	}
   400  	err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
   401  	if err != nil {
   402  		t.Errorf("Expected SendMail() to not fail. Got err: %s", err)
   403  	}
   404  }
   405  
   406  func TestOtherError(t *testing.T) {
   407  	m, l, cleanUp := setup(t)
   408  	defer cleanUp()
   409  
   410  	go listenForever(l, t, func(_ int, t *testing.T, conn net.Conn, _ *net.TCPConn) {
   411  		defer func() {
   412  			err := conn.Close()
   413  			if err != nil {
   414  				t.Errorf("conn.Close: %s", err)
   415  			}
   416  		}()
   417  		authenticateClient(t, conn)
   418  
   419  		buf := bufio.NewReader(conn)
   420  		err := expect(t, buf, "MAIL FROM:<<you-are-a-winner@example.com>> BODY=8BITMIME")
   421  		if err != nil {
   422  			return
   423  		}
   424  
   425  		_, _ = conn.Write([]byte("250 Sure. Go on. \r\n"))
   426  
   427  		err = expect(t, buf, "RCPT TO:<hi@bye.com>")
   428  		if err != nil {
   429  			return
   430  		}
   431  
   432  		_, _ = conn.Write([]byte("999 1.1.1 This would probably be bad?\r\n"))
   433  
   434  		err = expect(t, buf, "RSET")
   435  		if err != nil {
   436  			return
   437  		}
   438  
   439  		_, _ = conn.Write([]byte("250 Ok yr rset now\r\n"))
   440  	})
   441  
   442  	conn, err := m.Connect()
   443  	if err != nil {
   444  		t.Errorf("Failed to connect: %s", err)
   445  	}
   446  
   447  	err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
   448  	// We expect there to be an error
   449  	if err == nil {
   450  		t.Errorf("Expected SendMail() to return an error, got nil")
   451  	}
   452  	expected := "999 1.1.1 This would probably be bad?"
   453  	var rcptErr *textproto.Error
   454  	test.AssertErrorWraps(t, err, &rcptErr)
   455  	test.AssertEquals(t, rcptErr.Error(), expected)
   456  
   457  	m, l, cleanUp = setup(t)
   458  	defer cleanUp()
   459  
   460  	go listenForever(l, t, func(_ int, t *testing.T, conn net.Conn, _ *net.TCPConn) {
   461  		defer func() {
   462  			err := conn.Close()
   463  			if err != nil {
   464  				t.Errorf("conn.Close: %s", err)
   465  			}
   466  		}()
   467  		authenticateClient(t, conn)
   468  
   469  		buf := bufio.NewReader(conn)
   470  		err := expect(t, buf, "MAIL FROM:<<you-are-a-winner@example.com>> BODY=8BITMIME")
   471  		if err != nil {
   472  			return
   473  		}
   474  
   475  		_, _ = conn.Write([]byte("250 Sure. Go on. \r\n"))
   476  
   477  		err = expect(t, buf, "RCPT TO:<hi@bye.com>")
   478  		if err != nil {
   479  			return
   480  		}
   481  
   482  		_, _ = conn.Write([]byte("999 1.1.1 This would probably be bad?\r\n"))
   483  
   484  		err = expect(t, buf, "RSET")
   485  		if err != nil {
   486  			return
   487  		}
   488  
   489  		_, _ = conn.Write([]byte("nop\r\n"))
   490  	})
   491  	conn, err = m.Connect()
   492  	if err != nil {
   493  		t.Errorf("Failed to connect: %s", err)
   494  	}
   495  
   496  	err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
   497  	// We expect there to be an error
   498  	test.AssertError(t, err, "SendMail didn't fail as expected")
   499  	test.AssertEquals(t, err.Error(), "999 1.1.1 This would probably be bad? (also, on sending RSET: short response: nop)")
   500  }
   501  
   502  func TestReconnectAfterRST(t *testing.T) {
   503  	m, l, cleanUp := setup(t)
   504  	defer cleanUp()
   505  	const rstConns = 5
   506  
   507  	// Configure a test server that will RST and disconnect the first
   508  	// `closedConns` connections
   509  	go listenForever(l, t, rstHandler(rstConns))
   510  
   511  	// With a mailer client that has a max attempt > `closedConns` we expect no
   512  	// error. The message should be delivered after `closedConns` reconnect
   513  	// attempts.
   514  	conn, err := m.Connect()
   515  	if err != nil {
   516  		t.Errorf("Failed to connect: %s", err)
   517  	}
   518  	err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
   519  	if err != nil {
   520  		t.Errorf("Expected SendMail() to not fail. Got err: %s", err)
   521  	}
   522  }
   523  

View as plain text