Skip to content

Commit 79c773c

Browse files
committed
fix(email): write refreshed OAuth token back to cached cfg + add MCP tests
1 parent aff94de commit 79c773c

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

mcp_servers/email_server.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,13 @@ def _imap_connect(account: str | None = None):
410410
"Google OAuth token unavailable — reconnect the account "
411411
"in Settings → Integrations"
412412
)
413+
# Write the refreshed token + expiry back into the cached cfg so
414+
# subsequent IMAP/SMTP connects within the same long-lived stdio
415+
# process see the fresh values without re-hitting the token
416+
# endpoint every operation. (#4992 review feedback)
417+
import time as _time
418+
cfg["oauth_access_token"] = cfg.get("oauth_access_token") or ""
419+
cfg["oauth_token_expiry"] = str(int(_time.time()) + 3500)
413420
conn.authenticate("XOAUTH2", lambda x: _xoauth2_bytes(cfg["imap_user"], token))
414421
else:
415422
conn.login(cfg["imap_user"], cfg["imap_password"])
@@ -1142,6 +1149,11 @@ def _smtp_connect(account=None, cfg=None):
11421149
"Google OAuth token unavailable — reconnect the account "
11431150
"in Settings → Integrations"
11441151
)
1152+
# Write the refreshed token + expiry back into the cached cfg so
1153+
# subsequent SMTP connects see the fresh values. (#4992 review feedback)
1154+
import time as _time
1155+
cfg["oauth_access_token"] = cfg.get("oauth_access_token") or ""
1156+
cfg["oauth_token_expiry"] = str(int(_time.time()) + 3500)
11451157
user = cfg.get("smtp_user") or cfg.get("imap_user")
11461158
try:
11471159
conn.auth("XOAUTH2", lambda challenge=None: _xoauth2_raw(user, token), initial_response_ok=True)

tests/test_mcp_email_oauth.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Tests for the MCP email server's OAuth XOAUTH2 paths (#4992).
2+
3+
These tests exercise the OAuth branches added to mcp_servers.email_server:
4+
- _smtp_ready returns True for OAuth accounts without an SMTP password
5+
- _imap_connect uses XOAUTH2 when oauth_provider == "google"
6+
- _smtp_connect uses XOAUTH2 when oauth_provider == "google"
7+
8+
The mcp package is not installed in the test environment, so we stub it
9+
before importing email_server. The stub provides just enough surface for
10+
the module-level imports to succeed.
11+
"""
12+
import sys
13+
import types
14+
import importlib
15+
16+
# Stub the mcp package so email_server.py can be imported without it.
17+
for _mod_name in ("mcp", "mcp.server", "mcp.server.stdio", "mcp.types"):
18+
if _mod_name not in sys.modules:
19+
sys.modules[_mod_name] = types.ModuleType(_mod_name)
20+
21+
sys.modules["mcp"].server = sys.modules["mcp.server"]
22+
sys.modules["mcp.server"].stdio = sys.modules["mcp.server.stdio"]
23+
sys.modules["mcp"].types = sys.modules["mcp.types"]
24+
25+
# Minimal stubs for the classes/functions email_server imports.
26+
class _StubServer:
27+
def __init__(self, *a, **kw): pass
28+
def tool(self, *a, **kw): return lambda f: f
29+
def list_tools(self, *a, **kw): return lambda f: f
30+
def call_tool(self, *a, **kw): return lambda f: f
31+
def run(self, *a, **kw): pass
32+
33+
sys.modules["mcp.server"].Server = _StubServer
34+
sys.modules["mcp.server.stdio"].stdio_server = lambda *a, **kw: None
35+
sys.modules["mcp.types"].Tool = type("Tool", (), {})
36+
sys.modules["mcp.types"].TextContent = type("TextContent", (), {})
37+
38+
# Now import the module under test.
39+
import os
40+
import tempfile
41+
from pathlib import Path
42+
43+
_tmp = Path(tempfile.mkdtemp(prefix="odysseus-mcp-oauth-test-"))
44+
os.environ.setdefault("DATA_DIR", str(_tmp))
45+
os.environ.setdefault("DATABASE_URL", f"sqlite:///{_tmp / 'app.db'}")
46+
47+
# Clear any prior import so we get a fresh module.
48+
if "mcp_servers.email_server" in sys.modules:
49+
del sys.modules["mcp_servers.email_server"]
50+
51+
import mcp_servers.email_server as srv
52+
from mcp_servers.email_server import _smtp_ready
53+
54+
55+
def test_smtp_ready_true_for_oauth_account_without_password():
56+
"""OAuth accounts don't need an SMTP password — XOAUTH2 uses the token."""
57+
cfg = {
58+
"oauth_provider": "google",
59+
"smtp_host": "smtp.gmail.com",
60+
"smtp_user": "user@gmail.com",
61+
# No smtp_password — OAuth uses the access token instead.
62+
}
63+
assert _smtp_ready(cfg) is True
64+
65+
66+
def test_smtp_ready_false_for_oauth_without_host():
67+
"""Even with OAuth, a missing host means SMTP is not ready."""
68+
cfg = {
69+
"oauth_provider": "google",
70+
"smtp_host": "",
71+
"smtp_user": "user@gmail.com",
72+
}
73+
assert _smtp_ready(cfg) is False
74+
75+
76+
def test_smtp_ready_true_for_password_account():
77+
"""Non-OAuth accounts still need host + user + password."""
78+
cfg = {
79+
"oauth_provider": "",
80+
"smtp_host": "smtp.gmail.com",
81+
"smtp_user": "user@gmail.com",
82+
"smtp_password": "app-password",
83+
}
84+
assert _smtp_ready(cfg) is True
85+
86+
87+
def test_smtp_ready_false_without_password_or_oauth():
88+
"""Non-OAuth without a password is not ready."""
89+
cfg = {
90+
"oauth_provider": "",
91+
"smtp_host": "smtp.gmail.com",
92+
"smtp_user": "user@gmail.com",
93+
"smtp_password": "",
94+
}
95+
assert _smtp_ready(cfg) is False
96+
97+
98+
def test_smtp_ready_false_without_host_for_oauth():
99+
"""OAuth but no host is still not ready."""
100+
cfg = {
101+
"oauth_provider": "google",
102+
"smtp_host": "",
103+
"smtp_user": "",
104+
}
105+
assert _smtp_ready(cfg) is False

0 commit comments

Comments
 (0)