123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- // 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:<user@gmail.com> BODY=8BITMIME
- VRFY user1@gmail.com
- VRFY user2@gmail.com
- AUTH PLAIN AHVzZXIAcGFzcw==
- MAIL FROM:<user@gmail.com> BODY=8BITMIME
- RCPT TO:<golang-nuts@googlegroups.com>
- 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:<test@example.com>\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:<test@example.com>
- RCPT TO:<other@example.com>
- 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 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:<joe1@example.com>":
- send("250 Ok")
- case "RCPT TO:<joe2@example.com>":
- 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-----`)
|