mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-21 13:20:49 +01:00
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:
parent
37ac2de2af
commit
6809084e80
3 changed files with 136 additions and 12 deletions
24
client.go
24
client.go
|
@ -1190,6 +1190,7 @@ func (c *Client) auth() error {
|
|||
func (c *Client) sendSingleMsg(message *Msg) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
escSupport, _ := c.smtpClient.Extension("ENHANCEDSTATUSCODES")
|
||||
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
|
@ -1200,14 +1201,16 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
affectedMsg: message, errcode: getErrorCode(err),
|
||||
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
|
||||
}
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
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 {
|
||||
retError := &SendError{
|
||||
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 {
|
||||
retError.errlist = append(retError.errlist, resetSendErr)
|
||||
|
@ -1238,6 +1242,8 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
rcptSendErr.errcode = getErrorCode(err)
|
||||
rcptSendErr.enhancedStatusCode = getEnhancedStatusCode(err, escSupport)
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
@ -1251,20 +1257,23 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
affectedMsg: message, errcode: getErrorCode(err),
|
||||
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
|
||||
}
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
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 {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
affectedMsg: message, errcode: getErrorCode(err),
|
||||
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
@ -1272,7 +1281,8 @@ func (c *Client) sendSingleMsg(message *Msg) error {
|
|||
if err = c.Reset(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
affectedMsg: message, errcode: getErrorCode(err),
|
||||
enhancedStatusCode: getEnhancedStatusCode(err, escSupport),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -3148,6 +3148,59 @@ func TestClient_sendSingleMsg(t *testing.T) {
|
|||
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) {
|
||||
|
|
71
senderror.go
71
senderror.go
|
@ -6,6 +6,8 @@ package mail
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -60,11 +62,13 @@ const (
|
|||
// 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.
|
||||
type SendError struct {
|
||||
affectedMsg *Msg
|
||||
errlist []error
|
||||
isTemp bool
|
||||
rcpt []string
|
||||
Reason SendErrReason
|
||||
affectedMsg *Msg
|
||||
errcode int
|
||||
enhancedStatusCode string
|
||||
errlist []error
|
||||
isTemp bool
|
||||
rcpt []string
|
||||
Reason SendErrReason
|
||||
}
|
||||
|
||||
// SendErrReason represents a comparable reason on why the delivery failed
|
||||
|
@ -175,6 +179,27 @@ func (e *SendError) Msg() *Msg {
|
|||
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.
|
||||
//
|
||||
// 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 {
|
||||
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())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue