From 9069c9cdffd8f2e1a2444821c728b280269a71da Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:03:44 +0200 Subject: [PATCH 01/24] Add SCRAM-SHA support to SMTP authentication Introduced additional SMTP authentication mechanisms: SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS. Added corresponding error messages for unsupported authentication types. This enhances security options for SMTP connections. --- auth.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/auth.go b/auth.go index 4a59b14..d30a154 100644 --- a/auth.go +++ b/auth.go @@ -28,6 +28,11 @@ const ( // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. // https://developers.google.com/gmail/imap/xoauth2-protocol SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" + + SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" + SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" + SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" + SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" ) // SMTP Auth related static errors @@ -43,4 +48,16 @@ var ( // ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2") + + // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1") + + // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS") + + // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256") + + // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") ) From e8fc6cd78f80fdebb4439afc6f7a19946ab900fe Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:04:01 +0200 Subject: [PATCH 02/24] Add SCRAM-SHA support to SMTP authentication Introduced additional SMTP authentication mechanisms: SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS. Added corresponding error messages for unsupported authentication types. This enhances security options for SMTP connections. --- smtp/auth_scram.go | 280 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 smtp/auth_scram.go diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go new file mode 100644 index 0000000..2b7f814 --- /dev/null +++ b/smtp/auth_scram.go @@ -0,0 +1,280 @@ +// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package smtp + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "hash" + "io" + "strconv" + "strings" + + "golang.org/x/crypto/pbkdf2" + "golang.org/x/text/secure/precis" +) + +type scramAuth struct { + username, password, algorithm string + firstBareMsg, nonce, saltedPwd, authMessage []byte + iterations int + h func() hash.Hash + isPlus bool + tlsConnState *tls.ConnectionState + bindData []byte +} + +func ScramSHA256Auth(username, password string) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256", + h: sha256.New, + } +} + +func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256-PLUS", + h: sha256.New, + isPlus: true, + tlsConnState: tlsConnState, + } +} + +func ScramSHA1Auth(username, password string) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-1", + h: sha1.New, + } +} + +func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-1-PLUS", + h: sha1.New, + isPlus: true, + tlsConnState: tlsConnState, + } +} + +func (a *scramAuth) Start(_ *ServerInfo) (string, []byte, error) { + fmt.Printf("algo: %s\n", a.algorithm) + return a.algorithm, nil, nil +} + +func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + if len(fromServer) == 0 { + a.reset() + return a.initialClientMessage() + } + switch { + case bytes.HasPrefix(fromServer, []byte("r=")): + resp, err := a.handleServerFirstResponse(fromServer) + if err != nil { + a.reset() + return nil, err + } + return resp, nil + case bytes.HasPrefix(fromServer, []byte("v=")): + resp, err := a.handleServerValidationMessage(fromServer) + if err != nil { + a.reset() + return nil, err + } + return resp, nil + default: + a.reset() + return nil, errors.New("unexpected server response") + } + } + return nil, nil +} + +func (a *scramAuth) reset() { + a.nonce = nil + a.firstBareMsg = nil + a.saltedPwd = nil + a.authMessage = nil + a.iterations = 0 +} + +func (a *scramAuth) initialClientMessage() ([]byte, error) { + username, err := a.normalizeUsername() + if err != nil { + return nil, fmt.Errorf("username normalization failed: %w", err) + } + + nonceBuffer := make([]byte, 24) + if _, err := io.ReadFull(rand.Reader, nonceBuffer); err != nil { + return nil, fmt.Errorf("unable to generate client secret: %w", err) + } + a.nonce = make([]byte, base64.StdEncoding.EncodedLen(len(nonceBuffer))) + base64.StdEncoding.Encode(a.nonce, nonceBuffer) + + a.firstBareMsg = []byte("n=" + username + ",r=" + string(a.nonce)) + returnBytes := []byte("n,," + string(a.firstBareMsg)) + + if a.isPlus { + bindType := "tls-unique" + connState := a.tlsConnState + bindData := connState.TLSUnique + if connState.Version == tls.VersionTLS13 { + bindType = "tls-exporter" + bindData, err = connState.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32) + if err != nil { + return nil, fmt.Errorf("unable to export keying material: %w", err) + } + } + bindData = []byte("p=" + bindType + ",," + string(bindData)) + a.bindData = make([]byte, base64.StdEncoding.EncodedLen(len(bindData))) + base64.StdEncoding.Encode(a.bindData, bindData) + returnBytes = []byte("p=" + bindType + ",," + string(a.firstBareMsg)) + } + + return returnBytes, nil +} + +func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) { + parts := bytes.Split(fromServer, []byte(",")) + if len(parts) < 3 { + return nil, errors.New("not enough fields in the first server response") + } + if !bytes.HasPrefix(parts[0], []byte("r=")) { + return nil, errors.New("first part of the server response does not start with r=") + } + if !bytes.HasPrefix(parts[1], []byte("s=")) { + return nil, errors.New("second part of the server response does not start with s=") + } + if !bytes.HasPrefix(parts[2], []byte("i=")) { + return nil, errors.New("third part of the server response does not start with i=") + } + + combinedNonce := parts[0][2:] + if len(a.nonce) == 0 || !bytes.HasPrefix(combinedNonce, a.nonce) { + return nil, errors.New("server nonce does not start with our nonce") + } + a.nonce = combinedNonce + + encodedSalt := parts[1][2:] + salt := make([]byte, base64.StdEncoding.DecodedLen(len(encodedSalt))) + n, err := base64.StdEncoding.Decode(salt, encodedSalt) + if err != nil { + return nil, fmt.Errorf("invalid encoded salt: %w", err) + } + salt = salt[:n] + + iterations, err := strconv.Atoi(string(parts[2][2:])) + if err != nil { + return nil, fmt.Errorf("invalid iterations: %w", err) + } + a.iterations = iterations + + password, err := a.normalizeString(a.password) + if err != nil { + return nil, fmt.Errorf("unable to normalize password: %w", err) + } + + a.saltedPwd = pbkdf2.Key([]byte(password), salt, a.iterations, a.h().Size(), a.h) + + msgWithoutProof := []byte("c=biws,r=" + string(a.nonce)) + if a.isPlus { + msgWithoutProof = []byte("c=" + string(a.bindData) + ",r=" + string(a.nonce)) + } + a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof)) + + clientProof := a.computeClientProof() + + return []byte(string(msgWithoutProof) + ",p=" + string(clientProof)), nil +} + +func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, error) { + serverSignature := fromServer[2:] + computedServerSignature := a.computeServerSignature() + + if !hmac.Equal(serverSignature, computedServerSignature) { + return nil, errors.New("invalid server signature") + } + return []byte(""), nil +} + +func (a *scramAuth) computeHMAC(key, msg []byte) []byte { + mac := hmac.New(a.h, key) + mac.Write(msg) + return mac.Sum(nil) +} + +func (a *scramAuth) computeHash(key []byte) []byte { + hasher := a.h() + hasher.Write(key) + return hasher.Sum(nil) +} + +func (a *scramAuth) computeClientProof() []byte { + clientKey := a.computeHMAC(a.saltedPwd, []byte("Client Key")) + storedKey := a.computeHash(clientKey) + clientSignature := a.computeHMAC(storedKey[:], a.authMessage) + clientProof := make([]byte, len(clientSignature)) + for i := 0; i < len(clientSignature); i++ { + clientProof[i] = clientKey[i] ^ clientSignature[i] + } + buf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof))) + base64.StdEncoding.Encode(buf, clientProof) + return buf +} + +func (a *scramAuth) computeServerSignature() []byte { + serverKey := a.computeHMAC(a.saltedPwd, []byte("Server Key")) + serverSignature := a.computeHMAC(serverKey, a.authMessage) + buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature))) + base64.StdEncoding.Encode(buf, serverSignature) + return buf +} + +func (a *scramAuth) normalizeUsername() (string, error) { + // RFC 5802 section 5.1: the characters ',' or '=' in usernames are + // sent as '=2C' and '=3D' respectively. + replacer := strings.NewReplacer("=", "=3D", ",", "=2C") + username := replacer.Replace(a.username) + // RFC 5802 section 5.1: before sending the username to the server, + // the client SHOULD prepare the username using the "SASLprep" + // profile [RFC4013] of the "stringprep" algorithm [RFC3454] + // treating it as a query string (i.e., unassigned Unicode code + // points are allowed). If the preparation of the username fails or + // results in an empty string, the client SHOULD abort the + // authentication exchange. + // + // Since RFC 8265 obsoletes RFC 4013 we use it instead. + username, err := a.normalizeString(username) + if err != nil { + return "", fmt.Errorf("unable to normalize username: %w", err) + } + return username, nil +} + +func (a *scramAuth) normalizeString(s string) (string, error) { + s, err := precis.OpaqueString.String(s) + if err != nil { + return "", err + } + if s == "" { + return "", errors.New("normalized string is empty") + } + return s, nil +} From 4f1a60760dc3721b587bee2408eb36d656f5b2ea Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:04:16 +0200 Subject: [PATCH 03/24] Add support for SCRAM-SHA authentication methods Extended SMTP authentication to include SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS methods. This enhancement provides more secure and flexible authentication options for SMTP clients. --- client.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/client.go b/client.go index 6557913..f7ed9f1 100644 --- a/client.go +++ b/client.go @@ -785,6 +785,35 @@ func (c *Client) auth() error { return ErrXOauth2AuthNotSupported } c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA1: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) { + return ErrXOauth2AuthNotSupported + } + c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA1PLUS: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { + return ErrXOauth2AuthNotSupported + } + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + return err + } + c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) + case SMTPAuthSCRAMSHA256: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { + return ErrXOauth2AuthNotSupported + } + c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA256PLUS: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { + return ErrXOauth2AuthNotSupported + } + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + return err + } + c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState) + default: return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType) } From ebd171005d7d05213c780334ff3f03b3291b7a42 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:05:07 +0200 Subject: [PATCH 04/24] Update dependencies in go.mod and go.sum Added `golang.org/x/crypto v0.27.0` and `golang.org/x/text v0.18.0` to go.mod. Updated go.sum to reflect these changes for proper dependency management. --- go.mod | 5 +++++ go.sum | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/go.mod b/go.mod index b155b9b..1dcef3a 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,8 @@ module github.com/wneessen/go-mail go 1.16 + +require ( + golang.org/x/crypto v0.27.0 + golang.org/x/text v0.18.0 +) diff --git a/go.sum b/go.sum index e69de29..78b6dba 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,66 @@ +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +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= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/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= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.25.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= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +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= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 738f43e289434743677372279fb9f8d7c91ee2b9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:05:42 +0200 Subject: [PATCH 05/24] Add GetTLSConnectionState method to SMTP client Introduce a method to retrieve the TLS connection state of the client's current connection. This method checks if the connection uses TLS and is established, returning appropriate errors otherwise. --- smtp/smtp.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index 4ea1a3d..e834a09 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -565,6 +565,25 @@ func (c *Client) UpdateDeadline(timeout time.Duration) error { return nil } +// GetTLSConnectionState retrieves the TLS connection state of the client's current connection. +// Returns an error if the connection is not using TLS or if the connection is not established. +func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + if !c.tls { + return nil, errors.New("smtp: connection is not using TLS") + } + if c.conn == nil { + return nil, errors.New("smtp: connection is not established") + } + if conn, ok := c.conn.(*tls.Conn); ok { + cstate := conn.ConnectionState() + return &cstate, nil + } + return nil, errors.New("smtp: connection is not a TLS connection") +} + // debugLog checks if the debug flag is set and if so logs the provided message to // the log.Logger interface func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) { From b96badbd59dc8e9277819752697464af912cf2a3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:25:51 +0200 Subject: [PATCH 06/24] Add license file for go.sum Introduce a go.sum.license file to explicitly state the licensing terms for the go.sum file. This ensures proper attribution and compliance with open-source licensing requirements. --- go.sum.license | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.sum.license diff --git a/go.sum.license b/go.sum.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/go.sum.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT From c797f0be179afffca5aad615430465eca345cf68 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:27:05 +0200 Subject: [PATCH 07/24] Add REUSE.toml Replaced deprecated .reuse/dep5 with REUSE.toml config file --- .reuse/dep5 | 10 ---------- REUSE.toml | 9 +++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 .reuse/dep5 create mode 100644 REUSE.toml diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index 70261ce..0000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,10 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: go-mail -Upstream-Contact: Winni Neessen -Source: https://github.com/wneessen/go-mail - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..0bca544 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +# +# SPDX-License-Identifier: MIT + +version = 1 +SPDX-PackageName = "go-mail" +SPDX-PackageSupplier = "Winni Neessen " +SPDX-PackageDownloadLocation = "https://github.com/wneessen/go-mail" +annotations = [] From 3013975c6af6122a915dc360a16eec90bdc498ac Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:27:31 +0200 Subject: [PATCH 08/24] Rename and refactor SCRAM authentication methods Updated method names to more accurately reflect their authentication mechanisms (SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-256-PLUS). Revised corresponding comments to improve clarity and maintain consistency. --- smtp/auth_scram.go | 53 +++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index 2b7f814..14308fa 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors +// SPDX-FileCopyrightText: Copyright (c) 2024 The go-mail Authors // // SPDX-License-Identifier: MIT @@ -33,26 +33,8 @@ type scramAuth struct { bindData []byte } -func ScramSHA256Auth(username, password string) Auth { - return &scramAuth{ - username: username, - password: password, - algorithm: "SCRAM-SHA-256", - h: sha256.New, - } -} - -func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { - return &scramAuth{ - username: username, - password: password, - algorithm: "SCRAM-SHA-256-PLUS", - h: sha256.New, - isPlus: true, - tlsConnState: tlsConnState, - } -} - +// ScramSHA1Auth creates and returns a new SCRAM-SHA-1 authentication mechanism with the given +// username and password. func ScramSHA1Auth(username, password string) Auth { return &scramAuth{ username: username, @@ -62,6 +44,19 @@ func ScramSHA1Auth(username, password string) Auth { } } +// ScramSHA256Auth creates and returns a new SCRAM-SHA-256 authentication mechanism with the given +// username and password. +func ScramSHA256Auth(username, password string) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256", + h: sha256.New, + } +} + +// ScramSHA1PlusAuth returns an Auth instance configured for SCRAM-SHA-1-PLUS authentication with +// the provided username, password, and TLS connection state. func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { return &scramAuth{ username: username, @@ -73,11 +68,25 @@ func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionSt } } +// ScramSHA256PlusAuth returns an Auth instance configured for SCRAM-SHA-256-PLUS authentication with +// the provided username, password, and TLS connection state. +func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256-PLUS", + h: sha256.New, + isPlus: true, + tlsConnState: tlsConnState, + } +} + +// Start initializes the SCRAM authentication process and returns the selected algorithm, nil data, and no error. func (a *scramAuth) Start(_ *ServerInfo) (string, []byte, error) { - fmt.Printf("algo: %s\n", a.algorithm) return a.algorithm, nil, nil } +// Next processes the server's challenge and returns the client's response for SCRAM authentication. func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { if len(fromServer) == 0 { From 27838f5b1f8bde119bdc86f411e62ff9ca9511af Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:28:10 +0200 Subject: [PATCH 09/24] Improve TLS state handling and add SCRAM-SHA-256 auth support Replaced direct TLSConnectionState call with error handling for TLS state retrieval. Introduced SCRAM-SHA-256 support in the SMTP authentication process. --- client.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index f7ed9f1..b692af3 100644 --- a/client.go +++ b/client.go @@ -748,7 +748,11 @@ func (c *Client) tls() error { return err } } - _, c.isEncrypted = c.smtpClient.TLSConnectionState() + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + return fmt.Errorf("failed to get TLS connection state: %w", err) + } + c.isEncrypted = tlsConnState.HandshakeComplete } return nil } @@ -790,6 +794,11 @@ func (c *Client) auth() error { return ErrXOauth2AuthNotSupported } c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA256: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { + return ErrXOauth2AuthNotSupported + } + c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { return ErrXOauth2AuthNotSupported @@ -799,11 +808,6 @@ func (c *Client) auth() error { return err } c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) - case SMTPAuthSCRAMSHA256: - if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { - return ErrXOauth2AuthNotSupported - } - c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA256PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { return ErrXOauth2AuthNotSupported @@ -813,7 +817,6 @@ func (c *Client) auth() error { return err } c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState) - default: return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType) } From e8f3c444e6f7174a5532f08fd4e05a10a76abe15 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:28:53 +0200 Subject: [PATCH 10/24] Add SCRAM-SHA1-PLUS authentication tests Introduced two new unit tests for SCRAM-SHA1-PLUS authentication with TLS exporter and TLS unique options. These tests ensure proper client creation, connection, and disconnection processes are functioning as expected in online environments. --- client_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/client_test.go b/client_test.go index 2d37ce0..16ec031 100644 --- a/client_test.go +++ b/client_test.go @@ -1836,6 +1836,56 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } } +func TestClient_AuthSCRAMSHA1PLUS_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") + + client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), + WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), + 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_AuthSCRAMSHA1PLUS_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 + client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), + WithTLSConfig(tlsConfig), + WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), + 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) { From e5b87db448f5182a55a79e91902f45897f93d987 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:41:26 +0200 Subject: [PATCH 11/24] Update README to clarify library features and dependencies Revised the README to provide clearer explanations of the library's origins, dependencies, and features. Added details on the small dependency footprint and enhanced SMTP Auth methods, and emphasized the concurrency-safe reuse of SMTP connections. --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 260b888..cc2825f 100644 --- a/README.md +++ b/README.md @@ -18,33 +18,34 @@ SPDX-License-Identifier: CC0-1.0

go-mail logo

-The main idea of this library was to provide a simple interface to sending mails for +The main idea of this library was to provide a simple interface for sending mails to my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a full-fledged mail library. -go-mail follows idiomatic Go style and best practice. It's only dependency is the Go Standard Library. It combines a lot -of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks. +go-mail follows idiomatic Go style and best practice. It has a small dependency footprint by mainly relying on the +Go Standard Library and the Go extended packages. It combines a lot of functionality from the standard library to +give easy and convenient access to mail and SMTP related tasks. -Parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been forked/ported from the -[go-mail/mail](https://github.com/go-mail/mail) respectively [go-gomail/gomail](https://github.com/go-gomail/gomail) -which both seems to not be maintained anymore. +In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been +forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). In +the meantime most of the ported code has been refactored. -The smtp package of go-mail is forked from the original Go stdlib's `net/smtp` and then extended by the go-mail -team. +The smtp package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended +by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.). ## Features -Some of the features of this library: +Here are some highlights of go-mail's featureset: -* [X] Only Standard Library dependant +* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages) * [X] Modern, idiomatic Go * [X] Sane and secure defaults * [X] Explicit SSL/TLS support * [X] Implicit StartTLS support with different policies * [X] Makes use of contexts for a better control flow and timeout/cancelation handling -* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2) +* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS)) * [X] RFC5322 compliant mail address validation * [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.) -* [X] Reusing the same SMTP connection to send multiple mails +* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails * [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`) * [X] Support for different encodings * [X] Middleware support for 3rd-party libraries to alter mail messages From cace4890bc57a750dfb0c7b21023cde1a5c98376 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:47:07 +0200 Subject: [PATCH 12/24] Update README.md wordings for clarity Refined the wording in the README.md to enhance readability and clarity. Changed some sentences to past perfect tense and added backticks around `smtp` for consistency with code references. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cc2825f..2a4d997 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ go-mail follows idiomatic Go style and best practice. It has a small dependency Go Standard Library and the Go extended packages. It combines a lot of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks. -In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been -forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). In -the meantime most of the ported code has been refactored. +In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) had been +forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today +most of the ported code has been refactored. -The smtp package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended +The `smtp` package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.). ## Features From 687843ee53a37cc6172db2f5e2062017d4d11bbe Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:57:01 +0200 Subject: [PATCH 13/24] Enhance contributors section and add sponsors acknowledgment Updated the Authors/Contributors section to include a graphical representation of contributors and added special thanks to Maria Letta for the logo design. Introduced a new Sponsors section to acknowledge the support from sponsors. --- README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2a4d997..463d1f1 100644 --- a/README.md +++ b/README.md @@ -100,15 +100,18 @@ We provide example code in both our GoDocs as well as on our official Website (s check out our [Getting started](https://go-mail.dev/getting-started/introduction/) guide. ## Authors/Contributors -go-mail was initially authored and developed by [Winni Neessen](https://github.com/wneessen/). +go-mail was initially created and developed by [Winni Neessen](https://github.com/wneessen/), but over time a lot of amazing people +contributed ot the project. Big thanks to all of them for improving the go-mail project (be it writing code, testing +code, reviewing code, writing documenation or helping to translate the website): -Big thanks to the following people, for contributing to the go-mail project (either in form of code or by -reviewing code, writing documenation or helping to translate the website): -* [Christian Vette](https://github.com/cvette) -* [Dhia Gharsallaoui](https://github.com/dhia-gharsallaoui) -* [inliquid](https://github.com/inliquid) -* [iwittkau](https://github.com/iwittkau) -* [James Elliott](https://github.com/james-d-elliott) -* [Maria Letta](https://github.com/MariaLetta) (designed the go-mail logo) -* [Nicola Murino](https://github.com/drakkan) -* [sters](https://github.com/sters) + + + + +A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo! + +## Sponsors +We sincerely thank all our amazing sponsors for their generous support, without whom this project would not +have achieved such development and success as it has today. Your contributions do not go unnoticed! + +* [kolaente](https://github.com/kolaente) From abab0af2a378c3d85c3ded56ab7e08ac2fa3cb27 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:01:58 +0200 Subject: [PATCH 14/24] Simplify sponsor appreciation message Revise the sponsors section in README.md to convey gratitude more concisely. Removed redundant phrasing and made the message more direct while ensuring the intent remains clear. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 463d1f1..6a34055 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ code, reviewing code, writing documenation or helping to translate the website): A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo! ## Sponsors -We sincerely thank all our amazing sponsors for their generous support, without whom this project would not -have achieved such development and success as it has today. Your contributions do not go unnoticed! +We sincerely thank our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps +keeping up the project! * [kolaente](https://github.com/kolaente) From bcf70849821a12b3eed3345cc9dd778f63dd9d9f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:39:13 +0200 Subject: [PATCH 15/24] Add detailed documentation comments for SCRAM methods Enhanced code readability and maintainability by adding comprehensive documentation comments to all methods and struct definitions in the `smtp/auth_scram.go` file. This improves clarity on the functionality and usage of the SCRAM (Salted Challenge Response Authentication Mechanism) methods and structures. --- smtp/auth_scram.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index 14308fa..c70b210 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -23,6 +23,8 @@ import ( "golang.org/x/text/secure/precis" ) +// scramAuth represents a SCRAM (Salted Challenge Response Authentication Mechanism) client and +// satisfies the smtp.Auth interface. type scramAuth struct { username, password, algorithm string firstBareMsg, nonce, saltedPwd, authMessage []byte @@ -116,6 +118,7 @@ func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { return nil, nil } +// reset clears all authentication-related properties in the scramAuth instance, effectively resetting its state. func (a *scramAuth) reset() { a.nonce = nil a.firstBareMsg = nil @@ -124,6 +127,8 @@ func (a *scramAuth) reset() { a.iterations = 0 } +// initialClientMessage generates the initial message for SCRAM authentication, including a nonce and +// optional channel binding. func (a *scramAuth) initialClientMessage() ([]byte, error) { username, err := a.normalizeUsername() if err != nil { @@ -140,11 +145,16 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) { a.firstBareMsg = []byte("n=" + username + ",r=" + string(a.nonce)) returnBytes := []byte("n,," + string(a.firstBareMsg)) + // SCRAM-SHA-X-PLUS auth requires channel binding if a.isPlus { bindType := "tls-unique" connState := a.tlsConnState bindData := connState.TLSUnique - if connState.Version == tls.VersionTLS13 { + + // crypto/tl: no tls-unique channel binding value for this tls connection, possibly due to missing + // extended master key support and/or resumed connection + // RFC9266:122 tls-unique not defined for tls 1.3 and later + if bindData == nil || connState.Version >= tls.VersionTLS13 { bindType = "tls-exporter" bindData, err = connState.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32) if err != nil { @@ -160,6 +170,7 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) { return returnBytes, nil } +// handleServerFirstResponse processes the first response from the server in SCRAM authentication. func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) { parts := bytes.Split(fromServer, []byte(",")) if len(parts) < 3 { @@ -203,16 +214,19 @@ func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) a.saltedPwd = pbkdf2.Key([]byte(password), salt, a.iterations, a.h().Size(), a.h) msgWithoutProof := []byte("c=biws,r=" + string(a.nonce)) + + // A PLUS authentication requires the channel binding data if a.isPlus { msgWithoutProof = []byte("c=" + string(a.bindData) + ",r=" + string(a.nonce)) } - a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof)) + a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof)) clientProof := a.computeClientProof() return []byte(string(msgWithoutProof) + ",p=" + string(clientProof)), nil } +// handleServerValidationMessage verifies the server's signature during the SCRAM authentication process. func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, error) { serverSignature := fromServer[2:] computedServerSignature := a.computeServerSignature() @@ -223,18 +237,21 @@ func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, er return []byte(""), nil } +// computeHMAC generates a Hash-based Message Authentication Code (HMAC) using the specified key and message. func (a *scramAuth) computeHMAC(key, msg []byte) []byte { mac := hmac.New(a.h, key) mac.Write(msg) return mac.Sum(nil) } +// computeHash generates a hash of the given key using the configured hashing algorithm. func (a *scramAuth) computeHash(key []byte) []byte { hasher := a.h() hasher.Write(key) return hasher.Sum(nil) } +// computeClientProof generates the client proof as part of the SCRAM authentication process. func (a *scramAuth) computeClientProof() []byte { clientKey := a.computeHMAC(a.saltedPwd, []byte("Client Key")) storedKey := a.computeHash(clientKey) @@ -248,6 +265,8 @@ func (a *scramAuth) computeClientProof() []byte { return buf } +// computeServerSignature returns the computed base64-encoded server signature in the SCRAM +// authentication process. func (a *scramAuth) computeServerSignature() []byte { serverKey := a.computeHMAC(a.saltedPwd, []byte("Server Key")) serverSignature := a.computeHMAC(serverKey, a.authMessage) @@ -256,6 +275,9 @@ func (a *scramAuth) computeServerSignature() []byte { return buf } +// normalizeUsername replaces special characters in the username for SCRAM authentication +// and prepares it using the SASLprep profile as per RFC 8265, returning the normalized +// username or an error. func (a *scramAuth) normalizeUsername() (string, error) { // RFC 5802 section 5.1: the characters ',' or '=' in usernames are // sent as '=2C' and '=3D' respectively. @@ -277,10 +299,13 @@ func (a *scramAuth) normalizeUsername() (string, error) { return username, nil } +// normalizeString normalizes the input string according to the OpaqueString profile of the +// precis framework. It returns the normalized string or an error if normalization fails or +// results in an empty string. func (a *scramAuth) normalizeString(s string) (string, error) { s, err := precis.OpaqueString.String(s) if err != nil { - return "", err + return "", fmt.Errorf("failled to normalize string: %w", err) } if s == "" { return "", errors.New("normalized string is empty") From 324be9d0329af8934aeaad65d92cc0d5852b7ed9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:43:36 +0200 Subject: [PATCH 16/24] Refactor SCRAM tests to include SHA-256-PLUS Updated `TestClient_AuthSCRAMSHA1PLUS_tlsexporter` and `TestClient_AuthSCRAMSHA1PLUS_tlsunique` to test both SCRAM-SHA-1-PLUS and SCRAM-SHA-256-PLUS authentication types. Implemented table-driven tests to improve readability and maintainability. --- client_test.go | 78 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/client_test.go b/client_test.go index 16ec031..1c0641a 100644 --- a/client_test.go +++ b/client_test.go @@ -1836,7 +1836,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } } -func TestClient_AuthSCRAMSHA1PLUS_tlsexporter(t *testing.T) { +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") } @@ -1844,45 +1844,71 @@ func TestClient_AuthSCRAMSHA1PLUS_tlsexporter(t *testing.T) { username := os.Getenv("TEST_USER_SCRAM") password := os.Getenv("TEST_PASS_SCRAM") - client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), - WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, } - 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) + + 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_AuthSCRAMSHA1PLUS_tlsunique(t *testing.T) { +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 - client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), - WithTLSConfig(tlsConfig), - WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return + + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, } - 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) + + 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) + } + }) } } From 7499bae3eb810264493ed3fb2737cf364859948a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:45:02 +0200 Subject: [PATCH 17/24] Add unit tests for SCRAM-SHA authentication methods Introduce `TestClient_AuthSCRAMSHAX` to verify SCRAM-SHA-1 and SCRAM-SHA-256 authentication. These tests validate the creation, connection, and closing of clients with the respective authentication methods using environment-configured credentials. --- client_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/client_test.go b/client_test.go index 1c0641a..888bf8b 100644 --- a/client_test.go +++ b/client_test.go @@ -1836,6 +1836,42 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } } +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_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") From b69ad27de318de9276a90ba5b2b7db2667b554b6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:00:43 +0200 Subject: [PATCH 18/24] Add comments for SMTP authentication mechanisms Enhanced the documentation by adding detailed comments for each SMTP authentication type, specifying their references to RFC documents. Corrected comments for error variables to match the corresponding authentication schemas. --- auth.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/auth.go b/auth.go index d30a154..f1dad86 100644 --- a/auth.go +++ b/auth.go @@ -29,9 +29,20 @@ const ( // https://developers.google.com/gmail/imap/xoauth2-protocol SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" - SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" - SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" - SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" + // SMTPAuthSCRAMSHA1 represents the SCRAM-SHA-1 SMTP authentication mechanism + // https://datatracker.ietf.org/doc/html/rfc5802 + SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" + + // SMTPAuthSCRAMSHA1PLUS represents the "SCRAM-SHA-1-PLUS" authentication mechanism for SMTP. + // https://datatracker.ietf.org/doc/html/rfc5802 + SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" + + // SMTPAuthSCRAMSHA256 represents the SCRAM-SHA-256 authentication mechanism for SMTP. + // https://datatracker.ietf.org/doc/html/rfc7677 + SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" + + // SMTPAuthSCRAMSHA256PLUS represents the "SCRAM-SHA-256-PLUS" SMTP AUTH type. + // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" ) @@ -49,15 +60,15 @@ var ( // ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2") - // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1" schema ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1") - // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1-PLUS" schema ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS") - // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256" schema ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256") - // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256-PLUS" schema ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") ) From 8838414c3814e7469907ae0ba137b4e0ffdf0020 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:00:57 +0200 Subject: [PATCH 19/24] Fix incorrect error types for unsupported SMTP authentication Corrects the error messages returned for unsupported SMTP authentication types from ErrXOauth2AuthNotSupported to specific errors like ErrSCRAMSHA1AuthNotSupported, ErrSCRAMSHA256AuthNotSupported, and so on. This change improves the accuracy of error reporting for various SMTP authentication mechanisms. --- client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index b692af3..acfeaa2 100644 --- a/client.go +++ b/client.go @@ -791,17 +791,17 @@ func (c *Client) auth() error { c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA1AuthNotSupported } c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) case SMTPAuthSCRAMSHA256: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA256AuthNotSupported } c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA1PLUSAuthNotSupported } tlsConnState, err := c.smtpClient.GetTLSConnectionState() if err != nil { @@ -810,7 +810,7 @@ func (c *Client) auth() error { c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) case SMTPAuthSCRAMSHA256PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA256PLUSAuthNotSupported } tlsConnState, err := c.smtpClient.GetTLSConnectionState() if err != nil { From 5058fd522285aecd0de64e3bc384048e525b7215 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:01:10 +0200 Subject: [PATCH 20/24] Add test for SCRAM-SHA authentication failure cases Implemented tests for various SCRAM-SHA authentication methods including SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS with invalid credentials. This ensures that the client correctly handles and reports authentication failures. --- client_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/client_test.go b/client_test.go index 888bf8b..83d710c 100644 --- a/client_test.go +++ b/client_test.go @@ -1872,6 +1872,39 @@ func TestClient_AuthSCRAMSHAX(t *testing.T) { } } +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_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") From 15b9ddf0675fbceeff3091248030eea511df2bc2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:23:29 +0200 Subject: [PATCH 21/24] Refactor error handling for non-TLS SMTP connections Introduce a global error variable for non-TLS connections and update corresponding error handling across the codebase. This enhances readability and maintainability of the error management logic. --- client.go | 8 +++++++- smtp/smtp.go | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index acfeaa2..5d0e797 100644 --- a/client.go +++ b/client.go @@ -750,7 +750,13 @@ func (c *Client) tls() error { } tlsConnState, err := c.smtpClient.GetTLSConnectionState() if err != nil { - return fmt.Errorf("failed to get TLS connection state: %w", err) + switch { + case errors.Is(err, smtp.ErrNonTLSConnection): + c.isEncrypted = false + return nil + default: + return fmt.Errorf("failed to get TLS connection state: %w", err) + } } c.isEncrypted = tlsConnState.HandshakeComplete } diff --git a/smtp/smtp.go b/smtp/smtp.go index e834a09..0352133 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -36,6 +36,10 @@ import ( "github.com/wneessen/go-mail/log" ) +var ( + ErrNonTLSConnection = errors.New("connection is not using TLS") +) + // A Client represents a client connection to an SMTP server. type Client struct { // Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions. @@ -572,7 +576,7 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) { defer c.mutex.RUnlock() if !c.tls { - return nil, errors.New("smtp: connection is not using TLS") + return nil, ErrNonTLSConnection } if c.conn == nil { return nil, errors.New("smtp: connection is not established") From f823112a4d74d18f8837ba4e351d4a426cee1f23 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 20:32:41 +0200 Subject: [PATCH 22/24] Refactor: consolidate ErrNonTLSConnection variable The variable ErrNonTLSConnection has been simplified from a multi-line declaration to a single-line declaration. This increases code readability and maintains consistency with Go conventions. --- smtp/smtp.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 0352133..f9961c9 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -36,9 +36,7 @@ import ( "github.com/wneessen/go-mail/log" ) -var ( - ErrNonTLSConnection = errors.New("connection is not using TLS") -) +var ErrNonTLSConnection = errors.New("connection is not using TLS") // A Client represents a client connection to an SMTP server. type Client struct { From 986a988c5d2a3eb45a582231630b102345bcfb4c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 20:44:50 +0200 Subject: [PATCH 23/24] Reset SMTP auth when setting SMTP auth type This change ensures that the smtpAuth field is reset to nil whenever the SMTP auth type is updated. This prevents potential issues with mismatched authentication settings. --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index 5d0e797..4d1b0b1 100644 --- a/client.go +++ b/client.go @@ -578,6 +578,7 @@ func (c *Client) SetPassword(password string) { // SetSMTPAuth overrides the current SMTP AUTH type setting with the given value func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { c.smtpAuthType = authtype + c.smtpAuth = nil } // SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth From 72b3f53eb788979ea1bd8f9d85621f1d2d775280 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 20:45:07 +0200 Subject: [PATCH 24/24] Add tests for unsupported SCRAM-SHA authentications Introduce a new test case `TestClient_AuthSCRAMSHAX_unsupported` to validate handling of unsupported SCRAM-SHA authentication methods. This ensures the client returns the correct errors when setting unsupported auth types. --- client_test.go | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index 83d710c..481b9be 100644 --- a/client_test.go +++ b/client_test.go @@ -1905,6 +1905,41 @@ func TestClient_AuthSCRAMSHAX_fail(t *testing.T) { } } +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") @@ -2023,10 +2058,10 @@ func getTestConnection(auth bool) (*Client, error) { // We don't want to log authentication data in tests c.SetDebugLog(false) } - if err := c.DialWithContext(context.Background()); err != nil { + 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 { + if err = c.Close(); err != nil { return c, fmt.Errorf("disconnect from test server failed: %w", err) } return c, nil