This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user