# INTERFACE VERSION: 1 """MailProvider — the frozen provider contract (SPEC §5.1). This signature is a frozen contract (SPEC instruction 5): implementations may evolve, signatures may not without bumping the version comment above. """ from __future__ import annotations from collections.abc import AsyncIterator from dataclasses import dataclass from datetime import date, datetime from typing import Protocol class ProviderError(Exception): """Base class for provider errors; carries a human-readable ``.detail``.""" def __init__(self, detail: str) -> None: super().__init__(detail) self.detail = detail class ProviderAuthError(ProviderError): """Authentication failed (bad credentials / token).""" class ProviderConnectError(ProviderError): """Could not connect / TLS / greeting failure.""" class ProviderRejectedError(ProviderError): """The server refused for good (a permanent 5xx: recipient unknown, message rejected). Trying again would only be refused again.""" class ProviderNotSupported(ProviderError): """A capability (e.g. IDLE) is not supported by the server.""" @dataclass(frozen=True) class RemoteMailbox: path: str special_use: str | None # "inbox"|"sent"|"drafts"|"trash"|"junk"|"archive"|None uidvalidity: int @dataclass(frozen=True) class RemoteMessage: uid: int raw: bytes # full RFC822 flags: frozenset[str] internal_date: datetime gmail_labels: tuple[str, ...] = () # custom Gmail labels (Gmail accounts only) @dataclass(frozen=True) class FlagChange: uid: int flags: frozenset[str] @dataclass(frozen=True) class MailboxDelta: new: list[RemoteMessage] flag_changes: list[FlagChange] expunged_uids: list[int] new_uidvalidity: int | None # non-None => UIDVALIDITY changed, caller must full-resync new_highestmodseq: int | None class MailProvider(Protocol): async def verify(self) -> None: """Raises ProviderAuthError / ProviderConnectError with a human-readable .detail.""" ... async def list_mailboxes(self) -> list[RemoteMailbox]: ... def backfill( self, mailbox: str, *, newest_first: bool = True, batch_size: int = 200, since: date | None = None, before: date | None = None, ) -> AsyncIterator[list[RemoteMessage]]: """Yields batches until the mailbox is exhausted. Headers+body in one pass. ``since``/``before`` bound the import by internal date (SPEC §12 M2).""" ... async def message_count(self, mailbox: str, *, since: date | None = None) -> int: """Number of messages to import (progress denominator); whole mailbox or the ``since`` window.""" ... async def newest_uid(self, mailbox: str) -> int: """Current high-water UID; used by the ``new`` window to skip history.""" ... async def poll( self, mailbox: str, *, last_seen_uid: int, highestmodseq: int | None ) -> MailboxDelta: """One reconciliation pass. Uses QRESYNC/CONDSTORE when highestmodseq is not None, otherwise UID SEARCH + FETCH FLAGS diff.""" ... async def list_uids(self, mailbox: str) -> set[int]: """Full set of UIDs currently present in the mailbox (expunge reconciliation).""" ... async def idle(self, mailbox: str) -> None: """Blocks until the server signals change or 25 min elapse (re-issue IDLE). May raise ProviderNotSupported => sync loop falls back to poll() every POLL_WARM (default 180 s). """ ... async def push_flags( self, mailbox: str, uid: int, add: frozenset[str], remove: frozenset[str] ) -> None: ... async def move(self, uid: int, src: str, dst: str) -> int: # returns new UID ... async def delete_messages(self, mailbox: str, uids: list[int]) -> None: """Permanently delete messages: set \\Deleted then EXPUNGE (SPEC §6.3).""" ... async def append( self, mailbox: str, raw: bytes, flags: frozenset[str] ) -> int: # Sent/Drafts upload ... async def send( self, raw: bytes, envelope_from: str, envelope_to: list[str] ) -> None: # SMTP submission ...