update
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
"gomail/internal/crypto"
|
||||
"gomail/internal/db"
|
||||
"gomail/internal/imapclient"
|
||||
"gomail/internal/oauth2"
|
||||
)
|
||||
|
||||
const dialTimeout = 20 * time.Second
|
||||
@@ -39,27 +38,19 @@ type OAuth2Credential struct {
|
||||
// connections" story without IDLE/pooling machinery this pass doesn't build
|
||||
// yet, so simplicity wins: connect, do the one operation, disconnect. A
|
||||
// later pass can add persistent connections if per-operation latency matters.
|
||||
// IMAPProvider is password-authenticated only — db.ProviderIMAP accounts
|
||||
// are always created via LinkIMAPAccount with AuthTypePassword.
|
||||
// Gmail/M365 accounts (previously OAuth2-over-IMAP here) now use
|
||||
// GmailAPIProvider/GraphAPIProvider instead — see link.go's ProviderFor.
|
||||
type IMAPProvider struct {
|
||||
account *db.LinkedAccount
|
||||
mk *crypto.MasterKey
|
||||
database *db.DB // needed to persist a refreshed access token
|
||||
oauthConfig *oauth2.Config // nil for password-auth accounts
|
||||
account *db.LinkedAccount
|
||||
mk *crypto.MasterKey
|
||||
}
|
||||
|
||||
func NewIMAPProvider(account *db.LinkedAccount, mk *crypto.MasterKey) *IMAPProvider {
|
||||
return &IMAPProvider{account: account, mk: mk}
|
||||
}
|
||||
|
||||
// NewIMAPProviderOAuth2 is used for accounts.AuthTypeOAuth2 — the caller
|
||||
// supplies the provider's oauth2.Config (built from operator-configured
|
||||
// Client ID/Secret) so a stored access token can be refreshed transparently
|
||||
// on expiry. database is used to persist the refreshed token — refreshing
|
||||
// silently in memory only would force a re-refresh on every single
|
||||
// operation instead of once per real expiry.
|
||||
func NewIMAPProviderOAuth2(account *db.LinkedAccount, mk *crypto.MasterKey, database *db.DB, oauthConfig *oauth2.Config) *IMAPProvider {
|
||||
return &IMAPProvider{account: account, mk: mk, database: database, oauthConfig: oauthConfig}
|
||||
}
|
||||
|
||||
func (p *IMAPProvider) connect(ctx context.Context) (*imapclient.Client, error) {
|
||||
addr := fmt.Sprintf("%s:%d", p.account.IMAPHost, p.account.IMAPPort)
|
||||
// InsecureSkipVerify is a known gap, not a silent one: real ACME
|
||||
@@ -85,13 +76,6 @@ func (p *IMAPProvider) connect(ctx context.Context) (*imapclient.Client, error)
|
||||
}
|
||||
}
|
||||
|
||||
if p.account.AuthType == db.AuthTypeOAuth2 {
|
||||
if err := p.loginOAuth2(ctx, client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
plain, err := crypto.Decrypt(p.mk, p.account.ID, "linked-account-cred", p.account.CredentialEnc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypting stored credential: %w", err)
|
||||
@@ -106,49 +90,6 @@ func (p *IMAPProvider) connect(ctx context.Context) (*imapclient.Client, error)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// loginOAuth2 decrypts the stored OAuth2 credential, transparently refreshes
|
||||
// it if expired (persisting the new token so the next call doesn't have to
|
||||
// refresh again), and authenticates via SASL XOAUTH2.
|
||||
func (p *IMAPProvider) loginOAuth2(ctx context.Context, client *imapclient.Client) error {
|
||||
plain, err := crypto.Decrypt(p.mk, p.account.ID, "linked-account-cred", p.account.CredentialEnc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypting stored OAuth2 credential: %w", err)
|
||||
}
|
||||
var cred OAuth2Credential
|
||||
if err := json.Unmarshal(plain, &cred); err != nil {
|
||||
return fmt.Errorf("parsing stored OAuth2 credential: %w", err)
|
||||
}
|
||||
|
||||
if time.Now().UTC().After(cred.ExpiresAt) {
|
||||
if p.oauthConfig == nil {
|
||||
return fmt.Errorf("access token expired and no oauth2.Config available to refresh it")
|
||||
}
|
||||
newTok, err := p.oauthConfig.RefreshToken(ctx, cred.RefreshToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing OAuth2 token: %w", err)
|
||||
}
|
||||
cred.AccessToken = newTok.AccessToken
|
||||
cred.RefreshToken = newTok.RefreshToken
|
||||
cred.ExpiresAt = newTok.ExpiresAt
|
||||
|
||||
if p.database != nil {
|
||||
updated, err := json.Marshal(cred)
|
||||
if err == nil {
|
||||
if encUpdated, encErr := crypto.Encrypt(p.mk, p.account.ID, "linked-account-cred", updated); encErr == nil {
|
||||
p.database.Exec(`UPDATE linked_accounts SET credential_enc = ?, oauth_expires_at = ? WHERE id = ?`,
|
||||
encUpdated, cred.ExpiresAt, p.account.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sasl := oauth2.XOAUTH2SASLString(p.account.EmailAddress, cred.AccessToken)
|
||||
if err := client.LoginXOAUTH2(sasl); err != nil {
|
||||
return fmt.Errorf("XOAUTH2 login: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IMAPProvider) ListFolders(ctx context.Context) ([]Folder, error) {
|
||||
client, err := p.connect(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user