odysseus-dev/odysseus · error · RuntimeError
Google OAuth token unavailable — reconnect the account in Se
Error message
Google OAuth token unavailable — reconnect the account in Settings → Integrations
What it means
RuntimeError raised during IMAP AUTHENTICATE when oauth_provider=='google' but _get_valid_google_token yields no token — stored access token absent/expired and the refresh attempt failed. The surrounding except shuts the socket down before propagating so failed auths don't leak descriptors.
Source
Thrown at routes/email_helpers.py:1234
)
# Connection mode:
# STARTTLS on → plain + upgrade
# STARTTLS off + port 993 → implicit SSL (IMAPS)
# STARTTLS off + any other port → plain (local Dovecot, custom ports)
# The last branch is critical: previously this fell into IMAP4_SSL
# for any non-STARTTLS port, which would fail the TLS handshake on
# plain local servers (Dovecot on 31143, etc.).
conn = _open_imap_connection(
cfg["imap_host"],
cfg["imap_port"],
starttls=bool(cfg.get("imap_starttls")),
timeout=timeout,
)
try:
if cfg.get("oauth_provider") == "google":
token = _get_valid_google_token(cfg.get("account_id"), cfg)
if not token:
raise RuntimeError("Google OAuth token unavailable — reconnect the account in Settings → Integrations")
conn.authenticate("XOAUTH2", lambda x: _xoauth2_bytes(cfg["imap_user"], token))
else:
conn.login(cfg["imap_user"], cfg["imap_password"])
except Exception:
# A failed AUTHENTICATE (e.g. an Office 365 app password on an
# MFA-enabled tenant, #3174, or an expired/revoked OAuth token)
# otherwise orphans the already-connected socket; close it before
# propagating so a misconfigured account can't leak one descriptor
# per retry / background poller pass.
try:
conn.shutdown()
except Exception:
pass
raise
return conn
from contextlib import contextmanagerView on GitHub (pinned to f9235ebbf1)
Solutions
- Reconnect the Google account via Settings → Integrations to obtain a fresh refresh token.
- If the account should use password auth instead, clear oauth_provider so conn.login() is used.
- Disable background polling for accounts whose OAuth creds are known-bad until reconnected.
Example fix
# before: poller crashes each pass
with _imap(account_id, owner=owner) as conn: inbox_poll(conn)
# after: skip unrecoverable auth failures
try:
with _imap(account_id, owner=owner) as conn: inbox_poll(conn)
except RuntimeError as e:
if 'OAuth token unavailable' in str(e):
mark_account_needs_reconnect(account_id) Defensive patterns
Strategy: try-catch
Try / catch
try:
with _imap(account_id, owner=owner) as conn: poll(conn)
except RuntimeError as e:
if 'OAuth token unavailable' in str(e):
disable_polling(account_id); notify_reconnect_needed(account_id)
else: raise Prevention
- Skip background polls for accounts flagged needs-reconnect instead of erroring every pass.
- Reconnect Google immediately after revoking/changing anything in the Google account.
- Monitor for this message to catch silent refresh-token death early.
When it happens
Trigger: Polling or reading an inbox of a Google OAuth account whose refresh token was revoked or expired, whose token row was deleted, or whose secret-storage decryption fails; any background inbox poll after the user disconnected Google in Settings.
Common situations: Google revoking refresh tokens on inactive OAuth apps in testing mode; secret storage key rotated between deployments; user removed app access from their Google account but the local account row still says google.
Related errors
- Google OAuth token unavailable — reconnect the account
- HTTP ${res.status}
- data.error || 'failed'
- data.error
- Cleanup failed
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/e7c806b2858fe59a.
Report an issue: GitHub.