mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 14:10:50 +01:00
Compare commits
No commits in common. "9bafa969b84c6bd53ca36f586d345358fbe963f7" and "627216425fc8b66c603593187d918a36058fe2ff" have entirely different histories.
9bafa969b8
...
627216425f
11 changed files with 36 additions and 695 deletions
10
.reuse/dep5
Normal file
10
.reuse/dep5
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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: ...
|
50
README.md
50
README.md
|
@ -18,34 +18,33 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
<p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p>
|
<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 for sending mails to
|
The main idea of this library was to provide a simple interface to sending mails for
|
||||||
my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a full-fledged mail library.
|
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 has a small dependency footprint by mainly relying on the
|
go-mail follows idiomatic Go style and best practice. It's only dependency is the Go Standard Library. It combines a lot
|
||||||
Go Standard Library and the Go extended packages. It combines a lot of functionality from the standard library to
|
of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks.
|
||||||
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)) had been
|
Parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been forked/ported from the
|
||||||
forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today
|
[go-mail/mail](https://github.com/go-mail/mail) respectively [go-gomail/gomail](https://github.com/go-gomail/gomail)
|
||||||
most of the ported code has been refactored.
|
which both seems to not be maintained anymore.
|
||||||
|
|
||||||
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 is forked from the original Go stdlib's `net/smtp` and then extended by the go-mail
|
||||||
by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.).
|
team.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Here are some highlights of go-mail's featureset:
|
Some of the features of this library:
|
||||||
|
|
||||||
* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages)
|
* [X] Only Standard Library dependant
|
||||||
* [X] Modern, idiomatic Go
|
* [X] Modern, idiomatic Go
|
||||||
* [X] Sane and secure defaults
|
* [X] Sane and secure defaults
|
||||||
* [X] Explicit SSL/TLS support
|
* [X] Explicit SSL/TLS support
|
||||||
* [X] Implicit StartTLS support with different policies
|
* [X] Implicit StartTLS support with different policies
|
||||||
* [X] Makes use of contexts for a better control flow and timeout/cancelation handling
|
* [X] Makes use of contexts for a better control flow and timeout/cancelation handling
|
||||||
* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS))
|
* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2)
|
||||||
* [X] RFC5322 compliant mail address validation
|
* [X] RFC5322 compliant mail address validation
|
||||||
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
|
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
|
||||||
* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails
|
* [X] 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 attachments and inline embeds (from file system, `io.Reader` or `embed.FS`)
|
||||||
* [X] Support for different encodings
|
* [X] Support for different encodings
|
||||||
* [X] Middleware support for 3rd-party libraries to alter mail messages
|
* [X] Middleware support for 3rd-party libraries to alter mail messages
|
||||||
|
@ -100,18 +99,15 @@ 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.
|
check out our [Getting started](https://go-mail.dev/getting-started/introduction/) guide.
|
||||||
|
|
||||||
## Authors/Contributors
|
## Authors/Contributors
|
||||||
go-mail was initially created and developed by [Winni Neessen](https://github.com/wneessen/), but over time a lot of amazing people
|
go-mail was initially authored and developed by [Winni Neessen](https://github.com/wneessen/).
|
||||||
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):
|
|
||||||
|
|
||||||
<a href="https://github.com/wneessen/go-mail/graphs/contributors">
|
Big thanks to the following people, for contributing to the go-mail project (either in form of code or by
|
||||||
<img src="https://contrib.rocks/image?repo=wneessen/go-mail" />
|
reviewing code, writing documenation or helping to translate the website):
|
||||||
</a>
|
* [Christian Vette](https://github.com/cvette)
|
||||||
|
* [Dhia Gharsallaoui](https://github.com/dhia-gharsallaoui)
|
||||||
A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo!
|
* [inliquid](https://github.com/inliquid)
|
||||||
|
* [iwittkau](https://github.com/iwittkau)
|
||||||
## Sponsors
|
* [James Elliott](https://github.com/james-d-elliott)
|
||||||
We sincerely thank our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps
|
* [Maria Letta](https://github.com/MariaLetta) (designed the go-mail logo)
|
||||||
keeping up the project!
|
* [Nicola Murino](https://github.com/drakkan)
|
||||||
|
* [sters](https://github.com/sters)
|
||||||
* [kolaente](https://github.com/kolaente)
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
# 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
28
auth.go
|
@ -28,22 +28,6 @@ const (
|
||||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
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
|
// SMTP Auth related static errors
|
||||||
|
@ -59,16 +43,4 @@ var (
|
||||||
|
|
||||||
// ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema
|
// 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")
|
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")
|
|
||||||
)
|
)
|
||||||
|
|
41
client.go
41
client.go
|
@ -578,7 +578,6 @@ func (c *Client) SetPassword(password string) {
|
||||||
// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value
|
// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value
|
||||||
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) {
|
func (c *Client) SetSMTPAuth(authtype SMTPAuthType) {
|
||||||
c.smtpAuthType = authtype
|
c.smtpAuthType = authtype
|
||||||
c.smtpAuth = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth
|
// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth
|
||||||
|
@ -749,17 +748,7 @@ func (c *Client) tls() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsConnState, err := c.smtpClient.GetTLSConnectionState()
|
_, c.isEncrypted = c.smtpClient.TLSConnectionState()
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -796,34 +785,6 @@ func (c *Client) auth() error {
|
||||||
return ErrXOauth2AuthNotSupported
|
return ErrXOauth2AuthNotSupported
|
||||||
}
|
}
|
||||||
c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass)
|
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:
|
default:
|
||||||
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType)
|
return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType)
|
||||||
}
|
}
|
||||||
|
|
184
client_test.go
184
client_test.go
|
@ -1836,186 +1836,6 @@ 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
|
// getTestConnection takes environment variables to establish a connection to a real
|
||||||
// SMTP server to test all functionality that requires a connection
|
// SMTP server to test all functionality that requires a connection
|
||||||
func getTestConnection(auth bool) (*Client, error) {
|
func getTestConnection(auth bool) (*Client, error) {
|
||||||
|
@ -2058,10 +1878,10 @@ func getTestConnection(auth bool) (*Client, error) {
|
||||||
// We don't want to log authentication data in tests
|
// We don't want to log authentication data in tests
|
||||||
c.SetDebugLog(false)
|
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)
|
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, fmt.Errorf("disconnect from test server failed: %w", err)
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -5,8 +5,3 @@
|
||||||
module github.com/wneessen/go-mail
|
module github.com/wneessen/go-mail
|
||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
|
||||||
golang.org/x/crypto v0.27.0
|
|
||||||
golang.org/x/text v0.18.0
|
|
||||||
)
|
|
||||||
|
|
66
go.sum
66
go.sum
|
@ -1,66 +0,0 @@
|
||||||
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=
|
|
|
@ -1,3 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
|
@ -1,314 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
21
smtp/smtp.go
21
smtp/smtp.go
|
@ -36,8 +36,6 @@ import (
|
||||||
"github.com/wneessen/go-mail/log"
|
"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.
|
// A Client represents a client connection to an SMTP server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
|
// Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
|
||||||
|
@ -567,25 +565,6 @@ func (c *Client) UpdateDeadline(timeout time.Duration) error {
|
||||||
return nil
|
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
|
// debugLog checks if the debug flag is set and if so logs the provided message to
|
||||||
// the log.Logger interface
|
// the log.Logger interface
|
||||||
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
|
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {
|
||||||
|
|
Loading…
Reference in a new issue