218 lines
7.0 KiB
Python
218 lines
7.0 KiB
Python
#!/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: <url>, <url>, ... (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()
|