import logging import httpx from twisted.internet import defer, threads from config import settings _logger = logging.getLogger(__name__) _logger.setLevel(logging.INFO) class BackendClient: """Async HTTP client for IMAP backend communication. All public methods return Deferred via deferToThread to avoid blocking the Twisted reactor with synchronous HTTP calls. """ def __init__(self, password: str): self.password = password.strip() self._client = httpx.Client( base_url=settings.proxy_url, headers={ "Authorization": f"Bearer {password}", "x-custom-auth": settings.basic_password, "Content-Type": "application/json", }, timeout=settings.imap_http_timeout, ) def _get_endpoint(self, mailbox_name: str) -> str: if mailbox_name == "INBOX": return "/api/mails" elif mailbox_name == "SENT": return "/api/sendbox" raise ValueError(f"Unknown mailbox: {mailbox_name}") def _sync_get_message_count(self, mailbox_name: str) -> int: endpoint = self._get_endpoint(mailbox_name) res = self._client.get(f"{endpoint}?limit=1&offset=0") res.raise_for_status() return res.json()["count"] def _sync_get_messages( self, mailbox_name: str, limit: int, offset: int ) -> tuple[list[dict], int | None]: """Fetch messages from backend. Returns (results, count) where count is only valid when offset=0. """ endpoint = self._get_endpoint(mailbox_name) res = self._client.get(f"{endpoint}?limit={limit}&offset={offset}") res.raise_for_status() data = res.json() count = data.get("count") if offset == 0 else None return data["results"], count def get_message_count(self, mailbox_name: str) -> defer.Deferred: return threads.deferToThread(self._sync_get_message_count, mailbox_name) def get_messages( self, mailbox_name: str, limit: int, offset: int ) -> defer.Deferred: return threads.deferToThread( self._sync_get_messages, mailbox_name, limit, offset ) def close(self): self._client.close()