Compare commits

...

7 commits

Author SHA1 Message Date
508a2f2a6c
Refactor connection handling in tests
Replaced `getTestConnection` with `getTestConnectionNoTestPort` for tests that skip port configuration, improving connection setup flexibility. Added environment variable support for setting ports in `getTestClient` to streamline port configuration in tests.
2024-09-19 15:21:17 +02:00
f469ba977d
Add affected message ID to error log
Include the affected message ID in the error message to provide more context for debugging. This change ensures that each error log contains essential information about the specific message associated with the error.
2024-09-19 12:12:48 +02:00
0239318d94
Add affectedMsg field to SendError struct
Included the affected message in the SendError struct for better error tracking and debugging. This enhancement ensures that any errors encountered during the sending process can be directly associated with the specific message that caused them.
2024-09-19 12:09:23 +02:00
69e211682c
Add GetMessageID method to Msg
Introduced GetMessageID to retrieve the Message ID from the Msg
2024-09-19 11:46:53 +02:00
3bdb6f7cca
Refactor variable naming in Send method
Renamed variable `cerr` to `err` for consistency. This improves readability and standardizes error variable naming within the method.
2024-09-19 10:59:22 +02:00
277ae9be19
Refactor message sending logic
Consolidated the message sending logic into a single `sendSingleMsg` function to reduce duplication and improve code maintainability. This change simplifies the `Send` method in multiple Go version files by removing redundant code and calling the new helper function instead.
2024-09-19 10:56:01 +02:00
2e7156182a
Rename variable 'failed' to 'hasError' for clarity
Renamed the variable 'failed' to 'hasError' to better reflect its purpose and improve code readability. This change helps in understanding that the variable indicates the presence of an error rather than a generic failure.
2024-09-19 10:35:32 +02:00
7 changed files with 169 additions and 176 deletions

View file

@ -787,3 +787,84 @@ func (c *Client) auth() error {
} }
return nil 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
}

View file

@ -7,110 +7,16 @@
package mail package mail
import "strings"
// Send sends out the mail message // Send sends out the mail message
func (c *Client) Send(messages ...*Msg) error { func (c *Client) Send(messages ...*Msg) error {
if cerr := c.checkConn(); cerr != nil { if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)} return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
} }
var errs []*SendError var errs []*SendError
for _, message := range messages { for _, message := range messages {
message.sendError = nil if sendErr := c.sendSingleMsg(message); sendErr != nil {
if message.encoding == NoEncoding { messages[id].sendError = sendErr
if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok {
sendErr := &SendError{Reason: ErrNoUnencoded, isTemp: false}
message.sendError = sendErr
errs = append(errs, 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
} }
} }

View file

@ -9,7 +9,6 @@ package mail
import ( import (
"errors" "errors"
"strings"
) )
// Send sends out the mail message // Send sends out the mail message
@ -33,78 +32,3 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) {
return return
} }
// 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}
}
}
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)}
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
retError.errlist = append(retError.errlist, resetSendErr)
}
return retError
}
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, resetSendErr)
}
return rcptSendErr
}
writer, err := c.smtpClient.Data()
if err != nil {
return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)}
}
_, err = message.WriteTo(writer)
if err != nil {
return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)}
}
message.isDelivered = true
if err = writer.Close(); err != nil {
return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
}
if err = c.Reset(); err != nil {
return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
}
if err = c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
}
return nil
}

View file

@ -629,7 +629,7 @@ func TestClient_DialWithContext(t *testing.T) {
// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback // TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback
// port functionality // port functionality
func TestClient_DialWithContext_Fallback(t *testing.T) { func TestClient_DialWithContext_Fallback(t *testing.T) {
c, err := getTestConnection(true) c, err := getTestConnectionNoTestPort(true)
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
@ -1302,6 +1302,50 @@ func getTestConnection(auth bool) (*Client, error) {
return c, nil 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 // getTestClient takes environment variables to establish a client without connecting
// to the SMTP server // to the SMTP server
func getTestClient(auth bool) (*Client, error) { func getTestClient(auth bool) (*Client, error) {
@ -1357,7 +1401,14 @@ func getTestConnectionWithDSN(auth bool) (*Client, error) {
if th == "" { if th == "" {
return nil, fmt.Errorf("no TEST_HOST set") 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 { if err != nil {
return c, err return c, err
} }

11
msg.go
View file

@ -476,6 +476,17 @@ func (m *Msg) SetMessageID() {
m.SetMessageIDWithValue(messageID) 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 // SetMessageIDWithValue sets the message id for the mail
func (m *Msg) SetMessageIDWithValue(messageID string) { func (m *Msg) SetMessageIDWithValue(messageID string) {
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID)) m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))

View file

@ -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 // TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
func TestMsg_FromFormat(t *testing.T) { func TestMsg_FromFormat(t *testing.T) {
tests := []struct { tests := []struct {

View file

@ -93,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() return errMessage.String()
} }