Files
VacatureRadar/apps/sources/services/imap_import.py
T
Jens 67be350283
deploy / deploy (push) Canceled after 0s
Harden live mail OAuth and immutable releases
2026-07-30 02:00:09 +02:00

96 lines
3.7 KiB
Python

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.core.services.microsoft365 import acquire_exchange_access_token, xoauth2_bytes
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 = (
""
if connection.provider == MailboxConnection.Provider.OUTLOOK
else 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:
if connection.provider == MailboxConnection.Provider.OUTLOOK:
access_token = acquire_exchange_access_token()
client.authenticate(
"XOAUTH2", lambda _challenge: xoauth2_bytes(connection.username, access_token)
)
else:
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()