"""Shared IMAP operations used by mailcat sorters.""" from __future__ import annotations import email import email.header from email.message import Message from imap_utils import quote_mailbox from mail_routes import PREFIX, quarter_folder, route_from_subject HEADER_FETCH = "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])" def decode_header_value(value) -> str: """Decode an RFC 2047 header value into display text.""" if not value: return "" try: parts = email.header.decode_header(value) out = [] for raw, charset in parts: if isinstance(raw, bytes): out.append(raw.decode(charset or "utf-8", errors="replace")) else: out.append(str(raw)) return " ".join(out).strip() except Exception: return str(value or "") def fetch_body(data) -> bytes | None: """Return the first bytes payload from an IMAP FETCH response.""" if not data: return None for item in data: if isinstance(item, tuple) and len(item) >= 2 and isinstance(item[1], bytes): return item[1] return None def parse_header(raw_header: bytes) -> Message: """Parse an IMAP header payload into an email Message.""" return email.message_from_bytes(raw_header) def destination_from_header(raw_header: bytes) -> str | None: """Return the full IMAP destination folder for a fetched header.""" msg = parse_header(raw_header) from_raw = decode_header_value(msg.get("From", "")) subject = decode_header_value(msg.get("Subject", "")) date_str = msg.get("Date", "") destination = route_from_subject(from_raw, subject) if destination == "__FACTUUR_DATE__": destination = quarter_folder(date_str) if destination: return PREFIX + destination return None def ensure_mailbox(mail, folder: str) -> bool: """Create a mailbox and its parents if they do not already exist.""" parts = folder.split(".") for index in range(1, len(parts) + 1): parent = ".".join(parts[:index]) status, _ = mail.create(quote_mailbox(parent)) if status not in {"OK", "NO"}: return False return True def move_message(mail, uid: bytes, src: str, dst: str): """Copy a message to dst and mark it deleted in src.""" select_status, select_data = mail.select(quote_mailbox(src)) if select_status != "OK": return False, ("select_source", select_status, select_data) copy_status, copy_data = mail.uid("COPY", uid, quote_mailbox(dst)) if copy_status != "OK": return False, ("copy", copy_status, copy_data) store_status, store_data = mail.uid("STORE", uid, "+FLAGS", "\\Deleted") if store_status != "OK": return False, ("store_deleted", store_status, store_data) return True, None