Compare commits

..

17 commits

Author SHA1 Message Date
441d163e38
Merge pull request #369 from wneessen/dependabot/github_actions/fsfe/reuse-action-5.0.0
Bump fsfe/reuse-action from 4.0.0 to 5.0.0
2024-11-14 14:55:16 +01:00
dependabot[bot]
1a811f3bcf
Bump fsfe/reuse-action from 4.0.0 to 5.0.0
Bumps [fsfe/reuse-action](https://github.com/fsfe/reuse-action) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/fsfe/reuse-action/releases)
- [Commits](3ae3c6bdf1...bb774aa972)

---
updated-dependencies:
- dependency-name: fsfe/reuse-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 13:50:11 +00:00
da6bf26405
Merge pull request #368 from wneessen/feature/365_expose-error-code-in-senderror
Expose error code in SendError
2024-11-14 10:49:59 +01:00
2bde340428
Update SMTP test port variable and CI configuration
Changed the SMTP test server base port and updated the corresponding environment variable name to `TEST_BASEPORT_SMTP`. This ensures consistency across the test setup and CI workflow configuration.
2024-11-14 10:45:35 +01:00
a70dde5a4d
Add TEST_BASEPORT environment variable to CI workflow
In the CI configuration file, the TEST_BASEPORT environment variable was added to various job scopes. This ensures consistency and allows the test base port to be set properly across different OS versions and Go versions.
2024-11-14 10:41:10 +01:00
ca3f50552e
Allow configuration of test server port via environment variable
Moved TestServerPortBase initialization to use an environment variable `TEST_BASEPORT` if provided. This adjustment helps in specifying custom base ports for running tests, ensuring better flexibility in different testing environments.
2024-11-14 10:36:24 +01:00
bd655b768b
Refactor SendError initialization for better readability
Structured the initialization of SendError on connection errors to improve code readability and maintainability. This change affects the error handling in both client_120.go and client_119.go by spreading the error details across multiple lines.
2024-11-14 10:20:52 +01:00
c8d7cf86e1
Enhance error handling in Client's Send method
Added support for Enhanced Status Codes (ESC) when checking the SMTP client's extensions. The SendError struct now includes the error code and enhanced status code for improved diagnostics.
2024-11-14 10:17:18 +01:00
a5ac7c3370
Update error handling to include error code and status
Previously, only the isTemp flag was considered when aggregating errors. Now, the error code and enhanced status code from the last error are also included. This ensures more comprehensive error reporting and handling.
2024-11-13 23:04:39 +01:00
719e5b217c
Enhance error handling with ENHANCEDSTATUSCODES check
Added a check for the ENHANCEDSTATUSCODES extension and included error code and enhanced status code information in SendError. This helps in providing more detailed error reporting and troubleshooting.
2024-11-13 23:02:30 +01:00
f367db0278
Refactor error code functions and add enhanced status code tests
Renamed `getErrorCode` function to `errorCode` for consistency. Added new tests for the `enhancedStatusCode` function to validate its behavior with various error scenarios.
2024-11-13 22:53:18 +01:00
6268acac44
Refactor error handling by renaming functions.
Renamed `getErrorCode` to `errorCode` and `getEnhancedStatusCode` to `enhancedStatusCode` for consistency. Updated all references in `client.go` and `senderror.go` accordingly, improving readability and maintaining uniformity across the codebase.
2024-11-13 22:47:53 +01:00
e8fb977afe
Add tests for getErrorCode function
Introduce a suite of unit tests for the getErrorCode function to validate its behavior with various error types, including go-mail errors, permanent and temporary errors, wrapper errors, non-4xx/5xx errors, and non-3-digit codes.
2024-11-13 21:48:24 +01:00
615155bfc2
Add tests for SendError's enhanced status and error codes
Implemented new unit tests for SendError to validate the enhanced status code and error codes in various scenarios, including nil SendError cases, errors with no enhanced status code, and errors with both permanent and temporary error codes. This ensures the correctness of the error handling behavior across different conditions.
2024-11-13 21:27:27 +01:00
ad265cac57
Add ErrorCode method to SendError
Implemented ErrorCode method to retrieve the error code from the server response in SendError. This method distinguishes between server-generated errors and client-generated errors, returning 0 for errors generated by the client.
2024-11-13 21:26:11 +01:00
b9d9449252
Change test server port base for SMTP client tests
Updated the TestServerPortBase from 12025 to 30025 to avoid port conflicts with other services running on the common 12025 port. This adjustment aims to ensure that the tests run reliably in diverse environments.
2024-11-13 21:25:39 +01:00
6809084e80
Add enhanced status code and error code to SendError
Enhance error handling by adding error code and enhanced status code to the SendError struct. This allows for better troubleshooting and debugging by providing more detailed SMTP server responses.
2024-11-13 16:11:28 +01:00
8 changed files with 358 additions and 20 deletions

View file

@ -33,6 +33,8 @@ jobs:
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }} PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }} PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
TEST_HOST: ${{ secrets.TEST_HOST }} TEST_HOST: ${{ secrets.TEST_HOST }}
TEST_USER: ${{ secrets.TEST_USER }} TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }} TEST_PASS: ${{ secrets.TEST_PASS }}
@ -126,6 +128,9 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.19', '1.20', '1.21', '1.22', '1.23'] go: ['1.19', '1.20', '1.21', '1.22', '1.23']
env:
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
@ -149,6 +154,9 @@ jobs:
strategy: strategy:
matrix: matrix:
osver: ['14.1', '14.0', 13.4'] osver: ['14.1', '14.0', 13.4']
env:
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
@ -176,7 +184,7 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
- name: REUSE Compliance Check - name: REUSE Compliance Check
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0 uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0
sonarqube: sonarqube:
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }}) name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -189,6 +197,8 @@ jobs:
go: ['1.23'] go: ['1.23']
env: env:
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
TEST_HOST: ${{ secrets.TEST_HOST }} TEST_HOST: ${{ secrets.TEST_HOST }}
TEST_USER: ${{ secrets.TEST_USER }} TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }} TEST_PASS: ${{ secrets.TEST_PASS }}

