mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
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:
parent
f454ae8c41
commit
47bff15de9
5 changed files with 162 additions and 0 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
14
msg.go
|
@ -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
68
senderror.go
Normal 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()
|
||||||
|
}
|
Loading…
Reference in a new issue