mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 18:50:37 +01:00
Winni Neessen
d30a4a73c6
Introduce the QuickSend function for sending emails quickly with TLS and optional SMTP authentication. Added comprehensive unit tests to ensure QuickSend works correctly with different authentication mechanisms and handles various error scenarios.
368 lines
12 KiB
Go
368 lines
12 KiB
Go
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewAuthData(t *testing.T) {
|
|
t.Run("AuthData with username and password", func(t *testing.T) {
|
|
auth := NewAuthData("username", "password")
|
|
if !auth.Auth {
|
|
t.Fatal("expected auth to be true")
|
|
}
|
|
if auth.Username != "username" {
|
|
t.Fatalf("expected username to be %s, got %s", "username", auth.Username)
|
|
}
|
|
if auth.Password != "password" {
|
|
t.Fatalf("expected password to be %s, got %s", "password", auth.Password)
|
|
}
|
|
})
|
|
t.Run("AuthData with username and empty password", func(t *testing.T) {
|
|
auth := NewAuthData("username", "")
|
|
if !auth.Auth {
|
|
t.Fatal("expected auth to be true")
|
|
}
|
|
if auth.Username != "username" {
|
|
t.Fatalf("expected username to be %s, got %s", "username", auth.Username)
|
|
}
|
|
if auth.Password != "" {
|
|
t.Fatalf("expected password to be %s, got %s", "", auth.Password)
|
|
}
|
|
})
|
|
t.Run("AuthData with empty username and set password", func(t *testing.T) {
|
|
auth := NewAuthData("", "password")
|
|
if !auth.Auth {
|
|
t.Fatal("expected auth to be true")
|
|
}
|
|
if auth.Username != "" {
|
|
t.Fatalf("expected username to be %s, got %s", "", auth.Username)
|
|
}
|
|
if auth.Password != "password" {
|
|
t.Fatalf("expected password to be %s, got %s", "password", auth.Password)
|
|
}
|
|
})
|
|
t.Run("AuthData with empty data", func(t *testing.T) {
|
|
auth := NewAuthData("", "")
|
|
if !auth.Auth {
|
|
t.Fatal("expected auth to be true")
|
|
}
|
|
if auth.Username != "" {
|
|
t.Fatalf("expected username to be %s, got %s", "", auth.Username)
|
|
}
|
|
if auth.Password != "" {
|
|
t.Fatalf("expected password to be %s, got %s", "", auth.Password)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestQuickSend(t *testing.T) {
|
|
subject := "This is a test subject"
|
|
body := []byte("This is a test body\r\nWith multiple lines\r\n\r\nBest,\r\n The go-mail team")
|
|
sender := TestSenderValid
|
|
rcpts := []string{TestRcptValid}
|
|
t.Run("QuickSend with authentication and TLS", func(t *testing.T) {
|
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
|
defer cancelAuth()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
|
echoBuffer := bytes.NewBuffer(nil)
|
|
props := &serverProps{
|
|
EchoBuffer: echoBuffer,
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 30)
|
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
|
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err != nil {
|
|
t.Fatalf("failed to send email: %s", err)
|
|
}
|
|
|
|
props.BufferMutex.RLock()
|
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
|
props.BufferMutex.RUnlock()
|
|
|
|
expects := []struct {
|
|
line int
|
|
data string
|
|
}{
|
|
{8, "STARTTLS"},
|
|
{17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"},
|
|
{21, "MAIL FROM:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8"},
|
|
{23, "RCPT TO:<valid-to@domain.tld>"},
|
|
{30, "Subject: " + subject},
|
|
{33, "From: <valid-from@domain.tld>"},
|
|
{34, "To: <valid-to@domain.tld>"},
|
|
{35, "Content-Type: text/plain; charset=UTF-8"},
|
|
{36, "Content-Transfer-Encoding: quoted-printable"},
|
|
{38, "This is a test body"},
|
|
{39, "With multiple lines"},
|
|
{40, ""},
|
|
{41, "Best,"},
|
|
{42, " The go-mail team"},
|
|
}
|
|
for _, expect := range expects {
|
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
|
}
|
|
}
|
|
})
|
|
t.Run("QuickSend with authentication and TLS and multiple receipients", func(t *testing.T) {
|
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
|
defer cancelAuth()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
|
echoBuffer := bytes.NewBuffer(nil)
|
|
props := &serverProps{
|
|
EchoBuffer: echoBuffer,
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 30)
|
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
|
|
|
multiRcpts := []string{TestRcptValid, TestRcptValid, TestRcptValid}
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, multiRcpts, subject, body)
|
|
if err != nil {
|
|
t.Fatalf("failed to send email: %s", err)
|
|
}
|
|
|
|
props.BufferMutex.RLock()
|
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
|
props.BufferMutex.RUnlock()
|
|
|
|
expects := []struct {
|
|
line int
|
|
data string
|
|
}{
|
|
{8, "STARTTLS"},
|
|
{17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"},
|
|
{21, "MAIL FROM:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8"},
|
|
{23, "RCPT TO:<valid-to@domain.tld>"},
|
|
{25, "RCPT TO:<valid-to@domain.tld>"},
|
|
{27, "RCPT TO:<valid-to@domain.tld>"},
|
|
{34, "Subject: " + subject},
|
|
{37, "From: <valid-from@domain.tld>"},
|
|
{38, "To: <valid-to@domain.tld>, <valid-to@domain.tld>, <valid-to@domain.tld>"},
|
|
{39, "Content-Type: text/plain; charset=UTF-8"},
|
|
{40, "Content-Transfer-Encoding: quoted-printable"},
|
|
{42, "This is a test body"},
|
|
{43, "With multiple lines"},
|
|
{44, ""},
|
|
{45, "Best,"},
|
|
{46, " The go-mail team"},
|
|
}
|
|
for _, expect := range expects {
|
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
|
}
|
|
}
|
|
})
|
|
t.Run("QuickSend uses stronged authentication method", func(t *testing.T) {
|
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
|
defer cancelAuth()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
|
echoBuffer := bytes.NewBuffer(nil)
|
|
props := &serverProps{
|
|
EchoBuffer: echoBuffer,
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 30)
|
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
|
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err != nil {
|
|
t.Fatalf("failed to send email: %s", err)
|
|
}
|
|
|
|
props.BufferMutex.RLock()
|
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
|
props.BufferMutex.RUnlock()
|
|
|
|
expects := []struct {
|
|
line int
|
|
data string
|
|
}{
|
|
{17, "AUTH SCRAM-SHA-256-PLUS"},
|
|
}
|
|
for _, expect := range expects {
|
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
|
}
|
|
}
|
|
})
|
|
t.Run("QuickSend uses stronged authentication method without TLS", func(t *testing.T) {
|
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
|
defer cancelAuth()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
echoBuffer := bytes.NewBuffer(nil)
|
|
props := &serverProps{
|
|
EchoBuffer: echoBuffer,
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 30)
|
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
|
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err != nil {
|
|
t.Fatalf("failed to send email: %s", err)
|
|
}
|
|
|
|
props.BufferMutex.RLock()
|
|
resp := strings.Split(echoBuffer.String(), "\r\n")
|
|
props.BufferMutex.RUnlock()
|
|
|
|
expects := []struct {
|
|
line int
|
|
data string
|
|
}{
|
|
{7, "AUTH SCRAM-SHA-256"},
|
|
}
|
|
for _, expect := range expects {
|
|
if !strings.EqualFold(resp[expect.line], expect.data) {
|
|
t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line])
|
|
}
|
|
}
|
|
})
|
|
t.Run("QuickSend fails during DialAndSned", func(t *testing.T) {
|
|
ctxAuth, cancelAuth := context.WithCancel(context.Background())
|
|
defer cancelAuth()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
props := &serverProps{
|
|
FailOnMailFrom: true,
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxAuth, t, props); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 30)
|
|
addr := TestServerAddr + ":" + fmt.Sprint(serverPort)
|
|
testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
|
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err == nil {
|
|
t.Error("expected QuickSend to fail during DialAndSend")
|
|
}
|
|
expect := `failed to dial and send message: send failed: sending SMTP MAIL FROM command: 500 ` +
|
|
`5.5.2 Error: fail on MAIL FROM`
|
|
if !strings.EqualFold(err.Error(), expect) {
|
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
|
}
|
|
})
|
|
t.Run("QuickSend fails on server address without port", func(t *testing.T) {
|
|
addr := TestServerAddr
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err == nil {
|
|
t.Error("expected QuickSend to fail with invalid server address")
|
|
}
|
|
expect := "failed to split host and port from address: address 127.0.0.1: missing port in address"
|
|
if !strings.Contains(err.Error(), expect) {
|
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
|
}
|
|
})
|
|
t.Run("QuickSend fails on server address with invalid port", func(t *testing.T) {
|
|
addr := TestServerAddr + ":invalid"
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err == nil {
|
|
t.Error("expected QuickSend to fail with invalid server port")
|
|
}
|
|
expect := `failed to convert port to int: strconv.Atoi: parsing "invalid": invalid syntax`
|
|
if !strings.Contains(err.Error(), expect) {
|
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
|
}
|
|
})
|
|
t.Run("QuickSend fails on nil TLS config (test hook only)", func(t *testing.T) {
|
|
addr := TestServerAddr + ":587"
|
|
testHookTLSConfig = func() *tls.Config { return nil }
|
|
defer func() {
|
|
testHookTLSConfig = nil
|
|
}()
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body)
|
|
if err == nil {
|
|
t.Error("expected QuickSend to fail with nil-tlsConfig")
|
|
}
|
|
expect := `failed to set TLS config: invalid TLS config`
|
|
if !strings.Contains(err.Error(), expect) {
|
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
|
}
|
|
})
|
|
t.Run("QuickSend fails with invalid from address", func(t *testing.T) {
|
|
addr := TestServerAddr + ":587"
|
|
invalid := "invalid-fromdomain.tld"
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), invalid, rcpts, subject, body)
|
|
if err == nil {
|
|
t.Error("expected QuickSend to fail with invalid from address")
|
|
}
|
|
expect := `failed to set MAIL FROM address: failed to parse mail address "invalid-fromdomain.tld": ` +
|
|
`mail: missing '@' or angle-addr`
|
|
if !strings.Contains(err.Error(), expect) {
|
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
|
}
|
|
})
|
|
t.Run("QuickSend fails with invalid from address", func(t *testing.T) {
|
|
addr := TestServerAddr + ":587"
|
|
invalid := []string{"invalid-todomain.tld"}
|
|
_, err := QuickSend(addr, NewAuthData("username", "password"), sender, invalid, subject, body)
|
|
if err == nil {
|
|
t.Error("expected QuickSend to fail with invalid to address")
|
|
}
|
|
expect := `failed to set RCPT TO address: failed to parse mail address "invalid-todomain.tld": ` +
|
|
`mail: missing '@' or angle-add`
|
|
if !strings.Contains(err.Error(), expect) {
|
|
t.Errorf("expected error to contain %s, got %s", expect, err)
|
|
}
|
|
})
|
|
}
|