Initial status for developed files on dagda

This commit is contained in:
mailcat
2026-07-20 20:49:36 +02:00
commit 337378a362
102 changed files with 37778 additions and 0 deletions
+323
View File
@@ -0,0 +1,323 @@
#!/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 sys
import ssl
import smtplib
import re
import html as html_lib
import urllib.parse
import config
from datetime import date, timedelta, datetime
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 babel.dates import format_date
import babel
import random
# 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="")
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 invalid_locale(code):
return babel.Locale(code).language_name is None
def get_random_locale() -> (str, str, str):
"""Return the locale code, the local name and the english name of a language."""
all_ids = babel.localedata.locale_identifiers()
code = all_ids[random.randrange(0, len(all_ids))]
while (invalid_locale(code)):
code = all_ids[random.randrange(0, len(all_ids))]
return (code, babel.Locale(code).language_name, babel.Locale(code).english_name)
# ── 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"""<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="utf-8">
<style>
body {{ font-family: -apple-system, Arial, sans-serif; font-size: 14px;
color: #222; max-width: 700px; margin: 20px auto; }}
h1 {{ font-size: 18px; color: #1a1a2e; margin-bottom: 4px; }}
.meta {{ color: #666; font-size: 13px; margin-bottom: 24px; }}
h2 {{ font-size: 14px; font-weight: 600; color: #333;
background: #f4f4f8; padding: 6px 10px;
border-left: 3px solid #5c6bc0; margin: 20px 0 6px; }}
table {{ width: 100%; border-collapse: collapse; margin-bottom: 4px; }}
td {{ padding: 4px 6px; vertical-align: top; }}
.time {{ color: #888; width: 42px; white-space: nowrap; }}
.from {{ color: #555; width: 160px; }}
.subject {{ color: #222; }}
.footer {{ margin-top: 32px; font-size: 12px; color: #aaa;
border-top: 1px solid #eee; padding-top: 10px; }}
</style>
</head>
<body>
<h1>Automatisch gesorteerde mail — {datum_nl}</h1>
<p class="meta">{totaal} bericht{'en' if totaal != 1 else ''} in {len(berichten)} map{'pen' if len(berichten) != 1 else ''}</p>
"""
for folder in sorted(berichten.keys()):
items = berichten[folder]
label = html_lib.escape(leesbare_map(folder))
html += f'<h2>{label} <span style="font-weight:normal;color:#888">({len(items)})</span></h2>\n'
html += '<table>\n'
for item in items:
time = html_lib.escape(item["time"])
sender = html_lib.escape(item["from"])
subject = html_lib.escape(item["subject"])
url = apple_mail_url(item.get("message_id", ""))
if url:
subject_html = f'<a href="{html_lib.escape(url, quote=True)}">{subject}</a>'
else:
subject_html = subject
html += (
f'<tr>'
f'<td class="time">{time}</td>'
f'<td class="from">{sender}</td>'
f'<td class="subject">{subject_html}</td>'
f'</tr>\n'
)
html += '</table>\n'
html += f'<p class="footer">Gegenereerd door dagelijks_overzicht.py · {datetime.now().strftime("%Y-%m-%d %H:%M")}</p>'
html += '</body></html>'
return html
# ── e-mail versturen ──────────────────────────────────────────────────────────
def stuur_overzicht(html: str, zoekdatum: date, config: dict):
datum_nl = format_date(zoekdatum, "EEEE d MMMM yyyy", locale="nl") # zoekdatum.strftime("%-d %B %Y")
random_locale, random_lang, random_elang = get_random_locale()
datum_bl = format_date(zoekdatum, locale=random_locale)
msg = MIMEMultipart("alternative")
msg["Subject"] = f"Mailoverzicht van {datum_bl} ({random_lang} - {random_elang}) of {datum_nl}"
msg["From"] = config["from"]
msg["To"] = config["to"]
msg.attach(MIMEText(html, "html", "utf-8"))
context = ssl.create_default_context()
with smtplib.SMTP(config["smtp_host"], config["smtp_port"]) as server:
server.starttls(context=context)
server.login(config["username"], config["password"])
server.sendmail(config["from"], [config["to"]], msg.as_string())
print(f"Overzicht verstuurd naar {config['to']}")
# ── main ──────────────────────────────────────────────────────────────────────
def run():
args = sys.argv[1:]
cfg_dict = load_config()
if mail_to := arg_value(args, "--mail-to"):
cfg_dict["to"] = mail_to
print(f"Account: {cfg_dict['username']}\n")
# Datum bepalen
if datum := arg_value(args, "--datum"):
zoekdatum = date.fromisoformat(datum)
else:
zoekdatum = date.today() - timedelta(days=1) # standaard: gisteren
print(f"Ophalen berichten van {zoekdatum} ...")
try:
mail = imaplib.IMAP4_SSL(cfg_dict["imap_host"], cfg_dict["imap_port"])
mail.login(cfg_dict["username"], cfg_dict["password"])
except Exception as e:
print(f"IMAP verbinding mislukt: {e}")
sys.exit(1)
berichten = haal_berichten_op(mail, zoekdatum)
mail.logout()
totaal = sum(len(v) for v in berichten.values())
print(f"{totaal} berichten gevonden in {len(berichten)} mappen.")
html = bouw_html(berichten, zoekdatum)
stuur_overzicht(html, zoekdatum, cfg_dict)
if __name__ == "__main__":
run()