#!/usr/bin/env python3 """ Dagelijks overzicht van automatisch gesorteerde mail. Zoekt in alle IMAP-mappen (behalve Inbox, Sent, Trash, Spam, Drafts) naar berichten die gisteren zijn binnengekomen, en stuurt een HTML-samenvatting naar hans@australius.nl. Eenmalige setup: python3 dagelijks_overzicht.py --setup Handmatig testen: python3 dagelijks_overzicht.py [--gisteren | --datum 2026-07-01] [--mail-to adres] Plannen (macOS): zie dagelijks_overzicht.plist """ import imaplib import email import email.header import email.utils import json import sys import ssl import smtplib import re import html as html_lib import urllib.parse import config from datetime import date, timedelta, datetime from pathlib import Path from collections import defaultdict from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from imap_utils import list_folders, quote_mailbox from mail_imap_ops import fetch_body # Mappen die NIET in het overzicht komen (systeem + inbox zelf) UITSLUITINGEN = { "INBOX", "INBOX.Sent", "INBOX.Trash", "INBOX.Spam", "INBOX.Drafts", "INBOX.Spam", "INBOX.Archief", "INBOX.Archief.2020", "INBOX.Archief.2021", "INBOX.Archief.2022", "INBOX.Archief.2023", "INBOX.Archief.2024", "INBOX.Archief.2025", "INBOX.Archief.2026", } # Patroon: archief-mappen en technisch-interne mappen overslaan UITSLUIT_PATRONEN = re.compile( r"^INBOX\.(Archief|Trash|Spam|Sent|Drafts|Technisch)", re.I ) # ── config ──────────────────────────────────────────────────────────────────── def load_config(): """Laad account via centrale config.py. Gebruik --account om te wisselen.""" cfg = config.load() return { "username": cfg.username, "password": cfg.password, "imap_host": cfg.imap_host, "imap_port": cfg.imap_port, "smtp_host": cfg.smtp_host, "smtp_port": cfg.smtp_port, "from": cfg.username, "to": cfg.username, # stuur digest naar zichzelf } def arg_value(args: list[str], flag: str) -> str | None: """Return the value after a simple command-line flag.""" if flag not in args: return None idx = args.index(flag) if idx + 1 >= len(args): print(f"Ontbrekende waarde voor {flag}") sys.exit(2) return args[idx + 1] # ── helpers ─────────────────────────────────────────────────────────────────── def decode_hdr(value) -> str: if not value: return "" try: parts = email.header.decode_header(value) out = [] for raw, cs in parts: if isinstance(raw, bytes): out.append(raw.decode(cs or "utf-8", errors="replace")) else: out.append(str(raw)) return " ".join(out).strip() except Exception: return str(value or "") def imap_date(d: date) -> str: """Zet date om naar IMAP-zoekformaat: 02-Jul-2026""" return d.strftime("%d-%b-%Y") def leesbare_map(imap_naam: str) -> str: """Maak de mapnaam leesbaar door INBOX. te verwijderen.""" return imap_naam.removeprefix("INBOX.").replace(".", " › ") def apple_mail_url(message_id: str) -> str: """Return an Apple Mail message:// URL for a Message-ID header.""" clean = (message_id or "").strip() if not clean: return "" if not (clean.startswith("<") and clean.endswith(">")): clean = f"<{clean.strip('<>')}>" return "message://" + urllib.parse.quote(clean, safe="") # ── IMAP ophalen ────────────────────────────────────────────────────────────── def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, list[dict]]: """ Doorzoek alle relevante mappen op berichten van zoekdatum. Geeft {mapnaam: [{from, subject, time}, ...]} terug. """ since = imap_date(zoekdatum) before = imap_date(zoekdatum + timedelta(days=1)) alle_mappen = list_folders(mail) if not alle_mappen: return {} resultaat: dict[str, list[dict]] = defaultdict(list) for folder in sorted(alle_mappen): if folder in UITSLUITINGEN: continue if UITSLUIT_PATRONEN.match(folder): continue status, data = mail.select(quote_mailbox(folder), readonly=True) if status != "OK": continue # Zoek op INTERNALDATE (serverontvangst, niet Date-header) status, data = mail.search( None, f'SINCE {since} BEFORE {before}' ) if status != "OK" or not data[0]: continue ids = data[0].split() if not ids: continue for uid in ids: status, msg_data = mail.fetch( uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE MESSAGE-ID)])" ) raw_header = fetch_body(msg_data) if status != "OK" or raw_header is None: continue msg = email.message_from_bytes(raw_header) from_raw = decode_hdr(msg.get("From", "")) subject = decode_hdr(msg.get("Subject", "(geen onderwerp)")) date_str = msg.get("Date", "") message_id = (msg.get("Message-ID") or "").strip() # Tijdstip opmaken try: dt = email.utils.parsedate_to_datetime(date_str) tijdstip = dt.strftime("%H:%M") except Exception: tijdstip = "?" # Afzendernaam uit From-header m = re.match(r'^"?([^"<@\n]{2,})"?\s*<', from_raw) afzender = m.group(1).strip() if m else from_raw.split("@")[0] resultaat[folder].append({ "from": afzender[:40], "subject": subject[:80], "time": tijdstip, "message_id": message_id, }) return resultaat # ── HTML opbouwen ───────────────────────────────────────────────────────────── def bouw_html(berichten: dict[str, list[dict]], zoekdatum: date) -> str: datum_nl = zoekdatum.strftime("%-d %B %Y") totaal = sum(len(v) for v in berichten.values()) html = f"""
| {time} | ' f'{sender} | ' f'{subject_html} | ' f'