Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion outlook_web/services/imap.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,14 @@ def fetch_and_detail_imap_with_server(
if status != "OK":
return {"success": True, "emails": [], "detail": None}

for i, (msg_id_str, raw_email) in enumerate(_parse_batch_fetch_response(all_data or [])):
raw_by_id = {msg_id_str: raw_email for msg_id_str, raw_email in _parse_batch_fetch_response(all_data or [])}

for i, msg_id in enumerate(paged_ids):
msg_id_str = msg_id.decode("ascii", errors="ignore").strip()
raw_email = raw_by_id.get(msg_id_str)
if raw_email is None:
continue

msg = email.message_from_bytes(raw_email)
body_preview = get_email_body(msg)
email_item = {
Expand Down
11 changes: 10 additions & 1 deletion outlook_web/services/verification_channel_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,22 @@ def extract_verification_for_outlook(
reverse=True,
)[0]

latest_id = str(latest.get("id") or "")
if channel.startswith("imap_"):
detail = channel_result.get("detail")
detail_id = str((detail or {}).get("id") or "")
if latest_id and detail_id and detail_id != latest_id:
detail = fetch_email_detail_for_channel(
account=account,
channel=channel,
message_id=latest_id,
proxy_url=proxy_url,
)
else:
detail = fetch_email_detail_for_channel(
account=account,
channel=channel,
message_id=latest.get("id", ""),
message_id=latest_id,
proxy_url=proxy_url,
)

Expand Down
26 changes: 26 additions & 0 deletions tests/test_imap_connection_reuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,32 @@ def test_imap_connection_created_only_once(self, mock_token, mock_imap_cls):
self.assertEqual(mock_imap_cls.call_count, 1)
self.assertEqual(mock_conn.authenticate.call_count, 1)

@patch("outlook_web.services.imap.imaplib.IMAP4_SSL")
@patch("outlook_web.services.imap.get_access_token_imap_result")
def test_batch_fetch_response_is_reordered_to_requested_latest_first(self, mock_token, mock_imap_cls):
from outlook_web.services.imap import fetch_and_detail_imap_with_server

mock_token.return_value = self._mock_token_result(True)
raw_old = _build_rfc822_bytes("Old code", "Your code is 990595")
raw_middle = _build_rfc822_bytes("Middle code", "Your code is 118658")
raw_new = _build_rfc822_bytes("New code", "Your code is 701280")
mock_conn = self._setup_imap_mock(mock_imap_cls, search_ids=[b"1", b"2", b"3", b"4", b"5"])
mock_conn.fetch.return_value = (
"OK",
[
((b"3 (RFC822)", raw_old), b")"),
((b"4 (RFC822)", raw_middle), b")"),
((b"5 (RFC822)", raw_new), b")"),
],
)

result = fetch_and_detail_imap_with_server("user@test.com", "cid", "rt", folder="inbox", top=3)

self.assertTrue(result.get("success"))
self.assertEqual([item["id"] for item in result.get("emails", [])], ["5", "4", "3"])
self.assertEqual(result.get("detail", {}).get("id"), "5")
self.assertIn("701280", result.get("detail", {}).get("body", ""))


if __name__ == "__main__":
unittest.main()
71 changes: 71 additions & 0 deletions tests/test_verification_extract_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,74 @@ def test_log_channel_is_ai_fallback_when_ai_is_used(self):

self.assertIn("_log_channel", result)
self.assertEqual(result["_log_channel"], "ai_fallback")

def test_imap_detail_mismatch_refetches_latest_message_detail(self):
"""IMAP 连接复用返回的 detail 与最新邮件不一致时,应按 latest.id 重新取详情。"""
with self.app.app_context():
from outlook_web.services import verification_channel_routing as vcr

fake_account = {
"id": 3,
"email": "imap@outlook.com",
"account_type": "outlook",
"provider": "outlook",
"group_id": None,
"preferred_verification_channel": "imap_new",
"client_id": "cid",
"refresh_token": "rt",
}

fake_channel_result = {
"success": True,
"emails": [
{
"id": "3",
"subject": "Old code",
"from": "OpenAI",
"date": "Tue, 19 May 2026 10:01:15 +0000",
},
{
"id": "5",
"subject": "New code",
"from": "OpenAI",
"date": "Tue, 19 May 2026 10:38:27 +0000",
},
],
"detail": {
"id": "3",
"subject": "Old code",
"from": "OpenAI",
"date": "Tue, 19 May 2026 10:01:15 +0000",
"body": "Your code is 990595",
},
}
latest_detail = {
"id": "5",
"subject": "New code",
"from": "OpenAI",
"date": "Tue, 19 May 2026 10:38:27 +0000",
"body": "Your code is 701280",
}

with (
patch.object(vcr, "build_verification_channel_plan", return_value=["imap_new"]),
patch.object(vcr, "fetch_emails_and_detail_for_channel", return_value=fake_channel_result),
patch.object(vcr, "fetch_email_detail_for_channel", return_value=latest_detail) as mock_fetch_detail,
patch(
"outlook_web.services.graph.get_access_token_graph_result",
return_value={"success": False},
),
patch("outlook_web.repositories.accounts.update_preferred_verification_channel"),
):
result = vcr.extract_verification_for_outlook(
account=fake_account,
resolved_policy={"code_regex": r"(?<!\d)\d{6}(?!\d)", "code_length": "6-6"},
code_source="all",
expected_field="verification_code",
)

self.assertTrue(result.get("success"))
self.assertEqual(result.get("data", {}).get("matched_email_id"), "5")
self.assertEqual(result.get("data", {}).get("verification_code"), "701280")
mock_fetch_detail.assert_called_once()
self.assertEqual(mock_fetch_detail.call_args.kwargs.get("message_id"), "5")