From fbebcf96d80dcf2fa99985b7b727fef5250b3055 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 14:48:24 +0200 Subject: [PATCH 01/11] Add simple SMTP test server for unit testing Implemented a simple SMTP test server to facilitate unit testing. This server listens on a specified address and port, accepts connections, and processes common SMTP commands like HELO, MAIL FROM, RCPT TO, DATA, and QUIT. Several constants related to the server configuration were also added to the test file. --- client_test.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index efccea4..31b68bb 100644 --- a/client_test.go +++ b/client_test.go @@ -5,6 +5,7 @@ package mail import ( + "bufio" "context" "crypto/tls" "errors" @@ -21,11 +22,18 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// DefaultHost is used as default hostname for the Client -const DefaultHost = "localhost" - -// TestRcpt -const TestRcpt = "go-mail@mytrashmailer.com" +const ( + // DefaultHost is used as default hostname for the Client + DefaultHost = "localhost" + // TestRcpt is a trash mail address to send test mails to + TestRcpt = "go-mail@mytrashmailer.com" + // TestServerProto is the protocol used for the simple SMTP test server + TestServerProto = "tcp" + // TestServerAddr is the address the simple SMTP test server listens on + TestServerAddr = "127.0.0.1" + // TestServerPort is the port the simple SMTP test server listens on + TestServerPort = 2526 +) // TestNewClient tests the NewClient() method with its default options func TestNewClient(t *testing.T) { @@ -1545,3 +1553,141 @@ func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } + +func simpleSMTPServer(ctx context.Context, featureSet string) error { + listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) + if err != nil { + return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) + } + + defer func() { + fmt.Printf("closing listener\n") + if err := listener.Close(); err != nil { + fmt.Printf("unable to close listener: %s\n", err) + } + }() + + for { + select { + case <-ctx.Done(): + return nil + default: + connection, err := listener.Accept() + var opErr *net.OpError + if err != nil { + if errors.As(err, &opErr) && opErr.Temporary() { + continue + } + return fmt.Errorf("unable to accept connection: %w", err) + } + handleTestServerConnection(connection, featureSet) + } + } +} + +func handleTestServerConnection(connection net.Conn, featureSet string) { + defer func() { + if err := connection.Close(); err != nil { + fmt.Printf("unable to close connection: %s\n", err) + } + }() + + reader := bufio.NewReader(connection) + writer := bufio.NewWriter(connection) + + writeLine := func(data string) error { + _, err := writer.WriteString(data + "\r\n") + if err != nil { + return fmt.Errorf("unable to write line: %w", err) + } + return writer.Flush() + } + writeOK := func() { + _ = writeLine("250 2.0.0 OK") + } + + if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { + fmt.Printf("unable to write to client: %s\n", err) + return + } + + data, err := reader.ReadString('\n') + if err != nil { + fmt.Printf("unable to read from connection: %s\n", err) + } + if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { + fmt.Printf("expected EHLO, got %q", data) + os.Exit(1) + } + if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { + fmt.Printf("unable to write to connection: %s\n", err) + return + } + + for { + data, err = reader.ReadString('\n') + if err != nil { + if err == io.EOF { + fmt.Println("Connection closed by client.") + break + } + fmt.Println("Error reading data:", err) + break + } + + var datastring string + data = strings.TrimSpace(data) + switch { + case strings.HasPrefix(data, "MAIL FROM:"): + from := strings.TrimPrefix(data, "MAIL FROM:") + from = strings.ReplaceAll(from, "BODY=8BITMIME", "") + from = strings.ReplaceAll(from, "SMTPUTF8", "") + from = strings.TrimSpace(from) + if !strings.EqualFold(from, "") { + _ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) + break + } + writeOK() + case strings.HasPrefix(data, "RCPT TO:"): + to := strings.TrimPrefix(data, "RCPT TO:") + if !strings.EqualFold(to, "") { + _ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) + break + } + writeOK() + case strings.HasPrefix(data, "AUTH PLAIN"): + auth := strings.TrimPrefix(data, "AUTH PLAIN ") + if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") { + _ = writeLine("535 5.7.8 Error: authentication failed") + break + } + _ = writeLine("235 2.7.0 Authentication successful") + case strings.EqualFold(data, "DATA"): + _ = writeLine("354 End data with .") + for { + ddata, derr := reader.ReadString('\n') + if derr != nil { + fmt.Printf("failed to read DATA data from connection: %s\n", derr) + break + } + ddata = strings.TrimSpace(ddata) + if ddata == "." { + _ = writeLine("250 2.0.0 Ok: queued as 1234567890") + break + } + datastring += ddata + "\n" + } + case strings.EqualFold(data, "noop"), + strings.EqualFold(data, "rset"), + strings.EqualFold(data, "vrfy"): + writeOK() + break + case strings.EqualFold(data, "quit"): + _ = writeLine("221 2.0.0 Bye") + break + default: + _ = writeLine("500 5.5.2 Error: bad syntax") + } + fmt.Printf("DATA received: %s", datastring) + } +} From 0aa24c6f3af5d1716d801cbd993258cbd9e08059 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:05:26 +0200 Subject: [PATCH 02/11] Add methods to retrieve message ID and message from SendError Implement MessageID and Msg methods in SendError to allow retrieval of the message ID and the affected message, respectively. These methods handle cases where the error or the message is nil, returning an empty string or nil as appropriate. --- senderror.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/senderror.go b/senderror.go index 494bfd3..90c8c4a 100644 --- a/senderror.go +++ b/senderror.go @@ -118,6 +118,23 @@ func (e *SendError) IsTemp() bool { return e.isTemp } +// MessageID returns the message ID of the affected Msg that caused the error +// If no message ID was set for the Msg, an empty string will be returned +func (e *SendError) MessageID() string { + if e == nil || e.affectedMsg == nil { + return "" + } + return e.affectedMsg.GetMessageID() +} + +// Msg returns the pointer to the affected message that caused the error +func (e *SendError) Msg() *Msg { + if e == nil || e.affectedMsg == nil { + return nil + } + return e.affectedMsg +} + // String implements the Stringer interface for the SendErrReason func (r SendErrReason) String() string { switch r { From 6af6a28f78973c480a853e080b17507f386f7895 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:05:43 +0200 Subject: [PATCH 03/11] Add test for SendError with no encoding Introduced `TestClient_SendErrorNoEncoding` to verify client behavior when sending a message without encoding. Adjusted server connection handling for better error reporting and connection closure, replacing abrupt exits with returns where appropriate. --- client_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index 31b68bb..c1706bf 100644 --- a/client_test.go +++ b/client_test.go @@ -1259,6 +1259,72 @@ func TestClient_DialAndSendWithContext_withSendError(t *testing.T) { } } +func TestClient_SendErrorNoEncoding(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("invalid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("invalid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + message.SetEncoding(NoEncoding) + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrNoUnencoded { + t.Errorf("expected ErrNoUnencoded error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if sendErr.Msg() == nil { + t.Errorf("expected message to be set, but got nil") + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { @@ -1561,9 +1627,9 @@ func simpleSMTPServer(ctx context.Context, featureSet string) error { } defer func() { - fmt.Printf("closing listener\n") if err := listener.Close(); err != nil { fmt.Printf("unable to close listener: %s\n", err) + os.Exit(1) } }() @@ -1614,10 +1680,11 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { data, err := reader.ReadString('\n') if err != nil { fmt.Printf("unable to read from connection: %s\n", err) + return } if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { fmt.Printf("expected EHLO, got %q", data) - os.Exit(1) + return } if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { fmt.Printf("unable to write to connection: %s\n", err) @@ -1628,7 +1695,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { data, err = reader.ReadString('\n') if err != nil { if err == io.EOF { - fmt.Println("Connection closed by client.") break } fmt.Println("Error reading data:", err) @@ -1688,6 +1754,5 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { default: _ = writeLine("500 5.5.2 Error: bad syntax") } - fmt.Printf("DATA received: %s", datastring) } } From b8f0462ce389820f2ef0c571208992ac685a2f6b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:49:03 +0200 Subject: [PATCH 04/11] Add error handling tests for SMTP client Implemented multiple tests to cover various error scenarios in the SMTP client, including invalid email addresses and data transmission failures. Introduced `failReset` flag in `simpleSMTPServer` to simulate server reset failures. --- client_test.go | 374 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 365 insertions(+), 9 deletions(-) diff --git a/client_test.go b/client_test.go index c1706bf..d1e269a 100644 --- a/client_test.go +++ b/client_test.go @@ -1265,7 +1265,7 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1273,11 +1273,11 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled message := NewMsg() - if err := message.From("invalid-from@domain.tld"); err != nil { + if err := message.From("valid-from@domain.tld"); err != nil { t.Errorf("failed to set FROM address: %s", err) return } - if err := message.To("invalid-to@domain.tld"); err != nil { + if err := message.To("valid-to@domain.tld"); err != nil { t.Errorf("failed to set TO address: %s", err) return } @@ -1325,6 +1325,344 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { } } +func TestClient_SendErrorMailFrom(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("invalid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPMailFrom { + t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if sendErr.Msg() == nil { + t.Errorf("expected message to be set, but got nil") + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorMailFromReset(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("invalid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPMailFrom { + t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if len(sendErr.errlist) != 2 { + t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist)) + return + } + if !strings.EqualFold(sendErr.errlist[0].Error(), "503 5.1.2 Invalid from: ") { + t.Errorf("expected error: %q, but got %q", + "503 5.1.2 Invalid from: ", sendErr.errlist[0].Error()) + } + if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") { + t.Errorf("expected error: %q, but got %q", + "500 5.1.2 Error: reset failed", sendErr.errlist[1].Error()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorToReset(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("invalid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPRcptTo { + t.Errorf("expected ErrSMTPRcptTo error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if len(sendErr.errlist) != 2 { + t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist)) + return + } + if !strings.EqualFold(sendErr.errlist[0].Error(), "500 5.1.2 Invalid to: ") { + t.Errorf("expected error: %q, but got %q", + "500 5.1.2 Invalid to: ", sendErr.errlist[0].Error()) + } + if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") { + t.Errorf("expected error: %q, but got %q", + "500 5.1.2 Error: reset failed", sendErr.errlist[1].Error()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorDataClose(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "DATA close should fail") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPDataClose { + t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorDataWrite(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "DATA write should fail") + message.SetMessageIDWithValue("this.is.a.message.id") + message.SetGenHeader("X-Test-Header", "DATA write should fail") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPDataClose { + t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { @@ -1620,7 +1958,10 @@ func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } -func simpleSMTPServer(ctx context.Context, featureSet string) error { +// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. +// The provided featureSet represents in what the server responds to EHLO command +// failReset controls if a RSET succeeds +func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool) error { listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) if err != nil { return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) @@ -1646,12 +1987,12 @@ func simpleSMTPServer(ctx context.Context, featureSet string) error { } return fmt.Errorf("unable to accept connection: %w", err) } - handleTestServerConnection(connection, featureSet) + handleTestServerConnection(connection, featureSet, failReset) } } } -func handleTestServerConnection(connection net.Conn, featureSet string) { +func handleTestServerConnection(connection net.Conn, featureSet string, failReset bool) { defer func() { if err := connection.Close(); err != nil { fmt.Printf("unable to close connection: %s\n", err) @@ -1709,14 +2050,15 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { from = strings.ReplaceAll(from, "BODY=8BITMIME", "") from = strings.ReplaceAll(from, "SMTPUTF8", "") from = strings.TrimSpace(from) - if !strings.EqualFold(from, "") { + if !strings.EqualFold(from, "") { _ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) break } writeOK() case strings.HasPrefix(data, "RCPT TO:"): to := strings.TrimPrefix(data, "RCPT TO:") - if !strings.EqualFold(to, "") { + to = strings.TrimSpace(to) + if !strings.EqualFold(to, "") { _ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) break } @@ -1737,17 +2079,31 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { break } ddata = strings.TrimSpace(ddata) + if strings.EqualFold(ddata, "DATA write should fail") { + _ = writeLine("500 5.0.0 Error during DATA transmission") + break + } if ddata == "." { + if strings.Contains(datastring, "DATA close should fail") { + _ = writeLine("500 5.0.0 Error during DATA closing") + break + } _ = writeLine("250 2.0.0 Ok: queued as 1234567890") break } datastring += ddata + "\n" } case strings.EqualFold(data, "noop"), - strings.EqualFold(data, "rset"), strings.EqualFold(data, "vrfy"): writeOK() break + case strings.EqualFold(data, "rset"): + if failReset { + _ = writeLine("500 5.1.2 Error: reset failed") + break + } + writeOK() + break case strings.EqualFold(data, "quit"): _ = writeLine("221 2.0.0 Bye") break From 482194b4b345dca362fc29d321814b970cf74eeb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:52:05 +0200 Subject: [PATCH 05/11] Add TestClient_SendErrorReset to validate SMTP error handling This test checks the client's ability to handle SMTP reset errors when sending an email. It verifies the correct error type, ensures it is recognized as a permanent error, and confirms the correct message ID handling. --- client_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/client_test.go b/client_test.go index d1e269a..bfa799f 100644 --- a/client_test.go +++ b/client_test.go @@ -1663,6 +1663,68 @@ func TestClient_SendErrorDataWrite(t *testing.T) { } } +func TestClient_SendErrorReset(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPReset { + t.Errorf("expected ErrSMTPReset error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { From 157c1381426d5ab6459195b5b169c9e4a1c2e8f4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:58:51 +0200 Subject: [PATCH 06/11] Fix linter errors Replaced `err == io.EOF` with `errors.Is(err, io.EOF)` for better error comparison. Removed redundant `break` statements to streamline case logic and improve code readability. --- client_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index bfa799f..afd9cd9 100644 --- a/client_test.go +++ b/client_test.go @@ -2097,7 +2097,7 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese for { data, err = reader.ReadString('\n') if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } fmt.Println("Error reading data:", err) @@ -2158,17 +2158,14 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese case strings.EqualFold(data, "noop"), strings.EqualFold(data, "vrfy"): writeOK() - break case strings.EqualFold(data, "rset"): if failReset { _ = writeLine("500 5.1.2 Error: reset failed") break } writeOK() - break case strings.EqualFold(data, "quit"): _ = writeLine("221 2.0.0 Bye") - break default: _ = writeLine("500 5.5.2 Error: bad syntax") } From d5437f6b7aa44ae2a2689c5eba691c560bb45879 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:23:31 +0200 Subject: [PATCH 07/11] Remove redundant comments from sleep statements Removed unnecessary comments that were clarifying the purpose of sleep statements in the test cases. This makes the code cleaner and easier to maintain by reducing clutter. --- client_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index afd9cd9..56eb8c5 100644 --- a/client_test.go +++ b/client_test.go @@ -1270,7 +1270,7 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("valid-from@domain.tld"); err != nil { @@ -1336,7 +1336,7 @@ func TestClient_SendErrorMailFrom(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("invalid-from@domain.tld"); err != nil { @@ -1401,7 +1401,7 @@ func TestClient_SendErrorMailFromReset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("invalid-from@domain.tld"); err != nil { @@ -1475,7 +1475,7 @@ func TestClient_SendErrorToReset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("valid-from@domain.tld"); err != nil { From 8dfb121aec21774ed6c764c67680c1f57bbbd758 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:24:59 +0200 Subject: [PATCH 08/11] Update Go versions in GitHub Actions workflow Removed Go 1.21 and added Go 1.19 in the codecov.yml file to ensure compatibility with older projects and streamline the CI process. This helps in maintaining backward compatibility and avoids potential issues with unsupported Go versions. --- .github/workflows/codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2178e32..9ab2f52 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -36,7 +36,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: ['1.20', '1.21', '1.22', '1.23'] + go: ['1.19', '1.20', '1.23'] steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 From bd5a8a40b9b7738df74e857f6c598e992f46fd3a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:40:12 +0200 Subject: [PATCH 09/11] Fix error handling in Send method in client_119.go Refine Send method to correctly typecast and accumulate SendError instances. Introduce "errors" package import to utilize errors.As for precise error type checking, ensuring accurate error lists. This regression was introduced with PR #301 --- client_119.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client_119.go b/client_119.go index 8084913..7de5d59 100644 --- a/client_119.go +++ b/client_119.go @@ -7,16 +7,22 @@ package mail +import "errors" + // Send sends out the mail message func (c *Client) Send(messages ...*Msg) error { if err := c.checkConn(); err != nil { return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} } var errs []*SendError - for _, message := range messages { + for id, message := range messages { if sendErr := c.sendSingleMsg(message); sendErr != nil { messages[id].sendError = sendErr - errs = append(errs, sendErr) + + var msgSendErr *SendError + if errors.As(sendErr, &msgSendErr) { + errs = append(errs, msgSendErr) + } } } From 4ee11e840606a6f99e08cbaef45e511d7a768f88 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:44:15 +0200 Subject: [PATCH 10/11] Refactor SMTP server port handling in tests Modified tests to dynamically compute server ports from a base value, enhancing flexibility and preventing potential conflicts. Updated `simpleSMTPServer` function to accept a port parameter. --- client_test.go | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/client_test.go b/client_test.go index 56eb8c5..3d50a2d 100644 --- a/client_test.go +++ b/client_test.go @@ -31,8 +31,8 @@ const ( TestServerProto = "tcp" // TestServerAddr is the address the simple SMTP test server listens on TestServerAddr = "127.0.0.1" - // TestServerPort is the port the simple SMTP test server listens on - TestServerPort = 2526 + // TestServerPortBase is the base port for the simple SMTP test server + TestServerPortBase = 2025 ) // TestNewClient tests the NewClient() method with its default options @@ -1264,8 +1264,9 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { defer cancel() featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" + serverPort := TestServerPortBase + 1 go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1286,7 +1287,7 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { message.SetMessageIDWithValue("this.is.a.message.id") message.SetEncoding(NoEncoding) - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1329,9 +1330,10 @@ func TestClient_SendErrorMailFrom(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 2 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1351,7 +1353,7 @@ func TestClient_SendErrorMailFrom(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1394,9 +1396,10 @@ func TestClient_SendErrorMailFromReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 3 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1416,7 +1419,7 @@ func TestClient_SendErrorMailFromReset(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1468,9 +1471,10 @@ func TestClient_SendErrorToReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 4 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1490,7 +1494,7 @@ func TestClient_SendErrorToReset(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1542,9 +1546,10 @@ func TestClient_SendErrorDataClose(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 5 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1564,7 +1569,7 @@ func TestClient_SendErrorDataClose(t *testing.T) { message.SetBodyString(TypeTextPlain, "DATA close should fail") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1604,9 +1609,10 @@ func TestClient_SendErrorDataWrite(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 6 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1627,7 +1633,7 @@ func TestClient_SendErrorDataWrite(t *testing.T) { message.SetMessageIDWithValue("this.is.a.message.id") message.SetGenHeader("X-Test-Header", "DATA write should fail") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1657,19 +1663,16 @@ func TestClient_SendErrorDataWrite(t *testing.T) { sendErr.MessageID()) } } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } } func TestClient_SendErrorReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 7 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1689,7 +1692,7 @@ func TestClient_SendErrorReset(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -2023,8 +2026,8 @@ func (f faker) SetWriteDeadline(time.Time) error { return nil } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // The provided featureSet represents in what the server responds to EHLO command // failReset controls if a RSET succeeds -func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool) error { - listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) +func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, port int) error { + listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, port)) if err != nil { return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) } From 44df830348d99ebedef2ceb356d4b4b2e4a939b2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 17:00:08 +0200 Subject: [PATCH 11/11] Add tests for SendError MessageID and Msg methods Introduce two new tests in senderror_test.go: TestSendError_MessageID and TestSendError_Msg. These tests validate the behavior of the MessageID and Msg methods of the SendError type, ensuring correct handling of message ID and sender information. --- senderror_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/senderror_test.go b/senderror_test.go index 789b290..8b7bfb3 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -90,7 +90,55 @@ func TestSendError_IsTempNil(t *testing.T) { } } +func TestSendError_MessageID(t *testing.T) { + var se *SendError + err := returnSendError(ErrAmbiguous, false) + if !errors.As(err, &se) { + t.Errorf("error mismatch, expected error to be of type *SendError") + return + } + if errors.As(err, &se) { + if se.MessageID() == "" { + t.Errorf("sendError expected message-id, but got empty string") + } + if !strings.EqualFold(se.MessageID(), "") { + t.Errorf("sendError message-id expected: %s, but got: %s", "", + se.MessageID()) + } + } +} + +func TestSendError_Msg(t *testing.T) { + var se *SendError + err := returnSendError(ErrAmbiguous, false) + if !errors.As(err, &se) { + t.Errorf("error mismatch, expected error to be of type *SendError") + return + } + if errors.As(err, &se) { + if se.Msg() == nil { + t.Errorf("sendError expected msg pointer, but got nil") + } + from := se.Msg().GetFromString() + if len(from) == 0 { + t.Errorf("sendError expected msg from, but got empty string") + return + } + if !strings.EqualFold(from[0], "") { + t.Errorf("sendError message from expected: %s, but got: %s", "", + from[0]) + } + } +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { - return &SendError{Reason: r, isTemp: t} + message := NewMsg() + _ = message.From("toni.tester@domain.tld") + _ = message.To("tina.tester@domain.tld") + message.Subject("This is the subject") + message.SetBodyString(TypeTextPlain, "This is the message body") + message.SetMessageIDWithValue("this.is.a.message.id") + + return &SendError{Reason: r, isTemp: t, affectedMsg: message} }