package smtp_test import ( "bufio" "errors" "io" "io/ioutil" "log" "net" "strings" "testing" "github.com/emersion/go-smtp" ) type message struct { From string To []string Data []byte } type backend struct { messages []*message anonmsgs []*message implementLMTPData bool lmtpStatus []struct { addr string err error } lmtpStatusSync chan struct{} panicOnMail bool userErr error } func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) { if be.userErr != nil { return &session{}, be.userErr } if username != "username" || password != "password" { return nil, errors.New("Invalid username or password") } if be.implementLMTPData { return &lmtpSession{&session{backend: be}}, nil } return &session{backend: be}, nil } func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) { if be.userErr != nil { return &session{}, be.userErr } if be.implementLMTPData { return &lmtpSession{&session{backend: be, anonymous: true}}, nil } return &session{backend: be, anonymous: true}, nil } type lmtpSession struct { *session } type session struct { backend *backend anonymous bool msg *message } func (s *session) Reset() { s.msg = &message{} } func (s *session) Logout() error { return nil } func (s *session) Mail(from string, opts smtp.MailOptions) error { if s.backend.panicOnMail { panic("Everything is on fire!") } s.Reset() s.msg.From = from return nil } func (s *session) Rcpt(to string) error { s.msg.To = append(s.msg.To, to) return nil } func (s *session) Data(r io.Reader) error { if b, err := ioutil.ReadAll(r); err != nil { return err } else { s.msg.Data = b if s.anonymous { s.backend.anonmsgs = append(s.backend.anonmsgs, s.msg) } else { s.backend.messages = append(s.backend.messages, s.msg) } } return nil } func (s *session) LMTPData(r io.Reader, collector smtp.StatusCollector) error { if err := s.Data(r); err != nil { return err } for _, val := range s.backend.lmtpStatus { collector.SetStatus(val.addr, val.err) if s.backend.lmtpStatusSync != nil { s.backend.lmtpStatusSync <- struct{}{} } } return nil } type serverConfigureFunc func(*smtp.Server) var ( authDisabled = func(s *smtp.Server) { s.AuthDisabled = true } ) func testServer(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } be = new(backend) s = smtp.NewServer(be) s.Domain = "localhost" s.AllowInsecureAuth = true for _, f := range fn { f(s) } go s.Serve(l) c, err = net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal(err) } scanner = bufio.NewScanner(c) return } func testServerGreeted(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner = testServer(t, fn...) scanner.Scan() if scanner.Text() != "220 localhost ESMTP Service Ready" { t.Fatal("Invalid greeting:", scanner.Text()) } return } func testServerEhlo(t *testing.T, fn ...serverConfigureFunc) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner, caps map[string]bool) { be, s, c, scanner = testServerGreeted(t, fn...) io.WriteString(c, "EHLO localhost\r\n") scanner.Scan() if scanner.Text() != "250-Hello localhost" { t.Fatal("Invalid EHLO response:", scanner.Text()) } expectedCaps := []string{"PIPELINING", "8BITMIME"} caps = make(map[string]bool) for scanner.Scan() { s := scanner.Text() if strings.HasPrefix(s, "250 ") { caps[strings.TrimPrefix(s, "250 ")] = true break } else { if !strings.HasPrefix(s, "250-") { t.Fatal("Invalid capability response:", s) } caps[strings.TrimPrefix(s, "250-")] = true } } for _, cap := range expectedCaps { if !caps[cap] { t.Fatal("Missing capability:", cap) } } return } func TestServer_helo(t *testing.T) { _, s, c, scanner := testServerGreeted(t) defer s.Close() io.WriteString(c, "HELO localhost\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid HELO response:", scanner.Text()) } } func testServerAuthenticated(t *testing.T) (be *backend, s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { be, s, c, scanner, caps := testServerEhlo(t) if _, ok := caps["AUTH PLAIN"]; !ok { t.Fatal("AUTH PLAIN capability is missing when auth is enabled") } io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() if scanner.Text() != "334 " { t.Fatal("Invalid AUTH response:", scanner.Text()) } io.WriteString(c, "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "235 ") { t.Fatal("Invalid AUTH response:", scanner.Text()) } return } func TestServerEmptyFrom1(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } func TestServerEmptyFrom2(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:<>\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } func TestServerPanicRecover(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() s.Backend.(*backend).panicOnMail = true // Don't log panic in tests to not confuse people who run 'go test'. s.ErrorLog = log.New(ioutil.Discard, "", 0) io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "421 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } func TestServerBadESMTPVar(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: RABBIT\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } func TestServerBadSize(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: SIZE=rabbit\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } func TestServerTooBig(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: SIZE=4294967295\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } return } func TestServerEmptyTo(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } return } func TestServer(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RCPT response:", scanner.Text()) } io.WriteString(c, "DATA\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "354 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 1 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } msg := be.messages[0] if msg.From != "root@nsa.gov" { t.Fatal("Invalid mail sender:", msg.From) } if len(msg.To) != 1 || msg.To[0] != "root@gchq.gov.uk" { t.Fatal("Invalid mail recipients:", msg.To) } if string(msg.Data) != "Hey <3\n" { t.Fatal("Invalid mail data:", string(msg.Data)) } } func TestServer_authDisabled(t *testing.T) { _, s, c, scanner, caps := testServerEhlo(t, authDisabled) defer s.Close() defer c.Close() if _, ok := caps["AUTH PLAIN"]; ok { t.Fatal("AUTH PLAIN capability is present when auth is disabled") } io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() if scanner.Text() != "500 5.5.2 Syntax error, AUTH command unrecognized" { t.Fatal("Invalid AUTH response with auth disabled:", scanner.Text()) } } func TestServer_otherCommands(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() io.WriteString(c, "HELP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "502 ") { t.Fatal("Invalid HELP response:", scanner.Text()) } io.WriteString(c, "VRFY\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "252 ") { t.Fatal("Invalid VRFY response:", scanner.Text()) } io.WriteString(c, "NOOP\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid NOOP response:", scanner.Text()) } io.WriteString(c, "RSET\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid RSET response:", scanner.Text()) } io.WriteString(c, "QUIT\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "221 ") { t.Fatal("Invalid QUIT response:", scanner.Text()) } } func TestServer_tooManyInvalidCommands(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() // Let's assume XXXX is a non-existing command for i := 0; i < 4; i++ { io.WriteString(c, "XXXX\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid invalid command response:", scanner.Text()) } } scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid invalid command response:", scanner.Text()) } } func TestServer_tooLongMessage(t *testing.T) { be, s, c, scanner := testServerAuthenticated(t) defer s.Close() s.MaxMessageBytes = 50 io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "This is a very long message.\r\n") io.WriteString(c, "Much longer than you can possibly imagine.\r\n") io.WriteString(c, "And much longer than the server's MaxMessageBytes.\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "552 ") { t.Fatal("Invalid DATA response, expected an error but got:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 0 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func TestServer_tooLongLine(t *testing.T) { _, s, c, scanner := testServerAuthenticated(t) defer s.Close() io.WriteString(c, "MAIL FROM: "+strings.Repeat("A", 2000)) scanner.Scan() if !strings.HasPrefix(scanner.Text(), "500 ") { t.Fatal("Invalid response, expected an error but got:", scanner.Text()) } } func TestServer_anonymousUserError(t *testing.T) { be, s, c, scanner, _ := testServerEhlo(t) defer s.Close() defer c.Close() be.userErr = smtp.ErrAuthRequired io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if scanner.Text() != "502 5.7.0 Please authenticate first" { t.Fatal("Backend refused anonymous mail but client was permitted:", scanner.Text()) } } func TestServer_anonymousUserOK(t *testing.T) { be, s, c, scanner, _ := testServerEhlo(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: root@nsa.gov\r\n") scanner.Scan() io.WriteString(c, "RCPT TO:\r\n") scanner.Scan() io.WriteString(c, "DATA\r\n") scanner.Scan() io.WriteString(c, "Hey <3\r\n") io.WriteString(c, ".\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid DATA response:", scanner.Text()) } if len(be.messages) != 0 || len(be.anonmsgs) != 1 { t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs) } } func testStrictServer(t *testing.T) (s *smtp.Server, c net.Conn, scanner *bufio.Scanner) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } s = smtp.NewServer(new(backend)) s.Domain = "localhost" s.AllowInsecureAuth = true s.AuthDisabled = true s.Strict = true go s.Serve(l) c, err = net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal(err) } scanner = bufio.NewScanner(c) scanner.Scan() if scanner.Text() != "220 localhost ESMTP Service Ready" { t.Fatal("Invalid greeting:", scanner.Text()) } io.WriteString(c, "EHLO localhost\r\n") scanner.Scan() if scanner.Text() != "250-Hello localhost" { t.Fatal("Invalid EHLO response:", scanner.Text()) } expectedCaps := []string{"PIPELINING", "8BITMIME"} caps := make(map[string]bool) for scanner.Scan() { s := scanner.Text() if strings.HasPrefix(s, "250 ") { caps[strings.TrimPrefix(s, "250 ")] = true break } else { if !strings.HasPrefix(s, "250-") { t.Fatal("Invalid capability response:", s) } caps[strings.TrimPrefix(s, "250-")] = true } } for _, cap := range expectedCaps { if !caps[cap] { t.Fatal("Missing capability:", cap) } } return } func TestStrictServerGood(t *testing.T) { s, c, scanner := testStrictServer(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() if !strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } } func TestStrictServerBad(t *testing.T) { s, c, scanner := testStrictServer(t) defer s.Close() defer c.Close() io.WriteString(c, "MAIL FROM: root@nsa.gov\r\n") scanner.Scan() if strings.HasPrefix(scanner.Text(), "250 ") { t.Fatal("Invalid MAIL response:", scanner.Text()) } }