View file

@ -1190,6 +1190,7 @@ func (c *Client) auth() error {
func (c *Client) sendSingleMsg(message *Msg) error { func (c *Client) sendSingleMsg(message *Msg) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
escSupport, _ := c.smtpClient.Extension("ENHANCEDSTATUSCODES")
if message.encoding == NoEncoding { if message.encoding == NoEncoding {
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
@ -1200,14 +1201,16 @@ func (c *Client) sendSingleMsg(message *Msg) error {
if err != nil { if err != nil {
return &SendError{ return &SendError{
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
rcpts, err := message.GetRecipients() rcpts, err := message.GetRecipients()
if err != nil { if err != nil {
return &SendError{ return &SendError{
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
@ -1219,7 +1222,8 @@ func (c *Client) sendSingleMsg(message *Msg) error {
if err = c.smtpClient.Mail(from); err != nil { if err = c.smtpClient.Mail(from); err != nil {
retError := &SendError{ retError := &SendError{
Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
retError.errlist = append(retError.errlist, resetSendErr) retError.errlist = append(retError.errlist, resetSendErr)
@ -1238,6 +1242,8 @@ func (c *Client) sendSingleMsg(message *Msg) error {
rcptSendErr.errlist = append(rcptSendErr.errlist, err) rcptSendErr.errlist = append(rcptSendErr.errlist, err)
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
rcptSendErr.isTemp = isTempError(err) rcptSendErr.isTemp = isTempError(err)
rcptSendErr.errcode = errorCode(err)
rcptSendErr.enhancedStatusCode = enhancedStatusCode(err, escSupport)
hasError = true hasError = true
} }
} }
@ -1251,20 +1257,23 @@ func (c *Client) sendSingleMsg(message *Msg) error {
if err != nil { if err != nil {
return &SendError{ return &SendError{
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
_, err = message.WriteTo(writer) _, err = message.WriteTo(writer)
if err != nil { if err != nil {
return &SendError{ return &SendError{
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
if err = writer.Close(); err != nil { if err = writer.Close(); err != nil {
return &SendError{ return &SendError{
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
message.isDelivered = true message.isDelivered = true
@ -1272,7 +1281,8 @@ func (c *Client) sendSingleMsg(message *Msg) error {
if err = c.Reset(); err != nil { if err = c.Reset(); err != nil {
return &SendError{ return &SendError{
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message, affectedMsg: message, errcode: errorCode(err),
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
return nil return nil

View file

@ -27,8 +27,15 @@ import "errors"
// - An error that represents the sending result, which may include multiple SendErrors if // - An error that represents the sending result, which may include multiple SendErrors if
// any occurred; otherwise, returns nil. // any occurred; otherwise, returns nil.
func (c *Client) Send(messages ...*Msg) error { func (c *Client) Send(messages ...*Msg) error {
escSupport := false
if c.smtpClient != nil {
escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES")
}
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return &SendError{
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport),
}
} }
var errs []*SendError var errs []*SendError
for id, message := range messages { for id, message := range messages {
@ -50,9 +57,11 @@ func (c *Client) Send(messages ...*Msg) error {
returnErr.rcpt = append(returnErr.rcpt, errs[i].rcpt...) returnErr.rcpt = append(returnErr.rcpt, errs[i].rcpt...)
} }
// We assume that the isTemp flag from the last error we received should be the // We assume that the error codes and flags from the last error we received should be the
// indicator for the returned isTemp flag as well // indicator for the returned isTemp flag as well
returnErr.isTemp = errs[len(errs)-1].isTemp returnErr.isTemp = errs[len(errs)-1].isTemp
returnErr.errcode = errs[len(errs)-1].errcode
returnErr.enhancedStatusCode = errs[len(errs)-1].enhancedStatusCode
return returnErr return returnErr
} }

View file

@ -27,8 +27,15 @@ import (
// Returns: // Returns:
// - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. // - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil.
func (c *Client) Send(messages ...*Msg) (returnErr error) { func (c *Client) Send(messages ...*Msg) (returnErr error) {
escSupport := false
if c.smtpClient != nil {
escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES")
}
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} returnErr = &SendError{
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport),
}
return return
} }

View file

@ -17,6 +17,7 @@ import (
"net/mail" "net/mail"
"os" "os"
"reflect" "reflect"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -34,14 +35,15 @@ const (
TestServerProto = "tcp" TestServerProto = "tcp"
// TestServerAddr is the address the simple SMTP test server listens on // TestServerAddr is the address the simple SMTP test server listens on
TestServerAddr = "127.0.0.1" TestServerAddr = "127.0.0.1"
// TestServerPortBase is the base port for the simple SMTP test server
TestServerPortBase = 12025
// TestSenderValid is a test sender email address considered valid for sending test emails. // TestSenderValid is a test sender email address considered valid for sending test emails.
TestSenderValid = "valid-from@domain.tld" TestSenderValid = "valid-from@domain.tld"
// TestRcptValid is a test recipient email address considered valid for sending test emails. // TestRcptValid is a test recipient email address considered valid for sending test emails.
TestRcptValid = "valid-to@domain.tld" TestRcptValid = "valid-to@domain.tld"
) )
// TestServerPortBase is the base port for the simple SMTP test server
var TestServerPortBase int32 = 30025
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
var PortAdder atomic.Int32 var PortAdder atomic.Int32
@ -98,6 +100,18 @@ type logData struct {
Lines []logLine `json:"lines"` Lines []logLine `json:"lines"`
} }
func init() {
testPort := os.Getenv("TEST_BASEPORT")
if testPort == "" {
return
}
if port, err := strconv.Atoi(testPort); err == nil {
if port <= 65000 && port > 1023 {
TestServerPortBase = int32(port)
}
}
}
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
t.Run("create new Client", func(t *testing.T) { t.Run("create new Client", func(t *testing.T) {
client, err := NewClient(DefaultHost) client, err := NewClient(DefaultHost)
@ -3148,6 +3162,59 @@ func TestClient_sendSingleMsg(t *testing.T) {
t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason) t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason)
} }
}) })
t.Run("error code and enhanced status code support", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnMailFrom: 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 != nil {
t.Fatalf("failed to create new client: %s", err)
}
if err = client.DialWithContext(ctxDial); err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
t.Skip("failed to connect to the test server due to timeout")
}
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.Error("expected mail delivery to fail")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Fatalf("expected SendError, got %s", err)
}
if sendErr.errcode != 500 {
t.Errorf("expected error code 500, got %d", sendErr.errcode)
}
if !strings.EqualFold(sendErr.enhancedStatusCode, "5.5.2") {
t.Errorf("expected enhanced status code 5.5.2, got %s", sendErr.enhancedStatusCode)
}
})
} }
func TestClient_checkConn(t *testing.T) { func TestClient_checkConn(t *testing.T) {

View file

@ -6,6 +6,8 @@ package mail
import ( import (
"errors" "errors"
"regexp"
"strconv"
"strings" "strings"
) )
@ -61,6 +63,8 @@ const (
// the error is temporary or permanent. It also includes a reason code for the error. // the error is temporary or permanent. It also includes a reason code for the error.
type SendError struct { type SendError struct {
affectedMsg *Msg affectedMsg *Msg
errcode int
enhancedStatusCode string
errlist []error errlist []error
isTemp bool isTemp bool
rcpt []string rcpt []string
@ -175,6 +179,42 @@ func (e *SendError) Msg() *Msg {
return e.affectedMsg return e.affectedMsg
} }
// EnhancedStatusCode returns the enhanced status code of the server response if the
// server supports it, as described in RFC 2034.
//
// This function retrieves the enhanced status code of an error returned by the server. This
// requires that the receiving server supports this SMTP extension as described in RFC 2034.
// Since this is the SendError interface, we only collect status codes for error responses,
// meaning 4xx or 5xx. If the server does not support the ENHANCEDSTATUSCODES extension or
// the error did not include an enhanced status code, it will return an empty string.
//
// Returns:
// - The enhanced status code as returned by the server, or an empty string is not supported.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc2034
func (e *SendError) EnhancedStatusCode() string {
if e == nil {
return ""
}
return e.enhancedStatusCode
}
// ErrorCode returns the error code of the server response.
//
// This function retrieves the error code the error returned by the server. The error code will
// start with 5 on permanent errors and with 4 on a temporary error. If the error is not returned
// by the server, but is generated by go-mail, the code will be 0.
//
// Returns:
// - The error code as returned by the server, or 0 if not a server error.
func (e *SendError) ErrorCode() int {
if e == nil {
return 0
}
return e.errcode
}
// String satisfies the fmt.Stringer interface for the SendErrReason type. // String satisfies the fmt.Stringer interface for the SendErrReason type.
// //
// This function converts the SendErrReason into a human-readable string representation based // This function converts the SendErrReason into a human-readable string representation based
@ -224,3 +264,39 @@ func (r SendErrReason) String() string {
func isTempError(err error) bool { func isTempError(err error) bool {
return err.Error()[0] == '4' return err.Error()[0] == '4'
} }
func errorCode(err error) int {
rootErr := errors.Unwrap(err)
if rootErr != nil {
err = rootErr
}
firstrune := err.Error()[0]
if firstrune < 52 || firstrune > 53 {
return 0
}
code := err.Error()[0:3]
errcode, cerr := strconv.Atoi(code)
if cerr != nil {
return 0
}
return errcode
}
func enhancedStatusCode(err error, supported bool) string {
if err == nil || !supported {
return ""
}
rootErr := errors.Unwrap(err)
if rootErr != nil {
err = rootErr
}
firstrune := err.Error()[0]
if firstrune != 50 && firstrune != 52 && firstrune != 53 {
return ""
}
re, rerr := regexp.Compile(`\b([245])\.\d{1,3}\.\d{1,3}\b`)
if rerr != nil {
return ""
}
return re.FindString(err.Error())
}

View file

@ -6,6 +6,7 @@ package mail
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"testing" "testing"
) )
@ -218,6 +219,150 @@ func TestSendError_Msg(t *testing.T) {
}) })
} }
func TestSendError_EnhancedStatusCode(t *testing.T) {
t.Run("SendError with no enhanced status code", func(t *testing.T) {
err := &SendError{
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
}
if err.EnhancedStatusCode() != "" {
t.Errorf("expected empty enhanced status code, got: %s", err.EnhancedStatusCode())
}
})
t.Run("SendError with enhanced status code", func(t *testing.T) {
err := &SendError{
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
enhancedStatusCode: "5.7.1",
}
if err.EnhancedStatusCode() != "5.7.1" {
t.Errorf("expected enhanced status code: %s, got: %s", "5.7.1", err.EnhancedStatusCode())
}
})
t.Run("enhanced status code on nil error should return empty string", func(t *testing.T) {
var err *SendError
if err.EnhancedStatusCode() != "" {
t.Error("expected empty enhanced status code on nil-senderror")
}
})
}
func TestSendError_ErrorCode(t *testing.T) {
t.Run("ErrorCode with a go-mail error should return 0", func(t *testing.T) {
err := &SendError{
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
errcode: errorCode(ErrNoRcptAddresses),
}
if err.ErrorCode() != 0 {
t.Errorf("expected error code: %d, got: %d", 0, err.ErrorCode())
}
})
t.Run("SendError with permanent error", func(t *testing.T) {
err := &SendError{
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
errcode: errorCode(errors.New("535 5.7.8 Error: authentication failed")),
}
if err.ErrorCode() != 535 {
t.Errorf("expected error code: %d, got: %d", 535, err.ErrorCode())
}
})
t.Run("SendError with temporary error", func(t *testing.T) {
err := &SendError{
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
errcode: errorCode(errors.New("441 4.1.0 Server currently unavailable")),
}
if err.ErrorCode() != 441 {
t.Errorf("expected error code: %d, got: %d", 441, err.ErrorCode())
}
})
t.Run("error code on nil error should return 0", func(t *testing.T) {
var err *SendError
if err.ErrorCode() != 0 {
t.Error("expected 0 error code on nil-senderror")
}
})
}
func TestSendError_errorCode(t *testing.T) {
t.Run("errorCode with a go-mail error should return 0", func(t *testing.T) {
code := errorCode(ErrNoRcptAddresses)
if code != 0 {
t.Errorf("expected error code: %d, got: %d", 0, code)
}
})
t.Run("errorCode with permanent error", func(t *testing.T) {
code := errorCode(errors.New("535 5.7.8 Error: authentication failed"))
if code != 535 {
t.Errorf("expected error code: %d, got: %d", 535, code)
}
})
t.Run("errorCode with temporary error", func(t *testing.T) {
code := errorCode(errors.New("443 4.1.0 Server currently unavailable"))
if code != 443 {
t.Errorf("expected error code: %d, got: %d", 443, code)
}
})
t.Run("errorCode with wrapper error", func(t *testing.T) {
code := errorCode(fmt.Errorf("an error occured: %w", errors.New("443 4.1.0 Server currently unavailable")))
if code != 443 {
t.Errorf("expected error code: %d, got: %d", 443, code)
}
})
t.Run("errorCode with non-4xx and non-5xx error", func(t *testing.T) {
code := errorCode(errors.New("220 2.1.0 This is not an error"))
if code != 0 {
t.Errorf("expected error code: %d, got: %d", 0, code)
}
})
t.Run("errorCode with non 3-digit code", func(t *testing.T) {
code := errorCode(errors.New("4xx 4.1.0 The status code is invalid"))
if code != 0 {
t.Errorf("expected error code: %d, got: %d", 0, code)
}
})
}
func TestSendError_enhancedStatusCode(t *testing.T) {
t.Run("enhancedStatusCode with nil error should return empty string", func(t *testing.T) {
code := enhancedStatusCode(nil, true)
if code != "" {
t.Errorf("expected empty enhanced status code, got: %s", code)
}
})
t.Run("enhancedStatusCode with error but no support should return empty string", func(t *testing.T) {
code := enhancedStatusCode(errors.New("553 5.5.3 something went wrong"), false)
if code != "" {
t.Errorf("expected empty enhanced status code, got: %s", code)
}
})
t.Run("enhancedStatusCode with error and support", func(t *testing.T) {
code := enhancedStatusCode(errors.New("553 5.5.3 something went wrong"), true)
if code != "5.5.3" {
t.Errorf("expected enhanced status code: %s, got: %s", "5.5.3", code)
}
})
t.Run("enhancedStatusCode with wrapped error and support", func(t *testing.T) {
code := enhancedStatusCode(fmt.Errorf("this error is wrapped: %w", errors.New("553 5.5.3 something went wrong")), true)
if code != "5.5.3" {
t.Errorf("expected enhanced status code: %s, got: %s", "5.5.3", code)
}
})
t.Run("enhancedStatusCode with 3xx error", func(t *testing.T) {
code := enhancedStatusCode(errors.New("300 3.0.0 i don't know what i'm doing"), true)
if code != "" {
t.Errorf("expected enhanced status code to be empty, got: %s", code)
}
})
}
// returnSendError is a helper method to retunr a SendError with a specific reason // returnSendError is a helper method to retunr a SendError with a specific reason
func returnSendError(r SendErrReason, t bool) error { func returnSendError(r SendErrReason, t bool) error {
message := NewMsg() message := NewMsg()

View file

@ -30,6 +30,7 @@ import (
"io" "io"
"net" "net"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -46,13 +47,14 @@ const (
TestServerProto = "tcp" TestServerProto = "tcp"
// TestServerAddr is the address the simple SMTP test server listens on // TestServerAddr is the address the simple SMTP test server listens on
TestServerAddr = "127.0.0.1" TestServerAddr = "127.0.0.1"
// TestServerPortBase is the base port for the simple SMTP test server
TestServerPortBase = 30025
) )
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
var PortAdder atomic.Int32 var PortAdder atomic.Int32
// TestServerPortBase is the base port for the simple SMTP test server
var TestServerPortBase int32 = 20025
// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
// //
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
@ -231,6 +233,18 @@ var authTests = []authTest{
}, },
} }
func init() {
testPort := os.Getenv("TEST_BASEPORT_SMTP")
if testPort == "" {
return
}
if port, err := strconv.Atoi(testPort); err == nil {
if port <= 65000 && port > 1023 {
TestServerPortBase = int32(port)
}
}
}
func TestAuth(t *testing.T) { func TestAuth(t *testing.T) {
t.Run("Auth for all supported auth methods", func(t *testing.T) { t.Run("Auth for all supported auth methods", func(t *testing.T) {
for i, tt := range authTests { for i, tt := range authTests {