Files
MailRelay/modules/forwarder.py
T
2026-03-24 17:01:09 -04:00

121 lines
3.3 KiB
Python

"""Push emails to iCloud Mail via IMAP APPEND.
iCloud IMAP settings:
Host : imap.mail.me.com
Port : 993 (SSL)
Auth : email + app-specific password
The APPEND command places a message directly into a mailbox without going
through SMTP, so no "sent" copy is created and delivery is instant.
"""
import imaplib
import socket
from typing import Optional
from .database import mark_delivered
from .logger import get_logger
from .processor import RichEmail
log = get_logger(__name__)
ICLOUD_IMAP_HOST = "imap.mail.me.com"
ICLOUD_IMAP_PORT = 993
DEFAULT_MAILBOX = "INBOX"
CONNECT_TIMEOUT = 30 # seconds
class ForwarderError(Exception):
pass
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def push_emails(
emails: list[RichEmail],
icloud_email: str,
icloud_password: str,
mailbox: str = DEFAULT_MAILBOX,
) -> tuple[list[str], list[str]]:
"""APPEND each email to the iCloud mailbox.
Returns (succeeded_ids, failed_ids).
Records succeeded IDs as delivered in SQLite.
"""
if not emails:
return [], []
succeeded: list[str] = []
failed: list[str] = []
try:
conn = _connect(icloud_email, icloud_password)
except ForwarderError as exc:
log.error("IMAP connection failed: %s", exc)
return [], [e.message_id for e in emails]
try:
for rich in emails:
try:
_append(conn, rich, mailbox)
succeeded.append(rich.message_id)
log.info("IMAP pushed: %s", rich.message_id)
except Exception as exc:
log.error("Failed to push %s: %s", rich.message_id, exc)
failed.append(rich.message_id)
finally:
_logout(conn)
if succeeded:
mark_delivered(succeeded)
log.info("Marked %d message(s) as delivered.", len(succeeded))
return succeeded, failed
# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------
def _connect(email_addr: str, password: str) -> imaplib.IMAP4_SSL:
log.debug("Connecting to %s:%d", ICLOUD_IMAP_HOST, ICLOUD_IMAP_PORT)
try:
conn = imaplib.IMAP4_SSL(
ICLOUD_IMAP_HOST,
ICLOUD_IMAP_PORT,
)
except (OSError, socket.gaierror) as exc:
raise ForwarderError(f"Cannot reach {ICLOUD_IMAP_HOST}: {exc}") from exc
try:
conn.login(email_addr, password)
log.debug("IMAP login successful for %s", email_addr)
except imaplib.IMAP4.error as exc:
raise ForwarderError(f"IMAP authentication failed: {exc}") from exc
return conn
def _append(
conn: imaplib.IMAP4_SSL,
rich: RichEmail,
mailbox: str,
) -> None:
"""APPEND a single message to *mailbox*."""
# imaplib.IMAP4.append expects: mailbox, flags, date_time, message
# We pass None for flags and date_time to let the server set defaults.
status, data = conn.append(mailbox, None, None, rich.raw_bytes)
if status != "OK":
raise ForwarderError(
f"APPEND returned {status} for {rich.message_id}: {data}"
)
def _logout(conn: imaplib.IMAP4_SSL) -> None:
try:
conn.logout()
except Exception:
pass