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.
This commit is contained in:
Winni Neessen 2024-11-13 16:11:28 +01:00
parent 37ac2de2af
commit 6809084e80
Signed by: wneessen
GPG key ID: 5F3AF39B820C119D
3 changed files with 136 additions and 12 deletions

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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(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 = getErrorCode(err)
rcptSendErr.enhancedStatusCode = getEnhancedStatusCode(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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(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: getErrorCode(err),
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
} }
} }
return nil return nil

View file

@ -3148,6 +3148,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"
) )
@ -60,11 +62,13 @@ 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
errlist []error errcode int
isTemp bool enhancedStatusCode string
rcpt []string errlist []error
Reason SendErrReason isTemp bool
rcpt []string
Reason SendErrReason
} }
// SendErrReason represents a comparable reason on why the delivery failed // SendErrReason represents a comparable reason on why the delivery failed
@ -175,6 +179,27 @@ 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
}
// 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 +249,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 getErrorCode(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 getEnhancedStatusCode(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())
}