mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-06-21 15:37:34 +02:00
lib: consolidate SASL authentication code
SASL and OAuth2 authentication logic was duplicated across the IMAP worker and the SMTP sender code. Both implementations handled XOAUTH2, OAUTHBEARER, and token caching independently, making maintenance difficult and bug fixes error-prone. Move this shared logic into lib/auth where it can be reused by all backends. The IMAP worker no longer needs its own OAuth structs and configuration parsing since it can now rely on the common implementation also used by the SMTP sender. Signed-off-by: Robin Jarry <robin@jarry.cc> Reviewed-by: Simon Martin <simon@nasilyan.com>
This commit is contained in:
@@ -206,7 +206,7 @@ func sendHelper(composer *app.Composer, header *mail.Header, uri *url.URL, domai
|
||||
}
|
||||
sender, err := send.NewSender(
|
||||
composer.Worker(), uri, domain,
|
||||
from, rcpts,
|
||||
from, rcpts, composer.Account().Name(),
|
||||
folders, requestDSN,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -159,7 +159,7 @@ func (b Bounce) Execute(args []string) error {
|
||||
msg.Envelope.MessageId, addresses)
|
||||
|
||||
if sender, err = send.NewSender(acct.Worker(), uri,
|
||||
domain, config.From, rcpts, nil, false); err != nil {
|
||||
domain, config.From, rcpts, acct.Name(), nil, false); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func ParseScheme(uri *url.URL) (protocol string, mech string, err error) {
|
||||
protocol = ""
|
||||
mech = "plain"
|
||||
if uri.Scheme != "" {
|
||||
parts := strings.Split(uri.Scheme, "+")
|
||||
if len(parts) == 0 {
|
||||
return "", "", fmt.Errorf("Unknown scheme %s", uri.Scheme)
|
||||
}
|
||||
protocol = parts[0]
|
||||
parts = slices.Delete(parts, 0, 1)
|
||||
i := slices.Index(parts, "insecure")
|
||||
if i != -1 {
|
||||
protocol += "+insecure"
|
||||
parts = slices.Delete(parts, i, i+1)
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
mech = strings.Join(parts, "+")
|
||||
}
|
||||
}
|
||||
return protocol, mech, nil
|
||||
}
|
||||
|
||||
func NewSaslClient(mech string, uri *url.URL, acct string) (sasl.Client, error) {
|
||||
var saslClient sasl.Client
|
||||
|
||||
user := uri.User.Username()
|
||||
password, _ := uri.User.Password()
|
||||
|
||||
switch mech {
|
||||
case "", "none":
|
||||
saslClient = nil
|
||||
case "login":
|
||||
saslClient = sasl.NewLoginClient(user, password)
|
||||
case "plain":
|
||||
saslClient = sasl.NewPlainClient("", user, password)
|
||||
case "oauthbearer", "xoauth2":
|
||||
q := uri.Query()
|
||||
o := oauth2.Config{
|
||||
ClientID: q.Get("client_id"),
|
||||
ClientSecret: q.Get("client_secret"),
|
||||
Scopes: strings.Split(q.Get("scope"), " "),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: q.Get("token_endpoint"),
|
||||
},
|
||||
}
|
||||
password, err := GetAccessToken(&o, acct, mech, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mech == "xoauth2" {
|
||||
saslClient = NewXoauth2Client(user, password)
|
||||
} else {
|
||||
saslClient = sasl.NewOAuthBearerClient(
|
||||
&sasl.OAuthBearerOptions{
|
||||
Username: uri.User.Username(),
|
||||
Token: password,
|
||||
},
|
||||
)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported auth mechanism %q", mech)
|
||||
}
|
||||
return saslClient, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func exchangeRefreshToken(o *oauth2.Config, refreshToken string) (*oauth2.Token, error) {
|
||||
token := new(oauth2.Token)
|
||||
token.RefreshToken = refreshToken
|
||||
token.TokenType = "Bearer"
|
||||
return o.TokenSource(context.TODO(), token).Token()
|
||||
}
|
||||
|
||||
func tokenCachePath(account, mech string) string {
|
||||
return xdg.CachePath("aerc", account+"-"+mech+".token")
|
||||
}
|
||||
|
||||
func saveRefreshToken(refreshToken, account, mech string) error {
|
||||
p := tokenCachePath(account, mech)
|
||||
if err := os.MkdirAll(path.Dir(p), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, []byte(refreshToken), 0o600)
|
||||
}
|
||||
|
||||
func getRefreshToken(account, mech string) (string, error) {
|
||||
buf, err := os.ReadFile(tokenCachePath(account, mech))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func GetAccessToken(
|
||||
o *oauth2.Config, account, mech, password string,
|
||||
) (string, error) {
|
||||
if o.Endpoint.TokenURL == "" {
|
||||
return password, nil
|
||||
}
|
||||
usedCache := false
|
||||
if r, err := getRefreshToken(account, mech); err == nil && len(r) > 0 {
|
||||
password = string(r)
|
||||
usedCache = true
|
||||
}
|
||||
|
||||
token, err := exchangeRefreshToken(o, password)
|
||||
if err != nil {
|
||||
if usedCache {
|
||||
return "", fmt.Errorf("%w: try deleting %s",
|
||||
err, tokenCachePath(account, mech))
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if err := saveRefreshToken(token.RefreshToken, account, mech); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// This code is derived from the go-sasl library.
|
||||
//
|
||||
// Copyright (c) 2016 emersion
|
||||
// Copyright (c) 2022, Oracle and/or its affiliates.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
// An XOAUTH2 error.
|
||||
type Xoauth2Error struct {
|
||||
Status string `json:"status"`
|
||||
Schemes string `json:"schemes"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// Implements error.
|
||||
func (err *Xoauth2Error) Error() string {
|
||||
return fmt.Sprintf("XOAUTH2 authentication error (%v)", err.Status)
|
||||
}
|
||||
|
||||
const XoauthMechanism string = "XOAUTH2"
|
||||
|
||||
type xoauth2Client struct {
|
||||
Username string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (a *xoauth2Client) Start() (mech string, ir []byte, err error) {
|
||||
mech = XoauthMechanism
|
||||
ir = []byte("user=" + a.Username + "\x01auth=Bearer " + a.Token + "\x01\x01")
|
||||
return
|
||||
}
|
||||
|
||||
func (a *xoauth2Client) Next(challenge []byte) ([]byte, error) {
|
||||
// Server sent an error response
|
||||
xoauth2Err := &Xoauth2Error{}
|
||||
if err := json.Unmarshal(challenge, xoauth2Err); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, xoauth2Err
|
||||
}
|
||||
}
|
||||
|
||||
// An implementation of the XOAUTH2 authentication mechanism, as
|
||||
// described in https://developers.google.com/gmail/xoauth2_protocol.
|
||||
func NewXoauth2Client(username, token string) sasl.Client {
|
||||
return &xoauth2Client{username, token}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-sasl"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OAuthBearer struct {
|
||||
OAuth2 *oauth2.Config
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
func (c *OAuthBearer) ExchangeRefreshToken(refreshToken string) (*oauth2.Token, error) {
|
||||
token := new(oauth2.Token)
|
||||
token.RefreshToken = refreshToken
|
||||
token.TokenType = "Bearer"
|
||||
return c.OAuth2.TokenSource(context.TODO(), token).Token()
|
||||
}
|
||||
|
||||
func (c *OAuthBearer) Authenticate(username string, password string, client *client.Client) error {
|
||||
if ok, err := client.SupportAuth(sasl.OAuthBearer); err != nil || !ok {
|
||||
return fmt.Errorf("OAuthBearer not supported %w", err)
|
||||
}
|
||||
|
||||
if c.OAuth2.Endpoint.TokenURL != "" {
|
||||
token, err := c.ExchangeRefreshToken(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
password = token.AccessToken
|
||||
}
|
||||
|
||||
saslClient := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
|
||||
Username: username,
|
||||
Token: password,
|
||||
})
|
||||
|
||||
return client.Authenticate(saslClient)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package send
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -11,31 +10,6 @@ import (
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func parseScheme(uri *url.URL) (protocol string, auth string, err error) {
|
||||
protocol = ""
|
||||
auth = "plain"
|
||||
if uri.Scheme != "" {
|
||||
parts := strings.Split(uri.Scheme, "+")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
protocol = parts[0]
|
||||
case 2:
|
||||
if parts[1] == "insecure" {
|
||||
protocol = uri.Scheme
|
||||
} else {
|
||||
protocol = parts[0]
|
||||
auth = parts[1]
|
||||
}
|
||||
case 3:
|
||||
protocol = parts[0] + "+" + parts[1]
|
||||
auth = parts[2]
|
||||
default:
|
||||
return "", "", fmt.Errorf("Unknown scheme %s", uri.Scheme)
|
||||
}
|
||||
}
|
||||
return protocol, auth, nil
|
||||
}
|
||||
|
||||
func GetMessageIdHostname(sendWithHostname bool, from *mail.Address) (string, error) {
|
||||
if sendWithHostname {
|
||||
return os.Hostname()
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package send
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib"
|
||||
)
|
||||
|
||||
func newSaslClient(auth string, uri *url.URL) (sasl.Client, error) {
|
||||
var saslClient sasl.Client
|
||||
switch auth {
|
||||
case "":
|
||||
fallthrough
|
||||
case "none":
|
||||
saslClient = nil
|
||||
case "login":
|
||||
password, _ := uri.User.Password()
|
||||
saslClient = sasl.NewLoginClient(uri.User.Username(), password)
|
||||
case "plain":
|
||||
password, _ := uri.User.Password()
|
||||
saslClient = sasl.NewPlainClient("", uri.User.Username(), password)
|
||||
case "oauthbearer":
|
||||
q := uri.Query()
|
||||
oauth2 := &oauth2.Config{}
|
||||
if q.Get("token_endpoint") != "" {
|
||||
oauth2.ClientID = q.Get("client_id")
|
||||
oauth2.ClientSecret = q.Get("client_secret")
|
||||
oauth2.Scopes = []string{q.Get("scope")}
|
||||
oauth2.Endpoint.TokenURL = q.Get("token_endpoint")
|
||||
}
|
||||
password, _ := uri.User.Password()
|
||||
bearer := lib.OAuthBearer{
|
||||
OAuth2: oauth2,
|
||||
Enabled: true,
|
||||
}
|
||||
if bearer.OAuth2.Endpoint.TokenURL != "" {
|
||||
token, err := bearer.ExchangeRefreshToken(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password = token.AccessToken
|
||||
}
|
||||
saslClient = sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
|
||||
Username: uri.User.Username(),
|
||||
Token: password,
|
||||
})
|
||||
case "xoauth2":
|
||||
q := uri.Query()
|
||||
oauth2 := &oauth2.Config{}
|
||||
if q.Get("token_endpoint") != "" {
|
||||
oauth2.ClientID = q.Get("client_id")
|
||||
oauth2.ClientSecret = q.Get("client_secret")
|
||||
oauth2.Scopes = []string{q.Get("scope")}
|
||||
oauth2.Endpoint.TokenURL = q.Get("token_endpoint")
|
||||
}
|
||||
password, _ := uri.User.Password()
|
||||
bearer := lib.Xoauth2{
|
||||
OAuth2: oauth2,
|
||||
Enabled: true,
|
||||
}
|
||||
if bearer.OAuth2.Endpoint.TokenURL != "" {
|
||||
token, err := bearer.ExchangeRefreshToken(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password = token.AccessToken
|
||||
}
|
||||
saslClient = lib.NewXoauth2Client(uri.User.Username(), password)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported auth mechanism %s", auth)
|
||||
}
|
||||
return saslClient, nil
|
||||
}
|
||||
+4
-3
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/auth"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
)
|
||||
|
||||
@@ -17,10 +18,10 @@ import (
|
||||
// sender when finished.
|
||||
func NewSender(
|
||||
worker *types.Worker, uri *url.URL, domain string,
|
||||
from *mail.Address, rcpts []*mail.Address,
|
||||
from *mail.Address, rcpts []*mail.Address, account string,
|
||||
copyTo []string, requestDSN bool,
|
||||
) (io.WriteCloser, error) {
|
||||
protocol, auth, err := parseScheme(uri)
|
||||
protocol, mech, err := auth.ParseScheme(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,7 +30,7 @@ func NewSender(
|
||||
|
||||
switch protocol {
|
||||
case "smtp", "smtp+insecure", "smtps":
|
||||
w, err = newSmtpSender(protocol, auth, uri, domain, from, rcpts, requestDSN)
|
||||
w, err = newSmtpSender(protocol, mech, uri, domain, from, rcpts, account, requestDSN)
|
||||
case "jmap":
|
||||
w, err = newJmapSender(worker, from, rcpts, copyTo)
|
||||
case "":
|
||||
|
||||
+5
-3
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/auth"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/pkg/errors"
|
||||
@@ -81,8 +82,9 @@ func (s *smtpSender) Close() error {
|
||||
}
|
||||
|
||||
func newSmtpSender(
|
||||
protocol string, auth string, uri *url.URL, domain string,
|
||||
from *mail.Address, rcpts []*mail.Address, requestDSN bool,
|
||||
protocol string, mech string, uri *url.URL, domain string,
|
||||
from *mail.Address, rcpts []*mail.Address, account string,
|
||||
requestDSN bool,
|
||||
) (io.WriteCloser, error) {
|
||||
var err error
|
||||
var conn *smtp.Client
|
||||
@@ -101,7 +103,7 @@ func newSmtpSender(
|
||||
return nil, errors.Wrap(err, "Connection failed")
|
||||
}
|
||||
|
||||
saslclient, err := newSaslClient(auth, uri)
|
||||
saslclient, err := auth.NewSaslClient(mech, uri, account)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
//
|
||||
// This code is derived from the go-sasl library.
|
||||
//
|
||||
// Copyright (c) 2016 emersion
|
||||
// Copyright (c) 2022, Oracle and/or its affiliates.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-sasl"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// An XOAUTH2 error.
|
||||
type Xoauth2Error struct {
|
||||
Status string `json:"status"`
|
||||
Schemes string `json:"schemes"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// Implements error.
|
||||
func (err *Xoauth2Error) Error() string {
|
||||
return fmt.Sprintf("XOAUTH2 authentication error (%v)", err.Status)
|
||||
}
|
||||
|
||||
type xoauth2Client struct {
|
||||
Username string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (a *xoauth2Client) Start() (mech string, ir []byte, err error) {
|
||||
mech = "XOAUTH2"
|
||||
ir = []byte("user=" + a.Username + "\x01auth=Bearer " + a.Token + "\x01\x01")
|
||||
return
|
||||
}
|
||||
|
||||
func (a *xoauth2Client) Next(challenge []byte) ([]byte, error) {
|
||||
// Server sent an error response
|
||||
xoauth2Err := &Xoauth2Error{}
|
||||
if err := json.Unmarshal(challenge, xoauth2Err); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, xoauth2Err
|
||||
}
|
||||
}
|
||||
|
||||
// An implementation of the XOAUTH2 authentication mechanism, as
|
||||
// described in https://developers.google.com/gmail/xoauth2_protocol.
|
||||
func NewXoauth2Client(username, token string) sasl.Client {
|
||||
return &xoauth2Client{username, token}
|
||||
}
|
||||
|
||||
type Xoauth2 struct {
|
||||
OAuth2 *oauth2.Config
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
func (c *Xoauth2) ExchangeRefreshToken(refreshToken string) (*oauth2.Token, error) {
|
||||
token := new(oauth2.Token)
|
||||
token.RefreshToken = refreshToken
|
||||
token.TokenType = "Bearer"
|
||||
return c.OAuth2.TokenSource(context.TODO(), token).Token()
|
||||
}
|
||||
|
||||
func SaveRefreshToken(refreshToken string, acct string) error {
|
||||
p := xdg.CachePath("aerc", acct+"-xoauth2.token")
|
||||
_ = os.MkdirAll(xdg.CachePath("aerc"), 0o700)
|
||||
|
||||
return os.WriteFile(
|
||||
p,
|
||||
[]byte(refreshToken),
|
||||
0o600,
|
||||
)
|
||||
}
|
||||
|
||||
func GetRefreshToken(acct string) ([]byte, error) {
|
||||
p := xdg.CachePath("aerc", acct+"-xoauth2.token")
|
||||
return os.ReadFile(p)
|
||||
}
|
||||
|
||||
func (c *Xoauth2) Authenticate(
|
||||
username string,
|
||||
password string,
|
||||
account string,
|
||||
client *client.Client,
|
||||
) error {
|
||||
if ok, err := client.SupportAuth("XOAUTH2"); err != nil || !ok {
|
||||
return fmt.Errorf("Xoauth2 not supported %w", err)
|
||||
}
|
||||
|
||||
if c.OAuth2.Endpoint.TokenURL != "" {
|
||||
usedCache := false
|
||||
if r, err := GetRefreshToken(account); err == nil && len(r) > 0 {
|
||||
password = string(r)
|
||||
usedCache = true
|
||||
}
|
||||
|
||||
token, err := c.ExchangeRefreshToken(password)
|
||||
if err != nil {
|
||||
if usedCache {
|
||||
return fmt.Errorf("try removing cached refresh token. %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
password = token.AccessToken
|
||||
if err := SaveRefreshToken(token.RefreshToken, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
saslClient := NewXoauth2Client(username, password)
|
||||
|
||||
return client.Authenticate(saslClient)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"git.sr.ht/~rjarry/aerc/worker/lib"
|
||||
"git.sr.ht/~rjarry/aerc/worker/middleware"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func (w *IMAPWorker) handleConfigure(msg *types.Configure) error {
|
||||
@@ -23,50 +22,9 @@ func (w *IMAPWorker) handleConfigure(msg *types.Configure) error {
|
||||
return err
|
||||
}
|
||||
|
||||
w.config.scheme = u.Scheme
|
||||
if before, ok := strings.CutSuffix(w.config.scheme, "+insecure"); ok {
|
||||
w.config.scheme = before
|
||||
w.config.insecure = true
|
||||
}
|
||||
w.config.provider = w.providerFromURL(u.Host)
|
||||
w.config.url = u
|
||||
|
||||
if before, ok := strings.CutSuffix(w.config.scheme, "+oauthbearer"); ok {
|
||||
w.config.scheme = before
|
||||
w.config.oauthBearer.Enabled = true
|
||||
q := u.Query()
|
||||
|
||||
oauth2 := &oauth2.Config{}
|
||||
if q.Get("token_endpoint") != "" {
|
||||
oauth2.ClientID = q.Get("client_id")
|
||||
oauth2.ClientSecret = q.Get("client_secret")
|
||||
oauth2.Scopes = []string{q.Get("scope")}
|
||||
oauth2.Endpoint.TokenURL = q.Get("token_endpoint")
|
||||
}
|
||||
w.config.oauthBearer.OAuth2 = oauth2
|
||||
}
|
||||
|
||||
if before, ok := strings.CutSuffix(w.config.scheme, "+xoauth2"); ok {
|
||||
w.config.scheme = before
|
||||
w.config.xoauth2.Enabled = true
|
||||
q := u.Query()
|
||||
|
||||
oauth2 := &oauth2.Config{}
|
||||
if q.Get("token_endpoint") != "" {
|
||||
oauth2.ClientID = q.Get("client_id")
|
||||
oauth2.ClientSecret = q.Get("client_secret")
|
||||
oauth2.Scopes = []string{q.Get("scope")}
|
||||
oauth2.Endpoint.TokenURL = q.Get("token_endpoint")
|
||||
}
|
||||
w.config.xoauth2.OAuth2 = oauth2
|
||||
}
|
||||
|
||||
w.config.addr = u.Host
|
||||
if !strings.ContainsRune(w.config.addr, ':') {
|
||||
w.config.addr += ":" + w.config.scheme
|
||||
}
|
||||
|
||||
w.config.provider = w.providerFromURL(w.config.addr)
|
||||
|
||||
w.config.user = u.User
|
||||
w.config.folders = msg.Config.Folders
|
||||
w.config.headers = msg.Config.Headers
|
||||
w.config.headersExclude = msg.Config.HeadersExclude
|
||||
|
||||
+40
-30
@@ -5,13 +5,14 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib"
|
||||
"git.sr.ht/~rjarry/aerc/lib/auth"
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
// connect establishes a new tcp connection to the imap server, logs in and
|
||||
@@ -24,7 +25,17 @@ func (w *IMAPWorker) connect() (*client.Client, error) {
|
||||
c *client.Client
|
||||
)
|
||||
|
||||
conn, err = newTCPConn(w.config.addr, w.config.connection_timeout)
|
||||
protocol, mech, err := auth.ParseScheme(w.config.url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := w.config.url.Host
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr += ":" + protocol
|
||||
}
|
||||
|
||||
conn, err = newTCPConn(addr, w.config.connection_timeout)
|
||||
if conn == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,22 +55,24 @@ func (w *IMAPWorker) connect() (*client.Client, error) {
|
||||
}
|
||||
}
|
||||
|
||||
serverName, _, _ := net.SplitHostPort(w.config.addr)
|
||||
serverName, _, _ := net.SplitHostPort(addr)
|
||||
tlsConfig := &tls.Config{ServerName: serverName}
|
||||
|
||||
switch w.config.scheme {
|
||||
protocol, insecure := strings.CutSuffix(protocol, "+insecure")
|
||||
|
||||
switch protocol {
|
||||
case "imap":
|
||||
c, err = client.New(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !w.config.insecure {
|
||||
if !insecure {
|
||||
if err = c.StartTLS(tlsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case "imaps":
|
||||
if w.config.insecure {
|
||||
if insecure {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
@@ -68,36 +81,33 @@ func (w *IMAPWorker) connect() (*client.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown IMAP scheme %s", w.config.scheme)
|
||||
return nil, fmt.Errorf("Unknown IMAP scheme %s", protocol)
|
||||
}
|
||||
|
||||
c.ErrorLog = log.ErrorLogger()
|
||||
|
||||
if w.config.user != nil {
|
||||
username := w.config.user.Username()
|
||||
|
||||
// TODO: 2nd parameter false if no password is set. ask for it
|
||||
// if unset.
|
||||
password, _ := w.config.user.Password()
|
||||
|
||||
if w.config.oauthBearer.Enabled {
|
||||
if err := w.config.oauthBearer.Authenticate(
|
||||
username, password, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if w.config.xoauth2.Enabled {
|
||||
if err := w.config.xoauth2.Authenticate(
|
||||
username, password, w.config.name, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if plain, err := c.SupportAuth("PLAIN"); err != nil {
|
||||
if w.config.url.User != nil && mech == "" {
|
||||
if plain, err := c.SupportAuth("PLAIN"); err != nil {
|
||||
return nil, err
|
||||
} else if plain {
|
||||
auth := sasl.NewPlainClient("", username, password)
|
||||
if err := c.Authenticate(auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err := c.Login(username, password); err != nil {
|
||||
mech = "plain"
|
||||
} else {
|
||||
mech = "login"
|
||||
}
|
||||
}
|
||||
|
||||
saslClient, err := auth.NewSaslClient(mech, w.config.url, w.config.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if saslClient != nil {
|
||||
if ok, err := c.SupportAuth(strings.ToUpper(mech)); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
return nil, fmt.Errorf("%s auth not supported", mech)
|
||||
}
|
||||
err = c.Authenticate(saslClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"git.sr.ht/~rjarry/aerc/worker/handlers"
|
||||
"git.sr.ht/~rjarry/aerc/worker/imap/extensions"
|
||||
@@ -52,16 +51,11 @@ type imapClient struct {
|
||||
|
||||
type imapConfig struct {
|
||||
name string
|
||||
scheme string
|
||||
insecure bool
|
||||
addr string
|
||||
url *url.URL
|
||||
provider imapProvider
|
||||
user *url.Userinfo
|
||||
headers []string
|
||||
headersExclude []string
|
||||
folders []string
|
||||
oauthBearer lib.OAuthBearer
|
||||
xoauth2 lib.Xoauth2
|
||||
idle_timeout time.Duration
|
||||
idle_debounce time.Duration
|
||||
reconnect_maxwait time.Duration
|
||||
|
||||
Reference in New Issue
Block a user