Files
go-smtp/server_test.go
T
fox.cppandSimon Ser b42ac399d2 server: Implement per-recipient status for LMTP
A separate interface is used so backend implementation will be aware
of LMTP usage. This also avoids clutter in simple backend
implementations that don't support LMTP.

RFC 2033 (Section 4.2) says that we should send multiple status lines if
the same address was specified multiple times. We move responsibility of
doing this to the backend implementation and don't do any deduplication
themselves. This allows transparent forwarding for LMTP to be
implemented correctly.
2019-11-20 23:20:45 +01:00

624 lines
14 KiB
Go

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:<alice@wonderland.book>\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:<alice@wonderland.book> 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:<alice@wonderland.book> 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:<alice@wonderland.book> 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:<root@nsa.gov>\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:<root@nsa.gov>\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid MAIL response:", scanner.Text())
}
io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\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:<root@nsa.gov>\r\n")
scanner.Scan()
io.WriteString(c, "RCPT TO:<root@gchq.gov.uk>\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:<root@nsa.gov> "+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:<root@nsa.gov>\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:<root@gchq.gov.uk>\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:<root@nsa.gov>\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())
}
}