Add bounce mailbox processing
This commit is contained in:
@@ -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,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user