Add route management web interface

Prompts used since previous commit:
- Kun je ook een webinterface maken waarmee ik de regels kan beheren?
- Ik wil dat de webinterface benaderbaar is vanaf mijn hele interne netwerk. En ik wil alle regels kunnen beheren. Dus ook de bestaande ingebouwde routes.
- 1. basic auth
- 2. 4321

Special observations:
- Domain routes moved from Python source to domain_routes.json so the web UI can manage all previous built-in domain routes.
- web_config.json remains ignored; live sorter processes must be restarted after route edits to pick up changed rules.
This commit is contained in:
2026-07-05 00:19:53 +02:00
parent cfb96bc610
commit ae0813d519
8 changed files with 1462 additions and 308 deletions
+58 -103
View File
@@ -9,109 +9,64 @@ from pathlib import Path
PREFIX = "INBOX."
MAILINGLIST_ROUTES_FILE = Path(__file__).parent / "mailinglist_routes.json"
DOMAIN_ROUTES_FILE = Path(__file__).parent / "domain_routes.json"
DOMAIN_ROUTES: list[tuple[list[str], str]] = [
(["nl.abnamro.com", "abnamro.nl"], "Financieel.Bank.ABN AMRO"),
(["icscards.nl", "icsmarketing.icscards.nl", "service.icscards.nl"], "Financieel.Bank.ICS"),
(["npex.nl"], "Financieel.Bank.NPEX"),
(["n26.com"], "Financieel.Bank.N26"),
(["update.bunq.com", "hello.bunq.com"], "Financieel.Bank.bunq"),
(["snelstart.nl"], "Financieel.Bank.SnelStart"),
(["tellow.nl"], "Financieel.Bank.Tellow"),
(["mijnkantoorapp.nl", "dijkman-ac.nl"], "Financieel.Boekhouding.Dijkman"),
(["unive.nl", "nieuwsbrief.unive.nl"], "Financieel.Verzekering.Univé"),
(["heinenoord.nl"], "Financieel.Verzekering.VEZA"),
(["flextender.nl"], "Werk.Opdrachten.Flextender"),
(["onestopsourcing.nl"], "Werk.Opdrachten.OneStopSourcing"),
(["immens.nu"], "Werk.Opdrachten.IMMENS"),
(["circle8.nl"], "Werk.Opdrachten.Circle8"),
(["getthere.nl"], "Werk.Opdrachten.Get There"),
(["hey.meetjamie.ai", "notifications.meetjamie.ai"], "Diensten.AI.Jamie"),
(["huckr.ai"], "Diensten.AI.Huckr"),
(["maandag.com"], "Werk.Opdrachten.Maandag"),
(["futurexl.nl"], "Werk.Opdrachten.FutureXL"),
(["drenthe.nl"], "Werk.Opdrachten.Provincie Drenthe"),
(["rdw.nl"], "Werk.Opdrachten.RDW"),
(["plasbossinade.nl"], "Werk.Opdrachten.PlasBossinade"),
(["boip.int"], "Werk.Overig.BOIP"),
(["groningen.nl", "groningenbereikbaar.nl"], "Werk.Overig.Gemeente Groningen"),
(["kvk.nl"], "Administratie.KvK"),
(["eneco-emobility.com", "eneco.nl"], "Mobiliteit.EV.Eneco eMobility"),
(["shellrecharge.com"], "Mobiliteit.EV.Shell Recharge"),
(["fastned.nl"], "Mobiliteit.EV.FastNed"),
(["opngo.com"], "Mobiliteit.EV.OPnGO"),
(["tesla.com"], "Mobiliteit.EV.Tesla"),
(["mmsn.nl"], "Mobiliteit.EV.Mitsubishi"),
(["alfen.com"], "Mobiliteit.EV.Alfen"),
(["ns.nl", "email.ns.nl"], "Mobiliteit.OV & Reizen.NS"),
(["booking.com"], "Mobiliteit.OV & Reizen.Booking"),
(["flitsmeister.nl"], "Mobiliteit.OV & Reizen.Flitsmeister"),
(["postnl.nl", "edm.postnl.nl", "notificatie.postnl.nl"], "Bestellingen.PostNL"),
(["dhlparcel.nl", "dhlecommerce.nl"], "Bestellingen.DHL"),
(["gls-netherlands.com"], "Bestellingen.GLS"),
(["bol.com", "feedback.bol.com"], "Bestellingen.bol"),
(["coolblue.nl", "coolblue.eu"], "Bestellingen.Coolblue"),
(["allekabels.nl"], "Bestellingen.Allekabels"),
(["vikingdirect.nl", "delivery.vikingdirect.nl", "online.vikingdirect.nl"], "Bestellingen.Viking"),
(["123inkt.nl", "m.123inkt.nl"], "Bestellingen.123inkt"),
(["officecentre.nl"], "Bestellingen.Office Centre"),
(["sligro.nl"], "Bestellingen.Sligro"),
(["makro.nl"], "Bestellingen.Makro"),
(["smartphonehoesjes.nl"], "Bestellingen.Smartphonehoesjes"),
(["strato.com", "news.strato.com", "marketingradar.strato.com"], "Diensten.Hosting.STRATO"),
(["transip.nl"], "Diensten.Hosting.TransIP"),
(["cloud86.io", "cloud86.zendesk.com"], "Diensten.Hosting.Cloud86"),
(["godaddy.com"], "Diensten.Hosting.GoDaddy"),
(["letsencrypt.org"], "Diensten.Hosting.Lets Encrypt"),
(["backblaze.com"], "Diensten.Hosting.Backblaze"),
(["kpn.com", "campagnes.kpn.com"], "Diensten.Telecom.KPN"),
(["zakelijk.vodafone.nl", "vodafone.nl"], "Diensten.Telecom.Vodafone"),
(["nordaccount.com"], "Diensten.Beveiliging.Nord"),
(["info.expressvpn.com"], "Diensten.Beveiliging.ExpressVPN"),
(["email.remarkable.com"], "Diensten.Apparaten.reMarkable"),
(["1password.com"], "Diensten.Software.1Password"),
(["home.sophos.com", "cleverbridge.com"], "Diensten.Software.Sophos"),
(["cogsciapps.com"], "Diensten.Software.CogSci Apps"),
(["claude.com", "mail.anthropic.com"], "Diensten.AI.Claude"),
(["cursor.com", "mail.cursor.com"], "Diensten.AI.Cursor"),
(["mistral.ai", "mistralai.intercom-mail.eu"], "Diensten.AI.Mistral"),
(["openai.com", "chatgpt.com", "tm.openai.com"], "Diensten.AI.OpenAI"),
(["perplexity.ai"], "Diensten.AI.Perplexity"),
(["canva.com", "account.canva.com"], "Diensten.AI.Canva"),
(["microsoft.com"], "Diensten.Software.Microsoft"),
(["microsoft365.com"], "Diensten.Software.Microsoft Teams"),
(["market.envato.com"], "Diensten.Software.Envato"),
(["getmatter.com"], "Diensten.Software.Matter"),
(["mindnode.com"], "Diensten.Software.MindNode"),
(["papersapp.com"], "Diensten.Software.Papers"),
(["team.kahoot.com"], "Diensten.Software.Kahoot"),
(["coursera.org", "email.coursera.org", "m.mail.coursera.org", "m.learn.coursera.org"], "Nieuwsbrieven.Leren.Coursera"),
(["technologyreview.com", "fulfillment.technologyreview.com"], "Nieuwsbrieven.Leren.MIT Technology Review"),
(["mit.edu"], "Nieuwsbrieven.Leren.MIT Open Learning"),
(["storytellingwithdata.com"], "Nieuwsbrieven.Leren.Storytelling with Data"),
(["linkingyourthinking.com"], "Nieuwsbrieven.Leren.Nick Milo"),
(["evakeiffenheim.com"], "Nieuwsbrieven.Leren.Eva Keiffenheim"),
(["tiggelaar.nl"], "Nieuwsbrieven.Leren.Ben Tiggelaar"),
(["briankeating.com"], "Nieuwsbrieven.Leren.Brian Keating"),
(["beehiiv.com", "mail.beehiiv.com"], "Nieuwsbrieven.Leren.AI Report"),
(["elizabethbutlermd.com"], "Nieuwsbrieven.Leren.Elizabeth Butler"),
(["notify.orcid.org"], "Nieuwsbrieven.Leren.ORCID"),
(["crisismanager.nl"], "Nieuwsbrieven.Nieuws & Vakbladen.Vakblad Crisismanager"),
(["jaarbeurs.nl"], "Nieuwsbrieven.Nieuws & Vakbladen.Computable"),
(["presentation-guru.com"], "Nieuwsbrieven.Nieuws & Vakbladen.The Presentation Guru"),
(["lendahand.com"], "Nieuwsbrieven.Overig.Lendahand"),
(["thegoodroll.nl"], "Nieuwsbrieven.Overig.The Good Roll"),
(["tasks.clickup.com"], "Afmelden.Software.ClickUp"),
(["gemeenteprojecten.talent-pool.com"], "Afmelden.Werk.Gemeenteprojecten"),
(["engage.istockphoto.com"], "Afmelden.Creatief.iStock"),
(["dmarc.postmarkapp.com"], "Technisch.DMARC"),
]
DOMAIN_LOOKUP: dict[str, str] = {
domain.lower(): target
for domains, target in DOMAIN_ROUTES
for domain in domains
}
def _normalize_domain(value: str) -> str:
domain = value.strip().lower()
if not domain or "@" in domain or "/" in domain or any(ch.isspace() for ch in domain):
raise ValueError(f"Invalid domain route domain: {value!r}")
return domain
def _normalize_mailbox(value: str) -> str:
mailbox = value.strip()
if not mailbox or mailbox.startswith(PREFIX) or mailbox.startswith(".") or mailbox.endswith(".") or ".." in mailbox:
raise ValueError(f"Invalid domain route mailbox: {value!r}")
return mailbox
def normalize_domain_route(rule: dict) -> tuple[list[str], str]:
"""Return a validated domain route as (domains, mailbox)."""
if not isinstance(rule, dict):
raise ValueError(f"Invalid domain route entry: {rule!r}")
raw_domains = rule.get("domains")
if not isinstance(raw_domains, list):
raise ValueError(f"Domain route domains must be a list: {rule!r}")
domains = [_normalize_domain(str(item)) for item in raw_domains]
domains = list(dict.fromkeys(domains))
if not domains:
raise ValueError(f"Domain route must contain at least one domain: {rule!r}")
mailbox = _normalize_mailbox(str(rule.get("mailbox", "")))
return domains, mailbox
@cache
def domain_routes() -> list[tuple[list[str], str]]:
"""Return validated domain routing rules from JSON policy."""
raw = json.loads(DOMAIN_ROUTES_FILE.read_text(encoding="utf-8"))
if not isinstance(raw, list):
raise ValueError("domain_routes.json must contain a list")
return [normalize_domain_route(rule) for rule in raw]
@cache
def domain_lookup() -> dict[str, str]:
"""Return domain-to-mailbox lookup built from domain_routes.json."""
lookup: dict[str, str] = {}
for domains, target in domain_routes():
for domain in domains:
lookup[domain] = target
return lookup
# Compatibility for existing scripts/tests that imported DOMAIN_ROUTES directly.
DOMAIN_ROUTES = domain_routes()
DOMAIN_LOOKUP = domain_lookup()
FACTUUR_KEYWORDS = [
"factuur", "invoice", "rekening", "nota", "betaalverzoek",
@@ -179,7 +134,7 @@ def route_from_subject(from_addr: str, subject: str) -> str | None:
return "__FACTUUR_DATE__"
if destination := mailinglist_destination(from_addr):
return destination
return DOMAIN_LOOKUP.get(sender_domain(from_addr))
return domain_lookup().get(sender_domain(from_addr))
def is_source_folder(folder: str) -> bool:
@@ -189,7 +144,7 @@ def is_source_folder(folder: str) -> bool:
def destination_folders(start_year: int = 2020, end_year: int = 2026) -> list[str]:
"""Return all destination folders without the INBOX. prefix."""
folders = {target for _, target in DOMAIN_ROUTES}
folders = {target for _, target in domain_routes()}
folders.update(
rule["mailbox"]
for rule in mailinglist_routes()