#!/usr/bin/env python3 """ Afmelden van mailinglijsten op basis van decisions.json. Verwerkt List-Unsubscribe headers: HTTP (GET/POST) en mailto. Gebruik: python3 unsubscribe.py Log: unsubscribe_log.json (resultaat per lijst) """ import json import re import sys import time import smtplib import ssl import urllib.request import urllib.parse import urllib.error import config from email.mime.text import MIMEText from pathlib import Path DECISIONS_FILE = Path(__file__).parent / "decisions.json" LOG_FILE = Path(__file__).parent / "unsubscribe_log.json" REQUEST_TIMEOUT = 10 # seconden per HTTP-verzoek DELAY_BETWEEN = 1.5 # seconden tussen verzoeken (beleefd) # ── helpers ────────────────────────────────────────────────────────────────── def parse_unsub_header(raw: str) -> dict[str, list[str]]: """ Parseer List-Unsubscribe header naar {'http': [...], 'mailto': [...]}. Formaat: , , ... (RFC 2369) """ result = {"http": [], "mailto": []} for match in re.finditer(r"<([^>]+)>", raw): url = match.group(1).strip() if url.startswith("http"): result["http"].append(url) elif url.startswith("mailto:"): result["mailto"].append(url) return result def try_http_unsub(url: str) -> tuple[bool, str]: """Probeer afmelden via HTTP GET. Geeft (succes, melding).""" try: req = urllib.request.Request( url, headers={"User-Agent": "Mozilla/5.0 (compatible; list-unsubscribe)"}, ) with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: code = resp.getcode() return (code == 200), f"HTTP {code}" except urllib.error.HTTPError as e: return False, f"HTTP {e.code}" except Exception as e: return False, str(e) def try_mailto_unsub(mailto: str, smtp: smtplib.SMTP, from_addr: str) -> tuple[bool, str]: """Stuur een lege e-mail naar het afmeldadres.""" try: parsed = urllib.parse.urlparse(mailto) to_addr = parsed.path params = urllib.parse.parse_qs(parsed.query) subject = params.get("subject", ["unsubscribe"])[0] msg = MIMEText("") msg["From"] = from_addr msg["To"] = to_addr msg["Subject"] = subject smtp.sendmail(from_addr, [to_addr], msg.as_string()) return True, f"mailto verzonden → {to_addr}" except Exception as e: return False, str(e) # ── main ────────────────────────────────────────────────────────────────────── def run(): if not DECISIONS_FILE.exists(): print("decisions.json niet gevonden. Draai eerst review_mailinglists.py.") sys.exit(1) decisions = json.loads(DECISIONS_FILE.read_text(encoding="utf-8")) te_afmelden = { domain: info for domain, info in decisions.items() if isinstance(info, dict) and info.get("keuze") == "a" and info.get("unsubscribe") } handmatig = { domain: info for domain, info in decisions.items() if isinstance(info, dict) and info.get("keuze") == "a" and not info.get("unsubscribe") } print(f"Afmelden: {len(te_afmelden)} via List-Unsubscribe, " f"{len(handmatig)} handmatig (geen header).\n") # Laad bestaand log (herstart-safe) log: dict[str, dict] = {} if LOG_FILE.exists(): try: log = json.loads(LOG_FILE.read_text(encoding="utf-8")) al_klaar = sum(1 for v in log.values() if v.get("succes")) print(f"Bestaand log geladen: {al_klaar} al verwerkt.\n") except Exception: pass cfg = config.load() # SMTP verbinding voor mailto-afmeldingen smtp = None heeft_mailto = any( any(u.startswith("mailto:") for u in info.get("unsubscribe", [])) for info in te_afmelden.values() ) if heeft_mailto: print(f"Sommige afmeldingen gaan via e-mail (mailto:). Account: {cfg.username}") try: context = ssl.create_default_context() smtp = smtplib.SMTP(cfg.smtp_host, cfg.smtp_port) smtp.starttls(context=context) smtp.login(cfg.username, cfg.password) print("SMTP verbonden.\n") except Exception as e: print(f"SMTP mislukt: {e}\n mailto-afmeldingen worden overgeslagen.") smtp = None # Verwerk afmeldingen total = len(te_afmelden) for idx, (domain, info) in enumerate(te_afmelden.items(), 1): naam = info.get("naam", domain) if domain in log and log[domain].get("succes"): print(f"[{idx}/{total}] {naam} — al afgemeld, overgeslagen.") continue print(f"[{idx}/{total}] {naam} ({domain})") succes = False methode = "" melding = "" for raw_unsub in info.get("unsubscribe", []): parsed = parse_unsub_header(raw_unsub) # Probeer HTTP eerst for url in parsed["http"]: ok, msg = try_http_unsub(url) if ok: succes = True methode = "HTTP GET" melding = f"{msg} → {url[:80]}" break else: melding = f"{msg} → {url[:60]}" if succes: break # Dan mailto if smtp: for mailto in parsed["mailto"]: ok, msg = try_mailto_unsub(mailto, smtp, cfg.username) if ok: succes = True methode = "mailto" melding = msg break if succes: break status = "✓" if succes else "✗" print(f" {status} {methode or 'mislukt'} {melding}") log[domain] = { "naam": naam, "succes": succes, "methode": methode, "melding": melding, } LOG_FILE.write_text(json.dumps(log, indent=2, ensure_ascii=False), encoding="utf-8") time.sleep(DELAY_BETWEEN) if smtp: smtp.quit() # Samenvatting gelukt = sum(1 for v in log.values() if v.get("succes")) mislukt = sum(1 for v in log.values() if not v.get("succes")) print(f"\n{'─'*50}") print(f"Klaar. {gelukt} afgemeld, {mislukt} mislukt.") if mislukt: print("\nMislukt (handmatig afmelden):") for domain, v in log.items(): if not v.get("succes"): print(f" {v['naam']} ({domain})") if handmatig: print(f"\nGeen List-Unsubscribe header gevonden ({len(handmatig)}) — handmatig afmelden:") for domain, info in handmatig.items(): print(f" {info.get('naam', domain)} ({domain})") print(f"\nLog opgeslagen in: {LOG_FILE}") if __name__ == "__main__": run()