mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-18 17:00:38 +01:00
Merge pull request #301 from wneessen/feature/168_improve-error-handling
Improved error handling
This commit is contained in:
commit
619318ba0d
8 changed files with 205 additions and 189 deletions
97
client.go
97
client.go
|
@ -787,3 +787,100 @@ func (c *Client) auth() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails.
|
||||
// It is invoked by the public Send methods
|
||||
func (c *Client) sendSingleMsg(message *Msg) error {
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message}
|
||||
}
|
||||
}
|
||||
from, err := message.GetSender(false)
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
|
||||
if c.dsn {
|
||||
if c.dsnmrtype != "" {
|
||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
||||
}
|
||||
}
|
||||
if err = c.smtpClient.Mail(from); err != nil {
|
||||
retError := &SendError{
|
||||
Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
retError.errlist = append(retError.errlist, resetSendErr)
|
||||
}
|
||||
return retError
|
||||
}
|
||||
hasError := false
|
||||
rcptSendErr := &SendError{affectedMsg: message}
|
||||
rcptSendErr.errlist = make([]error, 0)
|
||||
rcptSendErr.rcpt = make([]string, 0)
|
||||
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
|
||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||
for _, rcpt := range rcpts {
|
||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
if hasError {
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr)
|
||||
}
|
||||
return rcptSendErr
|
||||
}
|
||||
writer, err := c.smtpClient.Data()
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
return &SendError{
|
||||
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
|
||||
affectedMsg: message,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
102
client_119.go
102
client_119.go
|
@ -7,110 +7,16 @@
|
|||
|
||||
package mail
|
||||
|
||||
import "strings"
|
||||
|
||||
// Send sends out the mail message
|
||||
func (c *Client) Send(messages ...*Msg) error {
|
||||
if cerr := c.checkConn(); cerr != nil {
|
||||
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)}
|
||||
if err := c.checkConn(); err != nil {
|
||||
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
}
|
||||
var errs []*SendError
|
||||
for _, message := range messages {
|
||||
message.sendError = nil
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
sendErr := &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||
message.sendError = sendErr
|
||||
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||
messages[id].sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
from, err := message.GetSender(false)
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.dsn {
|
||||
if c.dsnmrtype != "" {
|
||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
||||
}
|
||||
}
|
||||
if err = c.smtpClient.Mail(from); err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
sendErr.errlist = append(sendErr.errlist, resetSendErr)
|
||||
}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
failed := false
|
||||
rcptSendErr := &SendError{}
|
||||
rcptSendErr.errlist = make([]error, 0)
|
||||
rcptSendErr.rcpt = make([]string, 0)
|
||||
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
|
||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||
for _, rcpt := range rcpts {
|
||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
}
|
||||
message.sendError = rcptSendErr
|
||||
errs = append(errs, rcptSendErr)
|
||||
continue
|
||||
}
|
||||
writer, err := c.smtpClient.Data()
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
sendErr := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
message.isDelivered = true
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
sendErr := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
sendErr := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
message.sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ package mail
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Send sends out the mail message
|
||||
|
@ -18,92 +17,16 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) {
|
|||
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
return
|
||||
}
|
||||
for _, message := range messages {
|
||||
message.sendError = nil
|
||||
if message.encoding == NoEncoding {
|
||||
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
|
||||
message.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
}
|
||||
from, err := message.GetSender(false)
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
rcpts, err := message.GetRecipients()
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.dsn {
|
||||
if c.dsnmrtype != "" {
|
||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
||||
}
|
||||
}
|
||||
if err = c.smtpClient.Mail(from); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
returnErr = errors.Join(returnErr, resetSendErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
failed := false
|
||||
rcptSendErr := &SendError{}
|
||||
rcptSendErr.errlist = make([]error, 0)
|
||||
rcptSendErr.rcpt = make([]string, 0)
|
||||
rcptNotifyOpt := strings.Join(c.dsnrntype, ",")
|
||||
c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt)
|
||||
for _, rcpt := range rcpts {
|
||||
if err = c.smtpClient.Rcpt(rcpt); err != nil {
|
||||
rcptSendErr.Reason = ErrSMTPRcptTo
|
||||
rcptSendErr.errlist = append(rcptSendErr.errlist, err)
|
||||
rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt)
|
||||
rcptSendErr.isTemp = isTempError(err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
|
||||
returnErr = errors.Join(returnErr, resetSendErr)
|
||||
}
|
||||
message.sendError = rcptSendErr
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
writer, err := c.smtpClient.Data()
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
_, err = message.WriteTo(writer)
|
||||
if err != nil {
|
||||
message.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
message.isDelivered = true
|
||||
var errs []error
|
||||
defer func() {
|
||||
returnErr = errors.Join(errs...)
|
||||
}()
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.Reset(); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
continue
|
||||
}
|
||||
if err = c.checkConn(); err != nil {
|
||||
message.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||
returnErr = errors.Join(returnErr, message.sendError)
|
||||
for id, message := range messages {
|
||||
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||
messages[id].sendError = sendErr
|
||||
errs = append(errs, sendErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -629,7 +629,7 @@ func TestClient_DialWithContext(t *testing.T) {
|
|||
// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback
|
||||
// port functionality
|
||||
func TestClient_DialWithContext_Fallback(t *testing.T) {
|
||||
c, err := getTestConnection(true)
|
||||
c, err := getTestConnectionNoTestPort(true)
|
||||
if err != nil {
|
||||
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||
}
|
||||
|
@ -1302,6 +1302,50 @@ func getTestConnection(auth bool) (*Client, error) {
|
|||
return c, nil
|
||||
}
|
||||
|
||||
// getTestConnectionNoTestPort takes environment variables (except the port) to establish a
|
||||
// connection to a real SMTP server to test all functionality that requires a connection
|
||||
func getTestConnectionNoTestPort(auth bool) (*Client, error) {
|
||||
if os.Getenv("TEST_SKIP_ONLINE") != "" {
|
||||
return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests")
|
||||
}
|
||||
th := os.Getenv("TEST_HOST")
|
||||
if th == "" {
|
||||
return nil, fmt.Errorf("no TEST_HOST set")
|
||||
}
|
||||
sv := false
|
||||
if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" {
|
||||
sv = true
|
||||
}
|
||||
c, err := NewClient(th)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
c.tlsconfig.InsecureSkipVerify = sv
|
||||
if auth {
|
||||
st := os.Getenv("TEST_SMTPAUTH_TYPE")
|
||||
if st != "" {
|
||||
c.SetSMTPAuth(SMTPAuthType(st))
|
||||
}
|
||||
u := os.Getenv("TEST_SMTPAUTH_USER")
|
||||
if u != "" {
|
||||
c.SetUsername(u)
|
||||
}
|
||||
p := os.Getenv("TEST_SMTPAUTH_PASS")
|
||||
if p != "" {
|
||||
c.SetPassword(p)
|
||||
}
|
||||
// We don't want to log authentication data in tests
|
||||
c.SetDebugLog(false)
|
||||
}
|
||||
if err := c.DialWithContext(context.Background()); err != nil {
|
||||
return c, fmt.Errorf("connection to test server failed: %w", err)
|
||||
}
|
||||
if err := c.Close(); err != nil {
|
||||
return c, fmt.Errorf("disconnect from test server failed: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// getTestClient takes environment variables to establish a client without connecting
|
||||
// to the SMTP server
|
||||
func getTestClient(auth bool) (*Client, error) {
|
||||
|
@ -1357,7 +1401,14 @@ func getTestConnectionWithDSN(auth bool) (*Client, error) {
|
|||
if th == "" {
|
||||
return nil, fmt.Errorf("no TEST_HOST set")
|
||||
}
|
||||
c, err := NewClient(th, WithDSN())
|
||||
tp := 25
|
||||
if tps := os.Getenv("TEST_PORT"); tps != "" {
|
||||
tpi, err := strconv.Atoi(tps)
|
||||
if err == nil {
|
||||
tp = tpi
|
||||
}
|
||||
}
|
||||
c, err := NewClient(th, WithDSN(), WithPort(tp))
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
|
11
msg.go
11
msg.go
|
@ -476,6 +476,17 @@ func (m *Msg) SetMessageID() {
|
|||
m.SetMessageIDWithValue(messageID)
|
||||
}
|
||||
|
||||
// GetMessageID returns the message ID of the Msg as string value. If no message ID
|
||||
// is set, an empty string will be returned
|
||||
func (m *Msg) GetMessageID() string {
|
||||
if msgidheader, ok := m.genHeader[HeaderMessageID]; ok {
|
||||
if len(msgidheader) > 0 {
|
||||
return msgidheader[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetMessageIDWithValue sets the message id for the mail
|
||||
func (m *Msg) SetMessageIDWithValue(messageID string) {
|
||||
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))
|
||||
|
|
15
msg_test.go
15
msg_test.go
|
@ -805,6 +805,21 @@ func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMsg_GetMessageID(t *testing.T) {
|
||||
expected := "this.is.a.message.id"
|
||||
msg := NewMsg()
|
||||
msg.SetMessageIDWithValue(expected)
|
||||
val := msg.GetMessageID()
|
||||
if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) {
|
||||
t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val)
|
||||
}
|
||||
msg.genHeader[HeaderMessageID] = nil
|
||||
val = msg.GetMessageID()
|
||||
if val != "" {
|
||||
t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
|
||||
func TestMsg_FromFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
10
senderror.go
10
senderror.go
|
@ -56,10 +56,11 @@ const (
|
|||
|
||||
// SendError is an error wrapper for delivery errors of the Msg
|
||||
type SendError struct {
|
||||
Reason SendErrReason
|
||||
isTemp bool
|
||||
affectedMsg *Msg
|
||||
errlist []error
|
||||
isTemp bool
|
||||
rcpt []string
|
||||
Reason SendErrReason
|
||||
}
|
||||
|
||||
// SendErrReason represents a comparable reason on why the delivery failed
|
||||
|
@ -92,6 +93,11 @@ func (e *SendError) Error() string {
|
|||
}
|
||||
}
|
||||
}
|
||||
if e.affectedMsg != nil && e.affectedMsg.GetMessageID() != "" {
|
||||
errMessage.WriteString(", affected message ID: ")
|
||||
errMessage.WriteString(e.affectedMsg.GetMessageID())
|
||||
}
|
||||
|
||||
return errMessage.String()
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,13 @@ func TestSendError_IsTemp(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSendError_IsTempNil(t *testing.T) {
|
||||
var se *SendError
|
||||
if se.IsTemp() {
|
||||
t.Error("expected false on nil-senderror")
|
||||
}
|
||||
}
|
||||
|
||||
// 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