Skip to content

feat: Microsoft 365 OAuth2 IMAP support#219

Closed
wesm wants to merge 12 commits intomainfrom
microsoft-imap
Closed

feat: Microsoft 365 OAuth2 IMAP support#219
wesm wants to merge 12 commits intomainfrom
microsoft-imap

Conversation

@wesm
Copy link
Copy Markdown
Owner

@wesm wesm commented Mar 23, 2026

Summary

  • Add XOAUTH2 SASL authentication to the IMAP client for Microsoft 365 / Outlook.com
  • New internal/microsoft/ OAuth2 provider with Azure AD browser flow, PKCE (S256), and MS Graph /me email validation
  • Standalone add-o365 CLI command that auto-configures IMAP for outlook.office365.com
  • Sync routing and account removal updated to handle XOAUTH2 IMAP sources
  • Fully backward-compatible — existing IMAP password configs unchanged

New command

msgvault add-o365 user@outlook.com
msgvault add-o365 user@company.com --tenant my-tenant-id

Config

[microsoft]
client_id = "your-azure-app-client-id"
tenant_id = "common"  # optional, defaults to "common"

Requires an Azure AD app registration (free) with IMAP.AccessAsUser.All and User.Read delegated permissions, public client flow enabled.

Test plan

  • All existing tests pass (31 packages)
  • New unit tests: XOAUTH2 SASL format, AuthMethod config, Microsoft OAuth token storage/load/delete, MS Graph email validation (match, mismatch, UPN fallback)
  • go fmt, go vet clean
  • Smoke test: add-o365 --help works, add-o365 test@example.com shows correct config-missing error
  • Needs real-world testing: authorize against a real Outlook.com/M365 account with an Azure AD app registration, then sync-full

🤖 Generated with Claude Code

wesm and others added 12 commits March 23, 2026 16:57
Covers XOAUTH2 SASL auth in IMAP client, Microsoft OAuth2 provider,
add-o365 CLI command, and sync routing changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 bite-sized tasks: XOAUTH2 SASL client, auth method config, token
source in IMAP client, Microsoft OAuth manager, config section,
add-o365 CLI, sync routing, account removal, dependency, and final
verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… IMAP

Implements the XOAUTH2 SASL mechanism (sasl.Client interface) needed by
Microsoft Exchange Online IMAP, and adds the AuthMethod field to IMAP
Config for routing between password and xoauth2 authentication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bundles Microsoft OAuth2 browser flow + IMAP auto-configuration into a
single command. Configures outlook.office365.com with XOAUTH2 auth
method automatically after authorization succeeds.

Also includes remove-account Microsoft token cleanup from concurrent task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The MS Graph /me endpoint requires User.Read scope to return profile
data. Without it, the token validation step after OAuth authorization
would fail with HTTP 403.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Mar 23, 2026

roborev: Combined Review (2867856)

The PR introduces Microsoft 365 OAuth2 support, but requires critical
fixes to OAuth scope configuration and tenant persistence to function correctly.

High Severity

  • internal/microsoft/oauth.go:27
    ScopeIMAP is set to https:// outlook.office365.com/IMAP.AccessAsUser.All, but Exchange Online’s delegated IMAP scope is https://outlook.office.com/IMAP.AccessAsUser.All. Using the wrong resource URI will cause the OAuth flow to request the wrong permission and can prevent add -o365 from obtaining a usable IMAP token.
    Fix: Change the scope constant to the outlook.office.com resource and add a test that asserts the exact scope sent in the authorization request.

  • [internal/microsoft/oauth.go:33](/
    home/roborev/repos/msgvault/internal/microsoft/oauth.go)

    The requested scope set mixes an Outlook resource scope with User.Read from Microsoft Graph. Azure AD v2 token requests only allow scopes for a single resource (plus OIDC scopes like openid/offline _access), so this request can be rejected before the code ever reaches the /me validation call.
    Fix: Remove User.Read from this token request and validate the account from OIDC claims, or do a separate Graph authorization/token flow if /me is required.

Medium

Severity

  • cmd/msgvault/cmd/addo365.go:38, cmd/msgvault/cmd/sync.go:14
    9
    , [cmd/msgvault/cmd/syncfull.go:242](/home/roborev/repos/msgvault/cmd/msgvault/cmd/syncfull.
    go)

    add-o365 --tenant only affects the initial authorization flow; the chosen tenant is not stored with the account or token. Later syncs rebuild the Microsoft manager from global config/default common, so accounts onboarded with a custom tenant can stop refreshing or fail auth after the
    initial command finishes.
    Fix: Persist the tenant (and ideally the client ID) with the IMAP source or token file, and use those stored values when checking for tokens and constructing the refresh token source.

Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

@YourEconProf
Copy link
Copy Markdown

YourEconProf commented Mar 26, 2026

@wesm ,
I don't know what best practice is here, but it feels almost as if there might need to be a secondary README.md just for o365 accounts. I have it working for my o365 business account, and I'm documenting as i go.

First change for above is to change the format of config.toml:

[microsoft]
client_id = "your-azure-app-client-id"
tenant_id = "your-actual-tenant-id" 

as the tenant_id passed with --tenant when using add-o365 isn't saved to the account and expires 60 minutes later. Possible fixes are to either add the tenant_id to the db, or just explicitly add it in the config.toml (which is what I've done). The latter seems cleaner to me, and I think obviates having to use --tenant when creating the account. Though I haven't tested that yet.

I have succeeded with msgvault sync-full matt@o365acct.com --limit 100 and am now running without --limit

My next steps are to try to add a non-work o365 (i.e. hotmail) account.
Then I will try to have multiple o365 accounts included at once.

@YourEconProf
Copy link
Copy Markdown

@wesm my fork is located here

@wesm
Copy link
Copy Markdown
Owner Author

wesm commented Mar 27, 2026

closing in favor of #228

@wesm wesm closed this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants