Compare commits

..

7 commits

Author SHA1 Message Date
0c1908d7bf
Merge pull request #374 from wneessen/feature/auth-autodisover-unmarshal
Add support for SMTP auto-discovery authentication
2024-11-16 22:10:51 +01:00
f296f53c1e
Add support for SMTP auto-discovery authentication
Extended the `UnmarshalString` function in `auth.go` to recognize "auto", "autodiscover", and "autodiscovery" as `SMTPAuthAutoDiscover`. Corresponding test cases were also added to `auth_test.go` to ensure proper functionality.
2024-11-16 21:58:58 +01:00
c37ed7c723
Merge pull request #373 from wneessen/feature/smtp-auth-autoselect
Add SMTP authentication auto-discovery
2024-11-16 21:51:23 +01:00
d8df26bbc8
Fix regression 2024-11-16 21:46:46 +01:00
427e8fd1ed
Format code block consistently
Refactor the `preferList` definition in `client.go` for improved readability and consistency. This change ensures the code aligns with standard formatting practices.
2024-11-16 21:41:32 +01:00
6d3640a166
Fix auth type auto-discovery and add test cases
Refactor the auth type initialization to prevent incorrect assignments and handle empty supported lists. Added comprehensive test cases to verify auto-discovery selection of the strongest authentication method and ensure robustness against empty or invalid input.
2024-11-16 21:38:29 +01:00
ac9117dc50
Add SMTP authentication auto-discovery
Implemented a mechanism to automatically discover and select the strongest supported SMTP authentication type. This feature simplifies the authentication process for users and enhances security by prioritizing stronger mechanisms based on server capabilities. Corresponding tests and documentation have been updated.
2024-11-16 14:29:34 +01:00
4 changed files with 106 additions and 1 deletions

22
auth.go
View file

@ -136,6 +136,21 @@ const (
//
// https://datatracker.ietf.org/doc/html/rfc7677
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
// SMTPAuthAutoDiscover is a mechanism that dynamically discovers all authentication mechanisms
// supported by the SMTP server and selects the strongest available one.
//
// This type simplifies authentication by automatically negotiating the most secure mechanism
// offered by the server, based on a predefined security ranking. For instance, mechanisms like
// SCRAM-SHA-256(-PLUS) or XOAUTH2 are prioritized over weaker mechanisms such as CRAM-MD5 or PLAIN.
//
// The negotiation process ensures that mechanisms requiring additional capabilities (e.g.,
// SCRAM-SHA-X-PLUS with TLS channel binding) are only selected when the necessary prerequisites
// are in place, such as an active TLS-secured connection.
//
// By automating mechanism selection, SMTPAuthAutoDiscover minimizes configuration effort while
// maximizing security and compatibility with a wide range of SMTP servers.
SMTPAuthAutoDiscover SMTPAuthType = "AUTODISCOVER"
)
// SMTP Auth related static errors
@ -170,12 +185,19 @@ var (
// ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
// authentication type.
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
// ErrNoSupportedAuthDiscovered is returned when the SMTP Auth AutoDiscover process fails to identify
// any supported authentication mechanisms offered by the server.
ErrNoSupportedAuthDiscovered = errors.New("SMTP Auth autodiscover was not able to detect a supported " +
"authentication mechanism")
)
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
func (sa *SMTPAuthType) UnmarshalString(value string) error {
switch strings.ToLower(value) {
case "auto", "autodiscover", "autodiscovery":
*sa = SMTPAuthAutoDiscover
case "cram-md5", "crammd5", "cram":
*sa = SMTPAuthCramMD5
case "custom":

View file

@ -12,6 +12,9 @@ func TestSMTPAuthType_UnmarshalString(t *testing.T) {
authString string
expected SMTPAuthType
}{
{"AUTODISCOVER: auto", "auto", SMTPAuthAutoDiscover},
{"AUTODISCOVER: autodiscover", "autodiscover", SMTPAuthAutoDiscover},
{"AUTODISCOVER: autodiscovery", "autodiscovery", SMTPAuthAutoDiscover},
{"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5},
{"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5},
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},

View file

@ -1100,7 +1100,16 @@ func (c *Client) auth() error {
return fmt.Errorf("server does not support SMTP AUTH")
}
switch c.smtpAuthType {
authType := c.smtpAuthType
if c.smtpAuthType == SMTPAuthAutoDiscover {
discoveredType, err := c.authTypeAutoDiscover(smtpAuthType)
if err != nil {
return err
}
authType = discoveredType
}
switch authType {
case SMTPAuthPlain:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
@ -1172,6 +1181,36 @@ func (c *Client) auth() error {
return nil
}
func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) {
if supported == "" {
return "", ErrNoSupportedAuthDiscovered
}
preferList := []SMTPAuthType{
SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1,
SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin,
}
if !c.isEncrypted {
preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5}
}
mechs := strings.Split(supported, " ")
for _, item := range preferList {
if sliceContains(mechs, string(item)) {
return item, nil
}
}
return "", ErrNoSupportedAuthDiscovered
}
func sliceContains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// sendSingleMsg sends out a single message and returns an error if the transmission or
// delivery fails. It is invoked by the public Send methods.
//

View file

@ -2304,6 +2304,11 @@ func TestClient_auth(t *testing.T) {
name string
authType SMTPAuthType
}{
{"LOGIN via AUTODISCOVER", SMTPAuthAutoDiscover},
{"PLAIN via AUTODISCOVER", SMTPAuthAutoDiscover},
{"SCRAM-SHA-1 via AUTODISCOVER", SMTPAuthAutoDiscover},
{"SCRAM-SHA-256 via AUTODISCOVER", SMTPAuthAutoDiscover},
{"XOAUTH2 via AUTODISCOVER", SMTPAuthAutoDiscover},
{"CRAM-MD5", SMTPAuthCramMD5},
{"LOGIN", SMTPAuthLogin},
{"LOGIN-NOENC", SMTPAuthLoginNoEnc},
@ -2509,6 +2514,42 @@ func TestClient_auth(t *testing.T) {
})
}
func TestClient_authTypeAutoDiscover(t *testing.T) {
tests := []struct {
supported string
tls bool
expect SMTPAuthType
shouldFail bool
}{
{"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA256PLUS, false},
{"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA256, false},
{"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA1PLUS, false},
{"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA1, false},
{"LOGIN XOAUTH2 SCRAM-SHA-1-PLUS", false, SMTPAuthXOAUTH2, false},
{"PLAIN LOGIN CRAM-MD5", false, SMTPAuthCramMD5, false},
{"CRAM-MD5", false, SMTPAuthCramMD5, false},
{"PLAIN", true, SMTPAuthPlain, false},
{"LOGIN PLAIN", true, SMTPAuthPlain, false},
{"LOGIN PLAIN", false, "no secure mechanism", true},
{"", false, "supported list empty", true},
}
for _, tt := range tests {
t.Run("AutoDiscover selects the strongest auth type: "+string(tt.expect), func(t *testing.T) {
client := &Client{smtpAuthType: SMTPAuthAutoDiscover, isEncrypted: tt.tls}
authType, err := client.authTypeAutoDiscover(tt.supported)
if err != nil && !tt.shouldFail {
t.Fatalf("failed to auto discover auth type: %s", err)
}
if tt.shouldFail && err == nil {
t.Fatal("expected auto discover to fail")
}
if !tt.shouldFail && authType != tt.expect {
t.Errorf("expected strongest auth type: %s, got: %s", tt.expect, authType)
}
})
}
}
func TestClient_Send(t *testing.T) {
message := testMessage(t)
t.Run("connect and send email", func(t *testing.T) {