mirror of
https://github.com/wneessen/go-mail.git
synced 2024-10-06 02:05:57 +02:00
Reset connections to nil after Close, add RLock in HasConnection, and refine Close logic to handle already closed connections gracefully. Enhanced DialWithContext documentation and added tests for double-close scenarios to ensure robustness.
2715 lines
80 KiB
Go
2715 lines
80 KiB
Go
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/wneessen/go-mail/log"
|
|
"github.com/wneessen/go-mail/smtp"
|
|
)
|
|
|
|
const (
|
|
// DefaultHost is used as default hostname for the Client
|
|
DefaultHost = "localhost"
|
|
// TestRcpt is a trash mail address to send test mails to
|
|
TestRcpt = "couttifaddebro-1473@yopmail.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
|
|
func TestNewClient(t *testing.T) {
|
|
host := "mail.example.com"
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
shouldfail bool
|
|
}{
|
|
{"Default", "mail.example.com", false},
|
|
{"Empty host should fail", "", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(tt.host)
|
|
if err != nil && !tt.shouldfail {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.host != tt.host {
|
|
t.Errorf("failed to create new client. Host expected: %s, got: %s", host, c.host)
|
|
}
|
|
if c.connTimeout != DefaultTimeout {
|
|
t.Errorf("failed to create new client. Timeout expected: %s, got: %s", DefaultTimeout.String(),
|
|
c.connTimeout.String())
|
|
}
|
|
if c.port != DefaultPort {
|
|
t.Errorf("failed to create new client. Port expected: %d, got: %d", DefaultPort, c.port)
|
|
}
|
|
if c.tlspolicy != TLSMandatory {
|
|
t.Errorf("failed to create new client. TLS policy expected: %d, got: %d", TLSMandatory, c.tlspolicy)
|
|
}
|
|
if c.tlsconfig.ServerName != tt.host {
|
|
t.Errorf("failed to create new client. TLS config host expected: %s, got: %s",
|
|
host, c.tlsconfig.ServerName)
|
|
}
|
|
if c.tlsconfig.MinVersion != DefaultTLSMinVersion {
|
|
t.Errorf("failed to create new client. TLS config min versino expected: %d, got: %d",
|
|
DefaultTLSMinVersion, c.tlsconfig.MinVersion)
|
|
}
|
|
if c.ServerAddr() != fmt.Sprintf("%s:%d", tt.host, c.port) {
|
|
t.Errorf("failed to create new client. c.ServerAddr() expected: %s, got: %s",
|
|
fmt.Sprintf("%s:%d", tt.host, c.port), c.ServerAddr())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNewClient tests the NewClient() method with its custom options
|
|
func TestNewClientWithOptions(t *testing.T) {
|
|
host := "mail.example.com"
|
|
tests := []struct {
|
|
name string
|
|
option Option
|
|
shouldfail bool
|
|
}{
|
|
{"nil option", nil, true},
|
|
{"WithPort()", WithPort(465), false},
|
|
{"WithPort(); port is too high", WithPort(100000), true},
|
|
{"WithTimeout()", WithTimeout(time.Second * 5), false},
|
|
{"WithTimeout()", WithTimeout(-10), true},
|
|
{"WithSSL()", WithSSL(), false},
|
|
{"WithSSLPort(false)", WithSSLPort(false), false},
|
|
{"WithSSLPort(true)", WithSSLPort(true), false},
|
|
{"WithHELO()", WithHELO(host), false},
|
|
{"WithHELO(); helo is empty", WithHELO(""), true},
|
|
{"WithTLSPolicy()", WithTLSPolicy(TLSOpportunistic), false},
|
|
{"WithTLSPortPolicy()", WithTLSPortPolicy(TLSOpportunistic), false},
|
|
{"WithTLSConfig()", WithTLSConfig(&tls.Config{}), false},
|
|
{"WithTLSConfig(); config is nil", WithTLSConfig(nil), true},
|
|
{"WithSMTPAuth(NoAuth)", WithSMTPAuth(SMTPAuthNoAuth), false},
|
|
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
|
|
{
|
|
"WithSMTPAuthCustom()",
|
|
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")),
|
|
false,
|
|
},
|
|
{"WithUsername()", WithUsername("test"), false},
|
|
{"WithPassword()", WithPassword("test"), false},
|
|
{"WithDSN()", WithDSN(), false},
|
|
{"WithDSNMailReturnType()", WithDSNMailReturnType(DSNMailReturnFull), false},
|
|
{"WithDSNMailReturnType() wrong option", WithDSNMailReturnType("FAIL"), true},
|
|
{"WithDSNRcptNotifyType()", WithDSNRcptNotifyType(DSNRcptNotifySuccess), false},
|
|
{"WithDSNRcptNotifyType() wrong option", WithDSNRcptNotifyType("FAIL"), true},
|
|
{"WithoutNoop()", WithoutNoop(), false},
|
|
{"WithDebugLog()", WithDebugLog(), false},
|
|
{"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false},
|
|
{"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, nil
|
|
}), false},
|
|
|
|
{
|
|
"WithDSNRcptNotifyType() NEVER combination",
|
|
WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, tt.option, nil)
|
|
if err != nil && !tt.shouldfail {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
_ = c
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithHELO tests the WithHELO() option for the NewClient() method
|
|
func TestWithHELO(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
want string
|
|
}{
|
|
{"HELO test.de", "test.de", "test.de"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithHELO(tt.value))
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.helo != tt.want {
|
|
t.Errorf("failed to set custom HELO. Want: %s, got: %s", tt.want, c.helo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithPort tests the WithPort() option for the NewClient() method
|
|
func TestWithPort(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value int
|
|
want int
|
|
sf bool
|
|
}{
|
|
{"set port to 25", 25, 25, false},
|
|
{"set port to 465", 465, 465, false},
|
|
{"set port to 100000", 100000, 25, true},
|
|
{"set port to -10", -10, 25, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithPort(tt.value))
|
|
if err != nil && !tt.sf {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.port != tt.want {
|
|
t.Errorf("failed to set custom port. Want: %d, got: %d", tt.want, c.port)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithTimeout tests the WithTimeout() option for the NewClient() method
|
|
func TestWithTimeout(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value time.Duration
|
|
want time.Duration
|
|
sf bool
|
|
}{
|
|
{"set timeout to 5s", time.Second * 5, time.Second * 5, false},
|
|
{"set timeout to 30s", time.Second * 30, time.Second * 30, false},
|
|
{"set timeout to 1m", time.Minute, time.Minute, false},
|
|
{"set timeout to 0", 0, DefaultTimeout, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithTimeout(tt.value))
|
|
if err != nil && !tt.sf {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.connTimeout != tt.want {
|
|
t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.connTimeout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithTLSPolicy tests the WithTLSPolicy() option for the NewClient() method
|
|
func TestWithTLSPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value TLSPolicy
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"Policy: TLSMandatory", TLSMandatory, TLSMandatory.String(), false},
|
|
{"Policy: TLSOpportunistic", TLSOpportunistic, TLSOpportunistic.String(), false},
|
|
{"Policy: NoTLS", NoTLS, NoTLS.String(), false},
|
|
{"Policy: Invalid", -1, "UnknownPolicy", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithTLSPolicy(tt.value))
|
|
if err != nil && !tt.sf {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.tlspolicy.String() != tt.want {
|
|
t.Errorf("failed to set TLSPolicy. Want: %s, got: %s", tt.want, c.tlspolicy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithTLSPortPolicy tests the WithTLSPortPolicy() option for the NewClient() method
|
|
func TestWithTLSPortPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value TLSPolicy
|
|
want string
|
|
wantPort int
|
|
fbPort int
|
|
sf bool
|
|
}{
|
|
{"Policy: TLSMandatory", TLSMandatory, TLSMandatory.String(), 587, 0, false},
|
|
{"Policy: TLSOpportunistic", TLSOpportunistic, TLSOpportunistic.String(), 587, 25, false},
|
|
{"Policy: NoTLS", NoTLS, NoTLS.String(), 25, 0, false},
|
|
{"Policy: Invalid", -1, "UnknownPolicy", 587, 0, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithTLSPortPolicy(tt.value))
|
|
if err != nil && !tt.sf {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.tlspolicy.String() != tt.want {
|
|
t.Errorf("failed to set TLSPortPolicy. Want: %s, got: %s", tt.want, c.tlspolicy)
|
|
}
|
|
if c.port != tt.wantPort {
|
|
t.Errorf("failed to set TLSPortPolicy, wanted port: %d, got: %d", tt.wantPort, c.port)
|
|
}
|
|
if c.fallbackPort != tt.fbPort {
|
|
t.Errorf("failed to set TLSPortPolicy, wanted fallbakc port: %d, got: %d", tt.fbPort,
|
|
c.fallbackPort)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetTLSPolicy tests the SetTLSPolicy() method for the Client object
|
|
func TestSetTLSPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value TLSPolicy
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"Policy: TLSMandatory", TLSMandatory, TLSMandatory.String(), false},
|
|
{"Policy: TLSOpportunistic", TLSOpportunistic, TLSOpportunistic.String(), false},
|
|
{"Policy: NoTLS", NoTLS, NoTLS.String(), false},
|
|
{"Policy: Invalid", -1, "UnknownPolicy", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetTLSPolicy(tt.value)
|
|
if c.tlspolicy.String() != tt.want {
|
|
t.Errorf("failed to set TLSPolicy. Want: %s, got: %s", tt.want, c.tlspolicy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetTLSConfig tests the SetTLSConfig() method for the Client object
|
|
func TestSetTLSConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value *tls.Config
|
|
sf bool
|
|
}{
|
|
{"default config", &tls.Config{}, false},
|
|
{"nil config", nil, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if err := c.SetTLSConfig(tt.value); err != nil && !tt.sf {
|
|
t.Errorf("failed to set TLSConfig: %s", err)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetSSL tests the SetSSL() method for the Client object
|
|
func TestSetSSL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value bool
|
|
}{
|
|
{"SSL: on", true},
|
|
{"SSL: off", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetSSL(tt.value)
|
|
if c.useSSL != tt.value {
|
|
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetSSLPort tests the Client.SetSSLPort method
|
|
func TestClient_SetSSLPort(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value bool
|
|
fb bool
|
|
port int
|
|
fbPort int
|
|
}{
|
|
{"SSL: on, fb: off", true, false, 465, 0},
|
|
{"SSL: on, fb: on", true, true, 465, 25},
|
|
{"SSL: off, fb: off", false, false, 25, 0},
|
|
{"SSL: off, fb: on", false, true, 25, 25},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetSSLPort(tt.value, tt.fb)
|
|
if c.useSSL != tt.value {
|
|
t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value)
|
|
}
|
|
if c.port != tt.port {
|
|
t.Errorf("failed to set SSLPort, wanted port: %d, got: %d", c.port, tt.port)
|
|
}
|
|
if c.fallbackPort != tt.fbPort {
|
|
t.Errorf("failed to set SSLPort, wanted fallback port: %d, got: %d", c.fallbackPort,
|
|
tt.fbPort)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetUsername tests the SetUsername method for the Client object
|
|
func TestSetUsername(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"normal username", "testuser", "testuser", false},
|
|
{"empty username", "", "", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetUsername(tt.value)
|
|
if c.user != tt.want {
|
|
t.Errorf("failed to set username. Expected %s, got: %s", tt.want, c.user)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetPassword tests the SetPassword method for the Client object
|
|
func TestSetPassword(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"normal password", "testpass", "testpass", false},
|
|
{"empty password", "", "", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetPassword(tt.value)
|
|
if c.pass != tt.want {
|
|
t.Errorf("failed to set password. Expected %s, got: %s", tt.want, c.pass)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetSMTPAuth tests the SetSMTPAuth method for the Client object
|
|
func TestSetSMTPAuth(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value SMTPAuthType
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"SMTPAuth: LOGIN", SMTPAuthLogin, "LOGIN", false},
|
|
{"SMTPAuth: PLAIN", SMTPAuthPlain, "PLAIN", false},
|
|
{"SMTPAuth: CRAM-MD5", SMTPAuthCramMD5, "CRAM-MD5", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetSMTPAuth(tt.value)
|
|
if string(c.smtpAuthType) != tt.want {
|
|
t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.smtpAuthType))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithDSN tests the WithDSN method for the Client object
|
|
func TestWithDSN(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithDSN())
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if !c.requestDSN {
|
|
t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN)
|
|
}
|
|
if c.dsnReturnType != DSNMailReturnFull {
|
|
t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull,
|
|
c.dsnReturnType)
|
|
}
|
|
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) {
|
|
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
|
|
c.dsnRcptNotifyType[0])
|
|
}
|
|
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) {
|
|
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
|
|
c.dsnRcptNotifyType[1])
|
|
}
|
|
}
|
|
|
|
// TestWithDSNMailReturnType tests the WithDSNMailReturnType method for the Client object
|
|
func TestWithDSNMailReturnType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value DSNMailReturnOption
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"WithDSNMailReturnType: FULL", DSNMailReturnFull, "FULL", false},
|
|
{"WithDSNMailReturnType: HDRS", DSNMailReturnHeadersOnly, "HDRS", false},
|
|
{"WithDSNMailReturnType: INVALID", "INVALID", "", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithDSNMailReturnType(tt.value))
|
|
if err != nil && !tt.sf {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if string(c.dsnReturnType) != tt.want {
|
|
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithDSNRcptNotifyType tests the WithDSNRcptNotifyType method for the Client object
|
|
func TestWithDSNRcptNotifyType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value DSNRcptNotifyOption
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"WithDSNRcptNotifyType: NEVER", DSNRcptNotifyNever, "NEVER", false},
|
|
{"WithDSNRcptNotifyType: SUCCESS", DSNRcptNotifySuccess, "SUCCESS", false},
|
|
{"WithDSNRcptNotifyType: FAILURE", DSNRcptNotifyFailure, "FAILURE", false},
|
|
{"WithDSNRcptNotifyType: DELAY", DSNRcptNotifyDelay, "DELAY", false},
|
|
{"WithDSNRcptNotifyType: INVALID", "INVALID", "", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithDSNRcptNotifyType(tt.value))
|
|
if err != nil && !tt.sf {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if len(c.dsnRcptNotifyType) <= 0 && !tt.sf {
|
|
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
|
|
}
|
|
if !tt.sf && c.dsnRcptNotifyType[0] != tt.want {
|
|
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWithoutNoop tests the WithoutNoop method for the Client object
|
|
func TestWithoutNoop(t *testing.T) {
|
|
c, err := NewClient(DefaultHost, WithoutNoop())
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if !c.noNoop {
|
|
t.Errorf("WithoutNoop failed. c.noNoop expected to be: %t, got: %t", true, c.noNoop)
|
|
}
|
|
|
|
c, err = NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
if c.noNoop {
|
|
t.Errorf("WithoutNoop failed. c.noNoop expected to be: %t, got: %t", false, c.noNoop)
|
|
}
|
|
}
|
|
|
|
// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
|
|
func TestSetSMTPAuthCustom(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value smtp.Auth
|
|
want string
|
|
sf bool
|
|
}{
|
|
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
|
|
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
|
|
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
|
|
}
|
|
si := smtp.ServerInfo{TLS: true}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
return
|
|
}
|
|
c.SetSMTPAuthCustom(tt.value)
|
|
if c.smtpAuth == nil {
|
|
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
|
|
}
|
|
if c.smtpAuthType != SMTPAuthCustom {
|
|
t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s",
|
|
c.smtpAuthType)
|
|
}
|
|
p, _, err := c.smtpAuth.Start(&si)
|
|
if err != nil {
|
|
t.Errorf("SMTP Auth Start() method returned error: %s", err)
|
|
}
|
|
if p != tt.want {
|
|
t.Errorf("SMTP Auth Start() method is returned proto: %s, expected: %s", p, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClient_Close_double tests if a close on an already closed connection causes an error.
|
|
func TestClient_Close_double(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
if c.smtpClient == nil {
|
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
|
return
|
|
}
|
|
if !c.smtpClient.HasConnection() {
|
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Errorf("failed to close connection: %s", err)
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Errorf("failed 2nd close connection: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContext tests the DialWithContext method for the Client object
|
|
func TestClient_DialWithContext(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
if c.smtpClient == nil {
|
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
|
return
|
|
}
|
|
if !c.smtpClient.HasConnection() {
|
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
}
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("failed to close connection: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback
|
|
// port functionality
|
|
func TestClient_DialWithContext_Fallback(t *testing.T) {
|
|
c, err := getTestConnectionNoTestPort(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
c.SetTLSPortPolicy(TLSOpportunistic)
|
|
c.port = 999
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
if c.smtpClient == nil {
|
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
|
return
|
|
}
|
|
if !c.smtpClient.HasConnection() {
|
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Errorf("failed to close connection: %s", err)
|
|
}
|
|
|
|
c.port = 999
|
|
c.fallbackPort = 999
|
|
if err = c.DialWithContext(ctx); err == nil {
|
|
t.Error("dial with context was supposed to fail, but didn't")
|
|
return
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContext_Debug tests the DialWithContext method for the Client object with debug
|
|
// logging enabled on the SMTP client
|
|
func TestClient_DialWithContext_Debug(t *testing.T) {
|
|
c, err := getTestClient(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
if c.smtpClient == nil {
|
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
|
return
|
|
}
|
|
if !c.smtpClient.HasConnection() {
|
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
}
|
|
c.SetDebugLog(true)
|
|
if err = c.Close(); err != nil {
|
|
t.Errorf("failed to close connection: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContext_Debug_custom tests the DialWithContext method for the Client
|
|
// object with debug logging enabled and a custom logger on the SMTP client
|
|
func TestClient_DialWithContext_Debug_custom(t *testing.T) {
|
|
c, err := getTestClient(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
if c.smtpClient == nil {
|
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
|
return
|
|
}
|
|
if !c.smtpClient.HasConnection() {
|
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
}
|
|
c.SetDebugLog(true)
|
|
c.SetLogger(log.New(os.Stderr, log.LevelDebug))
|
|
if err = c.Close(); err != nil {
|
|
t.Errorf("failed to close connection: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContextInvalidHost tests the DialWithContext method with intentional breaking
|
|
// for the Client object
|
|
func TestClient_DialWithContextInvalidHost(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
c.host = "invalid.addr"
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err == nil {
|
|
t.Errorf("dial succeeded but was supposed to fail")
|
|
return
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContextInvalidHELO tests the DialWithContext method with intentional breaking
|
|
// for the Client object
|
|
func TestClient_DialWithContextInvalidHELO(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
c.helo = ""
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err == nil {
|
|
t.Errorf("dial succeeded but was supposed to fail")
|
|
return
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContextInvalidAuth tests the DialWithContext method with intentional breaking
|
|
// for the Client object
|
|
func TestClient_DialWithContextInvalidAuth(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
c.user = "invalid"
|
|
c.pass = "invalid"
|
|
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err == nil {
|
|
t.Errorf("dial succeeded but was supposed to fail")
|
|
return
|
|
}
|
|
}
|
|
|
|
// TestClient_checkConn tests the checkConn method with intentional breaking for the Client object
|
|
func TestClient_checkConn(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
if err = c.checkConn(); err == nil {
|
|
t.Errorf("connCheck() should fail but succeeded")
|
|
}
|
|
}
|
|
|
|
// TestClient_DiealWithContextOptions tests the DialWithContext method plus different options
|
|
// for the Client object
|
|
func TestClient_DialWithContextOptions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wantssl bool
|
|
wanttls TLSPolicy
|
|
sf bool
|
|
}{
|
|
{"Want SSL (should fail)", true, NoTLS, true},
|
|
{"Want Mandatory TLS", false, TLSMandatory, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
if tt.wantssl {
|
|
c.SetSSL(true)
|
|
}
|
|
if tt.wanttls != NoTLS {
|
|
c.SetTLSPolicy(tt.wanttls)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err = c.DialWithContext(ctx); err != nil && !tt.sf {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
if !tt.sf {
|
|
if c.smtpClient == nil && !tt.sf {
|
|
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
|
|
return
|
|
}
|
|
if !c.smtpClient.HasConnection() && !tt.sf {
|
|
t.Errorf("DialWithContext didn't fail but no connection found.")
|
|
return
|
|
}
|
|
if err = c.Reset(); err != nil {
|
|
t.Errorf("failed to reset connection: %s", err)
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Errorf("failed to close connection: %s", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClient_DialWithContextOptionDialContextFunc tests the DialWithContext method plus
|
|
// use dialContextFunc option for the Client object
|
|
func TestClient_DialWithContextOptionDialContextFunc(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
called := false
|
|
c.dialContextFunc = func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
called = true
|
|
return (&net.Dialer{}).DialContext(ctx, network, address)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err := c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial with context: %s", err)
|
|
return
|
|
}
|
|
|
|
if called == false {
|
|
t.Errorf("dialContextFunc supposed to be called but not called")
|
|
}
|
|
}
|
|
|
|
// TestClient_DialSendClose tests the Dial(), Send() and Close() method of Client
|
|
func TestClient_DialSendClose(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To(TestRcpt)
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cfn()
|
|
if err := c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("Dial() failed: %s", err)
|
|
}
|
|
if err := c.Send(m); err != nil {
|
|
t.Errorf("Send() failed: %s", err)
|
|
}
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("Close() failed: %s", err)
|
|
}
|
|
if !m.IsDelivered() {
|
|
t.Errorf("message should be delivered but is indicated no to")
|
|
}
|
|
}
|
|
|
|
// TestClient_DialAndSendWithContext tests the DialAndSendWithContext() method of Client
|
|
func TestClient_DialAndSendWithContext(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To(TestRcpt)
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
|
|
tests := []struct {
|
|
name string
|
|
to time.Duration
|
|
sf bool
|
|
}{
|
|
{"Timeout: 100s", time.Second * 100, false},
|
|
{"Timeout: 100ms", time.Millisecond * 100, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), tt.to)
|
|
defer cfn()
|
|
if err := c.DialAndSendWithContext(ctx, m); err != nil && !tt.sf {
|
|
t.Errorf("DialAndSendWithContext() failed: %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClient_DialAndSend tests the DialAndSend() method of Client
|
|
func TestClient_DialAndSend(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To(TestRcpt)
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
if err := c.DialAndSend(m); err != nil {
|
|
t.Errorf("DialAndSend() failed: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_DialAndSendWithDSN tests the DialAndSend() method of Client with DSN enabled
|
|
func TestClient_DialAndSendWithDSN(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To(TestRcpt)
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
|
|
c, err := getTestConnectionWithDSN(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
if err := c.DialAndSend(m); err != nil {
|
|
t.Errorf("DialAndSend() failed: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_DialSendCloseBroken tests the Dial(), Send() and Close() method of Client with broken settings
|
|
func TestClient_DialSendCloseBroken(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
from string
|
|
to string
|
|
closestart bool
|
|
closeearly bool
|
|
sf bool
|
|
}{
|
|
{"Invalid FROM", "foo@foo", TestRcpt, false, false, true},
|
|
{"Invalid TO", os.Getenv("TEST_FROM"), "foo@foo", false, false, true},
|
|
{"No FROM", "", TestRcpt, false, false, true},
|
|
{"No TO", os.Getenv("TEST_FROM"), "", false, false, true},
|
|
{"Close early", os.Getenv("TEST_FROM"), TestRcpt, false, true, true},
|
|
{"Close start", os.Getenv("TEST_FROM"), TestRcpt, true, false, true},
|
|
{"Close start/early", os.Getenv("TEST_FROM"), TestRcpt, true, true, true},
|
|
}
|
|
|
|
m := NewMsg(WithEncoding(NoEncoding))
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m.SetAddrHeaderIgnoreInvalid(HeaderFrom, tt.from)
|
|
m.SetAddrHeaderIgnoreInvalid(HeaderTo, tt.to)
|
|
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cfn()
|
|
if err := c.DialWithContext(ctx); err != nil && !tt.sf {
|
|
t.Errorf("Dail() failed: %s", err)
|
|
return
|
|
}
|
|
if tt.closestart {
|
|
_ = c.smtpClient.Close()
|
|
}
|
|
if err = c.Send(m); err != nil && !tt.sf {
|
|
t.Errorf("Send() failed: %s", err)
|
|
return
|
|
}
|
|
if tt.closeearly {
|
|
_ = c.smtpClient.Close()
|
|
}
|
|
if err = c.Close(); err != nil && !tt.sf {
|
|
t.Errorf("Close() failed: %s", err)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClient_DialSendCloseBrokenWithDSN tests the Dial(), Send() and Close() method of Client with
|
|
// broken settings and DSN enabled
|
|
func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
from string
|
|
to string
|
|
closestart bool
|
|
closeearly bool
|
|
sf bool
|
|
}{
|
|
{"Invalid FROM", "foo@foo", TestRcpt, false, false, true},
|
|
{"Invalid TO", os.Getenv("TEST_FROM"), "foo@foo", false, false, true},
|
|
{"No FROM", "", TestRcpt, false, false, true},
|
|
{"No TO", os.Getenv("TEST_FROM"), "", false, false, true},
|
|
{"Close early", os.Getenv("TEST_FROM"), TestRcpt, false, true, true},
|
|
{"Close start", os.Getenv("TEST_FROM"), TestRcpt, true, false, true},
|
|
{"Close start/early", os.Getenv("TEST_FROM"), TestRcpt, true, true, true},
|
|
}
|
|
|
|
m := NewMsg(WithEncoding(NoEncoding))
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m.SetAddrHeaderIgnoreInvalid(HeaderFrom, tt.from)
|
|
m.SetAddrHeaderIgnoreInvalid(HeaderTo, tt.to)
|
|
|
|
c, err := getTestConnectionWithDSN(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cfn()
|
|
if err := c.DialWithContext(ctx); err != nil && !tt.sf {
|
|
t.Errorf("Dail() failed: %s", err)
|
|
return
|
|
}
|
|
if tt.closestart {
|
|
_ = c.smtpClient.Close()
|
|
}
|
|
if err = c.Send(m); err != nil && !tt.sf {
|
|
t.Errorf("Send() failed: %s", err)
|
|
return
|
|
}
|
|
if tt.closeearly {
|
|
_ = c.smtpClient.Close()
|
|
}
|
|
if err = c.Close(); err != nil && !tt.sf {
|
|
t.Errorf("Close() failed: %s", err)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClient_Send_withBrokenRecipient tests the Send() method of Client with a broken and a working recipient
|
|
func TestClient_Send_withBrokenRecipient(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
var msgs []*Msg
|
|
rcpts := []string{"invalid@domain.tld", TestRcpt, "invalid@address.invalid"}
|
|
for _, rcpt := range rcpts {
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To(rcpt)
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
msgs = append(msgs, m)
|
|
}
|
|
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout)
|
|
defer cfn()
|
|
if err := c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial to sending server: %s", err)
|
|
}
|
|
if err := c.Send(msgs...); err != nil {
|
|
if !strings.Contains(err.Error(), "invalid@domain.tld") ||
|
|
!strings.Contains(err.Error(), "invalid@address.invalid") {
|
|
t.Errorf("sending mails to invalid addresses was supposed to fail but didn't")
|
|
}
|
|
if strings.Contains(err.Error(), TestRcpt) {
|
|
t.Errorf("sending mail to valid addresses failed: %s", err)
|
|
}
|
|
}
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("failed to close client connection: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestClient_auth tests the Dial(), Send() and Close() method of Client with broken settings
|
|
func TestClient_auth(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
auth SMTPAuthType
|
|
sf bool
|
|
}{
|
|
{"SMTP AUTH: PLAIN", SMTPAuthPlain, false},
|
|
{"SMTP AUTH: LOGIN", SMTPAuthLogin, false},
|
|
{"SMTP AUTH: CRAM-MD5", SMTPAuthCramMD5, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, err := getTestConnection(false)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), time.Second*5)
|
|
defer cfn()
|
|
if err := c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("auth() failed: could not Dial() => %s", err)
|
|
return
|
|
}
|
|
c.SetSMTPAuth(tt.auth)
|
|
c.SetUsername(os.Getenv("TEST_SMTPAUTH_USER"))
|
|
c.SetPassword(os.Getenv("TEST_SMTPAUTH_PASS"))
|
|
if err := c.auth(); err != nil && !tt.sf {
|
|
t.Errorf("auth() failed: %s", err)
|
|
}
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("auth() failed: could not Close() => %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClient_Send_MsgSendError tests the Client.Send method with a broken recipient and verifies
|
|
// that the SendError type works properly
|
|
func TestClient_Send_MsgSendError(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
var msgs []*Msg
|
|
rcpts := []string{"invalid@domain.tld", "invalid@address.invalid"}
|
|
for _, rcpt := range rcpts {
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To(rcpt)
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
msgs = append(msgs, m)
|
|
}
|
|
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout)
|
|
defer cfn()
|
|
if err := c.DialWithContext(ctx); err != nil {
|
|
t.Errorf("failed to dial to sending server: %s", err)
|
|
}
|
|
if err := c.Send(msgs...); err == nil {
|
|
t.Errorf("sending messages with broken recipients was supposed to fail but didn't")
|
|
}
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("failed to close client connection: %s", err)
|
|
}
|
|
for _, m := range msgs {
|
|
if !m.HasSendError() {
|
|
t.Errorf("message was expected to have a send error, but didn't")
|
|
}
|
|
se := &SendError{Reason: ErrSMTPRcptTo}
|
|
if !errors.Is(m.SendError(), se) {
|
|
t.Errorf("error mismatch, expected: %s, got: %s", se, m.SendError())
|
|
}
|
|
if m.SendErrorIsTemp() {
|
|
t.Errorf("message was not expected to be a temporary error, but reported as such")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestClient_DialAndSendWithContext_withSendError tests the Client.DialAndSendWithContext method
|
|
// with a broken recipient to make sure that the returned error satisfies the Msg.SendError type
|
|
func TestClient_DialAndSendWithContext_withSendError(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
m := NewMsg()
|
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
|
_ = m.To("invalid@domain.tld")
|
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
|
m.SetBulk()
|
|
m.SetDate()
|
|
m.SetMessageID()
|
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
|
|
|
c, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout)
|
|
defer cfn()
|
|
err = c.DialAndSendWithContext(ctx, m)
|
|
if err == nil {
|
|
t.Errorf("expected DialAndSendWithContext with broken mail recipient to fail, but didn't")
|
|
return
|
|
}
|
|
var se *SendError
|
|
if !errors.As(err, &se) {
|
|
t.Errorf("expected *SendError type as returned error, but didn't")
|
|
return
|
|
}
|
|
if se.IsTemp() {
|
|
t.Errorf("expected permanent error but IsTemp() returned true")
|
|
}
|
|
if m.IsDelivered() {
|
|
t.Errorf("message is indicated to be delivered but shouldn'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)
|
|
}
|
|
}
|
|
|
|
func TestClient_DialSendConcurrent_online(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
|
|
client, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
var messages []*Msg
|
|
for i := 0; i < 10; i++ {
|
|
message := NewMsg()
|
|
if err := message.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")); err != nil {
|
|
t.Errorf("failed to set FROM address: %s", err)
|
|
return
|
|
}
|
|
if err := message.To(TestRcpt); err != nil {
|
|
t.Errorf("failed to set TO address: %s", err)
|
|
return
|
|
}
|
|
message.Subject(fmt.Sprintf("Test subject for mail %d", i))
|
|
message.SetBodyString(TypeTextPlain, fmt.Sprintf("This is the test body of the mail no. %d", i))
|
|
message.SetMessageID()
|
|
messages = append(messages, message)
|
|
}
|
|
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to test server: %s", err)
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
for id, message := range messages {
|
|
wg.Add(1)
|
|
go func(curMsg *Msg, curID int) {
|
|
defer wg.Done()
|
|
if goroutineErr := client.Send(curMsg); err != nil {
|
|
t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr)
|
|
}
|
|
}(message, id)
|
|
}
|
|
wg.Wait()
|
|
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close server connection: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestClient_DialSendConcurrent_local(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
serverPort := TestServerPortBase + 20
|
|
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 * 500)
|
|
|
|
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)
|
|
}
|
|
|
|
var messages []*Msg
|
|
for i := 0; i < 20; i++ {
|
|
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")
|
|
messages = append(messages, message)
|
|
}
|
|
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to test server: %s", err)
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
for id, message := range messages {
|
|
wg.Add(1)
|
|
go func(curMsg *Msg, curID int) {
|
|
defer wg.Done()
|
|
if goroutineErr := client.Send(curMsg); err != nil {
|
|
t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr)
|
|
}
|
|
}(message, id)
|
|
}
|
|
wg.Wait()
|
|
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close server connection: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthSCRAMSHAX(t *testing.T) {
|
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
|
}
|
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
|
username := os.Getenv("TEST_USER_SCRAM")
|
|
password := os.Getenv("TEST_PASS_SCRAM")
|
|
|
|
tests := []struct {
|
|
name string
|
|
authtype SMTPAuthType
|
|
}{
|
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
|
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(hostname,
|
|
WithTLSPortPolicy(TLSMandatory),
|
|
WithSMTPAuth(tt.authtype),
|
|
WithUsername(username), WithPassword(password))
|
|
if err != nil {
|
|
t.Errorf("unable to create new client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to test server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close server connection: %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthLoginSuccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
featureSet string
|
|
}{
|
|
{"default", "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
|
{"mox server", "250-AUTH LOGIN\r\n250-X-MOX-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
|
{"null byte", "250-AUTH LOGIN\r\n250-X-NULLBYTE-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
|
{"bogus responses", "250-AUTH LOGIN\r\n250-X-BOGUS-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
|
{"empty responses", "250-AUTH LOGIN\r\n250-X-EMPTY-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
serverPort := TestServerPortBase + 40 + i
|
|
go func() {
|
|
if err := simpleSMTPServer(ctx, tt.featureSet, true, serverPort); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
client, err := NewClient(TestServerAddr,
|
|
WithPort(serverPort),
|
|
WithTLSPortPolicy(NoTLS),
|
|
WithSMTPAuth(SMTPAuthLogin),
|
|
WithUsername("toni@tester.com"),
|
|
WithPassword("V3ryS3cr3t+"))
|
|
if err != nil {
|
|
t.Errorf("unable to create new client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to test server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close server connection: %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthLoginFail(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
serverPort := TestServerPortBase + 50
|
|
featureSet := "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\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)
|
|
|
|
client, err := NewClient(TestServerAddr,
|
|
WithPort(serverPort),
|
|
WithTLSPortPolicy(NoTLS),
|
|
WithSMTPAuth(SMTPAuthLogin),
|
|
WithUsername("toni@tester.com"),
|
|
WithPassword("InvalidPassword"))
|
|
if err != nil {
|
|
t.Errorf("unable to create new client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err == nil {
|
|
t.Error("expected to fail to dial to test server, but it succeeded")
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthLoginFail_noTLS(t *testing.T) {
|
|
if os.Getenv("TEST_SKIP_ONLINE") != "" {
|
|
t.Skipf("env variable TEST_SKIP_ONLINE is set. Skipping online tests")
|
|
}
|
|
th := os.Getenv("TEST_HOST")
|
|
if th == "" {
|
|
t.Skipf("no host set. Skipping online tests")
|
|
}
|
|
tp := 587
|
|
if tps := os.Getenv("TEST_PORT"); tps != "" {
|
|
tpi, err := strconv.Atoi(tps)
|
|
if err == nil {
|
|
tp = tpi
|
|
}
|
|
}
|
|
client, err := NewClient(th, WithPort(tp), WithSMTPAuth(SMTPAuthLogin), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Errorf("failed to create new client: %s", err)
|
|
}
|
|
u := os.Getenv("TEST_SMTPAUTH_USER")
|
|
if u != "" {
|
|
client.SetUsername(u)
|
|
}
|
|
p := os.Getenv("TEST_SMTPAUTH_PASS")
|
|
if p != "" {
|
|
client.SetPassword(p)
|
|
}
|
|
// We don't want to log authentication data in tests
|
|
client.SetDebugLog(false)
|
|
|
|
if err = client.DialWithContext(context.Background()); err == nil {
|
|
t.Error("expected to fail to dial to test server, but it succeeded")
|
|
}
|
|
if !errors.Is(err, smtp.ErrUnencrypted) {
|
|
t.Errorf("expected error to be %s, but got %s", smtp.ErrUnencrypted, err)
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthSCRAMSHAX_fail(t *testing.T) {
|
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
|
}
|
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
|
|
|
tests := []struct {
|
|
name string
|
|
authtype SMTPAuthType
|
|
}{
|
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
|
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
|
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
|
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(hostname,
|
|
WithTLSPortPolicy(TLSMandatory),
|
|
WithSMTPAuth(tt.authtype),
|
|
WithUsername("invalid"), WithPassword("invalid"))
|
|
if err != nil {
|
|
t.Errorf("unable to create new client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err == nil {
|
|
t.Errorf("expected error but got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthSCRAMSHAX_unsupported(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
|
|
client, err := getTestConnection(true)
|
|
if err != nil {
|
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
authtype SMTPAuthType
|
|
expErr error
|
|
}{
|
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, ErrSCRAMSHA1AuthNotSupported},
|
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, ErrSCRAMSHA1PLUSAuthNotSupported},
|
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, ErrSCRAMSHA256AuthNotSupported},
|
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, ErrSCRAMSHA256PLUSAuthNotSupported},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client.SetSMTPAuth(tt.authtype)
|
|
client.SetTLSPolicy(TLSMandatory)
|
|
if err = client.DialWithContext(context.Background()); err == nil {
|
|
t.Errorf("expected error but got nil")
|
|
}
|
|
if !errors.Is(err, tt.expErr) {
|
|
t.Errorf("expected error %s, but got %s", tt.expErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) {
|
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
|
}
|
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
|
username := os.Getenv("TEST_USER_SCRAM")
|
|
password := os.Getenv("TEST_PASS_SCRAM")
|
|
|
|
tests := []struct {
|
|
name string
|
|
authtype SMTPAuthType
|
|
}{
|
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
|
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(hostname,
|
|
WithTLSPortPolicy(TLSMandatory),
|
|
WithSMTPAuth(tt.authtype),
|
|
WithUsername(username), WithPassword(password))
|
|
if err != nil {
|
|
t.Errorf("unable to create new client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to test server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close server connection: %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_AuthSCRAMSHAXPLUS_tlsunique(t *testing.T) {
|
|
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
|
|
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
|
|
}
|
|
hostname := os.Getenv("TEST_HOST_SCRAM")
|
|
username := os.Getenv("TEST_USER_SCRAM")
|
|
password := os.Getenv("TEST_PASS_SCRAM")
|
|
tlsConfig := &tls.Config{}
|
|
tlsConfig.MaxVersion = tls.VersionTLS12
|
|
tlsConfig.ServerName = hostname
|
|
|
|
tests := []struct {
|
|
name string
|
|
authtype SMTPAuthType
|
|
}{
|
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
|
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(hostname,
|
|
WithTLSPortPolicy(TLSMandatory),
|
|
WithTLSConfig(tlsConfig),
|
|
WithSMTPAuth(tt.authtype),
|
|
WithUsername(username), WithPassword(password))
|
|
if err != nil {
|
|
t.Errorf("unable to create new client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to test server: %s", err)
|
|
}
|
|
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
|
|
// SMTP server to test all functionality that requires a connection
|
|
func getTestConnection(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")
|
|
}
|
|
tp := 25
|
|
if tps := os.Getenv("TEST_PORT"); tps != "" {
|
|
tpi, err := strconv.Atoi(tps)
|
|
if err == nil {
|
|
tp = tpi
|
|
}
|
|
}
|
|
sv := false
|
|
if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" {
|
|
sv = true
|
|
}
|
|
c, err := NewClient(th, WithPort(tp))
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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")
|
|
}
|
|
tp := 25
|
|
if tps := os.Getenv("TEST_PORT"); tps != "" {
|
|
tpi, err := strconv.Atoi(tps)
|
|
if err == nil {
|
|
tp = tpi
|
|
}
|
|
}
|
|
sv := false
|
|
if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" {
|
|
sv = true
|
|
}
|
|
c, err := NewClient(th, WithPort(tp))
|
|
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)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// getTestConnectionWithDSN takes environment variables to establish a connection to a real
|
|
// SMTP server to test all functionality that requires a connection. It also enables DSN
|
|
func getTestConnectionWithDSN(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")
|
|
}
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
func TestXOAuth2OK(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
serverPort := TestServerPortBase + 30
|
|
featureSet := "250-AUTH XOAUTH2\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 * 500)
|
|
|
|
c, err := NewClient("127.0.0.1",
|
|
WithPort(serverPort),
|
|
WithTLSPortPolicy(TLSOpportunistic),
|
|
WithSMTPAuth(SMTPAuthXOAUTH2),
|
|
WithUsername("user"),
|
|
WithPassword("token"))
|
|
if err != nil {
|
|
t.Fatalf("unable to create new client: %v", err)
|
|
}
|
|
if err = c.DialWithContext(context.Background()); err != nil {
|
|
t.Fatalf("unexpected dial error: %v", err)
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Fatalf("disconnect from test server failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestXOAuth2Unsupported(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
serverPort := TestServerPortBase + 31
|
|
featureSet := "250-AUTH LOGIN 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 * 500)
|
|
|
|
c, err := NewClient("127.0.0.1",
|
|
WithPort(serverPort),
|
|
WithTLSPolicy(TLSOpportunistic),
|
|
WithSMTPAuth(SMTPAuthXOAUTH2),
|
|
WithUsername("user"),
|
|
WithPassword("token"))
|
|
if err != nil {
|
|
t.Fatalf("unable to create new client: %v", err)
|
|
}
|
|
if err = c.DialWithContext(context.Background()); err == nil {
|
|
t.Fatal("expected dial error got nil")
|
|
} else {
|
|
if !errors.Is(err, ErrXOauth2AuthNotSupported) {
|
|
t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err)
|
|
}
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Fatalf("disconnect from test server failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestXOAuth2OK_faker(t *testing.T) {
|
|
server := []string{
|
|
"220 Fake server ready ESMTP",
|
|
"250-fake.server",
|
|
"250-AUTH LOGIN XOAUTH2",
|
|
"250 8BITMIME",
|
|
"250 OK",
|
|
"235 2.7.0 Accepted",
|
|
"221 OK",
|
|
}
|
|
var wrote strings.Builder
|
|
var fake faker
|
|
fake.ReadWriter = struct {
|
|
io.Reader
|
|
io.Writer
|
|
}{
|
|
strings.NewReader(strings.Join(server, "\r\n")),
|
|
&wrote,
|
|
}
|
|
c, err := NewClient("fake.host",
|
|
WithDialContextFunc(getFakeDialFunc(fake)),
|
|
WithTLSPortPolicy(TLSOpportunistic),
|
|
WithSMTPAuth(SMTPAuthXOAUTH2),
|
|
WithUsername("user"),
|
|
WithPassword("token"))
|
|
if err != nil {
|
|
t.Fatalf("unable to create new client: %v", err)
|
|
}
|
|
if err = c.DialWithContext(context.Background()); err != nil {
|
|
t.Fatalf("unexpected dial error: %v", err)
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Fatalf("disconnect from test server failed: %v", err)
|
|
}
|
|
if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
|
|
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String())
|
|
}
|
|
}
|
|
|
|
func TestXOAuth2Unsupported_faker(t *testing.T) {
|
|
server := []string{
|
|
"220 Fake server ready ESMTP",
|
|
"250-fake.server",
|
|
"250-AUTH LOGIN PLAIN",
|
|
"250 8BITMIME",
|
|
"250 OK",
|
|
"221 OK",
|
|
}
|
|
var wrote strings.Builder
|
|
var fake faker
|
|
fake.ReadWriter = struct {
|
|
io.Reader
|
|
io.Writer
|
|
}{
|
|
strings.NewReader(strings.Join(server, "\r\n")),
|
|
&wrote,
|
|
}
|
|
c, err := NewClient("fake.host",
|
|
WithDialContextFunc(getFakeDialFunc(fake)),
|
|
WithTLSPortPolicy(TLSOpportunistic),
|
|
WithSMTPAuth(SMTPAuthXOAUTH2))
|
|
if err != nil {
|
|
t.Fatalf("unable to create new client: %v", err)
|
|
}
|
|
if err = c.DialWithContext(context.Background()); err == nil {
|
|
t.Fatal("expected dial error got nil")
|
|
} else {
|
|
if !errors.Is(err, ErrXOauth2AuthNotSupported) {
|
|
t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err)
|
|
}
|
|
}
|
|
if err = c.Close(); err != nil {
|
|
t.Fatalf("disconnect from test server failed: %v", err)
|
|
}
|
|
client := strings.Split(wrote.String(), "\r\n")
|
|
if len(client) != 4 {
|
|
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
|
|
}
|
|
if !strings.HasPrefix(client[0], "EHLO") {
|
|
t.Fatalf("expected EHLO, got %q", client[0])
|
|
}
|
|
if client[1] != "NOOP" {
|
|
t.Fatalf("expected NOOP, got %q", client[1])
|
|
}
|
|
if client[2] != "QUIT" {
|
|
t.Fatalf("expected QUIT, got %q", client[3])
|
|
}
|
|
}
|
|
|
|
func getFakeDialFunc(conn net.Conn) DialContextFunc {
|
|
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return conn, nil
|
|
}
|
|
}
|
|
|
|
type faker struct {
|
|
io.ReadWriter
|
|
}
|
|
|
|
func (f faker) Close() error { return nil }
|
|
func (f faker) LocalAddr() net.Addr { return nil }
|
|
func (f faker) RemoteAddr() net.Addr { return nil }
|
|
func (f faker) SetDeadline(time.Time) error { return nil }
|
|
func (f faker) SetReadDeadline(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 {
|
|
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 {
|
|
return
|
|
}
|
|
|
|
for {
|
|
data, err = reader.ReadString('\n')
|
|
if err != nil {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
|
|
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 XOAUTH2"):
|
|
auth := strings.TrimPrefix(data, "AUTH XOAUTH2 ")
|
|
if !strings.EqualFold(auth, "dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=") {
|
|
_ = writeLine("535 5.7.8 Error: authentication failed")
|
|
break
|
|
}
|
|
_ = writeLine("235 2.7.0 Authentication successful")
|
|
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.HasPrefix(data, "AUTH LOGIN"):
|
|
var username, password string
|
|
userResp := "VXNlcm5hbWU6"
|
|
passResp := "UGFzc3dvcmQ6"
|
|
if strings.Contains(featureSet, "250-X-MOX-LOGIN") {
|
|
userResp = ""
|
|
passResp = "UGFzc3dvcmQ="
|
|
}
|
|
if strings.Contains(featureSet, "250-X-NULLBYTE-LOGIN") {
|
|
userResp = "VXNlciBuYW1lAA=="
|
|
passResp = "UGFzc3dvcmQA"
|
|
}
|
|
if strings.Contains(featureSet, "250-X-BOGUS-LOGIN") {
|
|
userResp = "Qm9ndXM="
|
|
passResp = "Qm9ndXM="
|
|
}
|
|
if strings.Contains(featureSet, "250-X-EMPTY-LOGIN") {
|
|
userResp = ""
|
|
passResp = ""
|
|
}
|
|
_ = writeLine("334 " + userResp)
|
|
|
|
ddata, derr := reader.ReadString('\n')
|
|
if derr != nil {
|
|
fmt.Printf("failed to read username data from connection: %s\n", derr)
|
|
break
|
|
}
|
|
ddata = strings.TrimSpace(ddata)
|
|
username = ddata
|
|
_ = writeLine("334 " + passResp)
|
|
|
|
ddata, derr = reader.ReadString('\n')
|
|
if derr != nil {
|
|
fmt.Printf("failed to read password data from connection: %s\n", derr)
|
|
break
|
|
}
|
|
ddata = strings.TrimSpace(ddata)
|
|
password = ddata
|
|
|
|
if !strings.EqualFold(username, "dG9uaUB0ZXN0ZXIuY29t") ||
|
|
!strings.EqualFold(password, "VjNyeVMzY3IzdCs=") {
|
|
_ = 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")
|
|
}
|
|
}
|
|
}
|