Add bounce mailbox processing

This commit is contained in:
2026-07-31 22:48:07 +02:00
parent 2721ee6542
commit fe309dbff5
13 changed files with 1770 additions and 7 deletions
+143
View File
@@ -180,6 +180,29 @@ class ImapMailboxMessageResult:
message: ImapMailboxMessageDetail
@dataclass(frozen=True, slots=True)
class ImapRawMessageResult:
host: str
port: int
security: str
folder: str
uid: str
flags: list[str]
size_bytes: int
raw: bytes
@dataclass(frozen=True, slots=True)
class ImapUidListResult:
host: str
port: int
security: str
folder: str
uids: list[str]
uidvalidity: str | None
cursor_reset: bool = False
@dataclass(frozen=True, slots=True)
class ImapAppendResult:
host: str
@@ -1079,6 +1102,126 @@ def get_imap_message(*, imap_config: ImapConfig, folder: str, uid: str) -> ImapM
_log_imap_cleanup_failure("reading message", cleanup_exc)
def get_imap_raw_message(
*,
imap_config: ImapConfig,
folder: str,
uid: str,
) -> ImapRawMessageResult:
"""Fetch bounded raw MIME without changing mailbox flags."""
host, port = _require_imap_config(imap_config)
folder = (folder or "INBOX").strip() or "INBOX"
uid = str(uid).strip()
if not uid:
raise ImapConfigurationError("Message UID is required")
if is_mock_imap_host(imap_config.host):
record = get_record(uid, include_raw=True)
if not record or not _mock_folder_matches(record, folder):
raise ImapAppendError(
f"Mock mailbox message {uid!r} was not found in folder {folder!r}",
temporary=False,
)
raw = _mock_raw_bytes(record)
return ImapRawMessageResult(
host=host,
port=port,
security=imap_config.security.value,
folder=folder,
uid=uid,
flags=["\\Seen"] if record.get("kind") == "imap_append" else [],
size_bytes=len(raw),
raw=raw,
)
client = _open_imap(imap_config)
try:
_select_readonly(client, folder)
fetched_uid, flags, size_bytes, raw = _fetch_message_by_uid(client, uid)
return ImapRawMessageResult(
host=host,
port=port,
security=imap_config.security.value,
folder=folder,
uid=fetched_uid,
flags=flags,
size_bytes=size_bytes if size_bytes is not None else len(raw),
raw=raw,
)
finally:
try:
client.logout()
except Exception as cleanup_exc:
_log_imap_cleanup_failure("reading raw message", cleanup_exc)
def list_imap_uids_since(
*,
imap_config: ImapConfig,
folder: str,
highest_uid: int = 0,
expected_uidvalidity: str | None = None,
limit: int = 100,
) -> ImapUidListResult:
"""Return new UIDs oldest-first so a bounded watcher cannot skip a burst."""
host, port = _require_imap_config(imap_config)
folder = (folder or "INBOX").strip() or "INBOX"
bounded_limit = max(1, min(int(limit), 1_000))
if is_mock_imap_host(imap_config.host):
records = [
record
for record in list_records(limit=5_000)
if _mock_folder_matches(record, folder)
]
cursor_reset = bool(
expected_uidvalidity and expected_uidvalidity != "mock-v1"
)
effective_highest = 0 if cursor_reset else highest_uid
numeric = sorted(
int(value)
for record in records
for value in (str(record.get("id") or ""),)
if value.isdigit() and int(value) > effective_highest
)
return ImapUidListResult(
host=host,
port=port,
security=imap_config.security.value,
folder=folder,
uids=[str(value) for value in numeric[:bounded_limit]],
uidvalidity="mock-v1",
cursor_reset=cursor_reset,
)
client = _open_imap(imap_config)
try:
_total, uidvalidity = _select_readonly(client, folder)
cursor_reset = bool(
expected_uidvalidity and uidvalidity != expected_uidvalidity
)
effective_highest = 0 if cursor_reset else highest_uid
numeric = sorted(
int(uid)
for uid in _search_all_uids(client)
if uid.isdigit() and int(uid) > effective_highest
)
return ImapUidListResult(
host=host,
port=port,
security=imap_config.security.value,
folder=folder,
uids=[str(value) for value in numeric[:bounded_limit]],
uidvalidity=uidvalidity,
cursor_reset=cursor_reset,
)
finally:
try:
client.logout()
except Exception as cleanup_exc:
_log_imap_cleanup_failure("listing watcher UIDs", cleanup_exc)
def append_message_to_sent(
message_bytes: bytes,
*,