add Oauth2 support

fixes #129
This commit is contained in:
Nicola Murino 2023-05-28 14:31:04 +02:00
parent 13c8d0a32c
commit 6e4b348ccf
5 changed files with 204 additions and 5 deletions

View file

@ -19,6 +19,9 @@ const (
// SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954 // SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5" SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
) )
// SMTP Auth related static errors // SMTP Auth related static errors
@ -31,4 +34,7 @@ var (
// ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema // ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema
ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5") ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5")
// 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")
) )

View file

@ -32,6 +32,9 @@ const (
// DefaultTLSMinVersion is the minimum TLS version required for the connection // DefaultTLSMinVersion is the minimum TLS version required for the connection
// Nowadays TLS1.2 should be the sane default // Nowadays TLS1.2 should be the sane default
DefaultTLSMinVersion = tls.VersionTLS12 DefaultTLSMinVersion = tls.VersionTLS12
// DefaultXOAuth2Variant is the default XOAuth2 variant
DefaultXOAuth2Variant = smtp.XOAuth2VariantGoogle
) )
// DSNMailReturnOption is a type to define which MAIL RET option is used when a DSN // DSNMailReturnOption is a type to define which MAIL RET option is used when a DSN
@ -135,6 +138,9 @@ type Client struct {
// user is the SMTP AUTH username // user is the SMTP AUTH username
user string user string
// xoauthVariant sets the client to use the provided XOAuth2Variant variant
xoauthVariant smtp.XOAuth2Variant
// dl enables the debug logging on the SMTP client // dl enables the debug logging on the SMTP client
dl bool dl bool
@ -198,6 +204,7 @@ func NewClient(h string, o ...Option) (*Client, error) {
port: DefaultPort, port: DefaultPort,
tlsconfig: &tls.Config{ServerName: h, MinVersion: DefaultTLSMinVersion}, tlsconfig: &tls.Config{ServerName: h, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy, tlspolicy: DefaultTLSPolicy,
xoauthVariant: DefaultXOAuth2Variant,
} }
// Set default HELO/EHLO hostname // Set default HELO/EHLO hostname
@ -415,6 +422,14 @@ func WithDialContextFunc(f DialContextFunc) Option {
} }
} }
// WithXOAuth2Variant tells the client to use the provided XOAuth2 variant
func WithXOAuth2Variant(v smtp.XOAuth2Variant) Option {
return func(c *Client) error {
c.xoauthVariant = v
return nil
}
}
// TLSPolicy returns the currently set TLSPolicy as string // TLSPolicy returns the currently set TLSPolicy as string
func (c *Client) TLSPolicy() string { func (c *Client) TLSPolicy() string {
return c.tlspolicy.String() return c.tlspolicy.String()
@ -430,6 +445,11 @@ func (c *Client) SetTLSPolicy(p TLSPolicy) {
c.tlspolicy = p c.tlspolicy = p
} }
// SetXOAuth2Variant overrides the current XOAuth2Variant with the given XOAuth2Variant value
func (c *Client) SetXOAuth2Variant(v smtp.XOAuth2Variant) {
c.xoauthVariant = v
}
// SetSSL tells the Client wether to use SSL or not // SetSSL tells the Client wether to use SSL or not
func (c *Client) SetSSL(s bool) { func (c *Client) SetSSL(s bool) {
c.ssl = s c.ssl = s
@ -658,6 +678,11 @@ func (c *Client) auth() error {
return ErrCramMD5AuthNotSupported return ErrCramMD5AuthNotSupported
} }
c.sa = smtp.CRAMMD5Auth(c.user, c.pass) c.sa = smtp.CRAMMD5Auth(c.user, c.pass)
case SMTPAuthXOAUTH2:
if !strings.Contains(sat, string(SMTPAuthXOAUTH2)) {
return ErrXOauth2AuthNotSupported
}
c.sa = smtp.XOAuth2Auth(c.user, c.pass, c.xoauthVariant)
default: default:
return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype) return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype)
} }

View file

