Compare commits

..

No commits in common. "d4c6cb506c8ef8fc3828b16e107e5535a3fe860b" and "b03fbb4ae8675dcf7ffd12730918efd079189674" have entirely different histories.

3 changed files with 106 additions and 564 deletions

4
go.mod
View file

@ -7,6 +7,6 @@ module github.com/wneessen/go-mail
go 1.16 go 1.16
require ( require (
golang.org/x/crypto v0.29.0 golang.org/x/crypto v0.28.0
golang.org/x/text v0.20.0 golang.org/x/text v0.19.0
) )

14
go.sum
View file

@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -26,7 +26,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -37,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -46,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -55,8 +55,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View file

@ -17,22 +17,14 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"hash"
"io"
"net" "net"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"golang.org/x/crypto/pbkdf2"
) )
const ( const (
@ -390,11 +382,6 @@ func TestPlainAuth(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to connect to test server: %s", err) 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 connection: %s", err)
}
})
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
t.Errorf("failed to authenticate to test server: %s", err) t.Errorf("failed to authenticate to test server: %s", err)
} }
@ -543,11 +530,6 @@ func TestPlainAuth_noEnc(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to connect to test server: %s", err) 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 connection: %s", err)
}
})
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
t.Errorf("failed to authenticate to test server: %s", err) t.Errorf("failed to authenticate to test server: %s", err)
} }
@ -692,11 +674,6 @@ func TestLoginAuth(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to connect to test server: %s", err) 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 connection: %s", err)
}
})
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
t.Errorf("failed to authenticate to test server: %s", err) t.Errorf("failed to authenticate to test server: %s", err)
} }
@ -838,11 +815,6 @@ func TestLoginAuth_noEnc(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to connect to test server: %s", err) 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 connection: %s", err)
}
})
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
t.Errorf("failed to authenticate to test server: %s", err) t.Errorf("failed to authenticate to test server: %s", err)
} }
@ -876,33 +848,12 @@ func TestLoginAuth_noEnc(t *testing.T) {
}) })
} }
func TestXOAuth2Auth(t *testing.T) { /*
t.Run("XOAuth2 authentication all steps", func(t *testing.T) {
auth := XOAuth2Auth("user", "token")
proto, toserver, err := auth.Start(&ServerInfo{Name: "servername", TLS: true})
if err != nil {
t.Fatalf("failed to start XOAuth2 authentication: %s", err) func TestXOAuth2OK(t *testing.T) {
}
if proto != "XOAUTH2" {
t.Errorf("expected protocol to be XOAUTH2, got: %q", proto)
}
expected := []byte("user=user\x01auth=Bearer token\x01\x01")
if !bytes.Equal(expected, toserver) {
t.Errorf("expected server response to be: %q, got: %q", expected, toserver)
}
resp, err := auth.Next([]byte("nonsense"), true)
if err != nil {
t.Errorf("failed on first server challange: %s", err)
}
if !bytes.Equal([]byte(""), resp) {
t.Errorf("expected server response to be empty, got: %q", resp)
}
resp, err = auth.Next([]byte("nonsense"), false)
if err != nil {
t.Errorf("failed on first server challange: %s", err)
}
})
t.Run("XOAuth2 succeeds with faker", func(t *testing.T) {
server := []string{ server := []string{
"220 Fake server ready ESMTP", "220 Fake server ready ESMTP",
"250-fake.server", "250-fake.server",
@ -919,28 +870,30 @@ func TestXOAuth2Auth(t *testing.T) {
strings.NewReader(strings.Join(server, "\r\n")), strings.NewReader(strings.Join(server, "\r\n")),
&wrote, &wrote,
} }
client, err := NewClient(fake, "fake.host")
c, err := NewClient(fake, "fake.host")
if err != nil { if err != nil {
t.Fatalf("failed to create client on faker server: %s", err) t.Fatalf("NewClient: %v", err)
} }
t.Cleanup(func() { defer func() {
if err = client.Close(); err != nil { if err := c.Close(); err != nil {
t.Errorf("failed to close client connection: %s", err) t.Errorf("failed to close client: %s", err)
} }
}) }()
auth := XOAuth2Auth("user", "token") auth := XOAuth2Auth("user", "token")
if err = client.Auth(auth); err != nil { err = c.Auth(auth)
t.Errorf("failed to authenticate to faker server: %s", err) if err != nil {
t.Fatalf("XOAuth2 error: %v", err)
} }
// the Next method returns a nil response. It must not be sent. // the Next method returns a nil response. It must not be sent.
// The client request must end with the authentication. // The client request must end with the authentication.
if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") { if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String()) t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String())
} }
}) }
t.Run("XOAuth2 fails with faker", func(t *testing.T) {
func TestXOAuth2Error(t *testing.T) {
serverResp := []string{ serverResp := []string{
"220 Fake server ready ESMTP", "220 Fake server ready ESMTP",
"250-fake.server", "250-fake.server",
@ -959,270 +912,34 @@ func TestXOAuth2Auth(t *testing.T) {
strings.NewReader(strings.Join(serverResp, "\r\n")), strings.NewReader(strings.Join(serverResp, "\r\n")),
&wrote, &wrote,
} }
client, err := NewClient(fake, "fake.host")
c, err := NewClient(fake, "fake.host")
if err != nil { if err != nil {
t.Fatalf("failed to create client on faker server: %s", err) t.Fatalf("NewClient: %v", err)
} }
t.Cleanup(func() { defer func() {
if err = client.Close(); err != nil { if err := c.Close(); err != nil {
t.Errorf("failed to close client connection: %s", err) t.Errorf("failed to close client: %s", err)
} }
}) }()
auth := XOAuth2Auth("user", "token") auth := XOAuth2Auth("user", "token")
if err = client.Auth(auth); err == nil { err = c.Auth(auth)
t.Errorf("expected authentication to fail") if err == nil {
t.Fatal("expected auth error, got nil")
} }
resp := strings.Split(wrote.String(), "\r\n") client := strings.Split(wrote.String(), "\r\n")
if len(resp) != 5 { if len(client) != 5 {
t.Fatalf("unexpected number of client requests got %d; want 5", len(resp)) t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
} }
if resp[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" { if client[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" {
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", resp[1]) t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", client[1])
} }
// the Next method returns an empty response. It must be sent // the Next method returns an empty response. It must be sent
if resp[2] != "" { if client[2] != "" {
t.Fatalf("got %q; want empty response", resp[2]) t.Fatalf("got %q; want empty response", client[2])
}
})
t.Run("XOAuth2 authentication on test server succeeds", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-AUTH XOAUTH2\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FeatureSet: featureSet,
ListenPort: serverPort},
); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
auth := XOAuth2Auth("user", "token")
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
if 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 connection: %s", err)
}
})
if err = client.Auth(auth); err != nil {
t.Errorf("failed to authenticate to test server: %s", err)
}
})
t.Run("XOAuth2 authentication on test server fails", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-AUTH XOAUTH2\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
FailOnAuth: true,
FeatureSet: featureSet,
ListenPort: serverPort},
); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
auth := XOAuth2Auth("user", "token")
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
if err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
if err = client.Auth(auth); err == nil {
t.Errorf("expected authentication to fail")
}
})
}
func TestScramAuth(t *testing.T) {
tests := []struct {
name string
tls bool
authString string
hash func() hash.Hash
isPlus bool
}{
{"SCRAM-SHA-1 (no TLS)", false, "SCRAM-SHA-1", sha1.New, false},
{"SCRAM-SHA-256 (no TLS)", false, "SCRAM-SHA-256", sha256.New, false},
{"SCRAM-SHA-1 (with TLS)", true, "SCRAM-SHA-1", sha1.New, false},
{"SCRAM-SHA-256 (with TLS)", true, "SCRAM-SHA-256", sha256.New, false},
{"SCRAM-SHA-1-PLUS", true, "SCRAM-SHA-1-PLUS", sha1.New, true},
{"SCRAM-SHA-256-PLUS", true, "SCRAM-SHA-256-PLUS", sha256.New, true},
}
for _, tt := range tests {
t.Run(tt.name+" succeeds on test server", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := fmt.Sprintf("250-AUTH %s\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8", tt.authString)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
TestSCRAM: true,
HashFunc: tt.hash,
FeatureSet: featureSet,
ListenPort: serverPort,
SSLListener: tt.tls,
IsSCRAMPlus: tt.isPlus,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
var client *Client
switch tt.tls {
case true:
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
if err != nil {
fmt.Printf("error creating TLS cert: %s", err)
return
}
tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), &tlsConfig)
if err != nil {
t.Fatalf("failed to dial TLS server: %v", err)
}
client, err = NewClient(conn, TestServerAddr)
case false:
var err error
client, err = Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
if 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 connection: %s", err)
}
})
var auth Auth
switch tt.authString {
case "SCRAM-SHA-1":
auth = ScramSHA1Auth("username", "password")
case "SCRAM-SHA-256":
auth = ScramSHA256Auth("username", "password")
case "SCRAM-SHA-1-PLUS":
tlsConnState, err := client.GetTLSConnectionState()
if err != nil {
t.Fatalf("failed to get TLS connection state: %s", err)
}
auth = ScramSHA1PlusAuth("username", "password", tlsConnState)
case "SCRAM-SHA-256-PLUS":
tlsConnState, err := client.GetTLSConnectionState()
if err != nil {
t.Fatalf("failed to get TLS connection state: %s", err)
}
auth = ScramSHA256PlusAuth("username", "password", tlsConnState)
default:
t.Fatalf("unexpected auth string: %s", tt.authString)
}
if err := client.Auth(auth); err != nil {
t.Errorf("failed to authenticate to test server: %s", err)
}
})
t.Run(tt.name+" fails on test server", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := fmt.Sprintf("250-AUTH %s\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8", tt.authString)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
TestSCRAM: true,
HashFunc: tt.hash,
FeatureSet: featureSet,
ListenPort: serverPort,
SSLListener: tt.tls,
IsSCRAMPlus: tt.isPlus,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
var client *Client
switch tt.tls {
case true:
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
if err != nil {
fmt.Printf("error creating TLS cert: %s", err)
return
}
tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), &tlsConfig)
if err != nil {
t.Fatalf("failed to dial TLS server: %v", err)
}
client, err = NewClient(conn, TestServerAddr)
case false:
var err error
client, err = Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
if err != nil {
t.Fatalf("failed to connect to test server: %s", err)
}
}
var auth Auth
switch tt.authString {
case "SCRAM-SHA-1":
auth = ScramSHA1Auth("invalid", "password")
case "SCRAM-SHA-256":
auth = ScramSHA256Auth("invalid", "password")
case "SCRAM-SHA-1-PLUS":
tlsConnState, err := client.GetTLSConnectionState()
if err != nil {
t.Fatalf("failed to get TLS connection state: %s", err)
}
auth = ScramSHA1PlusAuth("invalid", "password", tlsConnState)
case "SCRAM-SHA-256-PLUS":
tlsConnState, err := client.GetTLSConnectionState()
if err != nil {
t.Fatalf("failed to get TLS connection state: %s", err)
}
auth = ScramSHA256PlusAuth("invalid", "password", tlsConnState)
default:
t.Fatalf("unexpected auth string: %s", tt.authString)
}
if err := client.Auth(auth); err == nil {
t.Error("expected authentication to fail")
}
})
}
t.Run("ScramAuth_Next with nonsense parameter", func(t *testing.T) {
auth := ScramSHA1Auth("username", "password")
_, err := auth.Next([]byte("x=nonsense"), true)
if err == nil {
t.Fatal("expected authentication to fail")
}
if !errors.Is(err, ErrUnexpectedServerResponse) {
t.Errorf("expected ErrUnexpectedServerResponse, got %s", err)
}
})
}
/*
func TestAuthSCRAMSHA1_OK(t *testing.T) { func TestAuthSCRAMSHA1_OK(t *testing.T) {
hostname := "127.0.0.1" hostname := "127.0.0.1"
@ -1500,6 +1217,17 @@ func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) {
panic("unexpected call") panic("unexpected call")
} }
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 }
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") server := strings.Join(strings.Split(basicServer, "\n"), "\r\n")
client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
@ -3288,19 +3016,6 @@ func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash)
*/ */
// faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes.
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 }
// testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s. // 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") } func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
@ -3319,11 +3034,8 @@ type serverProps struct {
FeatureSet string FeatureSet string
ListenPort int ListenPort int
SSLListener bool SSLListener bool
IsSCRAMPlus bool
IsTLS bool IsTLS bool
SupportDSN bool SupportDSN bool
TestSCRAM bool
HashFunc func() hash.Hash
} }
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
@ -3461,22 +3173,6 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
writeLine("535 5.7.8 Error: authentication failed") writeLine("535 5.7.8 Error: authentication failed")
break break
} }
if props.TestSCRAM {
parts := strings.Split(data, " ")
authMechanism := parts[1]
if authMechanism != "SCRAM-SHA-1" && authMechanism != "SCRAM-SHA-256" &&
authMechanism != "SCRAM-SHA-1-PLUS" && authMechanism != "SCRAM-SHA-256-PLUS" {
writeLine("504 Unrecognized authentication mechanism")
break
}
scram := &testSCRAMSMTP{
tlsServer: props.IsSCRAMPlus,
h: props.HashFunc,
}
writeLine("334 ")
scram.handleSCRAMAuth(connection)
break
}
writeLine("235 2.7.0 Authentication successful") writeLine("235 2.7.0 Authentication successful")
case strings.EqualFold(data, "DATA"): case strings.EqualFold(data, "DATA"):
if props.FailOnDataInit { if props.FailOnDataInit {
@ -3546,157 +3242,3 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
} }
} }
} }
// testSCRAMSMTP represents a part of the test server for SCRAM-based SMTP authentication.
// It does not do any acutal computation of the challenges but verifies that the expected
// fields are present. We have actual real authentication tests for all SCRAM modes in the
// go-mail client_test.go
type testSCRAMSMTP struct {
authMechanism string
nonce string
h func() hash.Hash
tlsServer bool
}
func (s *testSCRAMSMTP) handleSCRAMAuth(conn net.Conn) {
reader := bufio.NewReader(conn)
writer := bufio.NewWriter(conn)
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()
}
var authMsg string
data, err := reader.ReadString('\n')
if err != nil {
_ = writeLine("535 Authentication failed")
return
}
data = strings.TrimSpace(data)
decodedMessage, err := base64.StdEncoding.DecodeString(data)
if err != nil {
_ = writeLine("535 Authentication failed")
return
}
splits := strings.Split(string(decodedMessage), ",")
if len(splits) != 4 {
_ = writeLine("535 Authentication failed - expected 4 parts")
return
}
if !s.tlsServer && splits[0] != "n" {
_ = writeLine("535 Authentication failed - expected n to be in the first part")
return
}
if s.tlsServer && !strings.HasPrefix(splits[0], "p=") {
_ = writeLine("535 Authentication failed - expected p= to be in the first part")
return
}
if splits[2] != "n=username" {
_ = writeLine("535 Authentication failed - expected n=username to be in the third part")
return
}
if !strings.HasPrefix(splits[3], "r=") {
_ = writeLine("535 Authentication failed - expected r= to be in the fourth part")
return
}
authMsg = splits[2] + "," + splits[3]
clientNonce := s.extractNonce(string(decodedMessage))
if clientNonce == "" {
_ = writeLine("535 Authentication failed")
return
}
s.nonce = clientNonce + "server_nonce"
serverFirstMessage := fmt.Sprintf("r=%s,s=%s,i=4096", s.nonce,
base64.StdEncoding.EncodeToString([]byte("salt")))
_ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFirstMessage))))
authMsg = authMsg + "," + serverFirstMessage
data, err = reader.ReadString('\n')
if err != nil {
_ = writeLine("535 Authentication failed")
return
}
data = strings.TrimSpace(data)
decodedFinalMessage, err := base64.StdEncoding.DecodeString(data)
if err != nil {
_ = writeLine("535 Authentication failed")
return
}
splits = strings.Split(string(decodedFinalMessage), ",")
if !s.tlsServer && splits[0] != "c=biws" {
_ = writeLine("535 Authentication failed - expected c=biws to be in the first part")
return
}
if s.tlsServer {
if !strings.HasPrefix(splits[0], "c=") {
_ = writeLine("535 Authentication failed - expected c= to be in the first part")
return
}
channelBind, err := base64.StdEncoding.DecodeString(splits[0][2:])
if err != nil {
_ = writeLine("535 Authentication failed - base64 channel bind is not valid - " + err.Error())
return
}
if !strings.HasPrefix(string(channelBind), "p=") {
_ = writeLine("535 Authentication failed - expected channel binding to start with p=-")
return
}
cbType := string(channelBind[2:])
if !strings.HasPrefix(cbType, "tls-unique") && !strings.HasPrefix(cbType, "tls-exporter") {
_ = writeLine("535 Authentication failed - expected channel binding type tls-unique or tls-exporter")
return
}
}
if !strings.HasPrefix(splits[1], "r=") {
_ = writeLine("535 Authentication failed - expected r to be in the second part")
return
}
if !strings.Contains(splits[1], "server_nonce") {
_ = writeLine("535 Authentication failed - expected server_nonce to be in the second part")
return
}
if !strings.HasPrefix(splits[2], "p=") {
_ = writeLine("535 Authentication failed - expected p to be in the third part")
return
}
authMsg = authMsg + "," + splits[0] + "," + splits[1]
saltedPwd := pbkdf2.Key([]byte("password"), []byte("salt"), 4096, s.h().Size(), s.h)
mac := hmac.New(s.h, saltedPwd)
mac.Write([]byte("Server Key"))
skey := mac.Sum(nil)
mac.Reset()
mac = hmac.New(s.h, skey)
mac.Write([]byte(authMsg))
ssig := mac.Sum(nil)
mac.Reset()
serverFinalMessage := fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(ssig))
_ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFinalMessage))))
_, err = reader.ReadString('\n')
if err != nil {
_ = writeLine("535 Authentication failed")
return
}
_ = writeLine("235 Authentication successful")
}
func (s *testSCRAMSMTP) extractNonce(message string) string {
parts := strings.Split(message, ",")
for _, part := range parts {
if strings.HasPrefix(part, "r=") {
return part[2:]
}
}
return ""
}