mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
Proposal change for #90
Did a complete overhaul of the senderror.go. - the list of `errors.New()` has been replaced with constant itoa error reasons as `SendErrReason`. Instead, the `Error()` method now reports the corresponding error message based on the reason. - The `SendError` received a `Is()` method so that we can use `errors.Is()` for very specific error checking. I.e. we can check for `&SendErrors{Reason: ErrSMTPMailFrom, isTemp: true}`. This provides much more flexibility in the error checking capabilities - A `isTemp` field has been added to the `SendError` type, indicating whether the received error is temporary and can be retried or not. Accordingly, the `*Msg` now has a `SendErrorIsTemp()` method indicating the same. The decision is based on the first 3 characters returned from the SMTP server. If the error code is within the 4xx range, the error is seen as temporary - A test for the SendError type has been added
This commit is contained in:
parent
42a1fde51f
commit
78df991399
6 changed files with 189 additions and 49 deletions
|
@ -22,41 +22,42 @@ func (c *Client) Send(ml ...*Msg) error {
|
|||
if m.encoding == NoEncoding {
|
||||
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
||||
errs = append(errs, ErrServerNoUnencoded)
|
||||
m.sendError = SendError{Err: ErrServerNoUnencoded}
|
||||
m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||
continue
|
||||
}
|
||||
}
|
||||
f, err := m.GetSender(false)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
m.sendError = SendError{Err: ErrGetSender, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
rl, err := m.GetRecipients()
|
||||
if err != nil {
|
||||
m.sendError = SendError{Err: ErrGetRcpts, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.mail(f); err != nil {
|
||||
errs = append(errs, fmt.Errorf("sending MAIL FROM command failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPMailFrom, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
if reserr := c.sc.Reset(); reserr != nil {
|
||||
errs = append(errs, reserr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
failed := false
|
||||
rse := SendError{}
|
||||
rse.details = make([]error, 0)
|
||||
rse := &SendError{}
|
||||
rse.errlist = make([]error, 0)
|
||||
rse.rcpt = make([]string, 0)
|
||||
for _, r := range rl {
|
||||
if err := c.rcpt(r); err != nil {
|
||||
errs = append(errs, fmt.Errorf("sending RCPT TO command failed: %w", err))
|
||||
rse.Err = ErrSMTPRcptTo
|
||||
rse.details = append(rse.details, err)
|
||||
rse.Reason = ErrSMTPRcptTo
|
||||
rse.errlist = append(rse.errlist, err)
|
||||
rse.rcpt = append(rse.rcpt, r)
|
||||
rse.isTemp = isTempError(err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
|
@ -70,30 +71,30 @@ func (c *Client) Send(ml ...*Msg) error {
|
|||
w, err := c.sc.Data()
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("sending DATA command failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPData, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
_, err = m.WriteTo(w)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("sending mail content failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrWriteContent, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to close DATA writer: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPDataClose, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.Reset(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("sending RSET command failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPReset, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
if err := c.checkConn(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to check server connection: %w", err))
|
||||
m.sendError = SendError{Err: ErrConnCheck, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,41 +23,42 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
|
|||
if m.encoding == NoEncoding {
|
||||
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
||||
rerr = errors.Join(rerr, ErrServerNoUnencoded)
|
||||
m.sendError = SendError{Err: ErrServerNoUnencoded}
|
||||
m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||
continue
|
||||
}
|
||||
}
|
||||
f, err := m.GetSender(false)
|
||||
if err != nil {
|
||||
rerr = errors.Join(rerr, err)
|
||||
m.sendError = SendError{Err: ErrGetSender, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
rl, err := m.GetRecipients()
|
||||
if err != nil {
|
||||
rerr = errors.Join(rerr, err)
|
||||
m.sendError = SendError{Err: ErrGetRcpts, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.mail(f); err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("sending MAIL FROM command failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPMailFrom, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
if reserr := c.sc.Reset(); reserr != nil {
|
||||
rerr = errors.Join(rerr, reserr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
failed := false
|
||||
rse := SendError{}
|
||||
rse.details = make([]error, 0)
|
||||
rse := &SendError{}
|
||||
rse.errlist = make([]error, 0)
|
||||
rse.rcpt = make([]string, 0)
|
||||
for _, r := range rl {
|
||||
if err := c.rcpt(r); err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("sending RCPT TO command failed: %w", err))
|
||||
rse.Err = ErrSMTPRcptTo
|
||||
rse.details = append(rse.details, err)
|
||||
rse.Reason = ErrSMTPRcptTo
|
||||
rse.errlist = append(rse.errlist, err)
|
||||
rse.rcpt = append(rse.rcpt, r)
|
||||
rse.isTemp = isTempError(err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
|
@ -71,30 +72,30 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
|
|||
w, err := c.sc.Data()
|
||||
if err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("sending DATA command failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPData, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
_, err = m.WriteTo(w)
|
||||
if err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("sending mail content failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrWriteContent, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("failed to close DATA writer: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPDataClose, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.Reset(); err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("sending RSET command failed: %w", err))
|
||||
m.sendError = SendError{Err: ErrSMTPReset, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
continue
|
||||
}
|
||||
if err := c.checkConn(); err != nil {
|
||||
rerr = errors.Join(rerr, fmt.Errorf("failed to check server connection: %w", err))
|
||||
m.sendError = SendError{Err: ErrConnCheck, details: []error{err}}
|
||||
m.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1030,9 +1030,12 @@ func TestClient_Send_MsgSendError(t *testing.T) {
|
|||
if !m.HasSendError() {
|
||||
t.Errorf("message was expected to have a send error, but didn't")
|
||||
}
|
||||
se := SendError{Err: ErrSMTPRcptTo}
|
||||
if !errors.As(m.SendError(), &se) {
|
||||
t.Errorf("message with broken recipient was expected to return a ErrSMTPRcptTo error, but didn't")
|
||||
se := &SendError{Reason: ErrSMTPRcptTo}
|
||||
if !errors.Is(m.SendError(), se) {
|
||||
t.Errorf("error mismatch, expected: %s, got: %s", se, m.SendError())
|
||||
}
|
||||
if m.SendErrorIsTemp() {
|
||||
t.Errorf("message was not expected to be a temporary error, but reported as such")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
msg.go
10
msg.go
|
@ -966,6 +966,16 @@ func (m *Msg) HasSendError() bool {
|
|||
return m.sendError != nil
|
||||
}
|
||||
|
||||
// SendErrorIsTemp returns true if the Msg experienced an error during the message delivery and the
|
||||
// corresponding error was of temporary nature and should be retried later
|
||||
func (m *Msg) SendErrorIsTemp() bool {
|
||||
e, ok := m.sendError.(*SendError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return e.isTemp
|
||||
}
|
||||
|
||||
// SendError returns the senderror field of the Msg
|
||||
func (m *Msg) SendError() error {
|
||||
return m.sendError
|
||||
|
|
110
senderror.go
110
senderror.go
|
@ -5,68 +5,138 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// List of SendError errors
|
||||
var (
|
||||
// List of SendError reasons
|
||||
const (
|
||||
// ErrGetSender is returned if the Msg.GetSender method fails during a Client.Send
|
||||
ErrGetSender = errors.New("getting sender address")
|
||||
ErrGetSender SendErrReason = iota
|
||||
|
||||
// ErrGetRcpts is returned if the Msg.GetRecipients method fails during a Client.Send
|
||||
ErrGetRcpts = errors.New("getting recipient addresses")
|
||||
ErrGetRcpts
|
||||
|
||||
// ErrSMTPMailFrom is returned if the Msg delivery failed when sending the MAIL FROM command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPMailFrom = errors.New("sending SMTP MAIL FROM command")
|
||||
ErrSMTPMailFrom
|
||||
|
||||
// ErrSMTPRcptTo is returned if the Msg delivery failed when sending the RCPT TO command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPRcptTo = errors.New("sending SMTP RCPT TO command")
|
||||
ErrSMTPRcptTo
|
||||
|
||||
// ErrSMTPData is returned if the Msg delivery failed when sending the DATA command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPData = errors.New("sending SMTP DATA command")
|
||||
ErrSMTPData
|
||||
|
||||
// ErrSMTPDataClose is returned if the Msg delivery failed when trying to close the
|
||||
// Client data writer
|
||||
ErrSMTPDataClose = errors.New("closing SMTP DATA writer")
|
||||
ErrSMTPDataClose
|
||||
|
||||
// ErrSMTPReset is returned if the Msg delivery failed when sending the RSET command
|
||||
// to the sending SMTP server
|
||||
ErrSMTPReset = errors.New("sending SMTP RESET command")
|
||||
ErrSMTPReset
|
||||
|
||||
// ErrWriteContent is returned if the Msg delivery failed when sending Msg content
|
||||
// to the Client writer
|
||||
ErrWriteContent = errors.New("sending message content")
|
||||
ErrWriteContent
|
||||
|
||||
// ErrConnCheck is returned if the Msg delivery failed when checking if the SMTP
|
||||
// server connection is still working
|
||||
ErrConnCheck = errors.New("checking SMTP connection")
|
||||
ErrConnCheck
|
||||
|
||||
// ErrNoUnencoded is returned if the Msg delivery failed when the Msg is configured for
|
||||
// unencoded delivery but the server does not support this
|
||||
ErrNoUnencoded
|
||||
)
|
||||
|
||||
// SendError is an error wrapper for delivery errors of the Msg
|
||||
type SendError struct {
|
||||
Err error
|
||||
details []error
|
||||
Reason SendErrReason
|
||||
isTemp bool
|
||||
errlist []error
|
||||
rcpt []string
|
||||
}
|
||||
|
||||
// SendErrReason represents a comparable reason on why the delivery failed
|
||||
type SendErrReason int
|
||||
|
||||
// Error implements the error interface for the SendError type
|
||||
func (e SendError) Error() string {
|
||||
func (e *SendError) Error() string {
|
||||
if e.Reason > 9 {
|
||||
return "client_send: unknown error"
|
||||
}
|
||||
|
||||
var em strings.Builder
|
||||
_, _ = fmt.Fprintf(&em, "client_send: %s", e.Err)
|
||||
if len(e.details) > 0 {
|
||||
for i := range e.details {
|
||||
em.WriteString(fmt.Sprintf(", error_details: %s", e.details[i]))
|
||||
_, _ = fmt.Fprintf(&em, "client_send: %s", e.Reason)
|
||||
if len(e.errlist) > 0 {
|
||||
em.WriteRune(':')
|
||||
for i := range e.errlist {
|
||||
em.WriteRune(' ')
|
||||
em.WriteString(e.errlist[i].Error())
|
||||
if i != len(e.errlist)-1 {
|
||||
em.WriteString(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e.rcpt) > 0 {
|
||||
em.WriteString(", affected recipient(s): ")
|
||||
for i := range e.rcpt {
|
||||
em.WriteString(fmt.Sprintf(", rcpt: %s", e.rcpt[i]))
|
||||
em.WriteString(e.rcpt[i])
|
||||
if i != len(e.rcpt)-1 {
|
||||
em.WriteString(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
return em.String()
|
||||
}
|
||||
|
||||
// Is implements the errors.Is functionality and compares the SendErrReason
|
||||
func (e *SendError) Is(et error) bool {
|
||||
t, ok := et.(*SendError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return e.Reason == t.Reason && e.isTemp == t.isTemp
|
||||
}
|
||||
|
||||
// String implements the Stringer interface for the SendErrReason
|
||||
func (r SendErrReason) String() string {
|
||||
switch r {
|
||||
case ErrGetSender:
|
||||
return "getting sender address"
|
||||
case ErrGetRcpts:
|
||||
return "getting recipient addresses"
|
||||
case ErrSMTPMailFrom:
|
||||
return "sending SMTP MAIL FROM command"
|
||||
case ErrSMTPRcptTo:
|
||||
return "sending SMTP RCPT TO command"
|
||||
case ErrSMTPData:
|
||||
return "sending SMTP DATA command"
|
||||
case ErrSMTPDataClose:
|
||||
return "closing SMTP DATA writer"
|
||||
case ErrSMTPReset:
|
||||
return "sending SMTP RESET command"
|
||||
case ErrWriteContent:
|
||||
return "sending message content"
|
||||
case ErrConnCheck:
|
||||
return "checking SMTP connection"
|
||||
case ErrNoUnencoded:
|
||||
return ErrServerNoUnencoded.Error()
|
||||
}
|
||||
return "unknown reason"
|
||||
}
|
||||
|
||||
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature
|
||||
// and should be retried
|
||||
func isTempError(e error) bool {
|
||||
ec, err := strconv.Atoi(e.Error()[:3])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if ec >= 400 && ec <= 500 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
55
senderror_test.go
Normal file
55
senderror_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSendError_Error tests the SendError and SendErrReason error handling methods
|
||||
func TestSendError_Error(t *testing.T) {
|
||||
tl := []struct {
|
||||
n string
|
||||
r SendErrReason
|
||||
te bool
|
||||
}{
|
||||
{"ErrGetSender/temp", ErrGetSender, true},
|
||||
{"ErrGetSender/perm", ErrGetSender, false},
|
||||
{"ErrGetRcpts/temp", ErrGetRcpts, true},
|
||||
{"ErrGetRcpts/perm", ErrGetRcpts, false},
|
||||
{"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true},
|
||||
{"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false},
|
||||
{"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true},
|
||||
{"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false},
|
||||
{"ErrSMTPData/temp", ErrSMTPData, true},
|
||||
{"ErrSMTPData/perm", ErrSMTPData, false},
|
||||
{"ErrSMTPDataClose/temp", ErrSMTPDataClose, true},
|
||||
{"ErrSMTPDataClose/perm", ErrSMTPDataClose, false},
|
||||
{"ErrSMTPReset/temp", ErrSMTPReset, true},
|
||||
{"ErrSMTPReset/perm", ErrSMTPReset, false},
|
||||
{"ErrWriteContent/temp", ErrWriteContent, true},
|
||||
{"ErrWriteContent/perm", ErrWriteContent, false},
|
||||
{"ErrConnCheck/temp", ErrConnCheck, true},
|
||||
{"ErrConnCheck/perm", ErrConnCheck, false},
|
||||
{"ErrNoUnencoded/temp", ErrNoUnencoded, true},
|
||||
{"ErrNoUnencoded/perm", ErrNoUnencoded, false},
|
||||
{"Unknown/temp", 9999, true},
|
||||
{"Unknown/perm", 9999, false},
|
||||
}
|
||||
|
||||
for _, tt := range tl {
|
||||
t.Run(tt.n, func(t *testing.T) {
|
||||
if err := returnSendError(tt.r, tt.te); err != nil {
|
||||
exp := &SendError{Reason: tt.r, isTemp: tt.te}
|
||||
if !errors.Is(err, exp) {
|
||||
t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te,
|
||||
exp.Error(), exp.isTemp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// returnSendError is a helper method to retunr a SendError with a specific reason
|
||||
func returnSendError(r SendErrReason, t bool) error {
|
||||
return &SendError{Reason: r, isTemp: t}
|
||||
}
|
Loading…
Reference in a new issue