@ -101,6 +101,7 @@ func TestNewClientWithOptions(t *testing.T) {
}, },
{"WithUsername()", WithUsername("test"), false}, {"WithUsername()", WithUsername("test"), false},
{"WithPassword()", WithPassword("test"), false}, {"WithPassword()", WithPassword("test"), false},
{"WithXOAuth2Variant()", WithXOAuth2Variant(smtp.XOAuth2VariantMicrosoft), false},
{"WithDSN()", WithDSN(), false}, {"WithDSN()", WithDSN(), false},
{"WithDSNMailReturnType()", WithDSNMailReturnType(DSNMailReturnFull), false}, {"WithDSNMailReturnType()", WithDSNMailReturnType(DSNMailReturnFull), false},
{"WithDSNMailReturnType() wrong option", WithDSNMailReturnType("FAIL"), true}, {"WithDSNMailReturnType() wrong option", WithDSNMailReturnType("FAIL"), true},
@ -390,6 +391,57 @@ func TestSetSMTPAuth(t *testing.T) {
} }
} }
// TestWithXOAuth2Variant tests the WithXOAuth2Variant() option for the NewClient() method
func TestWithXOAuth2Variant(t *testing.T) {
tests := []struct {
name string
value smtp.XOAuth2Variant
want string
sf bool
}{
{"Variant: Google", smtp.XOAuth2VariantGoogle, smtp.XOAuth2VariantGoogle.String(), false},
{"Variant: Microsoft", smtp.XOAuth2VariantMicrosoft, smtp.XOAuth2VariantMicrosoft.String(), false},
{"Variant: Invalid", -1, "Unknown XOAuth2 variant", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := NewClient(DefaultHost, WithXOAuth2Variant(tt.value))
if err != nil && !tt.sf {
t.Errorf("failed to create new client: %s", err)
return
}
if c.xoauthVariant.String() != tt.want {
t.Errorf("failed to set XOAuth2 variant. Want: %s, got: %s", tt.want, c.xoauthVariant)
}
})
}
}
// TestSetXOAuth2Variant tests the SetXOAuth2Variant method for the Client object
func TestSetXOAuth2Variant(t *testing.T) {
tests := []struct {
name string
value smtp.XOAuth2Variant
want smtp.XOAuth2Variant
}{
{"Google", smtp.XOAuth2VariantGoogle, smtp.XOAuth2VariantGoogle},
{"Google", smtp.XOAuth2VariantMicrosoft, smtp.XOAuth2VariantMicrosoft},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := NewClient(DefaultHost)
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
c.SetXOAuth2Variant(tt.value)
if c.xoauthVariant != tt.want {
t.Errorf("failed to set XOAuthVariant. Expected %s, got: %s", tt.want, c.xoauthVariant)
}
})
}
}
// TestWithDSN tests the WithDSN method for the Client object // TestWithDSN tests the WithDSN method for the Client object
func TestWithDSN(t *testing.T) { func TestWithDSN(t *testing.T) {
c, err := NewClient(DefaultHost, WithDSN()) c, err := NewClient(DefaultHost, WithDSN())

71
smtp/auth_xoauth2.go Normal file
View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
import "fmt"
// XOAuth2Variant describes a int alias for the different XOAuth2 variants we support
type XOAuth2Variant int
// Supported XOAuth2 variants
const (
// XOAuth2VariantGoogle is the Google variant for "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol
XOAuth2VariantGoogle XOAuth2Variant = iota
// XOAuth2VariantMicrosoft is the Microsoft variant for "XOAUTH2" SASL authentication mechanism.
// https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
XOAuth2VariantMicrosoft
)
// String is a standard method to convert a XOAuth2Variant into a printable format
func (v XOAuth2Variant) String() string {
switch v {
case XOAuth2VariantGoogle:
return "Google"
case XOAuth2VariantMicrosoft:
return "Microsoft"
default:
return "Unknown XOAuth2 variant"
}
}
type xoauth2Auth struct {
username, token string
variant XOAuth2Variant
}
func XOAuth2Auth(username, token string, variant XOAuth2Variant) Auth {
return &xoauth2Auth{username, token, variant}
}
func (a *xoauth2Auth) getToken() []byte {
return []byte("user=" + a.username + "\x01" + "auth=Bearer " + a.token + "\x01\x01")
}
func (a *xoauth2Auth) Start(_ *ServerInfo) (string, []byte, error) {
switch a.variant {
case XOAuth2VariantGoogle:
return "XOAUTH2", a.getToken(), nil
case XOAuth2VariantMicrosoft:
return "XOAUTH2", nil, nil
default:
return "", nil, fmt.Errorf("unsupported XOAuth2 variant %d", a.variant)
}
}
func (a *xoauth2Auth) Next(_ []byte, more bool) ([]byte, error) {
if more {
switch a.variant {
case XOAuth2VariantGoogle:
return []byte(""), nil
case XOAuth2VariantMicrosoft:
return a.getToken(), nil
default:
return nil, fmt.Errorf("unsupported XOAuth2 variant %d", a.variant)
}
}
return nil, nil
}

View file

@ -69,6 +69,20 @@ var authTests = []authTest{
[]string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}, []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"},
[]bool{false, false}, []bool{false, false},
}, },
{
XOAuth2Auth("username", "token", XOAuth2VariantGoogle),
[]string{""},
"XOAUTH2",
[]string{"user=username\x01auth=Bearer token\x01\x01", ""},
[]bool{false},
},
{
XOAuth2Auth("username", "token", XOAuth2VariantMicrosoft),
[]string{""},
"XOAUTH2",
[]string{"", "user=username\x01auth=Bearer token\x01\x01"},
[]bool{false},
},
} }
func TestAuth(t *testing.T) { func TestAuth(t *testing.T) {
@ -193,6 +207,37 @@ func TestAuthLogin(t *testing.T) {
} }
} }
func TestXOAuth2VariantToString(t *testing.T) {
v := XOAuth2VariantGoogle
if val := v.String(); val != "Google" {
t.Errorf("got %q; want Google", val)
}
v = XOAuth2VariantMicrosoft
if val := v.String(); val != "Microsoft" {
t.Errorf("got %q; want Microsoft", val)
}
v = XOAuth2Variant(-1)
if val := v.String(); val != "Unknown XOAuth2 variant" {
t.Errorf("got %q; want Unknown XOAuth2 variant", val)
}
}
func TestXOAuth2UnsupportendVariant(t *testing.T) {
serverInfo := &ServerInfo{
Name: "servename",
Auth: []string{"XOAUTH2"},
}
auth := &xoauth2Auth{"username", "token", XOAuth2Variant(-1)}
_, _, err := auth.Start(serverInfo)
if err == nil {
t.Error("expected error for auth.Start() using an unsupported variant")
}
_, err = auth.Next(nil, true)
if err == nil {
t.Error("expected error for auth.Next() using an unsupported variant")
}
}
// Issue 17794: don't send a trailing space on AUTH command when there's no password. // Issue 17794: don't send a trailing space on AUTH command when there's no password.
func TestClientAuthTrimSpace(t *testing.T) { func TestClientAuthTrimSpace(t *testing.T) {
server := "220 hello world\r\n" + server := "220 hello world\r\n" +