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 TestExtensions(t *testing.T) {
   292  	fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
   293  		server = strings.Join(strings.Split(server, "\n"), "\r\n")
   294  
   295  		cmdbuf = &strings.Builder{}
   296  		bcmdbuf = bufio.NewWriter(cmdbuf)
   297  		var fake faker
   298  		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   299  		c = &Client{Text: textproto.NewConn(fake), localName: "localhost"}
   300  
   301  		return c, bcmdbuf, cmdbuf
   302  	}
   303  
   304  	t.Run("helo", func(t *testing.T) {
   305  		const (
   306  			basicServer = `250 mx.google.com at your service
   307  250 Sender OK
   308  221 Goodbye
   309  `
   310  
   311  			basicClient = `HELO localhost
   312  MAIL FROM:<user@gmail.com>
   313  QUIT
   314  `
   315  		)
   316  
   317  		c, bcmdbuf, cmdbuf := fake(basicServer)
   318  
   319  		if err := c.helo(); err != nil {
   320  			t.Fatalf("HELO failed: %s", err)
   321  		}
   322  		c.didHello = true
   323  		if err := c.Mail("user@gmail.com"); err != nil {
   324  			t.Fatalf("MAIL FROM failed: %s", err)
   325  		}
   326  		if err := c.Quit(); err != nil {
   327  			t.Fatalf("QUIT failed: %s", err)
   328  		}
   329  
   330  		bcmdbuf.Flush()
   331  		actualcmds := cmdbuf.String()
   332  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   333  		if client != actualcmds {
   334  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   335  		}
   336  	})
   337  
   338  	t.Run("ehlo", func(t *testing.T) {
   339  		const (
   340  			basicServer = `250-mx.google.com at your service
   341  250 SIZE 35651584
   342  250 Sender OK
   343  221 Goodbye
   344  `
   345  
   346  			basicClient = `EHLO localhost
   347  MAIL FROM:<user@gmail.com>
   348  QUIT
   349  `
   350  		)
   351  
   352  		c, bcmdbuf, cmdbuf := fake(basicServer)
   353  
   354  		if err := c.Hello("localhost"); err != nil {
   355  			t.Fatalf("EHLO failed: %s", err)
   356  		}
   357  		if ok, _ := c.Extension("8BITMIME"); ok {
   358  			t.Fatalf("Shouldn't support 8BITMIME")
   359  		}
   360  		if ok, _ := c.Extension("SMTPUTF8"); ok {
   361  			t.Fatalf("Shouldn't support SMTPUTF8")
   362  		}
   363  		if err := c.Mail("user@gmail.com"); err != nil {
   364  			t.Fatalf("MAIL FROM failed: %s", err)
   365  		}
   366  		if err := c.Quit(); err != nil {
   367  			t.Fatalf("QUIT failed: %s", err)
   368  		}
   369  
   370  		bcmdbuf.Flush()
   371  		actualcmds := cmdbuf.String()
   372  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   373  		if client != actualcmds {
   374  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   375  		}
   376  	})
   377  
   378  	t.Run("ehlo 8bitmime", func(t *testing.T) {
   379  		const (
   380  			basicServer = `250-mx.google.com at your service
   381  250-SIZE 35651584
   382  250 8BITMIME
   383  250 Sender OK
   384  221 Goodbye
   385  `
   386  
   387  			basicClient = `EHLO localhost
   388  MAIL FROM:<user@gmail.com> BODY=8BITMIME
   389  QUIT
   390  `
   391  		)
   392  
   393  		c, bcmdbuf, cmdbuf := fake(basicServer)
   394  
   395  		if err := c.Hello("localhost"); err != nil {
   396  			t.Fatalf("EHLO failed: %s", err)
   397  		}
   398  		if ok, _ := c.Extension("8BITMIME"); !ok {
   399  			t.Fatalf("Should support 8BITMIME")
   400  		}
   401  		if ok, _ := c.Extension("SMTPUTF8"); ok {
   402  			t.Fatalf("Shouldn't support SMTPUTF8")
   403  		}
   404  		if err := c.Mail("user@gmail.com"); err != nil {
   405  			t.Fatalf("MAIL FROM failed: %s", err)
   406  		}
   407  		if err := c.Quit(); err != nil {
   408  			t.Fatalf("QUIT failed: %s", err)
   409  		}
   410  
   411  		bcmdbuf.Flush()
   412  		actualcmds := cmdbuf.String()
   413  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   414  		if client != actualcmds {
   415  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   416  		}
   417  	})
   418  
   419  	t.Run("ehlo smtputf8", func(t *testing.T) {
   420  		const (
   421  			basicServer = `250-mx.google.com at your service
   422  250-SIZE 35651584
   423  250 SMTPUTF8
   424  250 Sender OK
   425  221 Goodbye
   426  `
   427  
   428  			basicClient = `EHLO localhost
   429  MAIL FROM:<user+📧@gmail.com> SMTPUTF8
   430  QUIT
   431  `
   432  		)
   433  
   434  		c, bcmdbuf, cmdbuf := fake(basicServer)
   435  
   436  		if err := c.Hello("localhost"); err != nil {
   437  			t.Fatalf("EHLO failed: %s", err)
   438  		}
   439  		if ok, _ := c.Extension("8BITMIME"); ok {
   440  			t.Fatalf("Shouldn't support 8BITMIME")
   441  		}
   442  		if ok, _ := c.Extension("SMTPUTF8"); !ok {
   443  			t.Fatalf("Should support SMTPUTF8")
   444  		}
   445  		if err := c.Mail("user+📧@gmail.com"); err != nil {
   446  			t.Fatalf("MAIL FROM failed: %s", err)
   447  		}
   448  		if err := c.Quit(); err != nil {
   449  			t.Fatalf("QUIT failed: %s", err)
   450  		}
   451  
   452  		bcmdbuf.Flush()
   453  		actualcmds := cmdbuf.String()
   454  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   455  		if client != actualcmds {
   456  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   457  		}
   458  	})
   459  
   460  	t.Run("ehlo 8bitmime smtputf8", func(t *testing.T) {
   461  		const (
   462  			basicServer = `250-mx.google.com at your service
   463  250-SIZE 35651584
   464  250-8BITMIME
   465  250 SMTPUTF8
   466  250 Sender OK
   467  221 Goodbye
   468  	`
   469  
   470  			basicClient = `EHLO localhost
   471  MAIL FROM:<user+📧@gmail.com> BODY=8BITMIME SMTPUTF8
   472  QUIT
   473  `
   474  		)
   475  
   476  		c, bcmdbuf, cmdbuf := fake(basicServer)
   477  
   478  		if err := c.Hello("localhost"); err != nil {
   479  			t.Fatalf("EHLO failed: %s", err)
   480  		}
   481  		c.didHello = true
   482  		if ok, _ := c.Extension("8BITMIME"); !ok {
   483  			t.Fatalf("Should support 8BITMIME")
   484  		}
   485  		if ok, _ := c.Extension("SMTPUTF8"); !ok {
   486  			t.Fatalf("Should support SMTPUTF8")
   487  		}
   488  		if err := c.Mail("user+📧@gmail.com"); err != nil {
   489  			t.Fatalf("MAIL FROM failed: %s", err)
   490  		}
   491  		if err := c.Quit(); err != nil {
   492  			t.Fatalf("QUIT failed: %s", err)
   493  		}
   494  
   495  		bcmdbuf.Flush()
   496  		actualcmds := cmdbuf.String()
   497  		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
   498  		if client != actualcmds {
   499  			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   500  		}
   501  	})
   502  }
   503  
   504  func TestNewClient(t *testing.T) {
   505  	server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
   506  	client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n")
   507  
   508  	var cmdbuf strings.Builder
   509  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   510  	out := func() string {
   511  		bcmdbuf.Flush()
   512  		return cmdbuf.String()
   513  	}
   514  	var fake faker
   515  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   516  	c, err := NewClient(fake, "fake.host")
   517  	if err != nil {
   518  		t.Fatalf("NewClient: %v\n(after %v)", err, out())
   519  	}
   520  	defer c.Close()
   521  	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
   522  		t.Fatalf("Expected AUTH supported")
   523  	}
   524  	if ok, _ := c.Extension("DSN"); ok {
   525  		t.Fatalf("Shouldn't support DSN")
   526  	}
   527  	if err := c.Quit(); err != nil {
   528  		t.Fatalf("QUIT failed: %s", err)
   529  	}
   530  
   531  	actualcmds := out()
   532  	if client != actualcmds {
   533  		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   534  	}
   535  }
   536  
   537  var newClientServer = `220 hello world
   538  250-mx.google.com at your service
   539  250-SIZE 35651584
   540  250-AUTH LOGIN PLAIN
   541  250 8BITMIME
   542  221 OK
   543  `
   544  
   545  var newClientClient = `EHLO localhost
   546  QUIT
   547  `
   548  
   549  func TestNewClient2(t *testing.T) {
   550  	server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n")
   551  	client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n")
   552  
   553  	var cmdbuf strings.Builder
   554  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   555  	var fake faker
   556  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   557  	c, err := NewClient(fake, "fake.host")
   558  	if err != nil {
   559  		t.Fatalf("NewClient: %v", err)
   560  	}
   561  	defer c.Close()
   562  	if ok, _ := c.Extension("DSN"); ok {
   563  		t.Fatalf("Shouldn't support DSN")
   564  	}
   565  	if err := c.Quit(); err != nil {
   566  		t.Fatalf("QUIT failed: %s", err)
   567  	}
   568  
   569  	bcmdbuf.Flush()
   570  	actualcmds := cmdbuf.String()
   571  	if client != actualcmds {
   572  		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   573  	}
   574  }
   575  
   576  var newClient2Server = `220 hello world
   577  502 EH?
   578  250-mx.google.com at your service
   579  250-SIZE 35651584
   580  250-AUTH LOGIN PLAIN
   581  250 8BITMIME
   582  221 OK
   583  `
   584  
   585  var newClient2Client = `EHLO localhost
   586  HELO localhost
   587  QUIT
   588  `
   589  
   590  func TestNewClientWithTLS(t *testing.T) {
   591  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
   592  	if err != nil {
   593  		t.Fatalf("loadcert: %v", err)
   594  	}
   595  
   596  	config := tls.Config{Certificates: []tls.Certificate{cert}}
   597  
   598  	ln, err := tls.Listen("tcp", "127.0.0.1:0", &config)
   599  	if err != nil {
   600  		ln, err = tls.Listen("tcp", "[::1]:0", &config)
   601  		if err != nil {
   602  			t.Fatalf("server: listen: %v", err)
   603  		}
   604  	}
   605  
   606  	go func() {
   607  		conn, err := ln.Accept()
   608  		if err != nil {
   609  			t.Errorf("server: accept: %v", err)
   610  			return
   611  		}
   612  		defer conn.Close()
   613  
   614  		_, err = conn.Write([]byte("220 SIGNS\r\n"))
   615  		if err != nil {
   616  			t.Errorf("server: write: %v", err)
   617  			return
   618  		}
   619  	}()
   620  
   621  	config.InsecureSkipVerify = true
   622  	conn, err := tls.Dial("tcp", ln.Addr().String(), &config)
   623  	if err != nil {
   624  		t.Fatalf("client: dial: %v", err)
   625  	}
   626  	defer conn.Close()
   627  
   628  	client, err := NewClient(conn, ln.Addr().String())
   629  	if err != nil {
   630  		t.Fatalf("smtp: newclient: %v", err)
   631  	}
   632  	if !client.tls {
   633  		t.Errorf("client.tls Got: %t Expected: %t", client.tls, true)
   634  	}
   635  }
   636  
   637  func TestHello(t *testing.T) {
   638  
   639  	if len(helloServer) != len(helloClient) {
   640  		t.Fatalf("Hello server and client size mismatch")
   641  	}
   642  
   643  	for i := 0; i < len(helloServer); i++ {
   644  		server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n")
   645  		client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n")
   646  		var cmdbuf strings.Builder
   647  		bcmdbuf := bufio.NewWriter(&cmdbuf)
   648  		var fake faker
   649  		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   650  		c, err := NewClient(fake, "fake.host")
   651  		if err != nil {
   652  			t.Fatalf("NewClient: %v", err)
   653  		}
   654  		defer c.Close()
   655  		c.localName = "customhost"
   656  		err = nil
   657  
   658  		switch i {
   659  		case 0:
   660  			err = c.Hello("hostinjection>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n")
   661  			if err == nil {
   662  				t.Errorf("Expected Hello to be rejected due to a message injection attempt")
   663  			}
   664  			err = c.Hello("customhost")
   665  		case 1:
   666  			err = c.StartTLS(nil)
   667  			if err.Error() == "502 Not implemented" {
   668  				err = nil
   669  			}
   670  		case 2:
   671  			err = c.Verify("test@example.com")
   672  		case 3:
   673  			c.tls = true
   674  			c.serverName = "smtp.google.com"
   675  			err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
   676  		case 4:
   677  			err = c.Mail("test@example.com")
   678  		case 5:
   679  			ok, _ := c.Extension("feature")
   680  			if ok {
   681  				t.Errorf("Expected FEATURE not to be supported")
   682  			}
   683  		case 6:
   684  			err = c.Reset()
   685  		case 7:
   686  			err = c.Quit()
   687  		case 8:
   688  			err = c.Verify("test@example.com")
   689  			if err != nil {
   690  				err = c.Hello("customhost")
   691  				if err != nil {
   692  					t.Errorf("Want error, got none")
   693  				}
   694  			}
   695  		case 9:
   696  			err = c.Noop()
   697  		default:
   698  			t.Fatalf("Unhandled command")
   699  		}
   700  
   701  		if err != nil {
   702  			t.Errorf("Command %d failed: %v", i, err)
   703  		}
   704  
   705  		bcmdbuf.Flush()
   706  		actualcmds := cmdbuf.String()
   707  		if client != actualcmds {
   708  			t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   709  		}
   710  	}
   711  }
   712  
   713  var baseHelloServer = `220 hello world
   714  502 EH?
   715  250-mx.google.com at your service
   716  250 FEATURE
   717  `
   718  
   719  var helloServer = []string{
   720  	"",
   721  	"502 Not implemented\n",
   722  	"250 User is valid\n",
   723  	"235 Accepted\n",
   724  	"250 Sender ok\n",
   725  	"",
   726  	"250 Reset ok\n",
   727  	"221 Goodbye\n",
   728  	"250 Sender ok\n",
   729  	"250 ok\n",
   730  }
   731  
   732  var baseHelloClient = `EHLO customhost
   733  HELO customhost
   734  `
   735  
   736  var helloClient = []string{
   737  	"",
   738  	"STARTTLS\n",
   739  	"VRFY test@example.com\n",
   740  	"AUTH PLAIN AHVzZXIAcGFzcw==\n",
   741  	"MAIL FROM:<test@example.com>\n",
   742  	"",
   743  	"RSET\n",
   744  	"QUIT\n",
   745  	"VRFY test@example.com\n",
   746  	"NOOP\n",
   747  }
   748  
   749  func TestSendMail(t *testing.T) {
   750  	server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n")
   751  	client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n")
   752  	var cmdbuf strings.Builder
   753  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   754  	l, err := net.Listen("tcp", "127.0.0.1:0")
   755  	if err != nil {
   756  		t.Fatalf("Unable to create listener: %v", err)
   757  	}
   758  	defer l.Close()
   759  
   760  	// prevent data race on bcmdbuf
   761  	var done = make(chan struct{})
   762  	go func(data []string) {
   763  
   764  		defer close(done)
   765  
   766  		conn, err := l.Accept()
   767  		if err != nil {
   768  			t.Errorf("Accept error: %v", err)
   769  			return
   770  		}
   771  		defer conn.Close()
   772  
   773  		tc := textproto.NewConn(conn)
   774  		for i := 0; i < len(data) && data[i] != ""; i++ {
   775  			tc.PrintfLine("%s", data[i])
   776  			for len(data[i]) >= 4 && data[i][3] == '-' {
   777  				i++
   778  				tc.PrintfLine("%s", data[i])
   779  			}
   780  			if data[i] == "221 Goodbye" {
   781  				return
   782  			}
   783  			read := false
   784  			for !read || data[i] == "354 Go ahead" {
   785  				msg, err := tc.ReadLine()
   786  				bcmdbuf.Write([]byte(msg + "\r\n"))
   787  				read = true
   788  				if err != nil {
   789  					t.Errorf("Read error: %v", err)
   790  					return
   791  				}
   792  				if data[i] == "354 Go ahead" && msg == "." {
   793  					break
   794  				}
   795  			}
   796  		}
   797  	}(strings.Split(server, "\r\n"))
   798  
   799  	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
   800  To: other@example.com
   801  Subject: SendMail test
   802  
   803  SendMail is working for me.
   804  `, "\n", "\r\n", -1)))
   805  	if err == nil {
   806  		t.Errorf("Expected SendMail to be rejected due to a message injection attempt")
   807  	}
   808  
   809  	err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
   810  To: other@example.com
   811  Subject: SendMail test
   812  
   813  SendMail is working for me.
   814  `, "\n", "\r\n", -1)))
   815  
   816  	if err != nil {
   817  		t.Errorf("%v", err)
   818  	}
   819  
   820  	<-done
   821  	bcmdbuf.Flush()
   822  	actualcmds := cmdbuf.String()
   823  	if client != actualcmds {
   824  		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   825  	}
   826  }
   827  
   828  var sendMailServer = `220 hello world
   829  502 EH?
   830  250 mx.google.com at your service
   831  250 Sender ok
   832  250 Receiver ok
   833  354 Go ahead
   834  250 Data ok
   835  221 Goodbye
   836  `
   837  
   838  var sendMailClient = `EHLO localhost
   839  HELO localhost
   840  MAIL FROM:<test@example.com>
   841  RCPT TO:<other@example.com>
   842  DATA
   843  From: test@example.com
   844  To: other@example.com
   845  Subject: SendMail test
   846  
   847  SendMail is working for me.
   848  .
   849  QUIT
   850  `
   851  
   852  func TestSendMailWithAuth(t *testing.T) {
   853  	l, err := net.Listen("tcp", "127.0.0.1:0")
   854  	if err != nil {
   855  		t.Fatalf("Unable to create listener: %v", err)
   856  	}
   857  	defer l.Close()
   858  
   859  	errCh := make(chan error)
   860  	go func() {
   861  		defer close(errCh)
   862  		conn, err := l.Accept()
   863  		if err != nil {
   864  			errCh <- fmt.Errorf("Accept: %v", err)
   865  			return
   866  		}
   867  		defer conn.Close()
   868  
   869  		tc := textproto.NewConn(conn)
   870  		tc.PrintfLine("220 hello world")
   871  		msg, err := tc.ReadLine()
   872  		if err != nil {
   873  			errCh <- fmt.Errorf("ReadLine error: %v", err)
   874  			return
   875  		}
   876  		const wantMsg = "EHLO localhost"
   877  		if msg != wantMsg {
   878  			errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg)
   879  			return
   880  		}
   881  		err = tc.PrintfLine("250 mx.google.com at your service")
   882  		if err != nil {
   883  			errCh <- fmt.Errorf("PrintfLine: %v", err)
   884  			return
   885  		}
   886  	}()
   887  
   888  	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
   889  To: other@example.com
   890  Subject: SendMail test
   891  
   892  SendMail is working for me.
   893  `, "\n", "\r\n", -1)))
   894  	if err == nil {
   895  		t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
   896  	}
   897  	if err.Error() != "smtp: server doesn't support AUTH" {
   898  		t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
   899  	}
   900  	err = <-errCh
   901  	if err != nil {
   902  		t.Fatalf("server error: %v", err)
   903  	}
   904  }
   905  
   906  func TestAuthFailed(t *testing.T) {
   907  	server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n")
   908  	client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n")
   909  	var cmdbuf strings.Builder
   910  	bcmdbuf := bufio.NewWriter(&cmdbuf)
   911  	var fake faker
   912  	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
   913  	c, err := NewClient(fake, "fake.host")
   914  	if err != nil {
   915  		t.Fatalf("NewClient: %v", err)
   916  	}
   917  	defer c.Close()
   918  
   919  	c.tls = true
   920  	c.serverName = "smtp.google.com"
   921  	err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
   922  
   923  	if err == nil {
   924  		t.Error("Auth: expected error; got none")
   925  	} else if err.Error() != "535 Invalid credentials\nplease see www.example.com" {
   926  		t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com")
   927  	}
   928  
   929  	bcmdbuf.Flush()
   930  	actualcmds := cmdbuf.String()
   931  	if client != actualcmds {
   932  		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
   933  	}
   934  }
   935  
   936  var authFailedServer = `220 hello world
   937  250-mx.google.com at your service
   938  250 AUTH LOGIN PLAIN
   939  535-Invalid credentials
   940  535 please see www.example.com
   941  221 Goodbye
   942  `
   943  
   944  var authFailedClient = `EHLO localhost
   945  AUTH PLAIN AHVzZXIAcGFzcw==
   946  *
   947  QUIT
   948  `
   949  
   950  func TestTLSClient(t *testing.T) {
   951  	if runtime.GOOS == "freebsd" || runtime.GOOS == "js" || runtime.GOOS == "wasip1" {
   952  		testenv.SkipFlaky(t, 19229)
   953  	}
   954  	ln := newLocalListener(t)
   955  	defer ln.Close()
   956  	errc := make(chan error)
   957  	go func() {
   958  		errc <- sendMail(ln.Addr().String())
   959  	}()
   960  	conn, err := ln.Accept()
   961  	if err != nil {
   962  		t.Fatalf("failed to accept connection: %v", err)
   963  	}
   964  	defer conn.Close()
   965  	if err := serverHandle(conn, t); err != nil {
   966  		t.Fatalf("failed to handle connection: %v", err)
   967  	}
   968  	if err := <-errc; err != nil {
   969  		t.Fatalf("client error: %v", err)
   970  	}
   971  }
   972  
   973  func TestTLSConnState(t *testing.T) {
   974  	ln := newLocalListener(t)
   975  	defer ln.Close()
   976  	clientDone := make(chan bool)
   977  	serverDone := make(chan bool)
   978  	go func() {
   979  		defer close(serverDone)
   980  		c, err := ln.Accept()
   981  		if err != nil {
   982  			t.Errorf("Server accept: %v", err)
   983  			return
   984  		}
   985  		defer c.Close()
   986  		if err := serverHandle(c, t); err != nil {
   987  			t.Errorf("server error: %v", err)
   988  		}
   989  	}()
   990  	go func() {
   991  		defer close(clientDone)
   992  		c, err := Dial(ln.Addr().String())
   993  		if err != nil {
   994  			t.Errorf("Client dial: %v", err)
   995  			return
   996  		}
   997  		defer c.Quit()
   998  		cfg := &tls.Config{ServerName: "example.com"}
   999  		testHookStartTLS(cfg) // set the RootCAs
  1000  		if err := c.StartTLS(cfg); err != nil {
  1001  			t.Errorf("StartTLS: %v", err)
  1002  			return
  1003  		}
  1004  		cs, ok := c.TLSConnectionState()
  1005  		if !ok {
  1006  			t.Errorf("TLSConnectionState returned ok == false; want true")
  1007  			return
  1008  		}
  1009  		if cs.Version == 0 || !cs.HandshakeComplete {
  1010  			t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs)
  1011  		}
  1012  	}()
  1013  	<-clientDone
  1014  	<-serverDone
  1015  }
  1016  
  1017  func newLocalListener(t *testing.T) net.Listener {
  1018  	ln, err := net.Listen("tcp", "127.0.0.1:0")
  1019  	if err != nil {
  1020  		ln, err = net.Listen("tcp6", "[::1]:0")
  1021  	}
  1022  	if err != nil {
  1023  		t.Fatal(err)
  1024  	}
  1025  	return ln
  1026  }
  1027  
  1028  type smtpSender struct {
  1029  	w io.Writer
  1030  }
  1031  
  1032  func (s smtpSender) send(f string) {
  1033  	s.w.Write([]byte(f + "\r\n"))
  1034  }
  1035  
  1036  // smtp server, finely tailored to deal with our own client only!
  1037  func serverHandle(c net.Conn, t *testing.T) error {
  1038  	send := smtpSender{c}.send
  1039  	send("220 127.0.0.1 ESMTP service ready")
  1040  	s := bufio.NewScanner(c)
  1041  	for s.Scan() {
  1042  		switch s.Text() {
  1043  		case "EHLO localhost":
  1044  			send("250-127.0.0.1 ESMTP offers a warm hug of welcome")
  1045  			send("250-STARTTLS")
  1046  			send("250 Ok")
  1047  		case "STARTTLS":
  1048  			send("220 Go ahead")
  1049  			keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
  1050  			if err != nil {
  1051  				return err
  1052  			}
  1053  			config := &tls.Config{Certificates: []tls.Certificate{keypair}}
  1054  			c = tls.Server(c, config)
  1055  			defer c.Close()
  1056  			return serverHandleTLS(c, t)
  1057  		default:
  1058  			t.Fatalf("unrecognized command: %q", s.Text())
  1059  		}
  1060  	}
  1061  	return s.Err()
  1062  }
  1063  
  1064  func serverHandleTLS(c net.Conn, t *testing.T) error {
  1065  	send := smtpSender{c}.send
  1066  	s := bufio.NewScanner(c)
  1067  	for s.Scan() {
  1068  		switch s.Text() {
  1069  		case "EHLO localhost":
  1070  			send("250 Ok")
  1071  		case "MAIL FROM:<joe1@example.com>":
  1072  			send("250 Ok")
  1073  		case "RCPT TO:<joe2@example.com>":
  1074  			send("250 Ok")
  1075  		case "DATA":
  1076  			send("354 send the mail data, end with .")
  1077  			send("250 Ok")
  1078  		case "Subject: test":
  1079  		case "":
  1080  		case "howdy!":
  1081  		case ".":
  1082  		case "QUIT":
  1083  			send("221 127.0.0.1 Service closing transmission channel")
  1084  			return nil
  1085  		default:
  1086  			t.Fatalf("unrecognized command during TLS: %q", s.Text())
  1087  		}
  1088  	}
  1089  	return s.Err()
  1090  }
  1091  
  1092  func init() {
  1093  	testRootCAs := x509.NewCertPool()
  1094  	testRootCAs.AppendCertsFromPEM(localhostCert)
  1095  	testHookStartTLS = func(config *tls.Config) {
  1096  		config.RootCAs = testRootCAs
  1097  	}
  1098  }
  1099  
  1100  func sendMail(hostPort string) error {
  1101  	from := "joe1@example.com"
  1102  	to := []string{"joe2@example.com"}
  1103  	return SendMail(hostPort, nil, from, to, []byte("Subject: test\n\nhowdy!"))
  1104  }
  1105  
  1106  // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
  1107  //
  1108  //	go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
  1109  //		--ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
  1110  var localhostCert = []byte(`
  1111  -----BEGIN CERTIFICATE-----
  1112  MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
  1113  EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
  1114  MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
  1115  gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa
  1116  JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30
  1117  LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw
  1118  DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
  1119  MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
  1120  AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi
  1121  NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4
  1122  n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF
  1123  tN8URjVmyEo=
  1124  -----END CERTIFICATE-----`)
  1125  
  1126  // localhostKey is the private key for localhostCert.
  1127  var localhostKey = []byte(testingKey(`
  1128  -----BEGIN RSA TESTING KEY-----
  1129  MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h
  1130  AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu
  1131  lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB
  1132  AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA
  1133  kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM
  1134  VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m
  1135  542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb
  1136  PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2
  1137  6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB
  1138  vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP
  1139  QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i
  1140  jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c
  1141  qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg==
  1142  -----END RSA TESTING KEY-----`))
  1143  
  1144  func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
  1145  

View as plain text