from __future__ import annotations import imaplib from collections.abc import Callable from contextlib import suppress from typing import Any from django.conf import settings from apps.sources.models import EmailMessageRecord, MailboxConnection from apps.sources.services.email_import import ingest_email, message_identity from apps.sources.services.mailbox_connections import decrypt_mailbox_password from apps.sources.services.tls import trusted_tls_context from apps.sources.services.url_security import validate_public_url ImapClientFactory = Callable[..., Any] HostValidator = Callable[[str], object] def poll_imap_mailbox( connection: MailboxConnection, *, client_factory: ImapClientFactory | None = None, host_validator: HostValidator = validate_public_url, ) -> dict[str, int | str]: """Import one bounded mailbox batch without letting one bad message abort the batch.""" if not connection.enabled: return {"status": "disabled", "imported": 0} if client_factory is None: client_factory = imaplib.IMAP4_SSL host_validator(f"https://{connection.imap_host}") password = decrypt_mailbox_password(connection) client_options = {"timeout": settings.IMAP_CONNECT_TIMEOUT_SECONDS} if client_factory is imaplib.IMAP4_SSL: client_options["ssl_context"] = trusted_tls_context() client = client_factory(connection.imap_host, connection.port, **client_options) counters = { "imported": 0, "duplicates": 0, "skipped_oversized": 0, "failed": 0, } try: client.login(connection.username, password) status, _ = client.select(connection.mailbox, readonly=True) if status != "OK": raise RuntimeError("IMAP-mailbox kon niet worden geopend") status, data = client.uid("search", None, "ALL") if status != "OK": raise RuntimeError("IMAP-zoekopdracht mislukt") uids = (data[0] or b"").split()[-settings.IMAP_MAX_MESSAGES_PER_POLL :] for uid in uids: try: status, payload = client.uid("fetch", uid, "(RFC822)") if status != "OK" or not payload: counters["failed"] += 1 continue raw = next((part[1] for part in payload if isinstance(part, tuple)), None) if not isinstance(raw, bytes): counters["failed"] += 1 continue if len(raw) > settings.IMAP_MAX_MESSAGE_BYTES: counters["skipped_oversized"] += 1 continue identity = message_identity(raw) if EmailMessageRecord.objects.filter( mailbox_connection=connection, message_id=identity ).exists(): counters["duplicates"] += 1 continue ingest_email( raw, mailbox=connection.mailbox, mailbox_connection=connection, platform=connection.platform, ) counters["imported"] += 1 except Exception: counters["failed"] += 1 return {"status": "ok", **counters} finally: with suppress(Exception): client.logout()