mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 05:40:50 +01:00
Compare commits
17 commits
29ad32e6d0
...
441d163e38
Author | SHA1 | Date | |
---|---|---|---|
441d163e38 | |||
|
1a811f3bcf | ||
da6bf26405 | |||
2bde340428 | |||
a70dde5a4d | |||
ca3f50552e | |||
bd655b768b | |||
c8d7cf86e1 | |||
a5ac7c3370 | |||
719e5b217c | |||
f367db0278 | |||
6268acac44 | |||
e8fb977afe | |||
615155bfc2 | |||
ad265cac57 | |||
b9d9449252 | |||
6809084e80 |
8 changed files with 358 additions and 20 deletions
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
@ -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 }}
|
||||||
|
|
24
client.go
24
client.go
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
76
senderror.go
76
senderror.go
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue