diff --git a/routes/email_routes.py b/routes/email_routes.py index 690e1361f..26a44e2d6 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -4699,6 +4699,13 @@ def setup_email_routes(): cfg = _get_email_config(owner=owner) cfg["smtp_password"] = "***" if cfg["smtp_password"] else "" cfg["imap_password"] = "***" if cfg["imap_password"] else "" + # `_get_email_config` includes encrypted OAuth fields for the server's + # IMAP/SMTP helpers. They are implementation secrets, not client + # configuration, so keep them out of this browser-facing response just + # as the account-list endpoint does. + cfg.pop("oauth_access_token", None) + cfg.pop("oauth_refresh_token", None) + cfg.pop("oauth_token_expiry", None) # Include preferences from settings.json settings = _load_settings() cfg["email_auto_summarize"] = bool(settings.get("email_auto_summarize", False)) diff --git a/tests/test_email_oauth.py b/tests/test_email_oauth.py index 0399f5b32..4fad82739 100644 --- a/tests/test_email_oauth.py +++ b/tests/test_email_oauth.py @@ -746,3 +746,53 @@ async def test_account_list_response_does_not_expose_token_values(): assert acct["oauth_provider"] == "google" # status is exposed assert "oauth_access_token" not in acct # token value is not assert "oauth_refresh_token" not in acct + + +@pytest.mark.asyncio +async def test_config_response_does_not_expose_oauth_storage_fields(): + """The client-facing config route must not serialize the encrypted token + fields returned by the internal transport helper.""" + from routes.email_routes import setup_email_routes + from src.secret_storage import encrypt as _enc + + encrypted_access = _enc("ya29.internal_access_token") + encrypted_refresh = _enc("1//internal_refresh_token") + internal_cfg = { + "account_id": "acct-config", + "account_name": "Google Workspace", + "smtp_host": "smtp.gmail.com", + "smtp_port": 587, + "smtp_security": "starttls", + "smtp_user": "alice@example.edu", + "smtp_password": "", + "imap_host": "imap.gmail.com", + "imap_port": 993, + "imap_user": "alice@example.edu", + "imap_password": "", + "imap_starttls": False, + "from_address": "alice@example.edu", + "oauth_provider": "google", + "oauth_access_token": encrypted_access, + "oauth_refresh_token": encrypted_refresh, + "oauth_token_expiry": "4102444800", + "display_name": "Alice", + } + + router = setup_email_routes() + get_config = None + for route in router.routes: + if route.path == "/api/email/config" and "GET" in getattr(route, "methods", set()): + get_config = route.endpoint + break + assert get_config is not None, "email config route not found" + + with mock.patch("routes.email_routes._get_email_config", return_value=internal_cfg.copy()), \ + mock.patch("routes.email_routes._load_settings", return_value={}): + result = await get_config(owner="alice") + + assert result["oauth_provider"] == "google" + assert "oauth_access_token" not in result + assert "oauth_refresh_token" not in result + assert "oauth_token_expiry" not in result + assert encrypted_access not in json.dumps(result) + assert encrypted_refresh not in json.dumps(result)