mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 14:10:50 +01:00
Compare commits
27 commits
db2ec99cd6
...
6f869e4efd
Author | SHA1 | Date | |
---|---|---|---|
6f869e4efd | |||
44df830348 | |||
4ee11e8406 | |||
bd5a8a40b9 | |||
8dfb121aec | |||
d5437f6b7a | |||
157c138142 | |||
482194b4b3 | |||
b8f0462ce3 | |||
6af6a28f78 | |||
0aa24c6f3a | |||
fbebcf96d8 | |||
619318ba0d | |||
d400379e2f | |||
fcbd202595 | |||
508a2f2a6c | |||
f469ba977d | |||
0239318d94 | |||
69e211682c | |||
3bdb6f7cca | |||
277ae9be19 | |||
2e7156182a | |||
8ee37abca2 | |||
ee726487f1 | |||
1dd73778d4 | |||
a747f5f74c | |||
0ea9631855 |
9 changed files with 911 additions and 195 deletions
2
.github/workflows/codecov.yml
vendored
2
.github/workflows/codecov.yml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
go: ['1.20', '1.21', '1.22', '1.23']
|
go: ['1.19', '1.20', '1.23']
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
|
|
97
client.go
97
client.go
|
@ -787,3 +787,100 @@ 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
|
||||||
|
}
|
||||||
|
|
106
client_119.go
106
client_119.go
|
@ -7,111 +7,23 @@
|
||||||
|
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import "strings"
|
import "errors"
|
||||||
|
|
||||||
// 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 id, 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)
|
|
||||||
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 {
|
var msgSendErr *SendError
|
||||||
if c.dsnmrtype != "" {
|
if errors.As(sendErr, &msgSendErr) {
|
||||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
errs = append(errs, msgSendErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
|
|
@ -9,7 +9,6 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send sends out the mail message
|
// 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)}
|
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
|
||||||
return
|
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 {
|
var errs []error
|
||||||
if c.dsnmrtype != "" {
|
defer func() {
|
||||||
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype))
|
returnErr = errors.Join(errs...)
|
||||||
}
|
}()
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
||||||
if err = writer.Close(); err != nil {
|
for id, message := range messages {
|
||||||
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)}
|
if sendErr := c.sendSingleMsg(message); sendErr != nil {
|
||||||
returnErr = errors.Join(returnErr, message.sendError)
|
messages[id].sendError = sendErr
|
||||||
continue
|
errs = append(errs, sendErr)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
694
client_test.go
694
client_test.go
|
@ -5,6 +5,7 @@
|
||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -21,11 +22,18 @@ import (
|
||||||
"github.com/wneessen/go-mail/smtp"
|
"github.com/wneessen/go-mail/smtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultHost is used as default hostname for the Client
|
const (
|
||||||
const DefaultHost = "localhost"
|
// DefaultHost is used as default hostname for the Client
|
||||||
|
DefaultHost = "localhost"
|
||||||
// TestRcpt
|
// TestRcpt is a trash mail address to send test mails to
|
||||||
const TestRcpt = "go-mail@mytrashmailer.com"
|
TestRcpt = "go-mail@mytrashmailer.com"
|
||||||
|
// TestServerProto is the protocol used for the simple SMTP test server
|
||||||
|
TestServerProto = "tcp"
|
||||||
|
// TestServerAddr is the address the simple SMTP test server listens on
|
||||||
|
TestServerAddr = "127.0.0.1"
|
||||||
|
// TestServerPortBase is the base port for the simple SMTP test server
|
||||||
|
TestServerPortBase = 2025
|
||||||
|
)
|
||||||
|
|
||||||
// TestNewClient tests the NewClient() method with its default options
|
// TestNewClient tests the NewClient() method with its default options
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
|
@ -629,7 +637,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)
|
||||||
}
|
}
|
||||||
|
@ -1251,6 +1259,475 @@ func TestClient_DialAndSendWithContext_withSendError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorNoEncoding(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
serverPort := TestServerPortBase + 1
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("valid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "Test body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
message.SetEncoding(NoEncoding)
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrNoUnencoded {
|
||||||
|
t.Errorf("expected ErrNoUnencoded error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if sendErr.Msg() == nil {
|
||||||
|
t.Errorf("expected message to be set, but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorMailFrom(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 2
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("invalid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "Test body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPMailFrom {
|
||||||
|
t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if sendErr.Msg() == nil {
|
||||||
|
t.Errorf("expected message to be set, but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorMailFromReset(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 3
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("invalid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "Test body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPMailFrom {
|
||||||
|
t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if len(sendErr.errlist) != 2 {
|
||||||
|
t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[0].Error(), "503 5.1.2 Invalid from: <invalid-from@domain.tld>") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"503 5.1.2 Invalid from: <invalid-from@domain.tld>", sendErr.errlist[0].Error())
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"500 5.1.2 Error: reset failed", sendErr.errlist[1].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorToReset(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 4
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("valid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("invalid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "Test body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPRcptTo {
|
||||||
|
t.Errorf("expected ErrSMTPRcptTo error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
if len(sendErr.errlist) != 2 {
|
||||||
|
t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[0].Error(), "500 5.1.2 Invalid to: <invalid-to@domain.tld>") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"500 5.1.2 Invalid to: <invalid-to@domain.tld>", sendErr.errlist[0].Error())
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") {
|
||||||
|
t.Errorf("expected error: %q, but got %q",
|
||||||
|
"500 5.1.2 Error: reset failed", sendErr.errlist[1].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorDataClose(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 5
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("valid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "DATA close should fail")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPDataClose {
|
||||||
|
t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorDataWrite(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 6
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("valid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "DATA write should fail")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
message.SetGenHeader("X-Test-Header", "DATA write should fail")
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPDataClose {
|
||||||
|
t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_SendErrorReset(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverPort := TestServerPortBase + 7
|
||||||
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
|
go func() {
|
||||||
|
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
|
||||||
|
t.Errorf("failed to start test server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
message := NewMsg()
|
||||||
|
if err := message.From("valid-from@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set FROM address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := message.To("valid-to@domain.tld"); err != nil {
|
||||||
|
t.Errorf("failed to set TO address: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.Subject("Test subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "Test body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
client, err := NewClient(TestServerAddr, WithPort(serverPort),
|
||||||
|
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
|
||||||
|
WithUsername("toni@tester.com"),
|
||||||
|
WithPassword("V3ryS3cr3t+"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create new client: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.DialWithContext(context.Background()); err != nil {
|
||||||
|
t.Errorf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Send(message); err == nil {
|
||||||
|
t.Error("expected Send() to fail but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendErr *SendError
|
||||||
|
if !errors.As(err, &sendErr) {
|
||||||
|
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
|
||||||
|
}
|
||||||
|
if errors.As(err, &sendErr) {
|
||||||
|
if sendErr.IsTemp() {
|
||||||
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
||||||
|
}
|
||||||
|
if sendErr.Reason != ErrSMTPReset {
|
||||||
|
t.Errorf("expected ErrSMTPReset error, but got %s", sendErr.Reason)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
|
||||||
|
sendErr.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close server connection: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -1302,6 +1779,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 +1878,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
|
||||||
}
|
}
|
||||||
|
@ -1494,3 +2022,155 @@ func (f faker) RemoteAddr() net.Addr { return nil }
|
||||||
func (f faker) SetDeadline(time.Time) error { return nil }
|
func (f faker) SetDeadline(time.Time) error { return nil }
|
||||||
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
func (f faker) SetReadDeadline(time.Time) error { return nil }
|
||||||
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
func (f faker) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
|
||||||
|
// The provided featureSet represents in what the server responds to EHLO command
|
||||||
|
// failReset controls if a RSET succeeds
|
||||||
|
func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, port int) error {
|
||||||
|
listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := listener.Close(); err != nil {
|
||||||
|
fmt.Printf("unable to close listener: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
connection, err := listener.Accept()
|
||||||
|
var opErr *net.OpError
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, &opErr) && opErr.Temporary() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to accept connection: %w", err)
|
||||||
|
}
|
||||||
|
handleTestServerConnection(connection, featureSet, failReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTestServerConnection(connection net.Conn, featureSet string, failReset bool) {
|
||||||
|
defer func() {
|
||||||
|
if err := connection.Close(); err != nil {
|
||||||
|
fmt.Printf("unable to close connection: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(connection)
|
||||||
|
writer := bufio.NewWriter(connection)
|
||||||
|
|
||||||
|
writeLine := func(data string) error {
|
||||||
|
_, err := writer.WriteString(data + "\r\n")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to write line: %w", err)
|
||||||
|
}
|
||||||
|
return writer.Flush()
|
||||||
|
}
|
||||||
|
writeOK := func() {
|
||||||
|
_ = writeLine("250 2.0.0 OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeLine("220 go-mail test server ready ESMTP"); err != nil {
|
||||||
|
fmt.Printf("unable to write to client: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("unable to read from connection: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") {
|
||||||
|
fmt.Printf("expected EHLO, got %q", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil {
|
||||||
|
fmt.Printf("unable to write to connection: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, err = reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Error reading data:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var datastring string
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(data, "MAIL FROM:"):
|
||||||
|
from := strings.TrimPrefix(data, "MAIL FROM:")
|
||||||
|
from = strings.ReplaceAll(from, "BODY=8BITMIME", "")
|
||||||
|
from = strings.ReplaceAll(from, "SMTPUTF8", "")
|
||||||
|
from = strings.TrimSpace(from)
|
||||||
|
if !strings.EqualFold(from, "<valid-from@domain.tld>") {
|
||||||
|
_ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeOK()
|
||||||
|
case strings.HasPrefix(data, "RCPT TO:"):
|
||||||
|
to := strings.TrimPrefix(data, "RCPT TO:")
|
||||||
|
to = strings.TrimSpace(to)
|
||||||
|
if !strings.EqualFold(to, "<valid-to@domain.tld>") {
|
||||||
|
_ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeOK()
|
||||||
|
case strings.HasPrefix(data, "AUTH PLAIN"):
|
||||||
|
auth := strings.TrimPrefix(data, "AUTH PLAIN ")
|
||||||
|
if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") {
|
||||||
|
_ = writeLine("535 5.7.8 Error: authentication failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = writeLine("235 2.7.0 Authentication successful")
|
||||||
|
case strings.EqualFold(data, "DATA"):
|
||||||
|
_ = writeLine("354 End data with <CR><LF>.<CR><LF>")
|
||||||
|
for {
|
||||||
|
ddata, derr := reader.ReadString('\n')
|
||||||
|
if derr != nil {
|
||||||
|
fmt.Printf("failed to read DATA data from connection: %s\n", derr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ddata = strings.TrimSpace(ddata)
|
||||||
|
if strings.EqualFold(ddata, "DATA write should fail") {
|
||||||
|
_ = writeLine("500 5.0.0 Error during DATA transmission")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ddata == "." {
|
||||||
|
if strings.Contains(datastring, "DATA close should fail") {
|
||||||
|
_ = writeLine("500 5.0.0 Error during DATA closing")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = writeLine("250 2.0.0 Ok: queued as 1234567890")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
datastring += ddata + "\n"
|
||||||
|
}
|
||||||
|
case strings.EqualFold(data, "noop"),
|
||||||
|
strings.EqualFold(data, "vrfy"):
|
||||||
|
writeOK()
|
||||||
|
case strings.EqualFold(data, "rset"):
|
||||||
|
if failReset {
|
||||||
|
_ = writeLine("500 5.1.2 Error: reset failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeOK()
|
||||||
|
case strings.EqualFold(data, "quit"):
|
||||||
|
_ = writeLine("221 2.0.0 Bye")
|
||||||
|
default:
|
||||||
|
_ = writeLine("500 5.5.2 Error: bad syntax")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
msg.go
11
msg.go
|
@ -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))
|
||||||
|
|
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
|
// 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 {
|
||||||
|
|
27
senderror.go
27
senderror.go
|
@ -56,10 +56,11 @@ const (
|
||||||
|
|
||||||
// SendError is an error wrapper for delivery errors of the Msg
|
// SendError is an error wrapper for delivery errors of the Msg
|
||||||
type SendError struct {
|
type SendError struct {
|
||||||
Reason SendErrReason
|
affectedMsg *Msg
|
||||||
isTemp bool
|
|
||||||
errlist []error
|
errlist []error
|
||||||
|
isTemp bool
|
||||||
rcpt []string
|
rcpt []string
|
||||||
|
Reason SendErrReason
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendErrReason represents a comparable reason on why the delivery failed
|
// 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()
|
return errMessage.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +118,23 @@ func (e *SendError) IsTemp() bool {
|
||||||
return e.isTemp
|
return e.isTemp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MessageID returns the message ID of the affected Msg that caused the error
|
||||||
|
// If no message ID was set for the Msg, an empty string will be returned
|
||||||
|
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
|
||||||
|
func (e *SendError) Msg() *Msg {
|
||||||
|
if e == nil || e.affectedMsg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.affectedMsg
|
||||||
|
}
|
||||||
|
|
||||||
// String implements the Stringer interface for the SendErrReason
|
// String implements the Stringer interface for the SendErrReason
|
||||||
func (r SendErrReason) String() string {
|
func (r SendErrReason) String() string {
|
||||||
switch r {
|
switch r {
|
||||||
|
|
|
@ -83,7 +83,62 @@ 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendError_MessageID(t *testing.T) {
|
||||||
|
var se *SendError
|
||||||
|
err := returnSendError(ErrAmbiguous, false)
|
||||||
|
if !errors.As(err, &se) {
|
||||||
|
t.Errorf("error mismatch, expected error to be of type *SendError")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.As(err, &se) {
|
||||||
|
if se.MessageID() == "" {
|
||||||
|
t.Errorf("sendError expected message-id, but got empty string")
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
|
||||||
|
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
|
||||||
|
se.MessageID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendError_Msg(t *testing.T) {
|
||||||
|
var se *SendError
|
||||||
|
err := returnSendError(ErrAmbiguous, false)
|
||||||
|
if !errors.As(err, &se) {
|
||||||
|
t.Errorf("error mismatch, expected error to be of type *SendError")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.As(err, &se) {
|
||||||
|
if se.Msg() == nil {
|
||||||
|
t.Errorf("sendError expected msg pointer, but got nil")
|
||||||
|
}
|
||||||
|
from := se.Msg().GetFromString()
|
||||||
|
if len(from) == 0 {
|
||||||
|
t.Errorf("sendError expected msg from, but got empty string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
|
||||||
|
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
|
||||||
|
from[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// returnSendError is a helper method to retunr a SendError with a specific reason
|
// returnSendError is a helper method to retunr a SendError with a specific reason
|
||||||
func returnSendError(r SendErrReason, t bool) error {
|
func returnSendError(r SendErrReason, t bool) error {
|
||||||
return &SendError{Reason: r, isTemp: t}
|
message := NewMsg()
|
||||||
|
_ = message.From("toni.tester@domain.tld")
|
||||||
|
_ = message.To("tina.tester@domain.tld")
|
||||||
|
message.Subject("This is the subject")
|
||||||
|
message.SetBodyString(TypeTextPlain, "This is the message body")
|
||||||
|
message.SetMessageIDWithValue("this.is.a.message.id")
|
||||||
|
|
||||||
|
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue