Source file src/net/smtp/smtp_test.go

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package smtp
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"fmt"
    13  	"internal/testenv"
    14  	"io"
    15  	"net"
    16  	"net/textproto"
    17  	"runtime"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  )
    22  
    23  type authTest struct {
    24  	auth       Auth
    25  	challenges []string
    26  	name       string
    27  	responses  []string
    28  }
    29  
    30  var authTests = []authTest{
    31  	{PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}},
    32  	{PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}},
    33  	{CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}},
    34  }
    35  
    36  func TestAuth(t *testing.T) {
    37  testLoop:
    38  	for i, test := range authTests {
    39  		name, resp, err := test.auth.Start(&ServerInfo{"testserver", true, nil})
    40  		if name != test.name {
    41  			t.Errorf("#%d got name %s, expected %s", i, name, test.name)
    42  		}
    43  		if !bytes.Equal(resp, []byte(test.responses[0])) {
    44  			t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0])
    45  		}
    46  		if err != nil {
    47  			t.Errorf("#%d error: %s", i, err)
    48  		}
    49  		for j := range test.challenges {
    50  			challenge := []byte(test.challenges[j])
    51  			expected := []byte(test.responses[j+1])
    52  			resp, err := test.auth.Next(challenge, true)
    53  			if err != nil {
    54  				t.Errorf("#%d error: %s", i, err)
    55  				continue testLoop
    56  			}
    57  			if !bytes.Equal(resp, expected) {
    58  				t.Errorf("#%d got %s, expected %s", i, resp, expected)
    59  				continue testLoop
    60  			}
    61  		}
    62  	}
    63  }
    64  
    65  func TestAuthPlain(t *testing.T) {
    66  
    67  	tests := []struct {
    68  		authName string
    69  		server   *ServerInfo
    70  		err      string
    71  	}{
    72  		{
    73  			authName: "servername",
    74  			server:   &ServerInfo{Name: "servername", TLS: true},
    75  		},
    76  		{
    77  			// OK to use PlainAuth on localhost without TLS
    78  			authName: "localhost",
    79  			server:   &ServerInfo{Name: "localhost", TLS: false},
    80  		},
    81  		{
    82  			// NOT OK on non-localhost, even if server says PLAIN is OK.
    83  			// (We don't know that the server is the real server.)
    84  			authName: "servername",
    85  			server:   &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
    86  			err:      "unencrypted connection",
    87  		},
    88  		{
    89  			authName: "servername",
    90  			server:   &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
    91  			err:      "unencrypted connection",
    92  		},
    93  		{
    94  			authName: "servername",
    95  			server:   &ServerInfo{Name: "attacker", TLS: true},
    96  			err:      "wrong host name",
    97  		},
    98  	}
    99  	for i, tt := range tests {
   100  		auth := PlainAuth("foo", "bar", "baz", tt.authName)
   101  		_, _, err := auth.Start(tt.server)
   102  		got := ""
   103  		if err != nil {
   104  			got = err.Error()
   105  		}
   106  		if got != tt.err {
   107  			t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
   108  		}
   109  	}
   110  }
   111  
   112  // Issue 17794: don't send a trailing space on AUTH command when there's no password.
   113  func TestClientAuthTrimSpace(t *testing.T) {
   114  	server := "220 hello world\r\n" +
   115  		"200 some more"
   116  	var wrote strings.Builder
   117  	var fake faker
   118  	fake.ReadWriter = struct {
   119  		io.Reader
   120  		io.Writer
   121  	}{
   122  		strings.NewReader(server),
   123  		&wrote,
   124  	}
   125  	c, err := NewClient(fake, "fake.host")
   126  	if err != nil {
   127  		t.Fatalf("NewClient: %v", err)
   128  	}
   129  	c.tls = true
   130  	c.didHello = true
   131  	c.Auth(toServerEmptyAuth{})
   132  	c.Close()
   133  	if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want {
   134  		t.Errorf("wrote %q; want %q", got, want)
   135  	}
   136  }
   137  
   138  // toServerEmptyAuth is an implementation of Auth that only implements
   139  // the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns
   140  // zero bytes for "toServer" so we can test that we don't send spaces at
   141  // the end of the line. See TestClientAuthTrimSpace.
   142  type toServerEmptyAuth struct{}
   143  
   144  func (toServerEmptyAuth) Start(server *ServerInfo) (proto string, toServer []byte, err error) {
   145  	return "FOOAUTH", nil, nil
   146  }
   147  
   148  func (toServerEmptyAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) {
   149  	panic("unexpected call")
   150  }
   151  
   152  type faker struct {
   153  	io.ReadWriter
   154  }
   155  
   156  func (f faker) Close() error                     { return nil }
   157  func (f faker) LocalAddr() net.Addr              { return nil }
   158  func (f faker) RemoteAddr() net.Addr             { return nil }
   159  func (f faker) SetDeadline(time.Time) error      { return nil }
   160  func (f faker) SetReadDeadline(time.Time) error  { return nil }
   161  func (f faker) SetWriteDeadline(time.Time) error { return nil }
   162  
   163  func TestBasic(t *testing.T) {
   164  	server := strings.Join(strings.Split(basicServer, "\n"), "\r\n")
   165  	client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   166  
   167  	var cmdbuf strings.Builder
   168  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   169  	var fake faker
   170  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   171  	c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
   172  
   173  	if err := c.helo(); err != nil {
   174  		t.Fatalf("HELO failed: %s", err)
   175  	}
   176  	if err := c.ehlo(); err == nil {
   177  		t.Fatalf("Expected first EHLO to fail")
   178  	}
   179  	if err := c.ehlo(); err != nil {
   180  		t.Fatalf("Second EHLO failed: %s", err)
   181  	}
   182  
   183  	c.didHello = true
   184  	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
   185  		t.Fatalf("Expected AUTH supported")
   186  	}
   187  	if ok, _ := c.Extension("DSN"); ok {
   188  		t.Fatalf("Shouldn't support DSN")
   189  	}
   190  
   191  	if err := c.Mail("user@gmail.com"); err == nil {
   192  		t.Fatalf("MAIL should require authentication")
   193  	}
   194  
   195  	if err := c.Verify("user1@gmail.com"); err == nil {
   196  		t.Fatalf("First VRFY: expected no verification")
   197  	}
   198  	if err := c.Verify("user2@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil {
   199  		t.Fatalf("VRFY should have failed due to a message injection attempt")
   200  	}
   201  	if err := c.Verify("user2@gmail.com"); err != nil {
   202  		t.Fatalf("Second VRFY: expected verification, got %s", err)
   203  	}
   204  
   205  	// fake TLS so authentication won't complain
   206  	c.tls = true
   207  	c.serverName = "smtp.google.com"
   208  	if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil {
   209  		t.Fatalf("AUTH failed: %s", err)
   210  	}
   211  
   212  	if err := c.Rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil {
   213  		t.Fatalf("RCPT should have failed due to a message injection attempt")
   214  	}
   215  	if err := c.Mail("user@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil {
   216  		t.Fatalf("MAIL should have failed due to a message injection attempt")
   217  	}
   218  	if err := c.Mail("user@gmail.com"); err != nil {
   219  		t.Fatalf("MAIL failed: %s", err)
   220  	}
   221  	if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil {
   222  		t.Fatalf("RCPT failed: %s", err)
   223  	}
   224  	msg := `From: user@gmail.com
   225  To: golang-nuts@googlegroups.com
   226  Subject: Hooray for Go
   227  
   228  Line 1
   229  .Leading dot line .
   230  Goodbye.`
   231  	w, err := c.Data()
   232  	if err != nil {
   233  		t.Fatalf("DATA failed: %s", err)
   234  	}
   235  	if _, err := w.Write([]byte(msg)); err != nil {
   236  		t.Fatalf("Data write failed: %s", err)
   237  	}
   238  	if err := w.Close(); err != nil {
   239  		t.Fatalf("Bad data response: %s", err)
   240  	}
   241  
   242  	if err := c.Quit(); err != nil {
   243  		t.Fatalf("QUIT failed: %s", err)
   244  	}
   245  
   246  	bcmdbuf.Flush()
   247  	actualcmds := cmdbuf.String()
   248  	if client != actualcmds {
   249  		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   250  	}
   251  }
   252  
   253  var basicServer = `250 mx.google.com at your service
   254  502 Unrecognized command.
   255  250-mx.google.com at your service
   256  250-SIZE 35651584
   257  250-AUTH LOGIN PLAIN
   258  250 8BITMIME
   259  530 Authentication required
   260  252 Send some mail, I'll try my best
   261  250 User is valid
   262  235 Accepted
   263  250 Sender OK
   264  250 Receiver OK
   265  354 Go ahead
   266  250 Data OK
   267  221 OK
   268  `
   269  
   270  var basicClient = `HELO localhost
   271  EHLO localhost
   272  EHLO localhost
   273  MAIL FROM:<user@gmail.com> BODY=8BITMIME
   274  VRFY user1@gmail.com
   275  VRFY user2@gmail.com
   276  AUTH PLAIN AHVzZXIAcGFzcw==
   277  MAIL FROM:<user@gmail.com> BODY=8BITMIME
   278  RCPT TO:<golang-nuts@googlegroups.com>
   279  DATA
   280  From: user@gmail.com
   281  To: golang-nuts@googlegroups.com
   282  Subject: Hooray for Go
   283  
   284  Line 1
   285  ..Leading dot line .
   286  Goodbye.
   287  .
   288  QUIT
   289  `
   290  
   291  func TestHELOFailed(t *testing.T) {
   292  	serverLines := `502 EH?
   293  502 EH?
   294  221 OK
   295  `
   296  	clientLines := `EHLO localhost
   297  HELO localhost
   298  QUIT
   299  `
   300  
   301  	server := strings.Join(strings.Split(serverLines, "\n"), "\r\n")
   302  	client := strings.Join(strings.Split(clientLines, "\n"), "\r\n")
   303  	var cmdbuf strings.Builder
   304  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   305  	var fake faker
   306  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   307  	c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
   308  
   309  	if err := c.Hello("localhost"); err == nil {
   310  		t.Fatal("expected EHLO to fail")
   311  	}
   312  	if err := c.Quit(); err != nil {
   313  		t.Errorf("QUIT failed: %s", err)
   314  	}
   315  	bcmdbuf.Flush()
   316  	actual := cmdbuf.String()
   317  	if client != actual {
   318  		t.Errorf("Got:\n%s\nWant:\n%s", actual, client)
   319  	}
   320  }
   321  
   322  func TestExtensions(t *testing.T) {
   323  	fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
   324  		server = strings.Join(strings.Split(server, "\n"), "\r\n")
   325  
   326  		cmdbuf = &strings.Builder{}
   327  		bcmdbuf = bufio.NewWriter(cmdbuf)
   328  		var fake faker
   329  		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   330  		c = &Client{Text: textproto.NewConn(fake), localName: "localhost"}
   331  
   332  		return c, bcmdbuf, cmdbuf
   333  	}
   334  
   335  	t.Run("helo", func(t *testing.T) {
   336  		const (
   337  			basicServer = `250 mx.google.com at your service
   338  250 Sender OK
   339  221 Goodbye
   340  `
   341  
   342  			basicClient = `HELO localhost
   343  MAIL FROM:<user@gmail.com>
   344  QUIT
   345  `
   346  		)
   347  
   348  		c, bcmdbuf, cmdbuf := fake(basicServer)
   349  
   350  		if err := c.helo(); err != nil {
   351  			t.Fatalf("HELO failed: %s", err)
   352  		}
   353  		c.didHello = true
   354  		if err := c.Mail("user@gmail.com"); err != nil {
   355  			t.Fatalf("MAIL FROM failed: %s", err)
   356  		}
   357  		if err := c.Quit(); err != nil {
   358  			t.Fatalf("QUIT failed: %s", err)
   359  		}
   360  
   361  		bcmdbuf.Flush()
   362  		actualcmds := cmdbuf.String()
   363  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   364  		if client != actualcmds {
   365  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   366  		}
   367  	})
   368  
   369  	t.Run("ehlo", func(t *testing.T) {
   370  		const (
   371  			basicServer = `250-mx.google.com at your service
   372  250 SIZE 35651584
   373  250 Sender OK
   374  221 Goodbye
   375  `
   376  
   377  			basicClient = `EHLO localhost
   378  MAIL FROM:<user@gmail.com>
   379  QUIT
   380  `
   381  		)
   382  
   383  		c, bcmdbuf, cmdbuf := fake(basicServer)
   384  
   385  		if err := c.Hello("localhost"); err != nil {
   386  			t.Fatalf("EHLO failed: %s", err)
   387  		}
   388  		if ok, _ := c.Extension("8BITMIME"); ok {
   389  			t.Fatalf("Shouldn't support 8BITMIME")
   390  		}
   391  		if ok, _ := c.Extension("SMTPUTF8"); ok {
   392  			t.Fatalf("Shouldn't support SMTPUTF8")
   393  		}
   394  		if err := c.Mail("user@gmail.com"); err != nil {
   395  			t.Fatalf("MAIL FROM failed: %s", err)
   396  		}
   397  		if err := c.Quit(); err != nil {
   398  			t.Fatalf("QUIT failed: %s", err)
   399  		}
   400  
   401  		bcmdbuf.Flush()
   402  		actualcmds := cmdbuf.String()
   403  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   404  		if client != actualcmds {
   405  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   406  		}
   407  	})
   408  
   409  	t.Run("ehlo 8bitmime", func(t *testing.T) {
   410  		const (
   411  			basicServer = `250-mx.google.com at your service
   412  250-SIZE 35651584
   413  250 8BITMIME
   414  250 Sender OK
   415  221 Goodbye
   416  `
   417  
   418  			basicClient = `EHLO localhost
   419  MAIL FROM:<user@gmail.com> BODY=8BITMIME
   420  QUIT
   421  `
   422  		)
   423  
   424  		c, bcmdbuf, cmdbuf := fake(basicServer)
   425  
   426  		if err := c.Hello("localhost"); err != nil {
   427  			t.Fatalf("EHLO failed: %s", err)
   428  		}
   429  		if ok, _ := c.Extension("8BITMIME"); !ok {
   430  			t.Fatalf("Should support 8BITMIME")
   431  		}
   432  		if ok, _ := c.Extension("SMTPUTF8"); ok {
   433  			t.Fatalf("Shouldn't support SMTPUTF8")
   434  		}
   435  		if err := c.Mail("user@gmail.com"); err != nil {
   436  			t.Fatalf("MAIL FROM failed: %s", err)
   437  		}
   438  		if err := c.Quit(); err != nil {
   439  			t.Fatalf("QUIT failed: %s", err)
   440  		}
   441  
   442  		bcmdbuf.Flush()
   443  		actualcmds := cmdbuf.String()
   444  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   445  		if client != actualcmds {
   446  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   447  		}
   448  	})
   449  
   450  	t.Run("ehlo smtputf8", func(t *testing.T) {
   451  		const (
   452  			basicServer = `250-mx.google.com at your service
   453  250-SIZE 35651584
   454  250 SMTPUTF8
   455  250 Sender OK
   456  221 Goodbye
   457  `
   458  
   459  			basicClient = `EHLO localhost
   460  MAIL FROM:<user+📧@gmail.com> SMTPUTF8
   461  QUIT
   462  `
   463  		)
   464  
   465  		c, bcmdbuf, cmdbuf := fake(basicServer)
   466  
   467  		if err := c.Hello("localhost"); err != nil {
   468  			t.Fatalf("EHLO failed: %s", err)
   469  		}
   470  		if ok, _ := c.Extension("8BITMIME"); ok {
   471  			t.Fatalf("Shouldn't support 8BITMIME")
   472  		}
   473  		if ok, _ := c.Extension("SMTPUTF8"); !ok {
   474  			t.Fatalf("Should support SMTPUTF8")
   475  		}
   476  		if err := c.Mail("user+📧@gmail.com"); err != nil {
   477  			t.Fatalf("MAIL FROM failed: %s", err)
   478  		}
   479  		if err := c.Quit(); err != nil {
   480  			t.Fatalf("QUIT failed: %s", err)
   481  		}
   482  
   483  		bcmdbuf.Flush()
   484  		actualcmds := cmdbuf.String()
   485  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   486  		if client != actualcmds {
   487  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   488  		}
   489  	})
   490  
   491  	t.Run("ehlo 8bitmime smtputf8", func(t *testing.T) {
   492  		const (
   493  			basicServer = `250-mx.google.com at your service
   494  250-SIZE 35651584
   495  250-8BITMIME
   496  250 SMTPUTF8
   497  250 Sender OK
   498  221 Goodbye
   499  	`
   500  
   501  			basicClient = `EHLO localhost
   502  MAIL FROM:<user+📧@gmail.com> BODY=8BITMIME SMTPUTF8
   503  QUIT
   504  `
   505  		)
   506  
   507  		c, bcmdbuf, cmdbuf := fake(basicServer)
   508  
   509  		if err := c.Hello("localhost"); err != nil {
   510  			t.Fatalf("EHLO failed: %s", err)
   511  		}
   512  		c.didHello = true
   513  		if ok, _ := c.Extension("8BITMIME"); !ok {
   514  			t.Fatalf("Should support 8BITMIME")
   515  		}
   516  		if ok, _ := c.Extension("SMTPUTF8"); !ok {
   517  			t.Fatalf("Should support SMTPUTF8")
   518  		}
   519  		if err := c.Mail("user+📧@gmail.com"); err != nil {
   520  			t.Fatalf("MAIL FROM failed: %s", err)
   521  		}
   522  		if err := c.Quit(); err != nil {
   523  			t.Fatalf("QUIT failed: %s", err)
   524  		}
   525  
   526  		bcmdbuf.Flush()
   527  		actualcmds := cmdbuf.String()
   528  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   529  		if client != actualcmds {
   530  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   531  		}
   532  	})
   533  }
   534  
   535  func TestNewClient(t *testing.T) {
   536  	server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
   537  	client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n")
   538  
   539  	var cmdbuf strings.Builder
   540  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   541  	out := func() string {
   542  		bcmdbuf.Flush()
   543  		return cmdbuf.String()
   544  	}
   545  	var fake faker
   546  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   547  	c, err := NewClient(fake, "fake.host")
   548  	if err != nil {
   549  		t.Fatalf("NewClient: %v\n(after %v)", err, out())
   550  	}
   551  	defer c.Close()
   552  	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
   553  		t.Fatalf("Expected AUTH supported")
   554  	}
   555  	if ok, _ := c.Extension("DSN"); ok {
   556  		t.Fatalf("Shouldn't support DSN")
   557  	}
   558  	if err := c.Quit(); err != nil {
   559  		t.Fatalf("QUIT failed: %s", err)
   560  	}
   561  
   562  	actualcmds := out()
   563  	if client != actualcmds {
   564  		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   565  	}
   566  }
   567  
   568  var newClientServer = `220 hello world
   569  250-mx.google.com at your service
   570  250-SIZE 35651584
   571  250-AUTH LOGIN PLAIN
   572  250 8BITMIME
   573  221 OK
   574  `
   575  
   576  var newClientClient = `EHLO localhost
   577  QUIT
   578  `
   579  
   580  func TestNewClient2(t *testing.T) {
   581  	server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n")
   582  	client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n")
   583  
   584  	var cmdbuf strings.Builder
   585  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   586  	var fake faker
   587  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   588  	c, err := NewClient(fake, "fake.host")
   589  	if err != nil {
   590  		t.Fatalf("NewClient: %v", err)
   591  	}
   592  	defer c.Close()
   593  	if ok, _ := c.Extension("DSN"); ok {
   594  		t.Fatalf("Shouldn't support DSN")
   595  	}
   596  	if err := c.Quit(); err != nil {
   597  		t.Fatalf("QUIT failed: %s", err)
   598  	}
   599  
   600  	bcmdbuf.Flush()
   601  	actualcmds := cmdbuf.String()
   602  	if client != actualcmds {
   603  		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   604  	}
   605  }
   606  
   607  var newClient2Server = `220 hello world
   608  502 EH?
   609  250-mx.google.com at your service
   610  250-SIZE 35651584
   611  250-AUTH LOGIN PLAIN
   612  250 8BITMIME
   613  221 OK
   614  `
   615  
   616  var newClient2Client = `EHLO localhost
   617  HELO localhost
   618  QUIT
   619  `
   620  
   621  func TestNewClientWithTLS(t *testing.T) {
   622  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
   623  	if err != nil {
   624  		t.Fatalf("loadcert: %v", err)
   625  	}
   626  
   627  	config := tls.Config{Certificates: []tls.Certificate{cert}}
   628  
   629  	ln, err := tls.Listen("tcp", "127.0.0.1:0", &config)
   630  	if err != nil {
   631  		ln, err = tls.Listen("tcp", "[::1]:0", &config)
   632  		if err != nil {
   633  			t.Fatalf("server: listen: %v", err)
   634  		}
   635  	}
   636  
   637  	go func() {
   638  		conn, err := ln.Accept()
   639  		if err != nil {
   640  			t.Errorf("server: accept: %v", err)
   641  			return
   642  		}
   643  		defer conn.Close()
   644  
   645  		_, err = conn.Write([]byte("220 SIGNS\r\n"))
   646  		if err != nil {
   647  			t.Errorf("server: write: %v", err)
   648  			return
   649  		}
   650  	}()
   651  
   652  	config.InsecureSkipVerify = true
   653  	conn, err := tls.Dial("tcp", ln.Addr().String(), &config)
   654  	if err != nil {
   655  		t.Fatalf("client: dial: %v", err)
   656  	}
   657  	defer conn.Close()
   658  
   659  	client, err := NewClient(conn, ln.Addr().String())
   660  	if err != nil {
   661  		t.Fatalf("smtp: newclient: %v", err)
   662  	}
   663  	if !client.tls {
   664  		t.Errorf("client.tls Got: %t Expected: %t", client.tls, true)
   665  	}
   666  }
   667  
   668  func TestHello(t *testing.T) {
   669  
   670  	if len(helloServer) != len(helloClient) {
   671  		t.Fatalf("Hello server and client size mismatch")
   672  	}
   673  
   674  	for i := 0; i < len(helloServer); i++ {
   675  		server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n")
   676  		client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n")
   677  		var cmdbuf strings.Builder
   678  		bcmdbuf := bufio.NewWriter(&cmdbuf)
   679  		var fake faker
   680  		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   681  		c, err := NewClient(fake, "fake.host")
   682  		if err != nil {
   683  			t.Fatalf("NewClient: %v", err)
   684  		}
   685  		defer c.Close()
   686  		c.localName = "customhost"
   687  		err = nil
   688  
   689  		switch i {
   690  		case 0:
   691  			err = c.Hello("hostinjection>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n")
   692  			if err == nil {
   693  				t.Errorf("Expected Hello to be rejected due to a message injection attempt")
   694  			}
   695  			err = c.Hello("customhost")
   696  		case 1:
   697  			err = c.StartTLS(nil)
   698  			if err.Error() == "502 Not implemented" {
   699  				err = nil
   700  			}
   701  		case 2:
   702  			err = c.Verify("test@example.com")
   703  		case 3:
   704  			c.tls = true
   705  			c.serverName = "smtp.google.com"
   706  			err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
   707  		case 4:
   708  			err = c.Mail("test@example.com")
   709  		case 5:
   710  			ok, _ := c.Extension("feature")
   711  			if ok {
   712  				t.Errorf("Expected FEATURE not to be supported")
   713  			}
   714  		case 6:
   715  			err = c.Reset()
   716  		case 7:
   717  			err = c.Quit()
   718  		case 8:
   719  			err = c.Verify("test@example.com")
   720  			if err != nil {
   721  				err = c.Hello("customhost")
   722  				if err != nil {
   723  					t.Errorf("Want error, got none")
   724  				}
   725  			}
   726  		case 9:
   727  			err = c.Noop()
   728  		default:
   729  			t.Fatalf("Unhandled command")
   730  		}
   731  
   732  		if err != nil {
   733  			t.Errorf("Command %d failed: %v", i, err)
   734  		}
   735  
   736  		bcmdbuf.Flush()
   737  		actualcmds := cmdbuf.String()
   738  		if client != actualcmds {
   739  			t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   740  		}
   741  	}
   742  }
   743  
   744  var baseHelloServer = `220 hello world
   745  502 EH?
   746  250-mx.google.com at your service
   747  250 FEATURE
   748  `
   749  
   750  var helloServer = []string{
   751  	"",
   752  	"502 Not implemented\n",
   753  	"250 User is valid\n",
   754  	"235 Accepted\n",
   755  	"250 Sender ok\n",
   756  	"",
   757  	"250 Reset ok\n",
   758  	"221 Goodbye\n",
   759  	"250 Sender ok\n",
   760  	"250 ok\n",
   761  }
   762  
   763  var baseHelloClient = `EHLO customhost
   764  HELO customhost
   765  `
   766  
   767  var helloClient = []string{
   768  	"",
   769  	"STARTTLS\n",
   770  	"VRFY test@example.com\n",
   771  	"AUTH PLAIN AHVzZXIAcGFzcw==\n",
   772  	"MAIL FROM:<test@example.com>\n",
   773  	"",
   774  	"RSET\n",
   775  	"QUIT\n",
   776  	"VRFY test@example.com\n",
   777  	"NOOP\n",
   778  }
   779  
   780  func TestSendMail(t *testing.T) {
   781  	server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n")
   782  	client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n")
   783  	var cmdbuf strings.Builder
   784  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   785  	l, err := net.Listen("tcp", "127.0.0.1:0")
   786  	if err != nil {
   787  		t.Fatalf("Unable to create listener: %v", err)
   788  	}
   789  	defer l.Close()
   790  
   791  	// prevent data race on bcmdbuf
   792  	var done = make(chan struct{})
   793  	go func(data []string) {
   794  
   795  		defer close(done)
   796  
   797  		conn, err := l.Accept()
   798  		if err != nil {
   799  			t.Errorf("Accept error: %v", err)
   800  			return
   801  		}
   802  		defer conn.Close()
   803  
   804  		tc := textproto.NewConn(conn)
   805  		for i := 0; i < len(data) && data[i] != ""; i++ {
   806  			tc.PrintfLine("%s", data[i])
   807  			for len(data[i]) >= 4 && data[i][3] == '-' {
   808  				i++
   809  				tc.PrintfLine("%s", data[i])
   810  			}
   811  			if data[i] == "221 Goodbye" {
   812  				return
   813  			}
   814  			read := false
   815  			for !read || data[i] == "354 Go ahead" {
   816  				msg, err := tc.ReadLine()
   817  				bcmdbuf.Write([]byte(msg + "\r\n"))
   818  				read = true
   819  				if err != nil {
   820  					t.Errorf("Read error: %v", err)
   821  					return
   822  				}
   823  				if data[i] == "354 Go ahead" && msg == "." {
   824  					break
   825  				}
   826  			}
   827  		}
   828  	}(strings.Split(server, "\r\n"))
   829  
   830  	err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}, []byte(strings.Replace(`From: test@example.com
   831  To: other@example.com
   832  Subject: SendMail test
   833  
   834  SendMail is working for me.
   835  `, "\n", "\r\n", -1)))
   836  	if err == nil {
   837  		t.Errorf("Expected SendMail to be rejected due to a message injection attempt")
   838  	}
   839  
   840  	err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
   841  To: other@example.com
   842  Subject: SendMail test
   843  
   844  SendMail is working for me.
   845  `, "\n", "\r\n", -1)))
   846  
   847  	if err != nil {
   848  		t.Errorf("%v", err)
   849  	}
   850  
   851  	<-done
   852  	bcmdbuf.Flush()
   853  	actualcmds := cmdbuf.String()
   854  	if client != actualcmds {
   855  		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   856  	}
   857  }
   858  
   859  var sendMailServer = `220 hello world
   860  502 EH?
   861  250 mx.google.com at your service
   862  250 Sender ok
   863  250 Receiver ok
   864  354 Go ahead
   865  250 Data ok
   866  221 Goodbye
   867  `
   868  
   869  var sendMailClient = `EHLO localhost
   870  HELO localhost
   871  MAIL FROM:<test@example.com>
   872  RCPT TO:<other@example.com>
   873  DATA
   874  From: test@example.com
   875  To: other@example.com
   876  Subject: SendMail test
   877  
   878  SendMail is working for me.
   879  .
   880  QUIT
   881  `
   882  
   883  func TestSendMailWithAuth(t *testing.T) {
   884  	l, err := net.Listen("tcp", "127.0.0.1:0")
   885  	if err != nil {
   886  		t.Fatalf("Unable to create listener: %v", err)
   887  	}
   888  	defer l.Close()
   889  
   890  	errCh := make(chan error)
   891  	go func() {
   892  		defer close(errCh)
   893  		conn, err := l.Accept()
   894  		if err != nil {
   895  			errCh <- fmt.Errorf("Accept: %v", err)
   896  			return
   897  		}
   898  		defer conn.Close()
   899  
   900  		tc := textproto.NewConn(conn)
   901  		tc.PrintfLine("220 hello world")
   902  		msg, err := tc.ReadLine()
   903  		if err != nil {
   904  			errCh <- fmt.Errorf("ReadLine error: %v", err)
   905  			return
   906  		}
   907  		const wantMsg = "EHLO localhost"
   908  		if msg != wantMsg {
   909  			errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg)
   910  			return
   911  		}
   912  		err = tc.PrintfLine("250 mx.google.com at your service")
   913  		if err != nil {
   914  			errCh <- fmt.Errorf("PrintfLine: %v", err)
   915  			return
   916  		}
   917  	}()
   918  
   919  	err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com"), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
   920  To: other@example.com
   921  Subject: SendMail test
   922  
   923  SendMail is working for me.
   924  `, "\n", "\r\n", -1)))
   925  	if err == nil {
   926  		t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
   927  	}
   928  	if err.Error() != "smtp: server doesn't support AUTH" {
   929  		t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
   930  	}
   931  	err = <-errCh
   932  	if err != nil {
   933  		t.Fatalf("server error: %v", err)
   934  	}
   935  }
   936  
   937  func TestAuthFailed(t *testing.T) {
   938  	server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n")
   939  	client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n")
   940  	var cmdbuf strings.Builder
   941  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   942  	var fake faker
   943  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   944  	c, err := NewClient(fake, "fake.host")
   945  	if err != nil {
   946  		t.Fatalf("NewClient: %v", err)
   947  	}
   948  	defer c.Close()
   949  
   950  	c.tls = true
   951  	c.serverName = "smtp.google.com"
   952  	err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
   953  
   954  	if err == nil {
   955  		t.Error("Auth: expected error; got none")
   956  	} else if err.Error() != "535 Invalid credentials\nplease see www.example.com" {
   957  		t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com")
   958  	}
   959  
   960  	bcmdbuf.Flush()
   961  	actualcmds := cmdbuf.String()
   962  	if client != actualcmds {
   963  		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   964  	}
   965  }
   966  
   967  var authFailedServer = `220 hello world
   968  250-mx.google.com at your service
   969  250 AUTH LOGIN PLAIN
   970  535-Invalid credentials
   971  535 please see www.example.com
   972  221 Goodbye
   973  `
   974  
   975  var authFailedClient = `EHLO localhost
   976  AUTH PLAIN AHVzZXIAcGFzcw==
   977  *
   978  QUIT
   979  `
   980  
   981  func TestTLSClient(t *testing.T) {
   982  	if runtime.GOOS == "freebsd" || runtime.GOOS == "js" || runtime.GOOS == "wasip1" {
   983  		testenv.SkipFlaky(t, 19229)
   984  	}
   985  	ln := newLocalListener(t)
   986  	defer ln.Close()
   987  	errc := make(chan error)
   988  	go func() {
   989  		errc <- sendMail(ln.Addr().String())
   990  	}()
   991  	conn, err := ln.Accept()
   992  	if err != nil {
   993  		t.Fatalf("failed to accept connection: %v", err)
   994  	}
   995  	defer conn.Close()
   996  	if err := serverHandle(conn, t); err != nil {
   997  		t.Fatalf("failed to handle connection: %v", err)
   998  	}
   999  	if err := <-errc; err != nil {
  1000  		t.Fatalf("client error: %v", err)
  1001  	}
  1002  }
  1003  
  1004  func TestTLSConnState(t *testing.T) {
  1005  	ln := newLocalListener(t)
  1006  	defer ln.Close()
  1007  	clientDone := make(chan bool)
  1008  	serverDone := make(chan bool)
  1009  	go func() {
  1010  		defer close(serverDone)
  1011  		c, err := ln.Accept()
  1012  		if err != nil {
  1013  			t.Errorf("Server accept: %v", err)
  1014  			return
  1015  		}
  1016  		defer c.Close()
  1017  		if err := serverHandle(c, t); err != nil {
  1018  			t.Errorf("server error: %v", err)
  1019  		}
  1020  	}()
  1021  	go func() {
  1022  		defer close(clientDone)
  1023  		c, err := Dial(ln.Addr().String())
  1024  		if err != nil {
  1025  			t.Errorf("Client dial: %v", err)
  1026  			return
  1027  		}
  1028  		defer c.Quit()
  1029  		cfg := &tls.Config{ServerName: "example.com"}
  1030  		testHookStartTLS(cfg) // set the RootCAs
  1031  		if err := c.StartTLS(cfg); err != nil {
  1032  			t.Errorf("StartTLS: %v", err)
  1033  			return
  1034  		}
  1035  		cs, ok := c.TLSConnectionState()
  1036  		if !ok {
  1037  			t.Errorf("TLSConnectionState returned ok == false; want true")
  1038  			return
  1039  		}
  1040  		if cs.Version == 0 || !cs.HandshakeComplete {
  1041  			t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs)
  1042  		}
  1043  	}()
  1044  	<-clientDone
  1045  	<-serverDone
  1046  }
  1047  
  1048  func newLocalListener(t *testing.T) net.Listener {
  1049  	ln, err := net.Listen("tcp", "127.0.0.1:0")
  1050  	if err != nil {
  1051  		ln, err = net.Listen("tcp6", "[::1]:0")
  1052  	}
  1053  	if err != nil {
  1054  		t.Fatal(err)
  1055  	}
  1056  	return ln
  1057  }
  1058  
  1059  type smtpSender struct {
  1060  	w io.Writer
  1061  }
  1062  
  1063  func (s smtpSender) send(f string) {
  1064  	s.w.Write([]byte(f + "\r\n"))
  1065  }
  1066  
  1067  // smtp server, finely tailored to deal with our own client only!
  1068  func serverHandle(c net.Conn, t *testing.T) error {
  1069  	send := smtpSender{c}.send
  1070  	send("220 127.0.0.1 ESMTP service ready")
  1071  	s := bufio.NewScanner(c)
  1072  	for s.Scan() {
  1073  		switch s.Text() {
  1074  		case "EHLO localhost":
  1075  			send("250-127.0.0.1 ESMTP offers a warm hug of welcome")
  1076  			send("250-STARTTLS")
  1077  			send("250 Ok")
  1078  		case "STARTTLS":
  1079  			send("220 Go ahead")
  1080  			keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
  1081  			if err != nil {
  1082  				return err
  1083  			}
  1084  			config := &tls.Config{Certificates: []tls.Certificate{keypair}}
  1085  			c = tls.Server(c, config)
  1086  			defer c.Close()
  1087  			return serverHandleTLS(c, t)
  1088  		default:
  1089  			t.Fatalf("unrecognized command: %q", s.Text())
  1090  		}
  1091  	}
  1092  	return s.Err()
  1093  }
  1094  
  1095  func serverHandleTLS(c net.Conn, t *testing.T) error {
  1096  	send := smtpSender{c}.send
  1097  	s := bufio.NewScanner(c)
  1098  	for s.Scan() {
  1099  		switch s.Text() {
  1100  		case "EHLO localhost":
  1101  			send("250 Ok")
  1102  		case "MAIL FROM:<joe1@example.com>":
  1103  			send("250 Ok")
  1104  		case "RCPT TO:<joe2@example.com>":
  1105  			send("250 Ok")
  1106  		case "DATA":
  1107  			send("354 send the mail data, end with .")
  1108  			send("250 Ok")
  1109  		case "Subject: test":
  1110  		case "":
  1111  		case "howdy!":
  1112  		case ".":
  1113  		case "QUIT":
  1114  			send("221 127.0.0.1 Service closing transmission channel")
  1115  			return nil
  1116  		default:
  1117  			t.Fatalf("unrecognized command during TLS: %q", s.Text())
  1118  		}
  1119  	}
  1120  	return s.Err()
  1121  }
  1122  
  1123  func init() {
  1124  	testRootCAs := x509.NewCertPool()
  1125  	testRootCAs.AppendCertsFromPEM(localhostCert)
  1126  	testHookStartTLS = func(config *tls.Config) {
  1127  		config.RootCAs = testRootCAs
  1128  	}
  1129  }
  1130  
  1131  func sendMail(hostPort string) error {
  1132  	from := "joe1@example.com"
  1133  	to := []string{"joe2@example.com"}
  1134  	return SendMail(hostPort, nil, from, to, []byte("Subject: test\n\nhowdy!"))
  1135  }
  1136  
  1137  // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
  1138  //
  1139  //	go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
  1140  //		--ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
  1141  var localhostCert = []byte(`
  1142  -----BEGIN CERTIFICATE-----
  1143  MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
  1144  EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
  1145  MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
  1146  gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa
  1147  JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30
  1148  LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw
  1149  DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
  1150  MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
  1151  AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi
  1152  NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4
  1153  n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF
  1154  tN8URjVmyEo=
  1155  -----END CERTIFICATE-----`)
  1156  
  1157  // localhostKey is the private key for localhostCert.
  1158  var localhostKey = []byte(testingKey(`
  1159  -----BEGIN RSA TESTING KEY-----
  1160  MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h
  1161  AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu
  1162  lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB
  1163  AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA
  1164  kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM
  1165  VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m
  1166  542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb
  1167  PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2
  1168  6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB
  1169  vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP
  1170  QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i
  1171  jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c
  1172  qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg==
  1173  -----END RSA TESTING KEY-----`))
  1174  
  1175  func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
  1176  

View as plain text