Compare commits

..

No commits in common. "a5ac7c33708413f39ad99001ef01570e65603280" and "37ac2de2af0133e738d405dd9044d8f1a27b63b4" have entirely different histories.

5 changed files with 15 additions and 306 deletions

View file

@ -1190,7 +1190,6 @@ 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 {
@ -1201,16 +1200,14 @@ 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, errcode: errorCode(err), affectedMsg: message,
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, errcode: errorCode(err), affectedMsg: message,
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
@ -1222,8 +1219,7 @@ 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, errcode: errorCode(err), affectedMsg: message,
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)
@ -1242,8 +1238,6 @@ 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
} }
} }
@ -1257,23 +1251,20 @@ 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, errcode: errorCode(err), affectedMsg: message,
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, errcode: errorCode(err), affectedMsg: message,
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, errcode: errorCode(err), affectedMsg: message,
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
message.isDelivered = true message.isDelivered = true
@ -1281,8 +1272,7 @@ 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, errcode: errorCode(err), affectedMsg: message,
enhancedStatusCode: enhancedStatusCode(err, escSupport),
} }
} }
return nil return nil

View file

@ -27,13 +27,8 @@ 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 {
@ -55,11 +50,9 @@ 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 error codes and flags from the last error we received should be the // We assume that the isTemp flag 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

@ -35,7 +35,7 @@ const (
// 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 is the base port for the simple SMTP test server
TestServerPortBase = 30025 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.
@ -3148,59 +3148,6 @@ 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,8 +6,6 @@ package mail
import ( import (
"errors" "errors"
"regexp"
"strconv"
"strings" "strings"
) )
@ -62,13 +60,11 @@ const (
// details about the affected message, a list of errors, the recipient list, and whether // details about the affected message, a list of errors, the recipient list, and whether
// 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 errlist []error
enhancedStatusCode string isTemp bool
errlist []error rcpt []string
isTemp bool Reason SendErrReason
rcpt []string
Reason SendErrReason
} }
// SendErrReason represents a comparable reason on why the delivery failed // SendErrReason represents a comparable reason on why the delivery failed
@ -179,42 +175,6 @@ 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
@ -264,39 +224,3 @@ 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,7 +6,6 @@ package mail
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"testing" "testing"
) )
@ -219,150 +218,6 @@ 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()