mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-09 23:42:55 +01:00
More changes in regard to #90
As proposed by @iwittkau the `SendError` type now has a `IsTemp()` method as well indicating to the user if the delivery error is retryable or not. Since we want to use it in the error response from the Client functions like `Send` or `DialAndSend` we need to return the SendError type not only as part of the `*Msg` but also as return value for these methods. Hence, the changes made for #85 been overhauled to return the new error type instead. In the pre Go1.20 version of the `Send()` method we need to return an accumulated version of the SendError type, since we don't have `errors.Join()` and therefore, if more than one error occurred during the delivery we return an ambiguous error reason since we can't tell which of the captured errors is main error. For more details the user can always check the `*Msg.SendError`
This commit is contained in:
parent
43efd6b3a8
commit
2c7ea3e532
4 changed files with 110 additions and 49 deletions
|
@ -7,44 +7,44 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send sends out the mail message
|
// Send sends out the mail message
|
||||||
func (c *Client) Send(ml ...*Msg) error {
|
func (c *Client) Send(ml ...*Msg) error {
|
||||||
var errs []error
|
if cerr := c.checkConn(); cerr != nil {
|
||||||
if err := c.checkConn(); err != nil {
|
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)}
|
||||||
return fmt.Errorf("failed to send mail: %w", err)
|
|
||||||
}
|
}
|
||||||
|
var errs []*SendError
|
||||||
for _, m := range ml {
|
for _, m := range ml {
|
||||||
m.sendError = nil
|
m.sendError = nil
|
||||||
if m.encoding == NoEncoding {
|
if m.encoding == NoEncoding {
|
||||||
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
||||||
errs = append(errs, ErrServerNoUnencoded)
|
se := &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||||
m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f, err := m.GetSender(false)
|
f, err := m.GetSender(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
se := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rl, err := m.GetRecipients()
|
rl, err := m.GetRecipients()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
se := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
errs = append(errs, err)
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.mail(f); err != nil {
|
if err := c.mail(f); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("sending MAIL FROM command failed: %w", err))
|
se := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
|
||||||
if reserr := c.sc.Reset(); reserr != nil {
|
if reserr := c.sc.Reset(); reserr != nil {
|
||||||
errs = append(errs, reserr)
|
se.errlist = append(se.errlist, reserr)
|
||||||
}
|
}
|
||||||
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
failed := false
|
failed := false
|
||||||
|
@ -53,7 +53,6 @@ func (c *Client) Send(ml ...*Msg) error {
|
||||||
rse.rcpt = make([]string, 0)
|
rse.rcpt = make([]string, 0)
|
||||||
for _, r := range rl {
|
for _, r := range rl {
|
||||||
if err := c.rcpt(r); err != nil {
|
if err := c.rcpt(r); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("sending RCPT TO command failed: %w", err))
|
|
||||||
rse.Reason = ErrSMTPRcptTo
|
rse.Reason = ErrSMTPRcptTo
|
||||||
rse.errlist = append(rse.errlist, err)
|
rse.errlist = append(rse.errlist, err)
|
||||||
rse.rcpt = append(rse.rcpt, r)
|
rse.rcpt = append(rse.rcpt, r)
|
||||||
|
@ -63,51 +62,67 @@ func (c *Client) Send(ml ...*Msg) error {
|
||||||
}
|
}
|
||||||
if failed {
|
if failed {
|
||||||
if reserr := c.sc.Reset(); reserr != nil {
|
if reserr := c.sc.Reset(); reserr != nil {
|
||||||
errs = append(errs, reserr)
|
rse.errlist = append(rse.errlist, err)
|
||||||
}
|
}
|
||||||
m.sendError = rse
|
m.sendError = rse
|
||||||
|
errs = append(errs, rse)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
w, err := c.sc.Data()
|
w, err := c.sc.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("sending DATA command failed: %w", err))
|
se := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = m.WriteTo(w)
|
_, err = m.WriteTo(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("sending mail content failed: %w", err))
|
se := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to close DATA writer: %w", err))
|
se := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Reset(); err != nil {
|
if err := c.Reset(); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("sending RSET command failed: %w", err))
|
se := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := c.checkConn(); err != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to check server connection: %w", err))
|
se := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
m.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = se
|
||||||
|
errs = append(errs, se)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
errtxt := ""
|
if len(errs) > 1 {
|
||||||
for i := range errs {
|
re := &SendError{Reason: ErrAmbiguous}
|
||||||
errtxt += fmt.Sprintf("%s", errs[i])
|
for i := range errs {
|
||||||
if i < len(errs) {
|
for _, e := range errs[i].errlist {
|
||||||
errtxt += "\n"
|
re.errlist = append(re.errlist, e)
|
||||||
|
}
|
||||||
|
for _, r := range errs[i].rcpt {
|
||||||
|
re.rcpt = append(re.rcpt, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We assume that the isTemp flage from the last error we received should be the
|
||||||
|
// indicator for the returned isTemp flag as well
|
||||||
|
re.isTemp = errs[len(errs)-1].isTemp
|
||||||
|
|
||||||
|
return re
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s", errtxt)
|
return errs[0]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,40 +9,39 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send sends out the mail message
|
// Send sends out the mail message
|
||||||
func (c *Client) Send(ml ...*Msg) (rerr error) {
|
func (c *Client) Send(ml ...*Msg) (rerr error) {
|
||||||
if err := c.checkConn(); err != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
rerr = fmt.Errorf("failed to send mail: %w", err)
|
rerr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, m := range ml {
|
for _, m := range ml {
|
||||||
m.sendError = nil
|
m.sendError = nil
|
||||||
if m.encoding == NoEncoding {
|
if m.encoding == NoEncoding {
|
||||||
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
if ok, _ := c.sc.Extension("8BITMIME"); !ok {
|
||||||
rerr = errors.Join(rerr, ErrServerNoUnencoded)
|
|
||||||
m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
m.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f, err := m.GetSender(false)
|
f, err := m.GetSender(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rerr = errors.Join(rerr, err)
|
|
||||||
m.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rl, err := m.GetRecipients()
|
rl, err := m.GetRecipients()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rerr = errors.Join(rerr, err)
|
|
||||||
m.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.mail(f); err != nil {
|
if err := c.mail(f); err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("sending MAIL FROM command failed: %w", err))
|
|
||||||
m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
if reserr := c.sc.Reset(); reserr != nil {
|
if reserr := c.sc.Reset(); reserr != nil {
|
||||||
rerr = errors.Join(rerr, reserr)
|
rerr = errors.Join(rerr, reserr)
|
||||||
}
|
}
|
||||||
|
@ -54,7 +53,6 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
|
||||||
rse.rcpt = make([]string, 0)
|
rse.rcpt = make([]string, 0)
|
||||||
for _, r := range rl {
|
for _, r := range rl {
|
||||||
if err := c.rcpt(r); err != nil {
|
if err := c.rcpt(r); err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("sending RCPT TO command failed: %w", err))
|
|
||||||
rse.Reason = ErrSMTPRcptTo
|
rse.Reason = ErrSMTPRcptTo
|
||||||
rse.errlist = append(rse.errlist, err)
|
rse.errlist = append(rse.errlist, err)
|
||||||
rse.rcpt = append(rse.rcpt, r)
|
rse.rcpt = append(rse.rcpt, r)
|
||||||
|
@ -67,35 +65,36 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
|
||||||
rerr = errors.Join(rerr, reserr)
|
rerr = errors.Join(rerr, reserr)
|
||||||
}
|
}
|
||||||
m.sendError = rse
|
m.sendError = rse
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
w, err := c.sc.Data()
|
w, err := c.sc.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("sending DATA command failed: %w", err))
|
|
||||||
m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = m.WriteTo(w)
|
_, err = m.WriteTo(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("sending mail content failed: %w", err))
|
|
||||||
m.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("failed to close DATA writer: %w", err))
|
|
||||||
m.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Reset(); err != nil {
|
if err := c.Reset(); err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("sending RSET command failed: %w", err))
|
|
||||||
m.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := c.checkConn(); err != nil {
|
if err := c.checkConn(); err != nil {
|
||||||
rerr = errors.Join(rerr, fmt.Errorf("failed to check server connection: %w", err))
|
|
||||||
m.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
m.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
|
rerr = errors.Join(rerr, m.sendError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1040,6 +1040,43 @@ func TestClient_Send_MsgSendError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClient_DialAndSendWithContext_withSendError tests the Client.DialAndSendWithContext method
|
||||||
|
// with a broken recipient to make sure that the returned error satisfies the Msg.SendError type
|
||||||
|
func TestClient_DialAndSendWithContext_withSendError(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||||
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
||||||
|
_ = m.To("invalid@domain.tld")
|
||||||
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
||||||
|
m.SetBulk()
|
||||||
|
m.SetDate()
|
||||||
|
m.SetMessageID()
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
||||||
|
|
||||||
|
c, err := getTestConnection(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
|
}
|
||||||
|
ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
|
defer cfn()
|
||||||
|
err = c.DialAndSendWithContext(ctx, m)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected DialAndSendWithContext with broken mail recipient to fail, but didn't")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var se *SendError
|
||||||
|
if !errors.As(err, &se) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but didn't")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if se.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getTestConnection takes environment variables to establish a connection to a real
|
// getTestConnection takes environment variables to establish a connection to a real
|
||||||
// SMTP server to test all functionality that requires a connection
|
// SMTP server to test all functionality that requires a connection
|
||||||
func getTestConnection(auth bool) (*Client, error) {
|
func getTestConnection(auth bool) (*Client, error) {
|
||||||
|
|
16
senderror.go
16
senderror.go
|
@ -6,7 +6,6 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -50,6 +49,10 @@ const (
|
||||||
// ErrNoUnencoded is returned if the Msg delivery failed when the Msg is configured for
|
// ErrNoUnencoded is returned if the Msg delivery failed when the Msg is configured for
|
||||||
// unencoded delivery but the server does not support this
|
// unencoded delivery but the server does not support this
|
||||||
ErrNoUnencoded
|
ErrNoUnencoded
|
||||||
|
|
||||||
|
// ErrAmbiguous is a generalized delivery error for the SendError type that is
|
||||||
|
// returned if the exact reason for the delivery failure is ambiguous
|
||||||
|
ErrAmbiguous
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendError is an error wrapper for delivery errors of the Msg
|
// SendError is an error wrapper for delivery errors of the Msg
|
||||||
|
@ -65,12 +68,12 @@ type SendErrReason int
|
||||||
|
|
||||||
// Error implements the error interface for the SendError type
|
// Error implements the error interface for the SendError type
|
||||||
func (e *SendError) Error() string {
|
func (e *SendError) Error() string {
|
||||||
if e.Reason > 9 {
|
if e.Reason > 10 {
|
||||||
return "client_send: unknown error"
|
return "client_send: unknown error"
|
||||||
}
|
}
|
||||||
|
|
||||||
var em strings.Builder
|
var em strings.Builder
|
||||||
_, _ = fmt.Fprintf(&em, "client_send: %s", e.Reason)
|
em.WriteString(e.Reason.String())
|
||||||
if len(e.errlist) > 0 {
|
if len(e.errlist) > 0 {
|
||||||
em.WriteRune(':')
|
em.WriteRune(':')
|
||||||
for i := range e.errlist {
|
for i := range e.errlist {
|
||||||
|
@ -102,6 +105,11 @@ func (e *SendError) Is(et error) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsTemp returns true if the delivery error is of temporary nature and can be retried
|
||||||
|
func (e *SendError) IsTemp() bool {
|
||||||
|
return e.isTemp
|
||||||
|
}
|
||||||
|
|
||||||
// String implements the Stringer interface for the SendErrReason
|
// String implements the Stringer interface for the SendErrReason
|
||||||
func (r SendErrReason) String() string {
|
func (r SendErrReason) String() string {
|
||||||
switch r {
|
switch r {
|
||||||
|
@ -125,6 +133,8 @@ func (r SendErrReason) String() string {
|
||||||
return "checking SMTP connection"
|
return "checking SMTP connection"
|
||||||
case ErrNoUnencoded:
|
case ErrNoUnencoded:
|
||||||
return ErrServerNoUnencoded.Error()
|
return ErrServerNoUnencoded.Error()
|
||||||
|
case ErrAmbiguous:
|
||||||
|
return "ambiguous reason, check Msg.SendError for message specific reasons"
|
||||||
}
|
}
|
||||||
return "unknown reason"
|
return "unknown reason"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue