Files
odysseus/tests/test_imap_mailbox_quoting.py
T
Léo 4b4ae592da refactor(routes): move the email modules into routes/email/
routes/email_routes.py (7,194 lines), routes/email_helpers.py (2,025) and
routes/email_pollers.py (1,764) were the largest flat email area left in
routes/. They now live in routes/email/, following the pattern fifteen route
areas already use: the canonical module under the package, a shim at the old
path that replaces itself in sys.modules with the canonical object.

The shim matters more here than the move. Dozens of email tests monkeypatch
module attributes, and several pop the module from sys.modules and re-import
it. Without identity between routes.email_routes and routes.email.email_routes
those patches would apply to a different object than the app uses.

Intra-package imports keep the flat path, matching routes/document/ and
routes/gallery/: the shim resolves them and the diff stays a move.

Eight path references in five test modules read these files as source rather
than importing them, so they now read the canonical location. That is the same
adjustment every previous conversion made, and it is why the note/document
shims mention source-introspection tests explicitly.

No behaviour change. Verified with the full suite and under several explicit
collection orders, since a byte-identical routes move has broken tests under
one ordering before.
2026-09-29 18:21:18 +02:00

141 lines
4.7 KiB
Python

"""Regression coverage for IMAP mailbox names that contain spaces.
imaplib does not quote mailbox arguments for SELECT/APPEND/MOVE/COPY, so callers
must quote names such as "[Gmail]/All Mail" or "Sent Items" themselves.
"""
from pathlib import Path
import pytest
pytest.importorskip("mcp")
import mcp_servers.email_server as es
class FakeListConn:
def __init__(self):
self.calls = []
def select(self, folder, readonly=False):
self.calls.append(("select", folder, readonly))
return "OK", []
def uid(self, command, *args):
self.calls.append(("uid", command, *args))
if command == "SEARCH":
return "OK", [b""]
return "OK", []
def logout(self):
self.calls.append(("logout",))
class FakeMoveConn:
def __init__(self):
self.calls = []
self.present_uids = {"123"}
def list(self):
self.calls.append(("list",))
return "OK", []
def select(self, folder, readonly=False):
self.calls.append(("select", folder, readonly))
return "OK", []
def uid(self, command, *args):
self.calls.append(("uid", command, *args))
if command == "FETCH":
return "OK", [b"1 (UID 123)"] if "123" in self.present_uids else []
if command == "MOVE":
return "NO", []
if command == "STORE" and args and args[0] == b"123":
self.present_uids.discard("123")
return "OK", []
def expunge(self):
self.calls.append(("expunge",))
def logout(self):
self.calls.append(("logout",))
def test_mcp_list_emails_quotes_spaced_folder_on_select(monkeypatch):
conn = FakeListConn()
monkeypatch.setattr(es, "_imap_connect", lambda account=None: conn)
assert es._list_emails(folder="Sent Items") == []
assert conn.calls[0] == ("select", '"Sent Items"', True)
def test_mcp_quote_helper_handles_spaced_and_quoted_mailboxes():
assert es._q("Sent Items") == '"Sent Items"'
assert es._q('[Gmail]/All Mail') == '"[Gmail]/All Mail"'
assert es._q('Label "Needs Reply"') == '"Label \\"Needs Reply\\""'
def test_known_imap_mailbox_call_sites_are_quoted():
mcp = Path("mcp_servers/email_server.py").read_text()
assert "conn.select(folder" not in mcp
assert "conn.select(source_folder" not in mcp
assert "imap.append(sent_folder" not in mcp
assert 'conn.uid("MOVE", _b(msg_set), dest_folder)' not in mcp
assert 'conn.uid("COPY", _b(msg_set), dest_folder)' not in mcp
assert 'conn.uid("MOVE", _b(uid), dest_folder)' not in mcp
assert 'conn.uid("COPY", _b(uid), dest_folder)' not in mcp
pollers = Path("routes/email/email_pollers.py").read_text()
assert "conn.select(sent_name" not in pollers
assert "imap.append(sent_folder" not in pollers
document_routes = Path("routes/document/document_routes.py").read_text()
assert "conn.select(doc.source_email_folder" not in document_routes
def test_mcp_move_message_quotes_destination_for_move_and_fallback_copy(monkeypatch):
conn = FakeMoveConn()
monkeypatch.setattr(es, "_imap_connect", lambda account=None: conn)
assert es._move_message("123", "INBOX", "[Gmail]/All Mail") is True
assert ("uid", "MOVE", b"123", '"[Gmail]/All Mail"') in conn.calls
assert ("uid", "COPY", b"123", '"[Gmail]/All Mail"') in conn.calls
def test_mcp_bulk_move_quotes_destination_for_move_and_fallback_copy(monkeypatch):
conn = FakeMoveConn()
monkeypatch.setattr(es, "_imap_connect", lambda account=None: conn)
assert es._bulk_move(["123"], "INBOX", "[Gmail]/All Mail") == 1
assert ("uid", "MOVE", b"123", '"[Gmail]/All Mail"') in conn.calls
assert ("uid", "COPY", b"123", '"[Gmail]/All Mail"') in conn.calls
class FakeNoOpBulkMoveConn(FakeMoveConn):
def uid(self, command, *args):
self.calls.append(("uid", command, *args))
if command == "FETCH":
rows = [f"{i} (UID {uid})".encode() for i, uid in enumerate(sorted(self.present_uids), 1)]
return "OK", rows
if command == "MOVE":
return "OK", [] # Server lies: reports success but moves nothing.
if command == "COPY":
return "OK", []
if command == "STORE":
self.present_uids.discard(args[0].decode())
return "OK", []
return "OK", []
def test_mcp_bulk_move_verifies_ok_noop_and_uses_copy_delete(monkeypatch):
conn = FakeNoOpBulkMoveConn()
monkeypatch.setattr(es, "_imap_connect", lambda account=None: conn)
assert es._bulk_move(["123"], "INBOX", "Trash", role="trash") == 1
assert ("uid", "MOVE", b"123", '"Trash"') in conn.calls
assert ("uid", "COPY", b"123", '"Trash"') in conn.calls
assert ("uid", "STORE", b"123", "+FLAGS", "\\Deleted") in conn.calls