WIP: feat(email): Microsoft 365 (Outlook) support via Microsoft Graph #334

Closed
codemonkey76 wants to merge 9 commits from codemonkey76/feat/microsoft-graph-email into main
codemonkey76 commented 2026-06-01 09:20:26 +02:00 (Migrated from github.com)

Summary

Adds first-class Microsoft 365 / Outlook email support via the Microsoft Graph API. Closes #159. Microsoft has disabled Basic Auth for Exchange Online, so Office 365 work/school accounts can't connect over IMAP/SMTP today; Graph works even on tenants that disable IMAP.

A single dispatch point — _graph_backend(account_id, owner) — returns a GraphBackend for accounts whose oauth_provider is "microsoft", and None otherwise, so every email route keeps its existing IMAP path as the default branch. GraphBackend returns the same dict shapes as the IMAP sync helpers, so route handlers and the frontend stay backend-agnostic.

⚠️ Stacked on #237 (Google OAuth2 for email). This branch builds on #237's OAuth scaffolding (the oauth_* columns, state signing, callback pattern), so the diff includes #237's commits. Please merge #237 first, then rebase this; or review the top 4 commits in isolation. The Microsoft-specific work is commits a29c51c → 3eff44b.

What's included

  • OAuth connect — Microsoft identity authorize/callback routes, token refresh with rotation, /me address+name import. Reuses #237's oauth_* columns (no new migration).
  • Read — folders, list (unread/sender filters + $count), read, mark-read.
  • Send — reuses the existing MIME builder, posted as base64 to Graph /sendMail (Graph files it in Sent Items, so the IMAP append is skipped).
  • Write — archive, move, trash, permanent delete, attachment list/download.
  • No .env required — Client ID/secret/tenant are entered in the UI (an "App credentials" panel in the account form), stored in settings.json with the secret encrypted; env vars remain a fallback. Also restores #237's Google "Connect" button, which had been orphaned when the Email tab was refactored into "Manage in Integrations".
  • Pollers skip Graph accounts cleanly (Graph-backed AI triage is a noted follow-up).
  • Tests — tests/test_email_graph.py (14 cases, mocked HTTP, no network).

Setup (operator, one-time)

Register an Azure app (Web platform), add the redirect URI shown in the UI, and grant delegated Graph permissions: offline_access, User.Read, Mail.ReadWrite, Mail.Send. Then enter Client ID/secret in the account's OAuth panel — no rebuild needed.

Test plan

  • Connected a real Office 365 account via the Microsoft consent flow; inbox lists + opens messages through Graph
  • Existing IMAP password accounts unaffected (default backend branch)
  • tests/test_email_graph.py passes
  • Send / archive / delete on a live account (reviewer)

Notes

  • Issue #158 (clarifying Outlook basic-auth error messages) is intentionally out of scope — separate task.
  • Folder/flag semantics differ on Graph (well-known folder names, isRead, no \Answered); common cases are mapped, \Answered is a no-op for Graph.

🤖 Generated with Claude Code

## Summary Adds first-class **Microsoft 365 / Outlook** email support via the **Microsoft Graph API**. Closes #159. Microsoft has disabled Basic Auth for Exchange Online, so Office 365 work/school accounts can't connect over IMAP/SMTP today; Graph works even on tenants that disable IMAP. A single dispatch point — `_graph_backend(account_id, owner)` — returns a `GraphBackend` for accounts whose `oauth_provider` is `"microsoft"`, and `None` otherwise, so every email route keeps its existing IMAP path as the default branch. `GraphBackend` returns the **same dict shapes** as the IMAP sync helpers, so route handlers and the frontend stay backend-agnostic. > ⚠️ **Stacked on #237** (Google OAuth2 for email). This branch builds on #237's OAuth scaffolding (the `oauth_*` columns, state signing, callback pattern), so the diff includes #237's commits. **Please merge #237 first**, then rebase this; or review the top 4 commits in isolation. The Microsoft-specific work is commits `a29c51c` → `3eff44b`. ## What's included - **OAuth connect** — Microsoft identity authorize/callback routes, token refresh with rotation, `/me` address+name import. Reuses #237's `oauth_*` columns (no new migration). - **Read** — folders, list (unread/sender filters + `$count`), read, mark-read. - **Send** — reuses the existing MIME builder, posted as base64 to Graph `/sendMail` (Graph files it in Sent Items, so the IMAP append is skipped). - **Write** — archive, move, trash, permanent delete, attachment list/download. - **No `.env` required** — Client ID/secret/tenant are entered in the UI (an "App credentials" panel in the account form), stored in `settings.json` with the secret encrypted; env vars remain a fallback. Also restores #237's Google "Connect" button, which had been orphaned when the Email tab was refactored into "Manage in Integrations". - **Pollers** skip Graph accounts cleanly (Graph-backed AI triage is a noted follow-up). - **Tests** — `tests/test_email_graph.py` (14 cases, mocked HTTP, no network). ## Setup (operator, one-time) Register an Azure app (Web platform), add the redirect URI shown in the UI, and grant delegated Graph permissions: `offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`. Then enter Client ID/secret in the account's OAuth panel — no rebuild needed. ## Test plan - [x] Connected a real Office 365 account via the Microsoft consent flow; inbox lists + opens messages through Graph - [x] Existing IMAP password accounts unaffected (default backend branch) - [x] `tests/test_email_graph.py` passes - [ ] Send / archive / delete on a live account (reviewer) ## Notes - Issue #158 (clarifying Outlook basic-auth error messages) is intentionally **out of scope** — separate task. - Folder/flag semantics differ on Graph (well-known folder names, `isRead`, no `\Answered`); common cases are mapped, `\Answered` is a no-op for Graph. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
codemonkey76 commented 2026-06-01 09:25:38 +02:00 (Migrated from github.com)

Review-order note: this PR is stacked on #237 (Google OAuth2 for email), which is still open. It reuses #237's OAuth scaffolding — the oauth_* columns, signed OAuth state, and callback pattern — so the diff currently includes #237's commits.

Suggested path:

  1. Merge #237 first.
  2. I'll then rebase this onto main so the diff is just the 4 Microsoft commits (a29c51c → 3eff44b) and take it out of draft.

Keeping it as a draft until then. The Microsoft-specific work is self-contained in those 4 commits if you'd like an early look. It also restores #237's "Connect with Google" button, which the earlier Email-tab → "Manage in Integrations" refactor had orphaned.

**Review-order note:** this PR is **stacked on #237** (Google OAuth2 for email), which is still open. It reuses #237's OAuth scaffolding — the `oauth_*` columns, signed OAuth state, and callback pattern — so the diff currently includes #237's commits. Suggested path: 1. Merge #237 first. 2. I'll then rebase this onto `main` so the diff is just the 4 Microsoft commits (`a29c51c` → `3eff44b`) and take it out of draft. Keeping it as a draft until then. The Microsoft-specific work is self-contained in those 4 commits if you'd like an early look. It also restores #237's "Connect with Google" button, which the earlier Email-tab → "Manage in Integrations" refactor had orphaned.
Owner

||Closing — out of scope.

||Closing — out of scope.
sleepy closed this pull request 2026-06-01 19:32:48 +02:00

Pull request closed

Sign in to join this conversation.
No description provided.