Compare commits

...

6 commits

Author SHA1 Message Date
9410381bfc
Merge pull request #378 from wneessen/feature/quicksend
Add QuickSend feature
2024-11-19 17:46:22 +01:00
f05654d5e5
Add exclusion rule for TLS settings in tests
Updated the .golangci.toml config to exclude gosec rule G402 in quicksend_test.go. This exclusion is intentional as these tests do not require TLS settings.
2024-11-19 17:29:37 +01:00
7ee4e47c8e
Add EchoBuffer to serverProps for capturing SMTP data
Introduced a new io.Writer field `EchoBuffer` and its associated `BufferMutex` to `serverProps`. Updated relevant test code to write SMTP transaction data to `EchoBuffer` if it is set, ensuring thread safety with `BufferMutex`.
2024-11-19 17:29:26 +01:00
d30a4a73c6
Add QuickSend function and unit tests
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.
2024-11-19 17:29:17 +01:00
25a0fb23a9
Merge pull request #377 from wneessen/dependabot/github_actions/step-security/harden-runner-2.10.2
Bump step-security/harden-runner from 2.10.1 to 2.10.2
2024-11-19 15:13:14 +01:00
dependabot[bot]
93fc646338
Bump step-security/harden-runner from 2.10.1 to 2.10.2
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.10.1 to 2.10.2.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](91182cccc0...0080882f6c)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 13:32:14 +00:00
7 changed files with 519 additions and 9 deletions

View file

@ -40,7 +40,7 @@ jobs:
TEST_PASS: ${{ secrets.TEST_PASS }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout Code
@ -73,7 +73,7 @@ jobs:
go: ['1.23']
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Setup go
@ -95,7 +95,7 @@ jobs:
cancel-in-progress: true
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout Code
@ -113,7 +113,7 @@ jobs:
cancel-in-progress: true
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Run govulncheck
@ -133,7 +133,7 @@ jobs:
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout Code
@ -178,7 +178,7 @@ jobs:
cancel-in-progress: true
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout Code
@ -204,7 +204,7 @@ jobs:
TEST_PASS: ${{ secrets.TEST_PASS }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout Code

View file

@ -45,7 +45,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit

View file

@ -35,7 +35,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit

View file

@ -72,3 +72,10 @@ text = "G505:"
linters = ["gosec"]
path = "smtp/smtp_test.go"
text = "G505:"
## These are tests which intentionally do not need any TLS settings
[[issues.exclude-rules]]
linters = ["gosec"]
path = "quicksend_test.go"
text = "G402:"

View file

@ -3665,6 +3665,8 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "
// serverProps represents the configuration properties for the SMTP server.
type serverProps struct {
BufferMutex sync.RWMutex
EchoBuffer io.Writer
FailOnAuth bool
FailOnDataInit bool
FailOnDataClose bool
@ -3754,6 +3756,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
if err != nil {
t.Logf("failed to write line: %s", err)
}
if props.EchoBuffer != nil {
props.BufferMutex.Lock()
if _, berr := props.EchoBuffer.Write([]byte(data + "\r\n")); berr != nil {
t.Errorf("failed write to echo buffer: %s", berr)
}
props.BufferMutex.Unlock()
}
_ = writer.Flush()
}
writeOK := func() {
@ -3770,6 +3779,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
break
}
time.Sleep(time.Millisecond)
if props.EchoBuffer != nil {
props.BufferMutex.Lock()
if _, berr := props.EchoBuffer.Write([]byte(data)); berr != nil {
t.Errorf("failed write to echo buffer: %s", berr)
}
props.BufferMutex.Unlock()
}
var datastring string
data = strings.TrimSpace(data)
@ -3830,6 +3846,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
t.Logf("failed to read data from connection: %s", derr)
break
}
if props.EchoBuffer != nil {
props.BufferMutex.Lock()
if _, berr := props.EchoBuffer.Write([]byte(ddata)); berr != nil {
t.Errorf("failed write to echo buffer: %s", berr)
}
props.BufferMutex.Unlock()
}
ddata = strings.TrimSpace(ddata)
if ddata == "." {
if props.FailOnDataClose {

112
quicksend.go Normal file
View file

@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"strconv"
)
type AuthData struct {
Auth bool
Username string
Password string
}
var testHookTLSConfig func() *tls.Config // nil, except for tests
// QuickSend is an all-in-one method for quickly sending simple text mails in go-mail.
//
// This method will create a new client that connects to the server at addr, switches to TLS if possible,
// authenticates with the optional AuthData provided in auth and create a new simple Msg with the provided
// subject string and message bytes as body. The message will be sent using from as sender address and will
// be delivered to every address in rcpts. QuickSend will always send as text/plain ContentType.
//
// For the SMTP authentication, if auth is not nil and AuthData.Auth is set to true, it will try to
// autodiscover the best SMTP authentication mechanism supported by the server. If auth is set to true
// but autodiscover is not able to find a suitable authentication mechanism or if the authentication
// fails, the mail delivery will fail completely.
//
// The content parameter should be an RFC 822-style email body. The lines of content should be CRLF terminated.
//
// Parameters:
// - addr: The hostname and port of the mail server, it must include a port, as in "mail.example.com:smtp".
// - auth: A AuthData pointer. If nil or if AuthData.Auth is set to false, not SMTP authentication will be performed.
// - from: The from address of the sender as string.
// - rcpts: A slice of strings of receipient addresses.
// - subject: The subject line as string.
// - content: A byte slice of the mail content
// - opts: Optional parameters for customizing the body part.
//
// Returns:
// - A pointer to the generated Msg.
// - An error if any step in the process of mail generation or delivery failed.
func QuickSend(addr string, auth *AuthData, from string, rcpts []string, subject string, content []byte) (*Msg, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("failed to split host and port from address: %w", err)
}
portnum, err := strconv.Atoi(port)
if err != nil {
return nil, fmt.Errorf("failed to convert port to int: %w", err)
}
client, err := NewClient(host, WithPort(portnum), WithTLSPolicy(TLSOpportunistic))
if err != nil {
return nil, fmt.Errorf("failed to create new client: %w", err)
}
if auth != nil && auth.Auth {
client.SetSMTPAuth(SMTPAuthAutoDiscover)
client.SetUsername(auth.Username)
client.SetPassword(auth.Password)
}
tlsConfig := client.tlsconfig
if testHookTLSConfig != nil {
tlsConfig = testHookTLSConfig()
}
if err = client.SetTLSConfig(tlsConfig); err != nil {
return nil, fmt.Errorf("failed to set TLS config: %w", err)
}
message := NewMsg()
if err = message.From(from); err != nil {
return nil, fmt.Errorf("failed to set MAIL FROM address: %w", err)
}
if err = message.To(rcpts...); err != nil {
return nil, fmt.Errorf("failed to set RCPT TO address: %w", err)
}
message.Subject(subject)
buffer := bytes.NewBuffer(content)
writeFunc := writeFuncFromBuffer(buffer)
message.SetBodyWriter(TypeTextPlain, writeFunc)
if err = client.DialAndSend(message); err != nil {
return nil, fmt.Errorf("failed to dial and send message: %w", err)
}
return message, nil
}
// NewAuthData creates a new AuthData instance with the provided username and password.
//
// This function initializes an AuthData struct with authentication enabled and sets the
// username and password fields.
//
// Parameters:
// - user: The username for authentication.
// - pass: The password for authentication.
//
// Returns:
// - A pointer to the initialized AuthData instance.
func NewAuthData(user, pass string) *AuthData {
return &AuthData{
Auth: true,
Username: user,
Password: pass,
}
}

368
quicksend_test.go Normal file
View file

@ -0,0 +1,368 @@
// 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)
}
})
}