odysseus-dev/odysseus · error · RuntimeError
Google OAuth token unavailable — reconnect the account
Error message
Google OAuth token unavailable — reconnect the account
What it means
RuntimeError raised inside the SMTP auth path of _send_smtp_message when the account uses oauth_provider=='google' but _get_valid_google_token returns falsy — i.e. no stored access token, no usable expiry, and a failed refresh via _refresh_google_token (revoked, expired refresh token, or never-stored credentials).
Source
Thrown at routes/email_helpers.py:174
return raw
port = int(cfg.get("smtp_port") or 465)
if port == 587:
return "starttls"
return "ssl"
def _send_smtp_message(cfg: dict, from_addr: str, recipients: list[str], message: str | bytes, timeout: int = 30) -> None:
"""Send through SMTP using the configured transport security mode."""
host = cfg["smtp_host"]
port = int(cfg.get("smtp_port") or 465)
user = cfg.get("smtp_user") or ""
password = cfg.get("smtp_password") or ""
def _auth_smtp(smtp):
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")
smtp.ehlo()
smtp.auth("XOAUTH2", lambda challenge=None: _xoauth2_raw(user, token), initial_response_ok=True)
elif user and password:
smtp.login(user, password)
security = _smtp_security_mode(cfg)
if security == "ssl":
with smtplib.SMTP_SSL(host, port, timeout=timeout) as smtp:
_auth_smtp(smtp)
smtp.sendmail(from_addr, recipients, message)
return
with smtplib.SMTP(host, port, timeout=timeout) as smtp:
if security == "starttls":
smtp.starttls()
_auth_smtp(smtp)
smtp.sendmail(from_addr, recipients, message)View on GitHub (pinned to f9235ebbf1)
Solutions
- Reconnect the Google account in Settings → Integrations to mint a new refresh token.
- Verify the stored oauth refresh token decrypts and exists in secret storage for this account_id.
- If the account should use plain SMTP auth, clear oauth_provider on the EmailAccount row so the user/password branch runs.
- Re-run the OAuth flow with the gmail.send scope granted and ensure offline access is approved.
Example fix
# before cfg['oauth_provider'] = 'google' # but tokens revoked # after: re-auth and persist fresh tokens new_tokens = run_google_oauth_flow() # scope gmail.send, prompt=consent store_account_tokens(account_id, new_tokens)
Defensive patterns
Strategy: try-catch
Try / catch
try { send_smtp(cfg, msg) } except RuntimeError as e:
if 'OAuth token unavailable' in str(e):
prompt_reconnect_google(account_id)
else: raise Prevention
- Surface a 'reconnect Google' action to the user when this fires instead of retrying.
- Periodically validate stored OAuth tokens with a cheap API call.
- Never leave oauth_provider='google' on rows whose tokens were cleared.
When it happens
Trigger: Sending mail through a Google OAuth account after the refresh token was revoked (password change, security checkup, app removed in Google Account), after the account was disconnected but oauth_provider still says 'google', or when decryption/refresh of stored tokens fails.
Common situations: Google revoked the long-lived refresh token due to 6-month inactivity in testing mode; token secret storage key rotated so decrypt returns empty; user reconnected SMTP password auth but the row kept oauth_provider='google'.
Related errors
- Google OAuth token unavailable — reconnect the account in Se
- Unsubscribe failed
- Email reminder was not sent
- Email account {cfg.get('account_name') or account} has no SM
- No SMTP-capable email account configured
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/9340f26402dc3a9e.
Report an issue: GitHub.