mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-21 21:30:50 +01:00
Compare commits
6 commits
b03fbb4ae8
...
d4c6cb506c
Author | SHA1 | Date | |
---|---|---|---|
d4c6cb506c | |||
c656226fd3 | |||
1af17f14e1 | |||
7b8a24f34a | |||
edc3de5484 | |||
|
62ea3f56af |
3 changed files with 564 additions and 106 deletions
4
go.mod
4
go.mod
|
@ -7,6 +7,6 @@ module github.com/wneessen/go-mail
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/text v0.20.0
|
||||
)
|
||||
|
|
14
go.sum
14
go.sum
|
@ -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.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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
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.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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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.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.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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-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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
|
@ -17,14 +17,22 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -382,6 +390,11 @@ func TestPlainAuth(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
@ -530,6 +543,11 @@ func TestPlainAuth_noEnc(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
@ -674,6 +692,11 @@ func TestLoginAuth(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
@ -815,6 +838,11 @@ func TestLoginAuth_noEnc(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
@ -848,98 +876,353 @@ 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)
|
||||
}
|
||||
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{
|
||||
"220 Fake server ready ESMTP",
|
||||
"250-fake.server",
|
||||
"250-AUTH XOAUTH2",
|
||||
"250 8BITMIME",
|
||||
"235 2.7.0 Accepted",
|
||||
}
|
||||
var wrote strings.Builder
|
||||
var fake faker
|
||||
fake.ReadWriter = struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}{
|
||||
strings.NewReader(strings.Join(server, "\r\n")),
|
||||
&wrote,
|
||||
}
|
||||
client, err := NewClient(fake, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client on faker server: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close client connection: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
if err = client.Auth(auth); err != nil {
|
||||
t.Errorf("failed to authenticate to faker server: %s", err)
|
||||
}
|
||||
|
||||
// the Next method returns a nil response. It must not be sent.
|
||||
// The client request must end with the authentication.
|
||||
if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
|
||||
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String())
|
||||
}
|
||||
})
|
||||
t.Run("XOAuth2 fails with faker", func(t *testing.T) {
|
||||
serverResp := []string{
|
||||
"220 Fake server ready ESMTP",
|
||||
"250-fake.server",
|
||||
"250-AUTH XOAUTH2",
|
||||
"250 8BITMIME",
|
||||
"334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==",
|
||||
"535 5.7.8 Username and Password not accepted",
|
||||
"221 2.0.0 closing connection",
|
||||
}
|
||||
var wrote strings.Builder
|
||||
var fake faker
|
||||
fake.ReadWriter = struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}{
|
||||
strings.NewReader(strings.Join(serverResp, "\r\n")),
|
||||
&wrote,
|
||||
}
|
||||
client, err := NewClient(fake, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client on faker server: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close client connection: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
if err = client.Auth(auth); err == nil {
|
||||
t.Errorf("expected authentication to fail")
|
||||
}
|
||||
resp := strings.Split(wrote.String(), "\r\n")
|
||||
if len(resp) != 5 {
|
||||
t.Fatalf("unexpected number of client requests got %d; want 5", len(resp))
|
||||
}
|
||||
if resp[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" {
|
||||
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", resp[1])
|
||||
}
|
||||
// the Next method returns an empty response. It must be sent
|
||||
if resp[2] != "" {
|
||||
t.Fatalf("got %q; want empty response", resp[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 TestXOAuth2OK(t *testing.T) {
|
||||
server := []string{
|
||||
"220 Fake server ready ESMTP",
|
||||
"250-fake.server",
|
||||
"250-AUTH XOAUTH2",
|
||||
"250 8BITMIME",
|
||||
"235 2.7.0 Accepted",
|
||||
}
|
||||
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, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("failed to close client: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
err = c.Auth(auth)
|
||||
if err != nil {
|
||||
t.Fatalf("XOAuth2 error: %v", err)
|
||||
}
|
||||
// the Next method returns a nil response. It must not be sent.
|
||||
// The client request must end with the authentication.
|
||||
if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
|
||||
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestXOAuth2Error(t *testing.T) {
|
||||
serverResp := []string{
|
||||
"220 Fake server ready ESMTP",
|
||||
"250-fake.server",
|
||||
"250-AUTH XOAUTH2",
|
||||
"250 8BITMIME",
|
||||
"334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==",
|
||||
"535 5.7.8 Username and Password not accepted",
|
||||
"221 2.0.0 closing connection",
|
||||
}
|
||||
var wrote strings.Builder
|
||||
var fake faker
|
||||
fake.ReadWriter = struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}{
|
||||
strings.NewReader(strings.Join(serverResp, "\r\n")),
|
||||
&wrote,
|
||||
}
|
||||
|
||||
c, err := NewClient(fake, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("failed to close client: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
err = c.Auth(auth)
|
||||
if err == nil {
|
||||
t.Fatal("expected auth error, got nil")
|
||||
}
|
||||
client := strings.Split(wrote.String(), "\r\n")
|
||||
if len(client) != 5 {
|
||||
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
|
||||
}
|
||||
if client[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" {
|
||||
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", client[1])
|
||||
}
|
||||
// the Next method returns an empty response. It must be sent
|
||||
if client[2] != "" {
|
||||
t.Fatalf("got %q; want empty response", client[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthSCRAMSHA1_OK(t *testing.T) {
|
||||
hostname := "127.0.0.1"
|
||||
|
@ -1217,17 +1500,6 @@ func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) {
|
|||
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) {
|
||||
server := strings.Join(strings.Split(basicServer, "\n"), "\r\n")
|
||||
client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
|
||||
|
@ -3016,6 +3288,19 @@ 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.
|
||||
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
||||
|
||||
|
@ -3034,8 +3319,11 @@ type serverProps struct {
|
|||
FeatureSet string
|
||||
ListenPort int
|
||||
SSLListener bool
|
||||
IsSCRAMPlus bool
|
||||
IsTLS bool
|
||||
SupportDSN bool
|
||||
TestSCRAM bool
|
||||
HashFunc func() hash.Hash
|
||||
}
|
||||
|
||||
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
|
||||
|
@ -3173,6 +3461,22 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
|
|||
writeLine("535 5.7.8 Error: authentication failed")
|
||||
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")
|
||||
case strings.EqualFold(data, "DATA"):
|
||||
if props.FailOnDataInit {
|
||||
|
@ -3242,3 +3546,157 @@ 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 ""
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue