mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-15 02:12:55 +01:00
Add STARTTLS and SSL test cases for SMTP client
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.
This commit is contained in:
parent
563ccbab4a
commit
c63b8b124e
1 changed files with 198 additions and 16 deletions
198
client_test.go
198
client_test.go
|
@ -34,7 +34,7 @@ const (
|
||||||
// TestServerAddr is the address the simple SMTP test server listens on
|
// TestServerAddr is the address the simple SMTP test server listens on
|
||||||
TestServerAddr = "127.0.0.1"
|
TestServerAddr = "127.0.0.1"
|
||||||
// TestServerPortBase is the base port for the simple SMTP test server
|
// TestServerPortBase is the base port for the simple SMTP test server
|
||||||
TestServerPortBase = 2025
|
TestServerPortBase = 12025
|
||||||
// TestPasswordValid is the password that the test server accepts as valid for SMTP auth
|
// TestPasswordValid is the password that the test server accepts as valid for SMTP auth
|
||||||
TestPasswordValid = "V3ryS3cr3t+"
|
TestPasswordValid = "V3ryS3cr3t+"
|
||||||
// TestUserValid is the username that the test server accepts as valid for SMTP auth
|
// TestUserValid is the username that the test server accepts as valid for SMTP auth
|
||||||
|
@ -44,6 +44,44 @@ const (
|
||||||
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
|
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
|
||||||
var PortAdder atomic.Int32
|
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.
|
// logLine represents a log entry with time, level, message, and direction details.
|
||||||
type logLine struct {
|
type logLine struct {
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
|
@ -1668,7 +1706,7 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
|
||||||
t.Cleanup(cancelDial)
|
t.Cleanup(cancelDial)
|
||||||
|
|
||||||
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDebugLog())
|
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create new client: %s", err)
|
t.Fatalf("failed to create new client: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -1744,13 +1782,13 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("connect should fail on HELO", func(t *testing.T) {
|
t.Run("connect should fail on HELO", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctxFail, cancelFail := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancelFail()
|
||||||
PortAdder.Add(1)
|
PortAdder.Add(1)
|
||||||
failServerPort := int(TestServerPortBase + PortAdder.Load())
|
failServerPort := int(TestServerPortBase + PortAdder.Load())
|
||||||
failFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
failFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
||||||
go func() {
|
go func() {
|
||||||
if err := simpleSMTPServer(ctx, &serverProps{
|
if err := simpleSMTPServer(ctxFail, &serverProps{
|
||||||
FailOnHelo: true,
|
FailOnHelo: true,
|
||||||
FeatureSet: failFeatureSet,
|
FeatureSet: failFeatureSet,
|
||||||
ListenPort: failServerPort,
|
ListenPort: failServerPort,
|
||||||
|
@ -1778,7 +1816,113 @@ func TestClient_DialWithContext(t *testing.T) {
|
||||||
t.Errorf("client has no connection")
|
t.Errorf("client has no connection")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// TODO: Implement tests for TLS/SSL and custom DialCtxFunc
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -3641,12 +3785,19 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData {
|
||||||
return logdata
|
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 {
|
type serverProps struct {
|
||||||
FailOnHelo bool
|
FailOnHelo bool
|
||||||
FailOnQuit bool
|
FailOnQuit bool
|
||||||
FailOnReset bool
|
FailOnReset bool
|
||||||
|
FailOnSTARTTLS bool
|
||||||
FeatureSet string
|
FeatureSet string
|
||||||
ListenPort int
|
ListenPort int
|
||||||
|
SSLListener bool
|
||||||
|
IsTLS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
|
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
|
||||||
|
@ -3656,9 +3807,23 @@ func simpleSMTPServer(ctx context.Context, props *serverProps) error {
|
||||||
if props == nil {
|
if props == nil {
|
||||||
return fmt.Errorf("no server properties provided")
|
return fmt.Errorf("no server properties provided")
|
||||||
}
|
}
|
||||||
listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort))
|
|
||||||
|
var listener net.Listener
|
||||||
|
var err error
|
||||||
|
if props.SSLListener {
|
||||||
|
keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err)
|
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() {
|
defer func() {
|
||||||
|
@ -3707,10 +3872,12 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) {
|
||||||
_ = writeLine("250 2.0.0 OK")
|
_ = writeLine("250 2.0.0 OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !props.IsTLS {
|
||||||
if err := writeLine("220 go-mail test server ready ESMTP"); err != nil {
|
if err := writeLine("220 go-mail test server ready ESMTP"); err != nil {
|
||||||
fmt.Printf("unable to write to client: %s\n", err)
|
fmt.Printf("unable to write to client: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
data, err := reader.ReadString('\n')
|
data, err := reader.ReadString('\n')
|
||||||
|
@ -3850,6 +4017,21 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) {
|
||||||
}
|
}
|
||||||
_ = writeLine("221 2.0.0 Bye")
|
_ = writeLine("221 2.0.0 Bye")
|
||||||
return
|
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:
|
default:
|
||||||
_ = writeLine("500 5.5.2 Error: bad syntax")
|
_ = writeLine("500 5.5.2 Error: bad syntax")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue