feat(email): add Google OAuth2 for Google Workspace / .edu IMAP & SMTP #237
Closed
FruitPnchSamuraiG wants to merge 5 commits from
FruitPnchSamuraiG/feat/google-oauth2-imap-smtp into main
pull from: FruitPnchSamuraiG/feat/google-oauth2-imap-smtp
merge into: sleepy:main
sleepy:main
sleepy:dev
sleepy:fix/950-webhook-reminder
sleepy:fix/948-skill-import-urls
sleepy:fix/947-deepseek-provider
sleepy:fix/946-slash-autocomplete
sleepy:fix/940-google-oauth2-email
sleepy:fix/939-plan-mode
sleepy:fix/938-security-hardening-suite
sleepy:fix/937-workspace-confinement
sleepy:fix/936-edit-file-code-nav-tools
sleepy:fix/effective-mode-undefined
sleepy:fix/async-generator-500
sleepy:refactor/split-small-routes-779
sleepy:refactor/split-session-routes-778
sleepy:refactor/split-llm-core-700
sleepy:refactor/split-chat-routes-724
sleepy:refactor/split-model-routes-701
sleepy:refactor/split-skills-routes-777
sleepy:refactor/split-document-routes-769
sleepy:refactor/split-ai-interaction-722
sleepy:refactor/split-cookbook-routes-775
sleepy:refactor/split-builtin-actions-723
sleepy:refactor/split-task-scheduler-774
sleepy:refactor/tool-execution-split-667
sleepy:fix/rag-health-coordinator-756
sleepy:fix/agent-core-missing-round-loop
sleepy:fix/rag-health-check-756
sleepy:fix/missing-readmes-782
sleepy:fix/search-files-split-772
sleepy:fix/dedup-search-impl-771
sleepy:fix/mcp-integrations-split-781
sleepy:fix/cookbook-helpers-split-785
sleepy:fix/search-cross-feature-776
sleepy:fix/graceful-llm-degradation-770
sleepy:fix/dedup-research-handler-773
sleepy:fix/cross-route-coupling-780
sleepy:fix/skills-typed-request-762
sleepy:fix/chat-stream-pydantic-731
sleepy:fix/chat-handler-imports-737
sleepy:fix/public-llm-exports-730
sleepy:fix/dedup-search-content-784
sleepy:fix/dedup-stream-parse-735
sleepy:fix/stream-error-boundary-734
sleepy:fix/typed-event-names-786
sleepy:fix/dedup-js-utilities-792
sleepy:fix/skills-index-lookup-761
sleepy:fix/xss-charname-innerhtml-788
sleepy:fix/coderunner-document-write-789
sleepy:fix/ai-interaction-thread-safety-729
sleepy:fix/client-side-api-key-regex-793
sleepy:fix/dead-loadingtext-796
sleepy:fix/active-streams-eviction-736
sleepy:fix/builtin-actions-sqlite-orm-732
sleepy:fix/llm-core-thread-safety-709
sleepy:fix/singleton-thread-safety-768
sleepy:fix/video-upload-multimodal-826
sleepy:fix/mic-button-audio-825
sleepy:fix/multimodal-capabilities-827
sleepy:fix/dedup-system-msg-717
sleepy:fix/service-dead-calls-750
sleepy:fix/db-session-context-manager-757
sleepy:fix/dedup-llmconfig-710
sleepy:fix/keyword-lists-764-v2
sleepy:fix/keyword-lists-764
sleepy:fix/llm-cache-ttl-708
sleepy:fix/validate-config-711
sleepy:fix/totp-constant-time-689
sleepy:fix/admin-tools-constant-681
sleepy:fix/dedup-truncate-670
sleepy:fix/threadsafe-extractions-audit-754
sleepy:fix/dedup-json-io-767
sleepy:fix/695-deduplicate-admin-check
sleepy:fix/upload-auth-dedup-765
sleepy:fix/705-deduplicate-network-constants
sleepy:fix/deduplicate-chunking-749
sleepy:fix/760-deduplicate-embed
sleepy:fix/dedup-validation-759
sleepy:fix/748-deduplicate-tokenization
sleepy:fix/add-readmes-728-742-782
sleepy:fix/key-file-permissions-706
sleepy:fix/embedding-retry-766
sleepy:fix/deterministic-doc-ids-753
sleepy:fix/search-facade-783
sleepy:fix/remove-dead-memory-746
sleepy:fix/remove-rag-manager-747
sleepy:fix/dead-docstring-763
sleepy:refactor/split-tool-schemas-666
sleepy:refactor/split-agent-loop-664
sleepy:refactor/split-tool-implementations-665
sleepy:fix/mark-stopped-500-663
sleepy:fix/streamingtts-scope-662
sleepy:fix/code-block-tool-parsing-661
No reviewers
Labels
Clear labels
area:chat
area:core
area:llm
area:routes
area:tools
bug
Something isn't working
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
refactor
wontfix
This will not be worked on
No labels
area:chat
area:core
area:llm
area:routes
area:tools
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
refactor
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
sleepy/odysseus!237
Loading…
Reference in a new issue
No description provided.
Delete branch "FruitPnchSamuraiG/feat/google-oauth2-imap-smtp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Google deprecated basic-auth (username + password) IMAP/SMTP access for all Google Workspace accounts in May 2025. This silently broke email for anyone using a
.edu, company Google, or any org-managed Google account — the error is[AUTHENTICATIONFAILED] Invalid credentialswith no clear explanation.Personal
@gmail.comaccounts still work via App Passwords, but Workspace accounts (which includes most university emails) cannot use App Passwords either — they require OAuth2.This affects a significant slice of users: students, researchers, and professionals whose primary email is org-managed Google.
Solution
Full Google OAuth2 (XOAUTH2) support for IMAP and SMTP. Users connect their account via a standard Google consent flow — no password ever stored.
Changes
Backend
core/database.py:oauth_provider,oauth_access_token,oauth_refresh_token,oauth_token_expiry,display_namecolumns onEmailAccount+ idempotent SQLite migrationroutes/email_helpers.py: XOAUTH2 auth in_imap_connect()and_send_smtp_message(), automatic token refresh via_refresh_google_token(), OAuth fields in_get_email_config()routes/email_routes.py:GET /api/email/oauth/google/authorize— builds and redirects to Google consent URLGET /api/email/oauth/google/callback— exchanges code for tokens, auto-fills IMAP/SMTP settings + display name from Google userinfo_smtp_ready()recognises OAuth accounts as send-capable_deliver()background closureFrom:header usesdisplay_nameviaemail.utils.formataddr()Frontend
static/js/settings.js: "Google Workspace / .edu" provider preset, "Connect with Google" button, success/error banner on OAuth redirect back, "Display Name" field for all accountsstatic/js/document.js:_accountCanSend()treats OAuth accounts as SMTP-capable (previously fell back silently to another account)Setup (self-hosted)
Users need to create their own Google Cloud OAuth app (one-time, ~5 min):
http://localhost:7000/api/email/oauth/google/callback.env:docker compose up -d --buildIMAP host, SMTP host, username, and display name are auto-filled after authorization. Tokens refresh automatically.
Test plan
.edu(Google Workspace) inbox loads via XOAUTH2 IMAPFrom:header_refresh_google_token()direct callThis is a useful direction for Workspace/.edu email, but I don't think it is merge-ready yet. Main blockers I see:\n\n- OAuth
stateis justaccount_id, and the callback route does not require the logged-in user or re-check ownership before writing tokens. Please use a nonce/signed state tied to the initiating user/account and validate it in the callback before updating the row.\n- The callback currently writes OAuth tokens todb.get(EmailAccount, account_id)without owner scoping. In multi-user mode that bypasses the account ownership protections used elsewhere.\n-static/js/settings.jshas smart quotes in HTML attributes around the new Name/Email/Display Name inputs (for exampleclass=”settings-row”,id=”eaf-name”). That will break DOM lookup/event handling in browsers; these need normal ASCII quotes.\n- The added send-config/error logs include account name, SMTP host/user, owner, and raw error text. I would keep OAuth/send logs less identifying because email addresses and provider errors are sensitive.\n\nThe XOAUTH2 helper path itself looks plausible, so I would keep this PR alive, but fix the OAuth state/ownership flow and the frontend quote issue before merge.Thank you for the thorough review; pushed a follow-up commit (
d2e436d) addressing every point:1. OAuth state — nonce/signed + validated in callback
State is now an HMAC-SHA256 token (keyed with the app secret from
secret_storage) encodingaccount_id + owner + random nonce, verified withhmac.compare_digestin the callback before any token write. The bareaccount_idstate is gone. Unit-tested: valid states round-trip, while tampered/forged/garbage states are all rejected.2. Callback ownership scoping
The callback now pulls
ownerfrom the verified state and re-checks it againstEmailAccount.ownerbefore writing tokens, matching the ownership guards used elsewhere. Single-user mode (owner == "") still accepts any account, consistent with_assert_owns_account.3. Smart quotes in settings.js
Fixed — the Name/Email/Display Name input rows now use plain ASCII quotes. Confirmed in-browser that the fields edit/save correctly (the
getElementByIdlookups resolve now).4. Sensitive data in logs
Stripped account name, SMTP host/user, owner, and raw provider error text from the send-config and OAuth logs. Failures now surface as generic error codes in the redirect rather than raw exception strings.
Re-verified end to end after the changes: IMAP receive and SMTP send both still work over XOAUTH2 (tested on a real Google Workspace /
.eduaccount), and reconnecting updates tokens without creating a duplicate account.On a personal note: it's been a journey, from watching LWIAY to the life-in-Japan transition. Thank you for all of it. I'm greatly inspired by the way you go about life, and grateful for the feedback and for keeping the PR alive. Happy to make any further changes you'd like before merge.
Pushed a couple more commits: added
tests/test_email_oauth.py(14 pure-function tests covering signed-state round-trip + tamper/forgery rejection,_smtp_readyfor OAuth accounts, and the XOAUTH2 SASL framing), and did a small self-review pass — consolidated the XOAUTH2 frame to a single helper (dropped an unused base64 variant + an inlined duplicate) and switched a couple ofraise Exceptioncalls toRuntimeErrorto match the convention insrc/. All 14 new tests + the existing 28 security regressions pass, and IMAP/SMTP are re-verified live against a real Workspace account. Still happy to adjust anything further.FYI — I've built Microsoft 365 / Outlook support (via Microsoft Graph) on top of this branch in #334 (draft). It reuses the OAuth scaffolding added here, so #334 is stacked behind this PR. No action needed; just flagging that #237 unblocks a second email-OAuth feature. 🙂
Pull request closed