Compare commits

..

26 commits

Author SHA1 Message Date
9bafa969b8
Merge pull request #310 from wneessen/feature/242_support-scram-sha
SCRAM-SHA-1(-PLUS) / SCRAM-SHA-256(-PLUS) support
2024-10-01 20:52:08 +02:00
72b3f53eb7
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.
2024-10-01 20:45:07 +02:00
986a988c5d
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.
2024-10-01 20:44:50 +02:00
f823112a4d
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.
2024-10-01 20:32:41 +02:00
15b9ddf067
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.
2024-10-01 17:23:29 +02:00
5058fd5222
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.
2024-10-01 17:01:10 +02:00
8838414c38
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.
2024-10-01 17:00:57 +02:00
b69ad27de3
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.
2024-10-01 17:00:43 +02:00
7499bae3eb
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.
2024-10-01 16:45:02 +02:00
324be9d032
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.
2024-10-01 16:43:36 +02:00
bcf7084982
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.
2024-10-01 16:39:13 +02:00
abab0af2a3
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.
2024-10-01 16:01:58 +02:00
687843ee53
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.
2024-10-01 15:57:01 +02:00
cace4890bc
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.
2024-10-01 15:47:07 +02:00
e5b87db448
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.
2024-10-01 15:41:26 +02:00
5b5991f17d
Merge branch 'main' into feature/242_support-scram-sha 2024-10-01 15:30:22 +02:00
e8f3c444e6
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.
2024-10-01 15:28:53 +02:00
27838f5b1f
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.
2024-10-01 15:28:10 +02:00
3013975c6a
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.
2024-10-01 15:27:31 +02:00
c797f0be17
Add REUSE.toml
Replaced deprecated .reuse/dep5 with REUSE.toml config file
2024-10-01 15:27:05 +02:00
b96badbd59
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.
2024-10-01 15:25:51 +02:00
738f43e289
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.
2024-10-01 11:05:42 +02:00
ebd171005d
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.
2024-10-01 11:05:07 +02:00
4f1a60760d
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.
2024-10-01 11:04:16 +02:00
e8fc6cd78f
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.
2024-10-01 11:04:01 +02:00
9069c9cdff
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.
2024-10-01 11:03:44 +02:00
11 changed files with 695 additions and 36 deletions

View file

@ -1,10 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: go-mail
Upstream-Contact: Winni Neessen <winni@neessen.dev>
Source: https://github.com/wneessen/go-mail
# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...

View file

@ -18,33 +18,34 @@ SPDX-License-Identifier: CC0-1.0
<p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p>
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)) 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 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
@ -99,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 href="https://github.com/wneessen/go-mail/graphs/contributors">
<img src="https://contrib.rocks/image?repo=wneessen/go-mail" />
</a>
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 our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps
keeping up the project!
* [kolaente](https://github.com/kolaente)

9
REUSE.toml Normal file
View file

@ -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 <winni@neessen.dev>"
SPDX-PackageDownloadLocation = "https://github.com/wneessen/go-mail"
annotations = []

28
auth.go
View file

@ -28,6 +28,22 @@ const (
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
// 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"
)
// SMTP Auth related static errors
@ -43,4 +59,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 "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 "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 "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 "SCRAM-SHA-256-PLUS" schema
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
)

View file

@ -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
@ -748,7 +749,17 @@ func (c *Client) tls() error {
return err
}
}
_, c.isEncrypted = c.smtpClient.TLSConnectionState()
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
if err != nil {
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
}
return nil
}
@ -785,6 +796,34 @@ func (c *Client) auth() error {
return ErrXOauth2AuthNotSupported
}
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass)
case SMTPAuthSCRAMSHA1:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) {
return ErrSCRAMSHA1AuthNotSupported
}
c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass)
case SMTPAuthSCRAMSHA256:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) {
return ErrSCRAMSHA256AuthNotSupported
}
c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass)
case SMTPAuthSCRAMSHA1PLUS:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) {
return ErrSCRAMSHA1PLUSAuthNotSupported
}
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
if err != nil {
return err
}
c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState)
case SMTPAuthSCRAMSHA256PLUS:
if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) {
return ErrSCRAMSHA256PLUSAuthNotSupported
}
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)
}

View file

