mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-15 02:12:55 +01:00
Winni Neessen
c63b8b124e
Extended test cases to include scenarios for STARTTLS and SSL connections. This includes handling TLS configurations, testing certificate handling, and tests for various authentication methods under TLS.
4039 lines
125 KiB
Go
4039 lines
125 KiB
Go
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync/atomic"
|
|
"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 = 12025
|
|
// TestPasswordValid is the password that the test server accepts as valid for SMTP auth
|
|
TestPasswordValid = "V3ryS3cr3t+"
|
|
// TestUserValid is the username that the test server accepts as valid for SMTP auth
|
|
TestUserValid = "toni@tester.com"
|
|
)
|
|
|
|
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
|
|
var PortAdder atomic.Int32
|
|
|
|
// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
|
|
//
|
|
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
|
|
// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
|
var localhostCert = []byte(`
|
|
-----BEGIN CERTIFICATE-----
|
|
MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
|
|
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
|
|
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
|
|
gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa
|
|
JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30
|
|
LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw
|
|
DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
|
|
MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
|
|
AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi
|
|
NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4
|
|
n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF
|
|
tN8URjVmyEo=
|
|
-----END CERTIFICATE-----`)
|
|
|
|
// localhostKey is the private key for localhostCert.
|
|
var localhostKey = []byte(testingKey(`
|
|
-----BEGIN RSA TESTING KEY-----
|
|
MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h
|
|
AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu
|
|
lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB
|
|
AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA
|
|
kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM
|
|
VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m
|
|
542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb
|
|
PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2
|
|
6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB
|
|
vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP
|
|
QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i
|
|
jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c
|
|
qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg==
|
|
-----END RSA TESTING KEY-----`))
|
|
|
|
// logLine represents a log entry with time, level, message, and direction details.
|
|
type logLine struct {
|
|
Time time.Time `json:"time"`
|
|
Level string `json:"level"`
|
|
Message string `json:"msg"`
|
|
Direction struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
} `json:"direction"`
|
|
}
|
|
|
|
type logData struct {
|
|
Lines []logLine `json:"lines"`
|
|
}
|
|
|
|
func TestNewClient(t *testing.T) {
|
|
t.Run("create new Client", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if client.smtpAuthType != SMTPAuthNoAuth {
|
|
t.Errorf("new Client failed. Expected smtpAuthType: %s, got: %s", SMTPAuthNoAuth,
|
|
client.smtpAuthType)
|
|
}
|
|
if client.connTimeout != DefaultTimeout {
|
|
t.Errorf("new Client failed. Expected connTimeout: %s, got: %s", DefaultTimeout.String(),
|
|
client.connTimeout.String())
|
|
}
|
|
if client.host != DefaultHost {
|
|
t.Errorf("new Client failed. Expected host: %s, got: %s", DefaultHost, client.host)
|
|
}
|
|
if client.port != DefaultPort {
|
|
t.Errorf("new Client failed. Expected port: %d, got: %d", DefaultPort, client.port)
|
|
}
|
|
if client.tlsconfig == nil {
|
|
t.Fatal("new Client failed. Expected tlsconfig but got nil")
|
|
}
|
|
if client.tlsconfig.MinVersion != DefaultTLSMinVersion {
|
|
t.Errorf("new Client failed. Expected tlsconfig min TLS version: %d, got: %d",
|
|
DefaultTLSMinVersion, client.tlsconfig.MinVersion)
|
|
}
|
|
if client.tlsconfig.ServerName != DefaultHost {
|
|
t.Errorf("new Client failed. Expected tlsconfig server name: %s, got: %s",
|
|
DefaultHost, client.tlsconfig.ServerName)
|
|
}
|
|
if client.tlspolicy != DefaultTLSPolicy {
|
|
t.Errorf("new Client failed. Expected tlsconfig policy: %s, got: %s", DefaultTLSPolicy,
|
|
client.tlspolicy)
|
|
}
|
|
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
t.Fatalf("failed to get hostname: %s", err)
|
|
}
|
|
if client.helo != hostname {
|
|
t.Errorf("new Client failed. Expected helo: %s, got: %s", hostname, client.helo)
|
|
}
|
|
})
|
|
t.Run("NewClient with empty hostname should fail", func(t *testing.T) {
|
|
_, err := NewClient("")
|
|
if err == nil {
|
|
t.Fatalf("NewClient with empty hostname should fail")
|
|
}
|
|
if !errors.Is(err, ErrNoHostname) {
|
|
t.Errorf("NewClient with empty hostname should fail with error: %s, got: %s", ErrNoHostname, err)
|
|
}
|
|
})
|
|
t.Run("NewClient with option", func(t *testing.T) {
|
|
hostname := "mail.example.com"
|
|
netDailer := net.Dialer{}
|
|
tlsDailer := tls.Dialer{NetDialer: &netDailer, Config: &tls.Config{}}
|
|
tests := []struct {
|
|
name string
|
|
option Option
|
|
expectFunc func(c *Client) error
|
|
shouldfail bool
|
|
expectErr *error
|
|
}{
|
|
{"nil option", nil, nil, false, nil},
|
|
{
|
|
"WithPort", WithPort(465),
|
|
func(c *Client) error {
|
|
if c.port != 465 {
|
|
return fmt.Errorf("failed to set custom port. Want: %d, got: %d", 465, c.port)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithPort but too high port number", WithPort(100000), nil, true,
|
|
&ErrInvalidPort,
|
|
},
|
|
{
|
|
"WithTimeout", WithTimeout(time.Second * 100),
|
|
func(c *Client) error {
|
|
if c.connTimeout != time.Second*100 {
|
|
return fmt.Errorf("failed to set custom timeout. Want: %d, got: %d", time.Second*100,
|
|
c.connTimeout)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTimeout but invalid timeout", WithTimeout(-10), nil, true,
|
|
&ErrInvalidTimeout,
|
|
},
|
|
{
|
|
"WithSSL", WithSSL(),
|
|
func(c *Client) error {
|
|
if !c.useSSL {
|
|
return fmt.Errorf("failed to set useSSL. Want: %t, got: %t", true, c.useSSL)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithSSLPort with no fallback", WithSSLPort(false),
|
|
func(c *Client) error {
|
|
if !c.useSSL {
|
|
return fmt.Errorf("failed to set useSSL. Want: %t, got: %t", true, c.useSSL)
|
|
}
|
|
if c.port != 465 {
|
|
return fmt.Errorf("failed to set ssl port. Want: %d, got: %d", 465, c.port)
|
|
}
|
|
if c.fallbackPort != 0 {
|
|
return fmt.Errorf("failed to set ssl fallbackport. Want: %d, got: %d", 0,
|
|
c.fallbackPort)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithSSLPort with fallback", WithSSLPort(true),
|
|
func(c *Client) error {
|
|
if !c.useSSL {
|
|
return fmt.Errorf("failed to set useSSL. Want: %t, got: %t", true, c.useSSL)
|
|
}
|
|
if c.port != 465 {
|
|
return fmt.Errorf("failed to set ssl port. Want: %d, got: %d", 465, c.port)
|
|
}
|
|
if c.fallbackPort != 25 {
|
|
return fmt.Errorf("failed to set ssl fallbackport. Want: %d, got: %d", 0,
|
|
c.fallbackPort)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDebugLog", WithDebugLog(),
|
|
func(c *Client) error {
|
|
if !c.useDebugLog {
|
|
return fmt.Errorf("failed to set enable debug log. Want: %t, got: %t", true,
|
|
c.useDebugLog)
|
|
}
|
|
if c.logAuthData {
|
|
return fmt.Errorf("failed to set enable debug log. Want logAuthData: %t, got: %t", true,
|
|
c.logAuthData)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithLogger log.Stdlog", WithLogger(log.New(os.Stderr, log.LevelDebug)),
|
|
func(c *Client) error {
|
|
if c.logger == nil {
|
|
return errors.New("failed to set logger. Want logger bug got got nil")
|
|
}
|
|
loggerType := reflect.TypeOf(c.logger).String()
|
|
if loggerType != "*log.Stdlog" {
|
|
return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s",
|
|
"*log.Stdlog", loggerType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithLogger log.JSONlog", WithLogger(log.NewJSON(os.Stderr, log.LevelDebug)),
|
|
func(c *Client) error {
|
|
if c.logger == nil {
|
|
return errors.New("failed to set logger. Want logger bug got got nil")
|
|
}
|
|
loggerType := reflect.TypeOf(c.logger).String()
|
|
if loggerType != "*log.JSONlog" {
|
|
return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s",
|
|
"*log.JSONlog", loggerType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithHELO", WithHELO(hostname),
|
|
func(c *Client) error {
|
|
if c.helo != hostname {
|
|
return fmt.Errorf("failed to set custom HELO. Want: %s, got: %s", hostname, c.helo)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithHELO fail with empty hostname", WithHELO(""), nil,
|
|
true, &ErrInvalidHELO,
|
|
},
|
|
{
|
|
"WithTLSPolicy TLSMandatory", WithTLSPolicy(TLSMandatory),
|
|
func(c *Client) error {
|
|
if c.tlspolicy != TLSMandatory {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSMandatory,
|
|
c.tlspolicy)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSPolicy TLSOpportunistic", WithTLSPolicy(TLSOpportunistic),
|
|
func(c *Client) error {
|
|
if c.tlspolicy != TLSOpportunistic {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic,
|
|
c.tlspolicy)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSPolicy NoTLS", WithTLSPolicy(NoTLS),
|
|
func(c *Client) error {
|
|
if c.tlspolicy != NoTLS {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic,
|
|
c.tlspolicy)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSPortPolicy TLSMandatory", WithTLSPortPolicy(TLSMandatory),
|
|
func(c *Client) error {
|
|
if c.tlspolicy != TLSMandatory {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSMandatory,
|
|
c.tlspolicy)
|
|
}
|
|
if c.port != 587 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587,
|
|
c.port)
|
|
}
|
|
if c.fallbackPort != 0 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d",
|
|
0, c.fallbackPort)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSPortPolicy TLSOpportunistic", WithTLSPortPolicy(TLSOpportunistic),
|
|
func(c *Client) error {
|
|
if c.tlspolicy != TLSOpportunistic {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic,
|
|
c.tlspolicy)
|
|
}
|
|
if c.port != 587 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587,
|
|
c.port)
|
|
}
|
|
if c.fallbackPort != 25 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d",
|
|
25, c.fallbackPort)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSPortPolicy NoTLS", WithTLSPortPolicy(NoTLS),
|
|
func(c *Client) error {
|
|
if c.tlspolicy != NoTLS {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic,
|
|
c.tlspolicy)
|
|
}
|
|
if c.port != 25 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 25,
|
|
c.port)
|
|
}
|
|
if c.fallbackPort != 0 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d",
|
|
0, c.fallbackPort)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSPortPolicy invalid", WithTLSPortPolicy(-1),
|
|
func(c *Client) error {
|
|
if c.tlspolicy.String() != "UnknownPolicy" {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", "UnknownPolicy",
|
|
c.tlspolicy)
|
|
}
|
|
if c.port != 587 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587,
|
|
c.port)
|
|
}
|
|
if c.fallbackPort != 0 {
|
|
return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d",
|
|
25, c.fallbackPort)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSConfig with empty tls.Config", WithTLSConfig(&tls.Config{}),
|
|
func(c *Client) error {
|
|
if c.tlsconfig == nil {
|
|
return errors.New("failed to set custom TLS config. Wanted policy but got nil")
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSConfig with custom tls.Config", WithTLSConfig(&tls.Config{ServerName: hostname}),
|
|
func(c *Client) error {
|
|
if c.tlsconfig == nil {
|
|
return errors.New("failed to set custom TLS config. Wanted policy but got nil")
|
|
}
|
|
if c.tlsconfig.ServerName != hostname {
|
|
return fmt.Errorf("failed to set custom TLS config. Want hostname: %s, got: %s",
|
|
hostname, c.tlsconfig.ServerName)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithTLSConfig with nil", WithTLSConfig(nil), nil,
|
|
true, &ErrInvalidTLSConfig,
|
|
},
|
|
{
|
|
"WithSMTPAuthCustom with PLAIN auth",
|
|
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
|
|
func(c *Client) error {
|
|
if c.smtpAuthType != SMTPAuthCustom {
|
|
return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth type: %s, "+
|
|
"got: %s", SMTPAuthCustom, c.smtpAuthType)
|
|
}
|
|
if c.smtpAuth == nil {
|
|
return errors.New("failed to set custom SMTP auth method. Wanted smtp auth method but" +
|
|
" got nil")
|
|
}
|
|
smtpAuthType := reflect.TypeOf(c.smtpAuth).String()
|
|
if smtpAuthType != "*smtp.plainAuth" {
|
|
return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth method of type: %s, "+
|
|
"got: %s", "*smtp.plainAuth", smtpAuthType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithSMTPAuthCustom with LOGIN auth",
|
|
WithSMTPAuthCustom(smtp.LoginAuth("", "", "", false)),
|
|
func(c *Client) error {
|
|
if c.smtpAuthType != SMTPAuthCustom {
|
|
return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth type: %s, "+
|
|
"got: %s", SMTPAuthCustom, c.smtpAuthType)
|
|
}
|
|
if c.smtpAuth == nil {
|
|
return errors.New("failed to set custom SMTP auth method. Wanted smtp auth method but" +
|
|
" got nil")
|
|
}
|
|
smtpAuthType := reflect.TypeOf(c.smtpAuth).String()
|
|
if smtpAuthType != "*smtp.loginAuth" {
|
|
return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth method of type: %s, "+
|
|
"got: %s", "*smtp.loginAuth", smtpAuthType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithSMTPAuthCustom with nil", WithSMTPAuthCustom(nil), nil,
|
|
true, &ErrSMTPAuthMethodIsNil,
|
|
},
|
|
{
|
|
"WithUsername", WithUsername("toni.tester"),
|
|
func(c *Client) error {
|
|
if c.user != "toni.tester" {
|
|
return fmt.Errorf("failed to set username. Want username: %s, got: %s",
|
|
"toni.tester", c.user)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithPassword", WithPassword("sU*p3rS3cr3t"),
|
|
func(c *Client) error {
|
|
if c.pass != "sU*p3rS3cr3t" {
|
|
return fmt.Errorf("failed to set password. Want password: %s, got: %s",
|
|
"sU*p3rS3cr3t", c.pass)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSN", WithDSN(),
|
|
func(c *Client) error {
|
|
if c.requestDSN != true {
|
|
return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true,
|
|
c.requestDSN)
|
|
}
|
|
if c.dsnReturnType != DSNMailReturnFull {
|
|
return fmt.Errorf("failed to enable DSN. Want dsnReturnType: %s, got: %s",
|
|
DSNMailReturnFull, c.dsnReturnType)
|
|
}
|
|
if len(c.dsnRcptNotifyType) != 2 {
|
|
return fmt.Errorf("failed to enable DSN. Want 2 default DSN Rcpt Notify types, got: %d",
|
|
len(c.dsnRcptNotifyType))
|
|
}
|
|
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) {
|
|
return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Failure type: %s, got: %s",
|
|
string(DSNRcptNotifyFailure), c.dsnRcptNotifyType[0])
|
|
}
|
|
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) {
|
|
return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Success type: %s, got: %s",
|
|
string(DSNRcptNotifySuccess), c.dsnRcptNotifyType[1])
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSNMailReturnType DSNMailReturnHeadersOnly",
|
|
WithDSNMailReturnType(DSNMailReturnHeadersOnly),
|
|
func(c *Client) error {
|
|
if c.requestDSN != true {
|
|
return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true,
|
|
c.requestDSN)
|
|
}
|
|
if c.dsnReturnType != DSNMailReturnHeadersOnly {
|
|
return fmt.Errorf("failed to enable DSN. Want dsnReturnType: %s, got: %s",
|
|
DSNMailReturnHeadersOnly, c.dsnReturnType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSNMailReturnType DSNMailReturnFull",
|
|
WithDSNMailReturnType(DSNMailReturnFull),
|
|
func(c *Client) error {
|
|
if c.requestDSN != true {
|
|
return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true,
|
|
c.requestDSN)
|
|
}
|
|
if c.dsnReturnType != DSNMailReturnFull {
|
|
return fmt.Errorf("failed to enable DSN. Want dsnReturnType: %s, got: %s",
|
|
DSNMailReturnFull, c.dsnReturnType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSNMailReturnType invalid", WithDSNMailReturnType("invalid"), nil,
|
|
true, &ErrInvalidDSNMailReturnOption,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType DSNRcptNotifyNever",
|
|
WithDSNRcptNotifyType(DSNRcptNotifyNever),
|
|
func(c *Client) error {
|
|
if c.requestDSN != true {
|
|
return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true,
|
|
c.requestDSN)
|
|
}
|
|
if len(c.dsnRcptNotifyType) != 1 {
|
|
return fmt.Errorf("failed to enable DSN. Want 1 DSN Rcpt Notify type, got: %d",
|
|
len(c.dsnRcptNotifyType))
|
|
}
|
|
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyNever) {
|
|
return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Never type: %s, got: %s",
|
|
string(DSNRcptNotifyNever), c.dsnRcptNotifyType[0])
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType DSNRcptNotifySuccess, DSNRcptNotifyFailure",
|
|
WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyFailure),
|
|
func(c *Client) error {
|
|
if c.requestDSN != true {
|
|
return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true,
|
|
c.requestDSN)
|
|
}
|
|
if len(c.dsnRcptNotifyType) != 2 {
|
|
return fmt.Errorf("failed to enable DSN. Want 2 DSN Rcpt Notify type, got: %d",
|
|
len(c.dsnRcptNotifyType))
|
|
}
|
|
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifySuccess) {
|
|
return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Success type: %s, got: %s",
|
|
string(DSNRcptNotifySuccess), c.dsnRcptNotifyType[0])
|
|
}
|
|
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifyFailure) {
|
|
return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Failure type: %s, got: %s",
|
|
string(DSNRcptNotifyFailure), c.dsnRcptNotifyType[1])
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType DSNRcptNotifyDelay",
|
|
WithDSNRcptNotifyType(DSNRcptNotifyDelay),
|
|
func(c *Client) error {
|
|
if c.requestDSN != true {
|
|
return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true,
|
|
c.requestDSN)
|
|
}
|
|
if len(c.dsnRcptNotifyType) != 1 {
|
|
return fmt.Errorf("failed to enable DSN. Want 1 DSN Rcpt Notify type, got: %d",
|
|
len(c.dsnRcptNotifyType))
|
|
}
|
|
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyDelay) {
|
|
return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Delay type: %s, got: %s",
|
|
string(DSNRcptNotifyDelay), c.dsnRcptNotifyType[0])
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType invalid", WithDSNRcptNotifyType("invalid"), nil,
|
|
true, &ErrInvalidDSNRcptNotifyOption,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType mix valid and invalid",
|
|
WithDSNRcptNotifyType(DSNRcptNotifyDelay, "invalid"), nil,
|
|
true, &ErrInvalidDSNRcptNotifyOption,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType mix NEVER with SUCCESS",
|
|
WithDSNRcptNotifyType(DSNRcptNotifyNever, DSNRcptNotifySuccess), nil,
|
|
true, &ErrInvalidDSNRcptNotifyCombination,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType mix NEVER with FAIL",
|
|
WithDSNRcptNotifyType(DSNRcptNotifyNever, DSNRcptNotifyFailure), nil,
|
|
true, &ErrInvalidDSNRcptNotifyCombination,
|
|
},
|
|
{
|
|
"WithDSNRcptNotifyType mix NEVER with DELAY",
|
|
WithDSNRcptNotifyType(DSNRcptNotifyNever, DSNRcptNotifyDelay), nil,
|
|
true, &ErrInvalidDSNRcptNotifyCombination,
|
|
},
|
|
{
|
|
"WithoutNoop", WithoutNoop(),
|
|
func(c *Client) error {
|
|
if !c.noNoop {
|
|
return fmt.Errorf("failed to disable Noop. Want noNoop: %t, got: %t", false, c.noNoop)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDialContextFunc with net.Dailer", WithDialContextFunc(netDailer.DialContext),
|
|
func(c *Client) error {
|
|
if c.dialContextFunc == nil {
|
|
return errors.New("failed to set dial context func, got: nil")
|
|
}
|
|
ctxType := reflect.TypeOf(c.dialContextFunc).String()
|
|
if ctxType != "mail.DialContextFunc" {
|
|
return fmt.Errorf("failed to set dial context func, want: %s, got: %s",
|
|
"mail.DialContextFunc", ctxType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDialContextFunc with tls.Dailer", WithDialContextFunc(tlsDailer.DialContext),
|
|
func(c *Client) error {
|
|
if c.dialContextFunc == nil {
|
|
return errors.New("failed to set dial context func, got: nil")
|
|
}
|
|
ctxType := reflect.TypeOf(c.dialContextFunc).String()
|
|
if ctxType != "mail.DialContextFunc" {
|
|
return fmt.Errorf("failed to set dial context func, want: %s, got: %s",
|
|
"mail.DialContextFunc", ctxType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDialContextFunc with custom dialer",
|
|
WithDialContextFunc(
|
|
func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, nil
|
|
},
|
|
),
|
|
func(c *Client) error {
|
|
if c.dialContextFunc == nil {
|
|
return errors.New("failed to set dial context func, got: nil")
|
|
}
|
|
ctxType := reflect.TypeOf(c.dialContextFunc).String()
|
|
if ctxType != "mail.DialContextFunc" {
|
|
return fmt.Errorf("failed to set dial context func, want: %s, got: %s",
|
|
"mail.DialContextFunc", ctxType)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
{
|
|
"WithDialContextFunc with nil", WithDialContextFunc(nil), nil,
|
|
true, &ErrDialContextFuncIsNil,
|
|
},
|
|
{
|
|
"WithLogAuthData", WithLogAuthData(),
|
|
func(c *Client) error {
|
|
if !c.logAuthData {
|
|
return fmt.Errorf("failed to enable auth data logging. Want logAuthData: %t, got: %t",
|
|
true, c.logAuthData)
|
|
}
|
|
return nil
|
|
},
|
|
false, nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, tt.option)
|
|
if !tt.shouldfail && err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if tt.shouldfail && err == nil {
|
|
t.Errorf("client creation was supposed to fail, but it didn't")
|
|
}
|
|
if tt.shouldfail && tt.expectErr != nil {
|
|
if !errors.Is(err, *tt.expectErr) {
|
|
t.Errorf("error for NewClient mismatch. Expected: %s, got: %s",
|
|
*tt.expectErr, err)
|
|
}
|
|
}
|
|
if tt.expectFunc != nil {
|
|
if err = tt.expectFunc(client); err != nil {
|
|
t.Errorf("NewClient with custom option failed: %s", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("NewClient WithSMTPAuth", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
option Option
|
|
expected SMTPAuthType
|
|
}{
|
|
{"CRAM-MD5", WithSMTPAuth(SMTPAuthCramMD5), SMTPAuthCramMD5},
|
|
{"LOGIN", WithSMTPAuth(SMTPAuthLogin), SMTPAuthLogin},
|
|
{"LOGIN-NOENC", WithSMTPAuth(SMTPAuthLoginNoEnc), SMTPAuthLoginNoEnc},
|
|
{"NOAUTH", WithSMTPAuth(SMTPAuthNoAuth), SMTPAuthNoAuth},
|
|
{"PLAIN", WithSMTPAuth(SMTPAuthPlain), SMTPAuthPlain},
|
|
{"PLAIN-NOENC", WithSMTPAuth(SMTPAuthPlainNoEnc), SMTPAuthPlainNoEnc},
|
|
{"SCRAM-SHA-1", WithSMTPAuth(SMTPAuthSCRAMSHA1), SMTPAuthSCRAMSHA1},
|
|
{"SCRAM-SHA-1-PLUS", WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), SMTPAuthSCRAMSHA1PLUS},
|
|
{"SCRAM-SHA-256", WithSMTPAuth(SMTPAuthSCRAMSHA256), SMTPAuthSCRAMSHA256},
|
|
{"SCRAM-SHA-256-PLUS", WithSMTPAuth(SMTPAuthSCRAMSHA256PLUS), SMTPAuthSCRAMSHA256PLUS},
|
|
{"XOAUTH2", WithSMTPAuth(SMTPAuthXOAUTH2), SMTPAuthXOAUTH2},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, tt.option)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if client.smtpAuthType != tt.expected {
|
|
t.Errorf("failed to set custom SMTP auth type. Want: %s, got: %s",
|
|
tt.expected, client.smtpAuthType)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_TLSPolicy(t *testing.T) {
|
|
t.Run("WithTLSPolicy fmt.Stringer interface", func(t *testing.T) {})
|
|
tests := []struct {
|
|
name string
|
|
value TLSPolicy
|
|
want string
|
|
}{
|
|
{"TLSMandatory", TLSMandatory, "TLSMandatory"},
|
|
{"TLSOpportunistic", TLSOpportunistic, "TLSOpportunistic"},
|
|
{"NoTLS", NoTLS, "NoTLS"},
|
|
{"Invalid", -1, "UnknownPolicy"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithTLSPolicy(tt.value))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.TLSPolicy()
|
|
if !strings.EqualFold(got, tt.want) {
|
|
t.Errorf("failed to get expected TLS policy string. Want: %s, got: %s", tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClient_ServerAddr(t *testing.T) {
|
|
t.Run("ServerAddr of default client", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.ServerAddr()
|
|
expected := fmt.Sprintf("%s:%d", DefaultHost, DefaultPort)
|
|
if !strings.EqualFold(expected, got) {
|
|
t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got)
|
|
}
|
|
})
|
|
t.Run("ServerAddr of with custom port", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithPort(587))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.ServerAddr()
|
|
expected := fmt.Sprintf("%s:%d", DefaultHost, 587)
|
|
if !strings.EqualFold(expected, got) {
|
|
t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got)
|
|
}
|
|
})
|
|
t.Run("ServerAddr of with port policy TLSMandatory", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithTLSPortPolicy(TLSMandatory))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.ServerAddr()
|
|
expected := fmt.Sprintf("%s:%d", DefaultHost, 587)
|
|
if !strings.EqualFold(expected, got) {
|
|
t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got)
|
|
}
|
|
})
|
|
t.Run("ServerAddr of with port policy TLSOpportunistic", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithTLSPortPolicy(TLSOpportunistic))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.ServerAddr()
|
|
expected := fmt.Sprintf("%s:%d", DefaultHost, 587)
|
|
if !strings.EqualFold(expected, got) {
|
|
t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got)
|
|
}
|
|
})
|
|
t.Run("ServerAddr of with port policy NoTLS", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithTLSPortPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.ServerAddr()
|
|
expected := fmt.Sprintf("%s:%d", DefaultHost, 25)
|
|
if !strings.EqualFold(expected, got) {
|
|
t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got)
|
|
}
|
|
})
|
|
t.Run("ServerAddr of with SSL", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithSSLPort(false))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
got := client.ServerAddr()
|
|
expected := fmt.Sprintf("%s:%d", DefaultHost, 465)
|
|
if !strings.EqualFold(expected, got) {
|
|
t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetTLSPolicy(t *testing.T) {
|
|
t.Run("SetTLSPolicy TLSMandatory", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPolicy(TLSMandatory)
|
|
if client.tlspolicy != TLSMandatory {
|
|
t.Errorf("failed to set expected TLS policy. Want: %s, got: %s",
|
|
TLSMandatory, client.tlspolicy)
|
|
}
|
|
})
|
|
t.Run("SetTLSPolicy TLSOpportunistic", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPolicy(TLSOpportunistic)
|
|
if client.tlspolicy != TLSOpportunistic {
|
|
t.Errorf("failed to set expected TLS policy. Want: %s, got: %s",
|
|
TLSOpportunistic, client.tlspolicy)
|
|
}
|
|
})
|
|
t.Run("SetTLSPolicy NoTLS", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPolicy(NoTLS)
|
|
if client.tlspolicy != NoTLS {
|
|
t.Errorf("failed to set expected TLS policy. Want: %s, got: %s",
|
|
NoTLS, client.tlspolicy)
|
|
}
|
|
})
|
|
t.Run("SetTLSPolicy to override WithTLSPolicy", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithTLSPolicy(TLSOpportunistic))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPolicy(TLSMandatory)
|
|
if client.tlspolicy != TLSMandatory {
|
|
t.Errorf("failed to set expected TLS policy. Want: %s, got: %s",
|
|
TLSMandatory, client.tlspolicy)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetTLSPortPolicy(t *testing.T) {
|
|
t.Run("SetTLSPortPolicy TLSMandatory", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPortPolicy(TLSMandatory)
|
|
if client.tlspolicy != TLSMandatory {
|
|
t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s",
|
|
TLSMandatory, client.tlspolicy)
|
|
}
|
|
if client.port != 587 {
|
|
t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 587, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 0,
|
|
client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetTLSPortPolicy TLSOpportunistic", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPortPolicy(TLSOpportunistic)
|
|
if client.tlspolicy != TLSOpportunistic {
|
|
t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s",
|
|
TLSOpportunistic, client.tlspolicy)
|
|
}
|
|
if client.port != 587 {
|
|
t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 587, client.port)
|
|
}
|
|
if client.fallbackPort != 25 {
|
|
t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 25,
|
|
client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetTLSPortPolicy NoTLS", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPortPolicy(NoTLS)
|
|
if client.tlspolicy != NoTLS {
|
|
t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s",
|
|
NoTLS, client.tlspolicy)
|
|
}
|
|
if client.port != 25 {
|
|
t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 25, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 0,
|
|
client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetTLSPortPolicy to override WithTLSPortPolicy", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithTLSPortPolicy(TLSOpportunistic), WithPort(25))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetTLSPortPolicy(TLSMandatory)
|
|
if client.tlspolicy != TLSMandatory {
|
|
t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s",
|
|
TLSMandatory, client.tlspolicy)
|
|
}
|
|
if client.port != 587 {
|
|
t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 587, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 0,
|
|
client.fallbackPort)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetSSL(t *testing.T) {
|
|
t.Run("SetSSL true", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSL(true)
|
|
if !client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", true)
|
|
}
|
|
})
|
|
t.Run("SetSSL false", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSL(false)
|
|
if client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", false)
|
|
}
|
|
})
|
|
t.Run("SetSSL to override WithSSL", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithSSL())
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSL(false)
|
|
if client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", false)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetSSLPort(t *testing.T) {
|
|
t.Run("SetSSLPort true no fallback", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSLPort(true, false)
|
|
if !client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", true)
|
|
}
|
|
if client.port != 465 {
|
|
t.Errorf("failed to set expected port: %d, got: %d", 465, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetSSLPort true with fallback", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSLPort(true, true)
|
|
if !client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", true)
|
|
}
|
|
if client.port != 465 {
|
|
t.Errorf("failed to set expected port: %d, got: %d", 465, client.port)
|
|
}
|
|
if client.fallbackPort != 25 {
|
|
t.Errorf("failed to set expected fallback: %d, got: %d", 25, client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetSSLPort false no fallback", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSLPort(false, false)
|
|
if client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", false)
|
|
}
|
|
if client.port != 25 {
|
|
t.Errorf("failed to set expected port: %d, got: %d", 25, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetSSLPort false with fallback (makes no sense)", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSLPort(false, true)
|
|
if client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", false)
|
|
}
|
|
if client.port != 25 {
|
|
t.Errorf("failed to set expected port: %d, got: %d", 25, client.port)
|
|
}
|
|
if client.fallbackPort != 25 {
|
|
t.Errorf("failed to set expected fallback: %d, got: %d", 25, client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetSSLPort to override WithSSL", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithSSL())
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSLPort(false, false)
|
|
if client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", false)
|
|
}
|
|
if client.port != 25 {
|
|
t.Errorf("failed to set expected port: %d, got: %d", 25, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort)
|
|
}
|
|
})
|
|
t.Run("SetSSLPort with custom port", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithPort(123))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSSLPort(false, false)
|
|
if client.useSSL {
|
|
t.Errorf("failed to set expected useSSL: %t", false)
|
|
}
|
|
if client.port != 123 {
|
|
t.Errorf("failed to set expected port: %d, got: %d", 123, client.port)
|
|
}
|
|
if client.fallbackPort != 0 {
|
|
t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetDebugLog(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctx, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
t.Run("SetDebugLog true", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetDebugLog(true)
|
|
if !client.useDebugLog {
|
|
t.Errorf("failed to set expected useDebugLog: %t", true)
|
|
}
|
|
})
|
|
t.Run("SetDebugLog false", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetDebugLog(false)
|
|
if client.useDebugLog {
|
|
t.Errorf("failed to set expected useDebugLog: %t", false)
|
|
}
|
|
})
|
|
t.Run("SetDebugLog true with active SMTP client", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
buffer := bytes.NewBuffer(nil)
|
|
client.SetLogger(log.New(buffer, log.LevelDebug))
|
|
client.SetDebugLog(true)
|
|
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("failed to close client to test server: %s", err)
|
|
}
|
|
})
|
|
|
|
if !client.useDebugLog {
|
|
t.Errorf("failed to set expected useDebugLog: %t", true)
|
|
}
|
|
if !strings.Contains(buffer.String(), "DEBUG: C --> S: EHLO") {
|
|
t.Errorf("failed to enable debug log. Expected string: %s in log buffer but didn't find it. "+
|
|
"Buffer: %s", "DEBUG: C --> S: EHLO", buffer.String())
|
|
}
|
|
})
|
|
t.Run("SetDebugLog false to override WithDebugLog", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDebugLog())
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
buffer := bytes.NewBuffer(nil)
|
|
client.SetLogger(log.New(buffer, log.LevelDebug))
|
|
client.SetDebugLog(false)
|
|
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("failed to close client to test server: %s", err)
|
|
}
|
|
})
|
|
|
|
if client.useDebugLog {
|
|
t.Errorf("failed to set expected useDebugLog: %t", false)
|
|
}
|
|
if buffer.Len() > 0 {
|
|
t.Errorf("failed to disable debug logger. Expected buffer to be empty but got: %d", buffer.Len())
|
|
}
|
|
})
|
|
t.Run("SetDebugLog true active SMTP client after dial", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("failed to close client to test server: %s", err)
|
|
}
|
|
})
|
|
|
|
buffer := bytes.NewBuffer(nil)
|
|
client.SetLogger(log.New(buffer, log.LevelDebug))
|
|
client.SetDebugLog(true)
|
|
if err = client.smtpClient.Noop(); err != nil {
|
|
t.Errorf("failed to send NOOP command: %s", err)
|
|
}
|
|
|
|
if !client.useDebugLog {
|
|
t.Errorf("failed to set expected useDebugLog: %t", true)
|
|
}
|
|
if !strings.Contains(buffer.String(), "DEBUG: C --> S: NOOP") {
|
|
t.Errorf("failed to enable debug log. Expected string: %s in log buffer but didn't find it. "+
|
|
"Buffer: %s", "DEBUG: C --> S: NOOP", buffer.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetTLSConfig(t *testing.T) {
|
|
t.Run("SetTLSConfig with &tls.Config", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.SetTLSConfig(&tls.Config{}); err != nil {
|
|
t.Errorf("failed to set expected TLSConfig: %s", err)
|
|
}
|
|
if client.tlsconfig == nil {
|
|
t.Fatalf("failed to set expected TLSConfig. TLSConfig is nil")
|
|
}
|
|
})
|
|
t.Run("SetTLSConfig with InsecureSkipVerify", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}); err != nil {
|
|
t.Errorf("failed to set expected TLSConfig: %s", err)
|
|
}
|
|
if client.tlsconfig == nil {
|
|
t.Fatalf("failed to set expected TLSConfig. TLSConfig is nil")
|
|
}
|
|
if !client.tlsconfig.InsecureSkipVerify {
|
|
t.Errorf("failed to set expected TLSConfig. Expected InsecureSkipVerify: %t, got: %t", true,
|
|
client.tlsconfig.InsecureSkipVerify)
|
|
}
|
|
})
|
|
t.Run("SetTLSConfig with nil should fail", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
err = client.SetTLSConfig(nil)
|
|
if err == nil {
|
|
t.Errorf("SetTLSConfig with nil should fail")
|
|
}
|
|
if !errors.Is(err, ErrInvalidTLSConfig) {
|
|
t.Errorf("SetTLSConfig was expected to fail with %s, got: %s", ErrInvalidTLSConfig, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetUsername(t *testing.T) {
|
|
t.Run("SetUsername", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetUsername("toni.tester")
|
|
if client.user != "toni.tester" {
|
|
t.Errorf("failed to set expected username, want: %s, got: %s", "toni.tester", client.user)
|
|
}
|
|
})
|
|
t.Run("SetUsername to override WithUsername", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithUsername("toni.tester"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetUsername("tina.tester")
|
|
if client.user != "tina.tester" {
|
|
t.Errorf("failed to set expected username, want: %s, got: %s", "tina.tester", client.user)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetPassword(t *testing.T) {
|
|
t.Run("SetPassword", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetPassword("sU*perS3crEt")
|
|
if client.pass != "sU*perS3crEt" {
|
|
t.Errorf("failed to set expected password, want: %s, got: %s", "sU*perS3crEt", client.pass)
|
|
}
|
|
})
|
|
t.Run("SetPassword to override WithPassword", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithPassword("sU*perS3crEt"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetPassword("Su%perS3crEt")
|
|
if client.pass != "Su%perS3crEt" {
|
|
t.Errorf("failed to set expected password, want: %s, got: %s", "Su%perS3crEt", client.pass)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetSMTPAuth(t *testing.T) {
|
|
t.Run("SetSMTPAuth", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
auth SMTPAuthType
|
|
expected SMTPAuthType
|
|
}{
|
|
{"CRAM-MD5", SMTPAuthCramMD5, SMTPAuthCramMD5},
|
|
{"LOGIN", SMTPAuthLogin, SMTPAuthLogin},
|
|
{"LOGIN-NOENC", SMTPAuthLoginNoEnc, SMTPAuthLoginNoEnc},
|
|
{"NOAUTH", SMTPAuthNoAuth, SMTPAuthNoAuth},
|
|
{"PLAIN", SMTPAuthPlain, SMTPAuthPlain},
|
|
{"PLAIN-NOENC", SMTPAuthPlainNoEnc, SMTPAuthPlainNoEnc},
|
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, SMTPAuthSCRAMSHA1},
|
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1PLUS},
|
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA256},
|
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256PLUS},
|
|
{"XOAUTH2", SMTPAuthXOAUTH2, SMTPAuthXOAUTH2},
|
|
}
|
|
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client.SetSMTPAuth(tt.auth)
|
|
if client.smtpAuthType != tt.expected {
|
|
t.Errorf("failed to set expected SMTPAuthType, want: %s, got: %s", tt.expected,
|
|
client.smtpAuthType)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("SetSMTPAuth to override WithSMTPAuth", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
auth SMTPAuthType
|
|
expected SMTPAuthType
|
|
}{
|
|
{"CRAM-MD5", SMTPAuthCramMD5, SMTPAuthCramMD5},
|
|
{"LOGIN", SMTPAuthLogin, SMTPAuthLogin},
|
|
{"LOGIN-NOENC", SMTPAuthLoginNoEnc, SMTPAuthLoginNoEnc},
|
|
{"NOAUTH", SMTPAuthNoAuth, SMTPAuthNoAuth},
|
|
{"PLAIN", SMTPAuthPlain, SMTPAuthPlain},
|
|
{"PLAIN-NOENC", SMTPAuthPlainNoEnc, SMTPAuthPlainNoEnc},
|
|
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, SMTPAuthSCRAMSHA1},
|
|
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1PLUS},
|
|
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA256},
|
|
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256PLUS},
|
|
{"XOAUTH2", SMTPAuthXOAUTH2, SMTPAuthXOAUTH2},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithSMTPAuth(SMTPAuthLogin))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if client.smtpAuthType != SMTPAuthLogin {
|
|
t.Fatalf("failed to create client with LOGIN auth, got: %s", client.smtpAuthType)
|
|
}
|
|
client.SetSMTPAuth(tt.auth)
|
|
if client.smtpAuthType != tt.expected {
|
|
t.Errorf("failed to set expected SMTPAuthType, want: %s, got: %s", tt.expected,
|
|
client.smtpAuthType)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("SetSMTPAuth override custom auth", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost,
|
|
WithSMTPAuthCustom(smtp.LoginAuth("", "", "", false)))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if client.smtpAuthType != SMTPAuthCustom {
|
|
t.Fatalf("failed to create client with Custom auth, got: %s", client.smtpAuthType)
|
|
}
|
|
client.SetSMTPAuth(SMTPAuthSCRAMSHA256)
|
|
if client.smtpAuthType != SMTPAuthSCRAMSHA256 {
|
|
t.Errorf("failed to set expected SMTPAuthType, want: %s, got: %s", SMTPAuthSCRAMSHA256,
|
|
client.smtpAuthType)
|
|
}
|
|
if client.smtpAuth != nil {
|
|
t.Errorf("failed to set expected SMTPAuth, want: nil, got: %s", client.smtpAuth)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetSMTPAuthCustom(t *testing.T) {
|
|
t.Run("SetSMTPAuthCustom", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
authFunc smtp.Auth
|
|
want string
|
|
}{
|
|
{"CRAM-MD5", smtp.CRAMMD5Auth("", ""), "*smtp.cramMD5Auth"},
|
|
{"LOGIN", smtp.LoginAuth("", "", "", false),
|
|
"*smtp.loginAuth"},
|
|
{"LOGIN-NOENC", smtp.LoginAuth("", "", "", true),
|
|
"*smtp.loginAuth"},
|
|
{"PLAIN", smtp.PlainAuth("", "", "", "", false),
|
|
"*smtp.plainAuth"},
|
|
{"PLAIN-NOENC", smtp.PlainAuth("", "", "", "", true),
|
|
"*smtp.plainAuth"},
|
|
{"SCRAM-SHA-1", smtp.ScramSHA1Auth("", ""), "*smtp.scramAuth"},
|
|
{"SCRAM-SHA-1-PLUS", smtp.ScramSHA1PlusAuth("", "", nil),
|
|
"*smtp.scramAuth"},
|
|
{"SCRAM-SHA-256", smtp.ScramSHA256Auth("", ""), "*smtp.scramAuth"},
|
|
{"SCRAM-SHA-256-PLUS", smtp.ScramSHA256PlusAuth("", "", nil),
|
|
"*smtp.scramAuth"},
|
|
{"XOAUTH2", smtp.XOAuth2Auth("", ""), "*smtp.xoauth2Auth"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetSMTPAuthCustom(tt.authFunc)
|
|
if client.smtpAuth == nil {
|
|
t.Errorf("failed to set custom SMTP auth, expected auth method but got nil")
|
|
}
|
|
if client.smtpAuthType != SMTPAuthCustom {
|
|
t.Errorf("failed to set custom SMTP auth, want auth type: %s, got: %s", SMTPAuthCustom,
|
|
client.smtpAuthType)
|
|
}
|
|
authType := reflect.TypeOf(client.smtpAuth).String()
|
|
if authType != tt.want {
|
|
t.Errorf("failed to set custom SMTP auth, expected auth method type: %s, got: %s",
|
|
tt.want, authType)
|
|
}
|
|
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_SetLogAuthData(t *testing.T) {
|
|
t.Run("SetLogAuthData true", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetLogAuthData(true)
|
|
if !client.logAuthData {
|
|
t.Errorf("failed to set logAuthData, want: true, got: %t", client.logAuthData)
|
|
}
|
|
})
|
|
t.Run("SetLogAuthData false", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost)
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetLogAuthData(false)
|
|
if client.logAuthData {
|
|
t.Errorf("failed to set logAuthData, want: false, got: %t", client.logAuthData)
|
|
}
|
|
})
|
|
t.Run("SetLogAuthData override WithLogAuthData", func(t *testing.T) {
|
|
client, err := NewClient(DefaultHost, WithLogAuthData())
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.SetLogAuthData(false)
|
|
if client.logAuthData {
|
|
t.Errorf("failed to set logAuthData, want: false, got: %t", client.logAuthData)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_Close(t *testing.T) {
|
|
t.Run("connect and close the Client", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctx, &serverProps{
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Fatalf("client has no connection")
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
})
|
|
t.Run("connect and double close the Client", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctx, &serverProps{
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Fatalf("client has no connection")
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
})
|
|
t.Run("test server will let close fail", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctx, &serverProps{
|
|
FailOnQuit: true,
|
|
FeatureSet: featureSet,
|
|
ListenPort: serverPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Fatalf("client has no connection")
|
|
}
|
|
if err = client.Close(); err == nil {
|
|
t.Errorf("close was supposed to fail, but didn't")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClient_DialWithContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
PortAdder.Add(1)
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctx, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
t.Run("connect and check connection", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
})
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Fatalf("client has no connection")
|
|
}
|
|
})
|
|
t.Run("fail on base port use fallback", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.port = 12345
|
|
client.fallbackPort = serverPort
|
|
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
})
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Fatalf("client has no connection")
|
|
}
|
|
})
|
|
t.Run("fail on invalid host", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.host = "invalid.addr"
|
|
|
|
if err = client.DialWithContext(ctxDial); err == nil {
|
|
t.Errorf("client with invalid host should fail")
|
|
}
|
|
if client.smtpClient != nil {
|
|
t.Errorf("client with invalid host should not have a smtp client")
|
|
}
|
|
})
|
|
t.Run("fail on invalid HELO", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.helo = ""
|
|
|
|
if err = client.DialWithContext(ctxDial); err == nil {
|
|
t.Errorf("client with invalid HELO should fail")
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
})
|
|
if client.smtpClient == nil {
|
|
t.Errorf("client with invalid HELO should still have a smtp client, got nil")
|
|
}
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Errorf("client with invalid HELO should still have a smtp client connection, got nil")
|
|
}
|
|
})
|
|
t.Run("fail on base port and fallback", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
client.port = 12345
|
|
client.fallbackPort = 12346
|
|
|
|
if err = client.DialWithContext(ctxDial); err == nil {
|
|
t.Fatalf("connection was supposed to fail, but didn't")
|
|
}
|
|
if client.smtpClient != nil {
|
|
t.Fatalf("client has connection")
|
|
}
|
|
})
|
|
t.Run("connect with full debug logging and auth logging", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
logBuffer := bytes.NewBuffer(nil)
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
|
|
WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)),
|
|
WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), WithPassword(TestPasswordValid))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close the client: %s", err)
|
|
}
|
|
})
|
|
|
|
logs := parseJSONLog(t, logBuffer)
|
|
if len(logs.Lines) == 0 {
|
|
t.Errorf("failed to enable debug logging, but no logs were found")
|
|
}
|
|
authFound := false
|
|
for _, logline := range logs.Lines {
|
|
if strings.EqualFold(logline.Message, "AUTH PLAIN AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") &&
|
|
logline.Direction.From == "client" && logline.Direction.To == "server" {
|
|
authFound = true
|
|
}
|
|
}
|
|
if !authFound {
|
|
t.Errorf("logAuthData not working, no authentication info found in logs")
|
|
}
|
|
})
|
|
t.Run("connect should fail on HELO", func(t *testing.T) {
|
|
ctxFail, cancelFail := context.WithCancel(context.Background())
|
|
defer cancelFail()
|
|
PortAdder.Add(1)
|
|
failServerPort := int(TestServerPortBase + PortAdder.Load())
|
|
failFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxFail, &serverProps{
|
|
FailOnHelo: true,
|
|
FeatureSet: failFeatureSet,
|
|
ListenPort: failServerPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(failServerPort), WithTLSPolicy(NoTLS))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err == nil {
|
|
t.Fatalf("connection was supposed to fail, but didn't")
|
|
}
|
|
if client.smtpClient == nil {
|
|
t.Fatalf("client has no smtp client")
|
|
}
|
|
if !client.smtpClient.HasConnection() {
|
|
t.Errorf("client has no connection")
|
|
}
|
|
})
|
|
t.Run("connect with failing auth", func(t *testing.T) {
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
|
|
WithSMTPAuth(SMTPAuthPlain), WithUsername("invalid"), WithPassword("invalid"))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err == nil {
|
|
t.Fatalf("connection was supposed to fail, but didn't")
|
|
}
|
|
})
|
|
t.Run("connect with STARTTLS", func(t *testing.T) {
|
|
ctxTLS, cancelTLS := context.WithCancel(context.Background())
|
|
defer cancelTLS()
|
|
PortAdder.Add(1)
|
|
tlsServerPort := int(TestServerPortBase + PortAdder.Load())
|
|
tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxTLS, &serverProps{
|
|
FeatureSet: tlsFeatureSet,
|
|
ListenPort: tlsServerPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
|
client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory),
|
|
WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid),
|
|
WithPassword(TestPasswordValid))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
})
|
|
t.Run("want STARTTLS, but server does not support it", func(t *testing.T) {
|
|
ctxTLS, cancelTLS := context.WithCancel(context.Background())
|
|
defer cancelTLS()
|
|
PortAdder.Add(1)
|
|
tlsServerPort := int(TestServerPortBase + PortAdder.Load())
|
|
tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxTLS, &serverProps{
|
|
FeatureSet: tlsFeatureSet,
|
|
ListenPort: tlsServerPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
|
client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory),
|
|
WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid),
|
|
WithPassword(TestPasswordValid))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err == nil {
|
|
t.Fatalf("connection was supposed to fail, but didn't")
|
|
}
|
|
})
|
|
t.Run("connect with SSL", func(t *testing.T) {
|
|
ctxSSL, cancelSSL := context.WithCancel(context.Background())
|
|
defer cancelSSL()
|
|
PortAdder.Add(1)
|
|
sslServerPort := int(TestServerPortBase + PortAdder.Load())
|
|
sslFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
go func() {
|
|
if err := simpleSMTPServer(ctxSSL, &serverProps{
|
|
SSLListener: true,
|
|
FeatureSet: sslFeatureSet,
|
|
ListenPort: sslServerPort,
|
|
}); err != nil {
|
|
t.Errorf("failed to start test server: %s", err)
|
|
return
|
|
}
|
|
}()
|
|
time.Sleep(time.Millisecond * 300)
|
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
|
t.Cleanup(cancelDial)
|
|
|
|
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
|
client, err := NewClient(DefaultHost, WithPort(sslServerPort), WithSSL(),
|
|
WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid),
|
|
WithPassword(TestPasswordValid))
|
|
if err != nil {
|
|
t.Fatalf("failed to create new client: %s", err)
|
|
}
|
|
if err = client.DialWithContext(ctxDial); err != nil {
|
|
t.Fatalf("failed to connect to the test server: %s", err)
|
|
}
|
|
if err := client.Close(); err != nil {
|
|
t.Fatalf("failed to close client: %s", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
// 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", false))
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestClient_DialWithContext_switchAuth(t *testing.T) {
|
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
|
}
|
|
|
|
// We start with no auth explicitly set
|
|
client, err := NewClient(
|
|
os.Getenv("TEST_HOST"),
|
|
WithTLSPortPolicy(TLSMandatory),
|
|
)
|
|
defer func() {
|
|
_ = client.Close()
|
|
}()
|
|
if err != nil {
|
|
t.Errorf("failed to create client: %s", err)
|
|
return
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to sending server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close client connection: %s", err)
|
|
}
|
|
|
|
// We switch to LOGIN auth, which the server supports
|
|
client.SetSMTPAuth(SMTPAuthLogin)
|
|
client.SetUsername(os.Getenv("TEST_SMTPAUTH_USER"))
|
|
client.SetPassword(os.Getenv("TEST_SMTPAUTH_PASS"))
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to sending server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close client connection: %s", err)
|
|
}
|
|
|
|
// We switch to CRAM-MD5, which the server does not support - error expected
|
|
client.SetSMTPAuth(SMTPAuthCramMD5)
|
|
if err = client.DialWithContext(context.Background()); err == nil {
|
|
t.Errorf("expected error when dialing with unsupported auth mechanism, got nil")
|
|
return
|
|
}
|
|
if !errors.Is(err, ErrCramMD5AuthNotSupported) {
|
|
t.Errorf("expected dial error: %s, but got: %s", ErrCramMD5AuthNotSupported, err)
|
|
}
|
|
|
|
// We switch to CUSTOM by providing PLAIN auth as function - the server supports this
|
|
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"),
|
|
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"), false))
|
|
if client.smtpAuthType != SMTPAuthCustom {
|
|
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
|
|
}
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to sending server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close client connection: %s", err)
|
|
}
|
|
|
|
// We switch back to explicit no authenticaiton
|
|
client.SetSMTPAuth(SMTPAuthNoAuth)
|
|
if err = client.DialWithContext(context.Background()); err != nil {
|
|
t.Errorf("failed to dial to sending server: %s", err)
|
|
}
|
|
if err = client.Close(); err != nil {
|
|
t.Errorf("failed to close client connection: %s", err)
|
|
}
|
|
|
|
// Finally we set an empty string as SMTPAuthType and expect and error. This way we can
|
|
// verify that we do not accidentaly skip authentication with an empty string SMTPAuthType
|
|
client.SetSMTPAuth("")
|
|
if err = client.DialWithContext(context.Background()); err == nil {
|
|
t.Errorf("expected error when dialing with empty auth mechanism, got nil")
|
|
}
|
|
}
|
|
|
|
// 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.Logf("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 }
|
|
*/
|
|
|
|
// parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs.
|
|
// In case of a decode error, it reports the error to the testing framework.
|
|
func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData {
|
|
t.Helper()
|
|
|
|
builder := strings.Builder{}
|
|
builder.WriteString(`{"lines":[`)
|
|
lines := strings.Split(buf.String(), "\n")
|
|
for i, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
builder.WriteString(line)
|
|
if i < len(lines)-2 {
|
|
builder.WriteString(`,`)
|
|
}
|
|
}
|
|
builder.WriteString("]}")
|
|
|
|
var logdata logData
|
|
readBuffer := bytes.NewBuffer(nil)
|
|
readBuffer.WriteString(builder.String())
|
|
if err := json.NewDecoder(readBuffer).Decode(&logdata); err != nil {
|
|
t.Errorf("failed to decode json log: %s", err)
|
|
}
|
|
return logdata
|
|
}
|
|
|
|
// testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s.
|
|
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
|
|
|
// serverProps represents the configuration properties for the SMTP server.
|
|
type serverProps struct {
|
|
FailOnHelo bool
|
|
FailOnQuit bool
|
|
FailOnReset bool
|
|
FailOnSTARTTLS bool
|
|
FeatureSet string
|
|
ListenPort int
|
|
SSLListener bool
|
|
IsTLS bool
|
|
}
|
|
|
|
// 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, props *serverProps) error {
|
|
if props == nil {
|
|
return fmt.Errorf("no server properties provided")
|
|
}
|
|
|
|
var listener net.Listener
|
|
var err error
|
|
if props.SSLListener {
|
|
keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read TLS keypair: %s", err)
|
|
}
|
|
tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}}
|
|
listener, err = tls.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort),
|
|
tlsConfig)
|
|
} else {
|
|
listener, err = net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort))
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("unable to listen on %s://%s: %w (SSL: %t)", TestServerProto, TestServerAddr, err,
|
|
props.SSLListener)
|
|
}
|
|
|
|
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, props)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleTestServerConnection(connection net.Conn, props *serverProps) {
|
|
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 !props.IsTLS {
|
|
if err := writeLine("220 go-mail test server ready ESMTP"); err != nil {
|
|
fmt.Printf("unable to write to client: %s\n", err)
|
|
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, "EHLO"), strings.HasPrefix(data, "HELO"):
|
|
if len(strings.Split(data, " ")) != 2 {
|
|
_ = writeLine("501 Syntax: EHLO hostname")
|
|
break
|
|
}
|
|
if props.FailOnHelo {
|
|
_ = writeLine("500 5.5.2 Error: fail on HELO")
|
|
break
|
|
}
|
|
if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil {
|
|
break
|
|
}
|
|
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(props.FeatureSet, "250-X-MOX-LOGIN") {
|
|
userResp = ""
|
|
passResp = "UGFzc3dvcmQ="
|
|
}
|
|
if strings.Contains(props.FeatureSet, "250-X-NULLBYTE-LOGIN") {
|
|
userResp = "VXNlciBuYW1lAA=="
|
|
passResp = "UGFzc3dvcmQA"
|
|
}
|
|
if strings.Contains(props.FeatureSet, "250-X-BOGUS-LOGIN") {
|
|
userResp = "Qm9ndXM="
|
|
passResp = "Qm9ndXM="
|
|
}
|
|
if strings.Contains(props.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 props.FailOnReset {
|
|
_ = writeLine("500 5.1.2 Error: reset failed")
|
|
break
|
|
}
|
|
writeOK()
|
|
case strings.EqualFold(data, "quit"):
|
|
if props.FailOnQuit {
|
|
_ = writeLine("500 5.1.2 Error: quit failed")
|
|
break
|
|
}
|
|
_ = writeLine("221 2.0.0 Bye")
|
|
return
|
|
case strings.EqualFold(data, "starttls"):
|
|
if props.FailOnSTARTTLS {
|
|
_ = writeLine("500 5.1.2 Error: starttls failed")
|
|
break
|
|
}
|
|
keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
|
|
if err != nil {
|
|
_ = writeLine("500 5.1.2 Error: starttls failed - " + err.Error())
|
|
break
|
|
}
|
|
_ = writeLine("220 Ready to start TLS")
|
|
tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}}
|
|
connection = tls.Server(connection, tlsConfig)
|
|
props.IsTLS = true
|
|
handleTestServerConnection(connection, props)
|
|
default:
|
|
_ = writeLine("500 5.5.2 Error: bad syntax")
|
|
}
|
|
}
|
|
}
|