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 ok, _ := c.sc.Extension("8BITMIME"); !ok {
errs = append(errs, ErrServerNoUnencoded)
m.sendError = SendError{Err: ErrServerNoUnencoded}
continue
}
}
f, err := m.GetSender(false)
if err != nil {
errs = append(errs, err)
m.sendError = SendError{Err: ErrGetSender, details: []error{err}}
continue
}
rl, err := m.GetRecipients()
if err != nil {
m.sendError = SendError{Err: ErrGetRcpts, details: []error{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}}
if reserr := c.sc.Reset(); reserr != nil {
errs = append(errs, reserr)
}
continue
}
failed := false
rse := SendError{}
rse.details = 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.rcpt = append(rse.rcpt, r)
failed = true
}
}
@ -53,30 +63,36 @@ func (c *Client) Send(ml ...*Msg) error {
if reserr := c.sc.Reset(); reserr != nil {
errs = append(errs, reserr)
}
m.sendError = rse
continue
}
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}}
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}}
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}}
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}}
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}}
continue
}
}

View file

@ -22,31 +22,41 @@ 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}
continue
}
}
f, err := m.GetSender(false)
if err != nil {
rerr = errors.Join(rerr, err)
m.sendError = SendError{Err: ErrGetSender, details: []error{err}}
continue
}
rl, err := m.GetRecipients()
if err != nil {
rerr = errors.Join(rerr, err)
m.sendError = SendError{Err: ErrGetRcpts, details: []error{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}}
if reserr := c.sc.Reset(); reserr != nil {
rerr = errors.Join(rerr, reserr)
}
continue
}
failed := false
rse := SendError{}
rse.details = 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.rcpt = append(rse.rcpt, r)
failed = true
}
}
@ -54,30 +64,36 @@ func (c *Client) Send(ml ...*Msg) (rerr error) {
if reserr := c.sc.Reset(); reserr != nil {
rerr = errors.Join(rerr, reserr)
}
m.sendError = rse
continue
}
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}}
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}}
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}}
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}}
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}}
}
}

View file

@ -7,6 +7,7 @@ package mail
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/smtp"
"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
// SMTP server to test all functionality that requires a connection
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 []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
@ -957,6 +960,17 @@ func (m *Msg) UpdateReader(r *Reader) {
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
// charset for the Msg
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()
}