@ -1836,6 +1836,186 @@ 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_AuthSCRAMSHAX_fail(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithSMTPAuth(tt.authtype),
WithUsername("invalid"), WithPassword("invalid"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err == nil {
t.Errorf("expected error but got nil")
}
})
}
}
func TestClient_AuthSCRAMSHAX_unsupported(t *testing.T) {
if os.Getenv("TEST_ALLOW_SEND") == "" {
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
}
client, err := getTestConnection(true)
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
tests := []struct {
name string
authtype SMTPAuthType
expErr error
}{
{"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, ErrSCRAMSHA1AuthNotSupported},
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, ErrSCRAMSHA1PLUSAuthNotSupported},
{"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, ErrSCRAMSHA256AuthNotSupported},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, ErrSCRAMSHA256PLUSAuthNotSupported},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client.SetSMTPAuth(tt.authtype)
client.SetTLSPolicy(TLSMandatory)
if err = client.DialWithContext(context.Background()); err == nil {
t.Errorf("expected error but got nil")
}
if !errors.Is(err, tt.expErr) {
t.Errorf("expected error %s, but got %s", tt.expErr, err)
}
})
}
}
func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
username := os.Getenv("TEST_USER_SCRAM")
password := os.Getenv("TEST_PASS_SCRAM")
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithSMTPAuth(tt.authtype),
WithUsername(username), WithPassword(password))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
})
}
}
func TestClient_AuthSCRAMSHAXPLUS_tlsunique(t *testing.T) {
if os.Getenv("TEST_ONLINE_SCRAM") == "" {
t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests")
}
hostname := os.Getenv("TEST_HOST_SCRAM")
username := os.Getenv("TEST_USER_SCRAM")
password := os.Getenv("TEST_PASS_SCRAM")
tlsConfig := &tls.Config{}
tlsConfig.MaxVersion = tls.VersionTLS12
tlsConfig.ServerName = hostname
tests := []struct {
name string
authtype SMTPAuthType
}{
{"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS},
{"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(hostname,
WithTLSPortPolicy(TLSMandatory),
WithTLSConfig(tlsConfig),
WithSMTPAuth(tt.authtype),
WithUsername(username), WithPassword(password))
if err != nil {
t.Errorf("unable to create new client: %s", err)
return
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
})
}
}
// getTestConnection takes environment variables to establish a connection to a real
// SMTP server to test all functionality that requires a connection
func getTestConnection(auth bool) (*Client, error) {
@ -1878,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

5
go.mod
View file

@ -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
)

66
go.sum
View file

@ -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=

3
go.sum.license Normal file
View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

314
smtp/auth_scram.go Normal file
View file

@ -0,0 +1,314 @@
// SPDX-FileCopyrightText: Copyright (c) 2024 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"
)
// 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
iterations int
h func() hash.Hash
isPlus bool
tlsConnState *tls.ConnectionState
bindData []byte
}
// 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,
password: password,
algorithm: "SCRAM-SHA-1",
h: sha1.New,
}
}
// 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,
password: password,
algorithm: "SCRAM-SHA-1-PLUS",
h: sha1.New,
isPlus: true,
tlsConnState: tlsConnState,
}
}
// 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) {
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 {
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
}
// reset clears all authentication-related properties in the scramAuth instance, effectively resetting its state.
func (a *scramAuth) reset() {
a.nonce = nil
a.firstBareMsg = nil
a.saltedPwd = nil
a.authMessage = nil
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 {
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))
// SCRAM-SHA-X-PLUS auth requires channel binding
if a.isPlus {
bindType := "tls-unique"
connState := a.tlsConnState
bindData := connState.TLSUnique
// 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 {
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
}
// 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 {
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))
// 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))
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()
if !hmac.Equal(serverSignature, computedServerSignature) {
return nil, errors.New("invalid server signature")
}
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)
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
}
// 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)
buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature)))
base64.StdEncoding.Encode(buf, serverSignature)
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.
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
}
// 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 "", fmt.Errorf("failled to normalize string: %w", err)
}
if s == "" {
return "", errors.New("normalized string is empty")
}
return s, nil
}

View file

@ -36,6 +36,8 @@ 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.
@ -565,6 +567,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, ErrNonTLSConnection
}
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{}) {