mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-21 21:30:50 +01:00
Winni Neessen
6809084e80
Enhance error handling by adding error code and enhanced status code to the SendError struct. This allows for better troubleshooting and debugging by providing more detailed SMTP server responses.
287 lines
8.6 KiB
Go
287 lines
8.6 KiB
Go
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// List of SendError reasons
|
|
const (
|
|
// ErrGetSender is returned if the Msg.GetSender method fails during a Client.Send
|
|
ErrGetSender SendErrReason = iota
|
|
|
|
// ErrGetRcpts is returned if the Msg.GetRecipients method fails during a Client.Send
|
|
ErrGetRcpts
|
|
|
|
// ErrSMTPMailFrom is returned if the Msg delivery failed when sending the MAIL FROM command
|
|
// to the sending SMTP server
|
|
ErrSMTPMailFrom
|
|
|
|
// ErrSMTPRcptTo is returned if the Msg delivery failed when sending the RCPT TO command
|
|
// to the sending SMTP server
|
|
ErrSMTPRcptTo
|
|
|
|
// ErrSMTPData is returned if the Msg delivery failed when sending the DATA command
|
|
// to the sending SMTP server
|
|
ErrSMTPData
|
|
|
|
// ErrSMTPDataClose is returned if the Msg delivery failed when trying to close the
|
|
// Client data writer
|
|
ErrSMTPDataClose
|
|
|
|
// ErrSMTPReset is returned if the Msg delivery failed when sending the RSET command
|
|
// to the sending SMTP server
|
|
ErrSMTPReset
|
|
|
|
// ErrWriteContent is returned if the Msg delivery failed when sending Msg content
|
|
// to the Client writer
|
|
ErrWriteContent
|
|
|
|
// ErrConnCheck is returned if the Msg delivery failed when checking if the SMTP
|
|
// server connection is still working
|
|
ErrConnCheck
|
|
|
|
// ErrNoUnencoded is returned if the Msg delivery failed when the Msg is configured for
|
|
// unencoded delivery but the server does not support this
|
|
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.
|
|
//
|
|
// This struct represents an error that occurs during the delivery of a message. It holds
|
|
// details about the affected message, a list of errors, the recipient list, and whether
|
|
// the error is temporary or permanent. It also includes a reason code for the error.
|
|
type SendError struct {
|
|
affectedMsg *Msg
|
|
errcode int
|
|
enhancedStatusCode string
|
|
errlist []error
|
|
isTemp bool
|
|
rcpt []string
|
|
Reason SendErrReason
|
|
}
|
|
|
|
// SendErrReason represents a comparable reason on why the delivery failed
|
|
type SendErrReason int
|
|
|
|
// Error implements the error interface for the SendError type.
|
|
//
|
|
// This function returns a detailed error message string for the SendError, including the
|
|
// reason for failure, list of errors, affected recipients, and the message ID of the
|
|
// affected message (if available). If the reason is unknown (greater than 10), it returns
|
|
// "unknown reason". The error message is built dynamically based on the content of the
|
|
// error list, recipient list, and message ID.
|
|
//
|
|
// Returns:
|
|
// - A string representing the error message.
|
|
func (e *SendError) Error() string {
|
|
if e.Reason > ErrAmbiguous {
|
|
return "unknown reason"
|
|
}
|
|
|
|
var errMessage strings.Builder
|
|
errMessage.WriteString(e.Reason.String())
|
|
if len(e.errlist) > 0 {
|
|
errMessage.WriteRune(':')
|
|
for i := range e.errlist {
|
|
errMessage.WriteRune(' ')
|
|
errMessage.WriteString(e.errlist[i].Error())
|
|
if i != len(e.errlist)-1 {
|
|
errMessage.WriteString(",")
|
|
}
|
|
}
|
|
}
|
|
if len(e.rcpt) > 0 {
|
|
errMessage.WriteString(", affected recipient(s): ")
|
|
for i := range e.rcpt {
|
|
errMessage.WriteString(e.rcpt[i])
|
|
if i != len(e.rcpt)-1 {
|
|
errMessage.WriteString(", ")
|
|
}
|
|
}
|
|
}
|
|
if e.affectedMsg != nil && e.affectedMsg.GetMessageID() != "" {
|
|
errMessage.WriteString(", affected message ID: ")
|
|
errMessage.WriteString(e.affectedMsg.GetMessageID())
|
|
}
|
|
|
|
return errMessage.String()
|
|
}
|
|
|
|
// Is implements the errors.Is functionality and compares the SendErrReason.
|
|
//
|
|
// This function allows for comparison between two errors by checking if the provided
|
|
// error matches the SendError type and, if so, compares the SendErrReason and the
|
|
// temporary status (isTemp) of both errors.
|
|
//
|
|
// Parameters:
|
|
// - errType: The error to compare against the current SendError.
|
|
//
|
|
// Returns:
|
|
// - true if the errors have the same reason and temporary status, false otherwise.
|
|
func (e *SendError) Is(errType error) bool {
|
|
var t *SendError
|
|
if errors.As(errType, &t) && t != nil {
|
|
return e.Reason == t.Reason && e.isTemp == t.isTemp
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsTemp returns true if the delivery error is of a temporary nature and can be retried.
|
|
//
|
|
// This function checks whether the SendError indicates a temporary error, which suggests
|
|
// that the delivery can be retried. If the SendError is nil, it returns false.
|
|
//
|
|
// Returns:
|
|
// - true if the error is temporary, false otherwise.
|
|
func (e *SendError) IsTemp() bool {
|
|
if e == nil {
|
|
return false
|
|
}
|
|
return e.isTemp
|
|
}
|
|
|
|
// MessageID returns the message ID of the affected Msg that caused the error.
|
|
//
|
|
// This function retrieves the message ID of the Msg associated with the SendError.
|
|
// If no message ID was set or if the SendError or Msg is nil, it returns an empty string.
|
|
//
|
|
// Returns:
|
|
// - The message ID as a string, or an empty string if no ID is available.
|
|
func (e *SendError) MessageID() string {
|
|
if e == nil || e.affectedMsg == nil {
|
|
return ""
|
|
}
|
|
return e.affectedMsg.GetMessageID()
|
|
}
|
|
|
|
// Msg returns the pointer to the affected message that caused the error.
|
|
//
|
|
// This function retrieves the Msg associated with the SendError. If the SendError or
|
|
// the affectedMsg is nil, it returns nil.
|
|
//
|
|
// Returns:
|
|
// - A pointer to the Msg that caused the error, or nil if not available.
|
|
func (e *SendError) Msg() *Msg {
|
|
if e == nil || e.affectedMsg == nil {
|
|
return nil
|
|
}
|
|
return e.affectedMsg
|
|
}
|
|
|
|
// EnhancedStatusCode returns the enhanced status code of the server response if the
|
|
// server supports it, as described in RFC 2034.
|
|
//
|
|
// This function retrieves the enhanced status code of an error returned by the server. This
|
|
// requires that the receiving server supports this SMTP extension as described in RFC 2034.
|
|
// Since this is the SendError interface, we only collect status codes for error responses,
|
|
// meaning 4xx or 5xx. If the server does not support the ENHANCEDSTATUSCODES extension or
|
|
// the error did not include an enhanced status code, it will return an empty string.
|
|
//
|
|
// Returns:
|
|
// - The enhanced status code as returned by the server, or an empty string is not supported.
|
|
//
|
|
// References:
|
|
// - https://datatracker.ietf.org/doc/html/rfc2034
|
|
func (e *SendError) EnhancedStatusCode() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
return e.enhancedStatusCode
|
|
}
|
|
|
|
// String satisfies the fmt.Stringer interface for the SendErrReason type.
|
|
//
|
|
// This function converts the SendErrReason into a human-readable string representation based
|
|
// on the error type. If the error reason does not match any predefined case, it returns
|
|
// "unknown reason".
|
|
//
|
|
// Returns:
|
|
// - A string representation of the SendErrReason.
|
|
func (r SendErrReason) String() string {
|
|
switch r {
|
|
case ErrGetSender:
|
|
return "getting sender address"
|
|
case ErrGetRcpts:
|
|
return "getting recipient addresses"
|
|
case ErrSMTPMailFrom:
|
|
return "sending SMTP MAIL FROM command"
|
|
case ErrSMTPRcptTo:
|
|
return "sending SMTP RCPT TO command"
|
|
case ErrSMTPData:
|
|
return "sending SMTP DATA command"
|
|
case ErrSMTPDataClose:
|
|
return "closing SMTP DATA writer"
|
|
case ErrSMTPReset:
|
|
return "sending SMTP RESET command"
|
|
case ErrWriteContent:
|
|
return "sending message content"
|
|
case ErrConnCheck:
|
|
return "checking SMTP connection"
|
|
case ErrNoUnencoded:
|
|
return ErrServerNoUnencoded.Error()
|
|
case ErrAmbiguous:
|
|
return "ambiguous reason, check Msg.SendError for message specific reasons"
|
|
}
|
|
return "unknown reason"
|
|
}
|
|
|
|
// isTempError checks if the given SMTP error is of a temporary nature and should be retried.
|
|
//
|
|
// This function inspects the error message and returns true if the first character of the
|
|
// error message is '4', indicating a temporary SMTP error that can be retried.
|
|
//
|
|
// Parameters:
|
|
// - err: The error to check.
|
|
//
|
|
// Returns:
|
|
// - true if the error is temporary, false otherwise.
|
|
func isTempError(err error) bool {
|
|
return err.Error()[0] == '4'
|
|
}
|
|
|
|
func getErrorCode(err error) int {
|
|
rootErr := errors.Unwrap(err)
|
|
if rootErr != nil {
|
|
err = rootErr
|
|
}
|
|
firstrune := err.Error()[0]
|
|
if firstrune < 52 || firstrune > 53 {
|
|
return 0
|
|
}
|
|
code := err.Error()[0:3]
|
|
errcode, cerr := strconv.Atoi(code)
|
|
if cerr != nil {
|
|
return 0
|
|
}
|
|
return errcode
|
|
}
|
|
|
|
func getEnhancedStatusCode(err error, supported bool) string {
|
|
if err == nil || !supported {
|
|
return ""
|
|
}
|
|
rootErr := errors.Unwrap(err)
|
|
if rootErr != nil {
|
|
err = rootErr
|
|
}
|
|
firstrune := err.Error()[0]
|
|
if firstrune != 50 && firstrune != 52 && firstrune != 53 {
|
|
return ""
|
|
}
|
|
re, rerr := regexp.Compile(`\b([245])\.\d{1,3}\.\d{1,3}\b`)
|
|
if rerr != nil {
|
|
return ""
|
|
}
|
|
return re.FindString(err.Error())
|
|
}
|