mirror of
https://github.com/emersion/go-smtp
synced 2026-07-04 19:28:44 +00:00
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.
624 lines
14 KiB
Go
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())
|
|
}
|
|
}
|