96 lines
3.7 KiB
Python
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()
|