diff --git a/client.go b/client.go index 1d175d1..fac9a34 100644 --- a/client.go +++ b/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 +} diff --git a/client_119.go b/client_119.go index 52e4b3f..8084913 100644 --- a/client_119.go +++ b/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 - 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 + if sendErr := c.sendSingleMsg(message); sendErr != nil { + messages[id].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 } } diff --git a/client_120.go b/client_120.go index ed80fee..4f82aa7 100644 --- a/client_120.go +++ b/client_120.go @@ -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) } } diff --git a/client_test.go b/client_test.go index 0ad309a..efccea4 100644 --- a/client_test.go +++ b/client_test.go @@ -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 } diff --git a/msg.go b/msg.go index a909d04..11d8ab1 100644 --- a/msg.go +++ b/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)) diff --git a/msg_test.go b/msg_test.go index 16cd196..0656ff2 100644 --- a/msg_test.go +++ b/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 { diff --git a/senderror.go b/senderror.go index dfe7502..494bfd3 100644 --- a/senderror.go +++ b/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 - errlist []error - rcpt []string + 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() } diff --git a/senderror_test.go b/senderror_test.go index e83df00..789b290 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -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}