{"record":{"id":"9340f26402dc3a9e","repo":"odysseus-dev/odysseus","slug":"google-oauth-token-unavailable-reconnect-the-acc","errorCode":null,"errorMessage":"Google OAuth token unavailable — reconnect the account","messagePattern":"Google OAuth token unavailable — reconnect the account","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"routes/email_helpers.py","lineNumber":174,"sourceCode":"        return raw\n    port = int(cfg.get(\"smtp_port\") or 465)\n    if port == 587:\n        return \"starttls\"\n    return \"ssl\"\n\n\ndef _send_smtp_message(cfg: dict, from_addr: str, recipients: list[str], message: str | bytes, timeout: int = 30) -> None:\n    \"\"\"Send through SMTP using the configured transport security mode.\"\"\"\n    host = cfg[\"smtp_host\"]\n    port = int(cfg.get(\"smtp_port\") or 465)\n    user = cfg.get(\"smtp_user\") or \"\"\n    password = cfg.get(\"smtp_password\") or \"\"\n\n    def _auth_smtp(smtp):\n        if cfg.get(\"oauth_provider\") == \"google\":\n            token = _get_valid_google_token(cfg.get(\"account_id\"), cfg)\n            if not token:\n                raise RuntimeError(\"Google OAuth token unavailable — reconnect the account\")\n            smtp.ehlo()\n            smtp.auth(\"XOAUTH2\", lambda challenge=None: _xoauth2_raw(user, token), initial_response_ok=True)\n        elif user and password:\n            smtp.login(user, password)\n\n    security = _smtp_security_mode(cfg)\n\n    if security == \"ssl\":\n        with smtplib.SMTP_SSL(host, port, timeout=timeout) as smtp:\n            _auth_smtp(smtp)\n            smtp.sendmail(from_addr, recipients, message)\n        return\n\n    with smtplib.SMTP(host, port, timeout=timeout) as smtp:\n        if security == \"starttls\":\n            smtp.starttls()\n        _auth_smtp(smtp)\n        smtp.sendmail(from_addr, recipients, message)","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/email_helpers.py#L156-L192","documentation":"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).","triggerScenarios":"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.","commonSituations":"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'.","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."],"exampleFix":"# before\ncfg['oauth_provider'] = 'google'  # but tokens revoked\n# after: re-auth and persist fresh tokens\nnew_tokens = run_google_oauth_flow()  # scope gmail.send, prompt=consent\nstore_account_tokens(account_id, new_tokens)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { send_smtp(cfg, msg) } except RuntimeError as e:\n    if 'OAuth token unavailable' in str(e):\n        prompt_reconnect_google(account_id)\n    else: raise","preventionTips":["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."],"tags":["oauth2","gmail","smtp","token-refresh","email"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}