Introduction of a Msg error type as proposal for #90

This PR introduces the `SendError` type which implements the error interface.

A new `senderror` field has been added to the `Msg` as well, so introduce this type to it.

I've also added different error variables that indicate the different things that can go wrong during mail delivery. These variables can be checked for, for each `Msg` using the `errors.As` method

The `Error()` method of `SendError` will return a detailed error string on why the `Msg` could not be delivered.

Additionally, `HasSendError()` and `SendError()` methods have been added to `Msg`. While `HasSendError()` simply returns a bool in case a `Msg` failed during delivery, the `SendError()` will return the full `SendError` error interface.
This commit is contained in:
Winni Neessen 2022-12-31 12:40:42 +01:00
parent f454ae8c41
commit 47bff15de9
Signed by: wneessen
GPG key ID: 385AC9889632126E
5 changed files with 162 additions and 0 deletions

View file

@ -21,31 +21,41 @@ func (c *Client) Send(ml ...*Msg) error {
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) errs = append(errs, ErrServerNoUnencoded)
m.sendError = SendError{Err: ErrServerNoUnencoded}
continue continue
} }
} }
f, err := m.GetSender(false) f, err := m.GetSender(false)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
m.sendError = SendError{Err: ErrGetSender, details: []error{err}}
continue continue
} }
rl, err := m.GetRecipients() rl, err := m.GetRecipients()
if err != nil { if err != nil {
m.sendError = SendError{Err: ErrGetRcpts, details: []error{err}}
errs = append(errs, err) errs = append(errs, err)
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)) errs = append(errs, fmt.Errorf("sending MAIL FROM command failed: %w", err))
m.sendError = SendError{Err: ErrSMTPMailFrom, details: []error{err}}
if reserr := c.sc.Reset(); reserr != nil { if reserr := c.sc.Reset(); reserr != nil {
errs = append(errs, reserr) errs = append(errs, reserr)
} }
continue continue
} }
failed := false failed := false
rse := SendError{}
rse.details = make([]error, 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)) errs = append(errs, fmt.Errorf("sending RCPT TO command failed: %w", err))
rse.Err = ErrSMTPRcptTo
rse.details = append(rse.details, err)
rse.rcpt = append(rse.rcpt, r)
failed = true failed = true
} }
} }
@ -53,30 +63,36 @@ func (c *Client) Send(ml ...*Msg) error {
if reserr := c.sc.Reset(); reserr != nil { if reserr := c.sc.Reset(); reserr != nil {
errs = append(errs, reserr) errs = append(errs, reserr)
} }
m.sendError = 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)) errs = append(errs, fmt.Errorf("sending DATA command failed: %w", err))
m.sendError = SendError{Err: ErrSMTPData, details: []error{err}}
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)) errs = append(errs, fmt.Errorf("sending mail content failed: %w", err))
m.sendError = SendError{Err: ErrWriteContent, details: []error{err}}
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)) errs = append(errs, fmt.Errorf("failed to close DATA writer: %w", err))
m.sendError = SendError{Err: ErrSMTPDataClose, details: []error{err}}
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)) errs = append(errs, fmt.Errorf("sending RSET command failed: %w", err))
m.sendError = SendError{Err: ErrSMTPReset, details: []error{err}}
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)) errs = append(errs, fmt.Errorf("failed to check server connection: %w", err))
m.sendError = SendError{Err: ErrConnCheck, details: []error{err}}
continue continue
} }
} }

View file

@ -22,31 +22,41 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
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) rerr = errors.Join(rerr, ErrServerNoUnencoded)
m.sendError = SendError{Err: ErrServerNoUnencoded}
continue continue
} }
} }
f, err := m.GetSender(false) f, err := m.GetSender(false)
if err != nil { if err != nil {
rerr = errors.Join(rerr, err) rerr = errors.Join(rerr, err)
m.sendError = SendError{Err: ErrGetSender, details: []error{err}}
continue continue
} }
rl, err := m.GetRecipients() rl, err := m.GetRecipients()
if err != nil { if err != nil {
rerr = errors.Join(rerr, err) rerr = errors.Join(rerr, err)
m.sendError = SendError{Err: ErrGetRcpts, details: []error{err}}
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)) rerr = errors.Join(rerr, fmt.Errorf("sending MAIL FROM command failed: %w", err))
m.sendError = SendError{Err: ErrSMTPMailFrom, details: []error{err}}
if reserr := c.sc.Reset(); reserr != nil { if reserr := c.sc.Reset(); reserr != nil {
rerr = errors.Join(rerr, reserr) rerr = errors.Join(rerr, reserr)
} }
continue continue
} }
failed := false failed := false
rse := SendError{}
rse.details = make([]error, 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)) rerr = errors.Join(rerr, fmt.Errorf("sending RCPT TO command failed: %w", err))
rse.Err = ErrSMTPRcptTo
rse.details = append(rse.details, err)
rse.rcpt = append(rse.rcpt, r)
failed = true failed = true
} }
} }
@ -54,30 +64,36 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
if reserr := c.sc.Reset(); reserr != nil { if reserr := c.sc.Reset(); reserr != nil {
rerr = errors.Join(rerr, reserr) rerr = errors.Join(rerr, reserr)
} }
m.sendError = rse
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)) rerr = errors.Join(rerr, fmt.Errorf("sending DATA command failed: %w", err))
m.sendError = SendError{Err: ErrSMTPData, details: []error{err}}
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)) rerr = errors.Join(rerr, fmt.Errorf("sending mail content failed: %w", err))
m.sendError = SendError{Err: ErrWriteContent, details: []error{err}}
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)) rerr = errors.Join(rerr, fmt.Errorf("failed to close DATA writer: %w", err))
m.sendError = SendError{Err: ErrSMTPDataClose, details: []error{err}}
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)) rerr = errors.Join(rerr, fmt.Errorf("sending RSET command failed: %w", err))
m.sendError = SendError{Err: ErrSMTPReset, details: []error{err}}
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)) rerr = errors.Join(rerr, fmt.Errorf("failed to check server connection: %w", err))
m.sendError = SendError{Err: ErrConnCheck, details: []error{err}}
} }
} }

