// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package smtp import ( "bufio" "bytes" "crypto/tls" "crypto/x509" "io" "net" "net/textproto" "strings" "testing" "time" ) type authTest struct { auth Auth challenges []string name string responses []string } var authTests = []authTest{ {PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}}, {PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}}, {CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}}, } func TestAuth(t *testing.T) { testLoop: for i, test := range authTests { name, resp, err := test.auth.Start(&ServerInfo{"testserver", true, nil}) if name != test.name { t.Errorf("#%d got name %s, expected %s", i, name, test.name) } if !bytes.Equal(resp, []byte(test.responses[0])) { t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0]) } if err != nil { t.Errorf("#%d error: %s", i, err) } for j := range test.challenges { challenge := []byte(test.challenges[j]) expected := []byte(test.responses[j+1]) resp, err := test.auth.Next(challenge, true) if err != nil { t.Errorf("#%d error: %s", i, err) continue testLoop } if !bytes.Equal(resp, expected) { t.Errorf("#%d got %s, expected %s", i, resp, expected) continue testLoop } } } } func TestAuthPlain(t *testing.T) { auth := PlainAuth("foo", "bar", "baz", "servername") tests := []struct { server *ServerInfo err string }{ { server: &ServerInfo{Name: "servername", TLS: true}, }, { // Okay; explicitly advertised by server. server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, }, { server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, err: "unencrypted connection", }, { server: &ServerInfo{Name: "attacker", TLS: true}, err: "wrong host name", }, } for i, tt := range tests { _, _, err := auth.Start(tt.server) got := "" if err != nil { got = err.Error() } if got != tt.err { t.Errorf("%d. got error = %q; want %q", i, got, tt.err) } } } type faker struct { io.ReadWriter } func (f faker) Close() error { return nil } func (f faker) LocalAddr() net.Addr { return nil } func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c := &Client{Text: textproto.NewConn(fake), localName: "localhost"} if err := c.helo(); err != nil { t.Fatalf("HELO failed: %s", err) } if err := c.ehlo(); err == nil { t.Fatalf("Expected first EHLO to fail") } if err := c.ehlo(); err != nil { t.Fatalf("Second EHLO failed: %s", err) } c.didHello = true if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { t.Fatalf("Expected AUTH supported") } if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } if err := c.Mail("user@gmail.com"); err == nil { t.Fatalf("MAIL should require authentication") } if err := c.Verify("user1@gmail.com"); err == nil { t.Fatalf("First VRFY: expected no verification") } if err := c.Verify("user2@gmail.com"); err != nil { t.Fatalf("Second VRFY: expected verification, got %s", err) } // fake TLS so authentication won't complain c.tls = true c.serverName = "smtp.google.com" if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil { t.Fatalf("AUTH failed: %s", err) } if err := c.Mail("user@gmail.com"); err != nil { t.Fatalf("MAIL failed: %s", err) } if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 .Leading dot line . Goodbye.` w, err := c.Data() if err != nil { t.Fatalf("DATA failed: %s", err) } if _, err := w.Write([]byte(msg)); err != nil { t.Fatalf("Data write failed: %s", err) } if err := w.Close(); err != nil { t.Fatalf("Bad data response: %s", err) } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var basicServer = `250 mx.google.com at your service 502 Unrecognized command. 250-mx.google.com at your service 250-SIZE 35651584 250-AUTH LOGIN PLAIN 250 8BITMIME 530 Authentication required 252 Send some mail, I'll try my best 250 User is valid 235 Accepted 250 Sender OK 250 Receiver OK 354 Go ahead 250 Data OK 221 OK ` var basicClient = `HELO localhost EHLO localhost EHLO localhost MAIL FROM: BODY=8BITMIME VRFY user1@gmail.com VRFY user2@gmail.com AUTH PLAIN AHVzZXIAcGFzcw== MAIL FROM: BODY=8BITMIME RCPT TO: DATA From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 ..Leading dot line . Goodbye. . QUIT ` func TestNewClient(t *testing.T) { server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) out := func() string { bcmdbuf.Flush() return cmdbuf.String() } var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v\n(after %v)", err, out()) } defer c.Close() if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { t.Fatalf("Expected AUTH supported") } if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } actualcmds := out() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var newClientServer = `220 hello world 250-mx.google.com at your service 250-SIZE 35651584 250-AUTH LOGIN PLAIN 250 8BITMIME 221 OK ` var newClientClient = `EHLO localhost QUIT ` func TestNewClient2(t *testing.T) { server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n") client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } defer c.Close() if ok, _ := c.Extension("DSN"); ok { t.Fatalf("Shouldn't support DSN") } if err := c.Quit(); err != nil { t.Fatalf("QUIT failed: %s", err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var newClient2Server = `220 hello world 502 EH? 250-mx.google.com at your service 250-SIZE 35651584 250-AUTH LOGIN PLAIN 250 8BITMIME 221 OK ` var newClient2Client = `EHLO localhost HELO localhost QUIT ` func TestHello(t *testing.T) { if len(helloServer) != len(helloClient) { t.Fatalf("Hello server and client size mismatch") } for i := 0; i < len(helloServer); i++ { server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n") client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } defer c.Close() c.localName = "customhost" err = nil switch i { case 0: err = c.Hello("customhost") case 1: err = c.StartTLS(nil) if err.Error() == "502 Not implemented" { err = nil } case 2: err = c.Verify("test@example.com") case 3: c.tls = true c.serverName = "smtp.google.com" err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")) case 4: err = c.Mail("test@example.com") case 5: ok, _ := c.Extension("feature") if ok { t.Errorf("Expected FEATURE not to be supported") } case 6: err = c.Reset() case 7: err = c.Quit() case 8: err = c.Verify("test@example.com") if err != nil { err = c.Hello("customhost") if err != nil { t.Errorf("Want error, got none") } } default: t.Fatalf("Unhandled command") } if err != nil { t.Errorf("Command %d failed: %v", i, err) } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } } var baseHelloServer = `220 hello world 502 EH? 250-mx.google.com at your service 250 FEATURE ` var helloServer = []string{ "", "502 Not implemented\n", "250 User is valid\n", "235 Accepted\n", "250 Sender ok\n", "", "250 Reset ok\n", "221 Goodbye\n", "250 Sender ok\n", } var baseHelloClient = `EHLO customhost HELO customhost ` var helloClient = []string{ "", "STARTTLS\n", "VRFY test@example.com\n", "AUTH PLAIN AHVzZXIAcGFzcw==\n", "MAIL FROM:\n", "", "RSET\n", "QUIT\n", "VRFY test@example.com\n", } func TestSendMail(t *testing.T) { server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n") client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Unable to to create listener: %v", err) } defer l.Close() // prevent data race on bcmdbuf var done = make(chan struct{}) go func(data []string) { defer close(done) conn, err := l.Accept() if err != nil { t.Errorf("Accept error: %v", err) return } defer conn.Close() tc := textproto.NewConn(conn) for i := 0; i < len(data) && data[i] != ""; i++ { tc.PrintfLine(data[i]) for len(data[i]) >= 4 && data[i][3] == '-' { i++ tc.PrintfLine(data[i]) } if data[i] == "221 Goodbye" { return } read := false for !read || data[i] == "354 Go ahead" { msg, err := tc.ReadLine() bcmdbuf.Write([]byte(msg + "\r\n")) read = true if err != nil { t.Errorf("Read error: %v", err) return } if data[i] == "354 Go ahead" && msg == "." { break } } } }(strings.Split(server, "\r\n")) err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com To: other@example.com Subject: SendMail test SendMail is working for me. `, "\n", "\r\n", -1))) if err != nil { t.Errorf("%v", err) } <-done bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var sendMailServer = `220 hello world 502 EH? 250 mx.google.com at your service 250 Sender ok 250 Receiver ok 354 Go ahead 250 Data ok 221 Goodbye ` var sendMailClient = `EHLO localhost HELO localhost MAIL FROM: RCPT TO: DATA From: test@example.com To: other@example.com Subject: SendMail test SendMail is working for me. . QUIT ` func TestAuthFailed(t *testing.T) { server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n") client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n") var cmdbuf bytes.Buffer bcmdbuf := bufio.NewWriter(&cmdbuf) var fake faker fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) c, err := NewClient(fake, "fake.host") if err != nil { t.Fatalf("NewClient: %v", err) } defer c.Close() c.tls = true c.serverName = "smtp.google.com" err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")) if err == nil { t.Error("Auth: expected error; got none") } else if err.Error() != "535 Invalid credentials\nplease see www.example.com" { t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com") } bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) } } var authFailedServer = `220 hello world 250-mx.google.com at your service 250 AUTH LOGIN PLAIN 535-Invalid credentials 535 please see www.example.com 221 Goodbye ` var authFailedClient = `EHLO localhost AUTH PLAIN AHVzZXIAcGFzcw== * QUIT ` func TestTLSClient(t *testing.T) { ln := newLocalListener(t) defer ln.Close() errc := make(chan error) go func() { errc <- sendMail(ln.Addr().String()) }() conn, err := ln.Accept() if err != nil { t.Fatalf("failed to accept connection: %v", err) } defer conn.Close() if err := serverHandle(conn, t); err != nil { t.Fatalf("failed to handle connection: %v", err) } if err := <-errc; err != nil { t.Fatalf("client error: %v", err) } } func TestTLSConnState(t *testing.T) { ln := newLocalListener(t) defer ln.Close() clientDone := make(chan bool) serverDone := make(chan bool) go func() { defer close(serverDone) c, err := ln.Accept() if err != nil { t.Errorf("Server accept: %v", err) return } defer c.Close() if err := serverHandle(c, t); err != nil { t.Errorf("server error: %v", err) } }() go func() { defer close(clientDone) c, err := Dial(ln.Addr().String()) if err != nil { t.Errorf("Client dial: %v", err) return } defer c.Quit() cfg := &tls.Config{ServerName: "example.com"} testHookStartTLS(cfg) // set the RootCAs if err := c.StartTLS(cfg); err != nil { t.Errorf("StartTLS: %v", err) return } cs, ok := c.TLSConnectionState() if !ok { t.Errorf("TLSConnectionState returned ok == false; want true") return } if cs.Version == 0 || !cs.HandshakeComplete { t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs) } }() <-clientDone <-serverDone } func newLocalListener(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { ln, err = net.Listen("tcp6", "[::1]:0") } if err != nil { t.Fatal(err) } return ln } type smtpSender struct { w io.Writer } func (s smtpSender) send(f string) { s.w.Write([]byte(f + "\r\n")) } // smtp server, finely tailored to deal with our own client only! func serverHandle(c net.Conn, t *testing.T) error { send := smtpSender{c}.send send("220 127.0.0.1 ESMTP service ready") s := bufio.NewScanner(c) for s.Scan() { switch s.Text() { case "EHLO localhost": send("250-127.0.0.1 ESMTP offers a warm hug of welcome") send("250-STARTTLS") send("250 Ok") case "STARTTLS": send("220 Go ahead") keypair, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { return err } config := &tls.Config{Certificates: []tls.Certificate{keypair}} c = tls.Server(c, config) defer c.Close() return serverHandleTLS(c, t) default: t.Fatalf("unrecognized command: %q", s.Text()) } } return s.Err() } func serverHandleTLS(c net.Conn, t *testing.T) error { send := smtpSender{c}.send s := bufio.NewScanner(c) for s.Scan() { switch s.Text() { case "EHLO localhost": send("250 Ok") case "MAIL FROM:": send("250 Ok") case "RCPT TO:": send("250 Ok") case "DATA": send("354 send the mail data, end with .") send("250 Ok") case "Subject: test": case "": case "howdy!": case ".": case "QUIT": send("221 127.0.0.1 Service closing transmission channel") return nil default: t.Fatalf("unrecognized command during TLS: %q", s.Text()) } } return s.Err() } func init() { testRootCAs := x509.NewCertPool() testRootCAs.AppendCertsFromPEM(localhostCert) testHookStartTLS = func(config *tls.Config) { config.RootCAs = testRootCAs } } func sendMail(hostPort string) error { host, _, err := net.SplitHostPort(hostPort) if err != nil { return err } auth := PlainAuth("", "", "", host) from := "joe1@example.com" to := []string{"joe2@example.com"} return SendMail(hostPort, auth, from, to, []byte("Subject: test\n\nhowdy!")) } // (copied from net/http/httptest) // localhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end // of ASN.1 time). // generated from src/crypto/tls: // go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var localhostCert = []byte(`-----BEGIN CERTIFICATE----- MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA== -----END CERTIFICATE-----`) // localhostKey is the private key for localhostCert. var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0 0IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA 1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE= -----END RSA PRIVATE KEY-----`)