diff --git a/auth.go b/auth.go index 66254ee..e4adfe6 100644 --- a/auth.go +++ b/auth.go @@ -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,6 +185,11 @@ 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 diff --git a/client.go b/client.go index 9b3251e..c301f45 100644 --- a/client.go +++ b/client.go @@ -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. // diff --git a/client_test.go b/client_test.go index 2ac8811..97289b8 100644 --- a/client_test.go +++ b/client_test.go @@ -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) {