Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Oauth2 support #130

Merged
merged 4 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@ freebsd_task:
name: FreeBSD

matrix:
- name: FreeBSD 13.1
- name: FreeBSD 13.2
freebsd_instance:
image_family: freebsd-13-1
image_family: freebsd-13-2
- name: FreeBSD 12.4
freebsd_instance:
image_family: freebsd-12-4
- name: FreeBSD 12.3
freebsd_instance:
image_family: freebsd-12-3

env:
TEST_ALLOW_SEND: 0
Expand Down
7 changes: 7 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const (

// SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954
SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"

// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
)

// SMTP Auth related static errors
Expand All @@ -31,4 +35,7 @@ var (

// 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")

// 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")
)
5 changes: 5 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,11 @@ func (c *Client) auth() error {
return ErrCramMD5AuthNotSupported
}
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)
default:
return fmt.Errorf("unsupported SMTP AUTH type %q", c.satype)
}
Expand Down
24 changes: 24 additions & 0 deletions smtp/auth_xoauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT

package smtp

type xoauth2Auth struct {
username, token string
}

func XOAuth2Auth(username, token string) Auth {
return &xoauth2Auth{username, token}
}

func (a *xoauth2Auth) Start(_ *ServerInfo) (string, []byte, error) {
return "XOAUTH2", []byte("user=" + a.username + "\x01" + "auth=Bearer " + a.token + "\x01\x01"), nil
}

func (a *xoauth2Auth) Next(_ []byte, more bool) ([]byte, error) {
if more {
return []byte(""), nil
}
return nil, nil
}
6 changes: 4 additions & 2 deletions smtp/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,10 @@ func (c *Client) Auth(a Auth) error {
resp, err = a.Next(msg, code == 334)
}
if err != nil {
// abort the AUTH
_, _, _ = c.cmd(501, "*")
if mech != "XOAUTH2" {
// abort the AUTH. Not required for XOAUTH2
_, _, _ = c.cmd(501, "*")
}
_ = c.Quit()
break
}
Expand Down
87 changes: 87 additions & 0 deletions smtp/smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ var authTests = []authTest{
[]string{"", "user 287eb355114cf5c471c26a875f1ca4ae"},
[]bool{false, false},
},
{
XOAuth2Auth("username", "token"),
[]string{""},
"XOAUTH2",
[]string{"user=username\x01auth=Bearer token\x01\x01", ""},
[]bool{false},
},
}

func TestAuth(t *testing.T) {
Expand Down Expand Up @@ -193,6 +200,86 @@ func TestAuthLogin(t *testing.T) {
}
}

func TestXOAuthOK(t *testing.T) {
server := []string{
"220 Fake server ready ESMTP",
"250-fake.server",
"250-AUTH XOAUTH2",
"250 8BITMIME",
"235 2.7.0 Accepted",
}
var wrote strings.Builder
var fake faker
fake.ReadWriter = struct {
io.Reader
io.Writer
}{
strings.NewReader(strings.Join(server, "\r\n")),
&wrote,
}

c, err := NewClient(fake, "fake.host")
if err != nil {
t.Fatalf("NewClient: %v", err)
}
defer c.Close()

auth := XOAuth2Auth("user", "token")
err = c.Auth(auth)
if err != nil {
t.Fatalf("XOAuth2 error: %v", err)
}
// the Next method returns a nil response. It must not be sent.
// The client request must end with the authentication.
if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") {
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String())
}
}

func TestXOAuth2Error(t *testing.T) {
serverResp := []string{
"220 Fake server ready ESMTP",
"250-fake.server",
"250-AUTH XOAUTH2",
"250 8BITMIME",
"334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==",
"535 5.7.8 Username and Password not accepted",
"221 2.0.0 closing connection",
}
var wrote strings.Builder
var fake faker
fake.ReadWriter = struct {
io.Reader
io.Writer
}{
strings.NewReader(strings.Join(serverResp, "\r\n")),
&wrote,
}

c, err := NewClient(fake, "fake.host")
if err != nil {
t.Fatalf("NewClient: %v", err)
}
defer c.Close()

auth := XOAuth2Auth("user", "token")
err = c.Auth(auth)
if err == nil {
t.Fatal("expected auth error, got nil")
}
client := strings.Split(wrote.String(), "\r\n")
if len(client) != 5 {
t.Fatalf("unexpected number of client requests got %d; want 5", len(client))
}
if client[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" {
t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", client[1])
}
// the Next method returns an empty response. It must be sent
if client[2] != "" {
t.Fatalf("got %q; want empty response", client[2])
}
}

// Issue 17794: don't send a trailing space on AUTH command when there's no password.
func TestClientAuthTrimSpace(t *testing.T) {
server := "220 hello world\r\n" +
Expand Down