Refactor and extend client email tests

Refactor existing email sending tests by organizing multiple edge cases and adding robust test coverage. This includes adding checks for invalid sender/recipient addresses, handling DSN support, and ensuring proper client server interactions during failures like DATA init, DATA close, and MAIL FROM.
This commit is contained in:
Winni Neessen 2024-10-24 12:03:56 +02:00
parent 45ebcb95b3
commit 1399a3331a
Signed by: wneessen
GPG key ID: 385AC9889632126E

View file

@ -13,6 +13,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/mail"
"os" "os"
"reflect" "reflect"
"strings" "strings"
@ -2229,7 +2230,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) {
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() { go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{ if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnData: true, FailOnDataClose: true,
FeatureSet: featureSet, FeatureSet: featureSet,
ListenPort: serverPort, ListenPort: serverPort,
}); err != nil { }); err != nil {
@ -2559,7 +2560,6 @@ func TestClient_Send(t *testing.T) {
} }
func TestClient_sendSingleMsg(t *testing.T) { func TestClient_sendSingleMsg(t *testing.T) {
message := testMessage(t)
t.Run("connect and send email", func(t *testing.T) { t.Run("connect and send email", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -2577,6 +2577,8 @@ func TestClient_sendSingleMsg(t *testing.T) {
}() }()
time.Sleep(time.Millisecond * 30) time.Sleep(time.Millisecond * 30)
message := testMessage(t)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial) t.Cleanup(cancelDial)
@ -2593,6 +2595,426 @@ func TestClient_sendSingleMsg(t *testing.T) {
t.Errorf("failed to send message: %s", err) t.Errorf("failed to send message: %s", err)
} }
}) })
t.Run("server does not support 8BITMIME", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
message.SetEncoding(NoEncoding)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
})
t.Run("fail on invalid sender address", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
message.addrHeader["From"] = []*mail.Address{
{Name: "invalid", Address: "invalid"},
}
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrSMTPMailFrom {
t.Errorf("expected ErrSMTPMailFrom, got %s", sendErr.Reason)
}
})
t.Run("fail with no sender address", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
message.addrHeader["From"] = nil
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrGetSender {
t.Errorf("expected ErrGetSender, got %s", sendErr.Reason)
}
})
t.Run("fail with no recepient addresses", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
message.addrHeader["To"] = nil
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrGetRcpts {
t.Errorf("expected ErrGetRcpts, got %s", sendErr.Reason)
}
})
t.Run("connect and send email with DSN", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FeatureSet: featureSet,
ListenPort: serverPort,
SupportDSN: true,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDSN())
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err != nil {
t.Errorf("failed to send message: %s", err)
}
})
t.Run("connect and send email but fail on reset", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnReset: true,
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrSMTPReset {
t.Errorf("expected ErrSMTPReset, got %s", sendErr.Reason)
}
})
t.Run("connect and send email but with mix of valid and invalid rcpts", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnReset: true,
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
message.addrHeader["To"] = append(message.addrHeader["To"], &mail.Address{Name: "invalid", Address: "invalid"})
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrSMTPRcptTo {
t.Errorf("expected ErrSMTPRcptTo, got %s", sendErr.Reason)
}
})
t.Run("connect and send email but fail on mail to and reset", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnMailFrom: true,
FailOnReset: true,
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrSMTPMailFrom {
t.Errorf("expected ErrSMTPMailFrom, got %s", sendErr.Reason)
}
})
t.Run("connect and send email but fail on data init", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnDataInit: true,
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrSMTPData {
t.Errorf("expected ErrSMTPData, got %s", sendErr.Reason)
}
})
t.Run("connect and send email but fail on data close", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnDataClose: true,
FeatureSet: featureSet,
ListenPort: serverPort,
}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
message := testMessage(t)
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
t.Cleanup(func() {
if err := client.Close(); err != nil {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.sendSingleMsg(message); err == nil {
t.Errorf("client should have failed to send message")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected SendError, got %T", err)
}
if sendErr.Reason != ErrSMTPDataClose {
t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason)
}
})
} }
// TestClient_onlinetests will perform some additional tests on a actual live mail server. These tests are only // TestClient_onlinetests will perform some additional tests on a actual live mail server. These tests are only
@ -4126,8 +4548,10 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "
// serverProps represents the configuration properties for the SMTP server. // serverProps represents the configuration properties for the SMTP server.
type serverProps struct { type serverProps struct {
FailOnAuth bool FailOnAuth bool
FailOnData bool FailOnDataInit bool
FailOnDataClose bool
FailOnHelo bool FailOnHelo bool
FailOnMailFrom bool
FailOnQuit bool FailOnQuit bool
FailOnReset bool FailOnReset bool
FailOnSTARTTLS bool FailOnSTARTTLS bool
@ -4135,6 +4559,7 @@ type serverProps struct {
ListenPort int ListenPort int
SSLListener bool SSLListener bool
IsTLS bool IsTLS bool
SupportDSN bool
} }
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
@ -4238,9 +4663,16 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) writeLine("250-localhost.localdomain\r\n" + props.FeatureSet)
break break
case strings.HasPrefix(data, "MAIL FROM:"): case strings.HasPrefix(data, "MAIL FROM:"):
if props.FailOnMailFrom {
writeLine("500 5.5.2 Error: fail on MAIL FROM")
break
}
from := strings.TrimPrefix(data, "MAIL FROM:") from := strings.TrimPrefix(data, "MAIL FROM:")
from = strings.ReplaceAll(from, "BODY=8BITMIME", "") from = strings.ReplaceAll(from, "BODY=8BITMIME", "")
from = strings.ReplaceAll(from, "SMTPUTF8", "") from = strings.ReplaceAll(from, "SMTPUTF8", "")
if props.SupportDSN {
from = strings.ReplaceAll(from, "RET=FULL", "")
}
from = strings.TrimSpace(from) from = strings.TrimSpace(from)
if !strings.EqualFold(from, "<valid-from@domain.tld>") { if !strings.EqualFold(from, "<valid-from@domain.tld>") {
writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from))
@ -4249,6 +4681,9 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
writeOK() writeOK()
case strings.HasPrefix(data, "RCPT TO:"): case strings.HasPrefix(data, "RCPT TO:"):
to := strings.TrimPrefix(data, "RCPT TO:") to := strings.TrimPrefix(data, "RCPT TO:")
if props.SupportDSN {
to = strings.ReplaceAll(to, "NOTIFY=FAILURE,SUCCESS", "")
}
to = strings.TrimSpace(to) to = strings.TrimSpace(to)
if !strings.EqualFold(to, "<valid-to@domain.tld>") { if !strings.EqualFold(to, "<valid-to@domain.tld>") {
writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to))
@ -4262,6 +4697,10 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
} }
writeLine("235 2.7.0 Authentication successful") writeLine("235 2.7.0 Authentication successful")
case strings.EqualFold(data, "DATA"): case strings.EqualFold(data, "DATA"):
if props.FailOnDataInit {
writeLine("503 5.5.1 Error: fail on DATA init")
break
}
writeLine("354 End data with <CR><LF>.<CR><LF>") writeLine("354 End data with <CR><LF>.<CR><LF>")
for { for {
ddata, derr := reader.ReadString('\n') ddata, derr := reader.ReadString('\n')
@ -4271,7 +4710,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
} }
ddata = strings.TrimSpace(ddata) ddata = strings.TrimSpace(ddata)
if ddata == "." { if ddata == "." {
if props.FailOnData { if props.FailOnDataClose {
writeLine("500 5.0.0 Error during DATA transmission") writeLine("500 5.0.0 Error during DATA transmission")
break break
} }