View file

@ -7,6 +7,7 @@ package mail
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net/smtp" "net/smtp"
"os" "os"
@ -989,6 +990,53 @@ func TestValidateLine(t *testing.T) {
} }
} }
// TestClient_Send_MsgSendError tests the Send() method of Client with a broken and verifies
// that the SendError type works properly
func TestClient_Send_MsgSendError(t *testing.T) {
if os.Getenv("TEST_ALLOW_SEND") == "" {
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
}
var msgs []*Msg
rcpts := []string{"invalid@domain.tld", "invalid@address.invalid"}
for _, rcpt := range rcpts {
m := NewMsg()
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
_ = m.To(rcpt)
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")
msgs = append(msgs, m)
}
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()
if err := c.DialWithContext(ctx); err != nil {
t.Errorf("failed to dial to sending server: %s", err)
}
if err := c.Send(msgs...); err == nil {
t.Errorf("sending messages with broken recipients was supposed to fail but didn't")
}
if err := c.Close(); err != nil {
t.Errorf("failed to close client connection: %s", err)
}
for _, m := range msgs {
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")
}
}
}
// 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) {

14
msg.go
View file

@ -90,6 +90,9 @@ type Msg struct {
// middlewares is the list of middlewares to apply to the Msg before sending in FIFO order // middlewares is the list of middlewares to apply to the Msg before sending in FIFO order
middlewares []Middleware middlewares []Middleware
// sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation
sendError error
} }
// SendmailPath is the default system path to the sendmail binary // SendmailPath is the default system path to the sendmail binary
@ -957,6 +960,17 @@ func (m *Msg) UpdateReader(r *Reader) {
r.err = err r.err = err
} }
// HasSendError returns true if the Msg experienced an error during the message delivery and the
// senderror field of the Msg is not nil
func (m *Msg) HasSendError() bool {
return m.sendError != nil
}
// SendError returns the senderror field of the Msg
func (m *Msg) SendError() error {
return m.sendError
}
// encodeString encodes a string based on the configured message encoder and the corresponding // encodeString encodes a string based on the configured message encoder and the corresponding
// charset for the Msg // charset for the Msg
func (m *Msg) encodeString(s string) string { func (m *Msg) encodeString(s string) string {

68
senderror.go Normal file
View file

@ -0,0 +1,68 @@
package mail
import (
"errors"
"fmt"
"strings"
)
// List of SendError errors
var (
// ErrGetSender is returned if the Msg.GetSender method fails during a Client.Send
ErrGetSender = errors.New("getting sender address")
// ErrGetRcpts is returned if the Msg.GetRecipients method fails during a Client.Send
ErrGetRcpts = errors.New("getting recipient addresses")
// 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")
// 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")
// 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")
// ErrSMTPDataClose is returned if the Msg delivery failed when trying to close the
// Client data writer
ErrSMTPDataClose = errors.New("closing SMTP DATA writer")
// 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")
// ErrWriteContent is returned if the Msg delivery failed when sending Msg content
// to the Client writer
ErrWriteContent = errors.New("sending message content")
// ErrConnCheck is returned if the Msg delivery failed when checking if the SMTP
// server connection is still working
ErrConnCheck = errors.New("checking SMTP connection")
)
// SendError is an error wrapper for delivery errors of the Msg
type SendError struct {
Err error
details []error
rcpt []string
}
// Error implements the error interface for the SendError type
func (e SendError) Error() string {
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]))
}
}
if len(e.rcpt) > 0 {
for i := range e.rcpt {
em.WriteString(fmt.Sprintf(", rcpt: %s", e.rcpt[i]))
}
}
return em.String()
}