← Torna alla mappa

Metodologia di scoperta MX

Osservatorio sulla Sovranità Digitale della Posta Elettronica della Pubblica Amministrazione Italiana

Come abbiamo determinato chi gestisce la posta di ciascun ente pubblico italiano (PEC esclusa).

Principio guida. Meglio un ente classificato come non risolto che un ente classificato in modo fuorviante. Nessuna euristica permissiva: ogni metodo di scoperta segue regole esplicite e verificabili, validate dal modulo is_legit_email_domain. Quando una regola non si attiva, l'ente resta unknown — mai assegnato a un MX "plausibile ma non provato".
Limite della fonte (dipendenza core). IndicePA è l'anagrafe ufficiale degli enti, ma non è una base dati pulita da cui inferire in modo immediato e senza rielaborazioni il dominio di posta "normale" (non-PEC) di ciascun ente: i domini email vi sono spesso incoerenti, incompleti o assenti. L'intera pipeline qui descritta esiste proprio per rielaborare IndicePA (estrazione, correzione, validazione, arricchimento) — i nostri dati non sono una lettura diretta della fonte. La bonifica continua di IndicePA è una dipendenza funzionale core, tracciata come progetto dedicato: mxmap.it#2 — Software per un IndicePA ben manutenuto e bonificato (misura autonoma della qualità del dato + cicli di segnalazione, anche via PEC, verso enti e AgID).

Pipeline di scoperta — panoramica

Ogni ente attraversa una catena di tentativi. Il primo che produce un MX accettato dal validatore vince:

  1. Seed-time (fetch_indicepa.py): si parte dal campo Sito_istituzionale di IndicePA, con sei tier di correzione automatica (override manuale, enrichment LLM curato, enrichment PEC-only Wikidata, AOO/UO Tier-6).
  2. Preprocess (preprocess.py): risoluzione DNS (MX/SPF/DKIM/CNAME/ASN/tenant) sul dominio risultante.
  3. Recover (recover_it_unknowns.py): per ogni ente ancora unknown con domain_fallbacks (mail non-PEC del record IndicePA), si tentano i fallback uno a uno, gated da is_legit_email_domain.
  4. Postprocess (postprocess.py): banner SMTP + scraping del sito (gated).
  5. Finalize (finalize_it_unknowns.py): cinque strategie sugli ultimi unknowns — Tier-6 AOO/UO, PEC pubblica, Wikidata P856, scraping homepage, ricerca DuckDuckGo + scraping (tutte gated).

Ogni stage scrive il campo mx_discovery_method con la tag canonica del metodo applicato. Il tag viene mostrato nel popup dell'ente con tooltip esplicativo e link a questa pagina.

Validatore is_legit_email_domain

Nucleo della pipeline. Per ogni dominio candidato proveniente da scraping, fallback IndicePA o ricerca web, decide se è legittimamente associato all'ente. Default: REJECT. Accetta solo se scatta una di queste regole esplicite:

  1. Match esatto — il dominio candidato è identico al dominio seed dell'ente.
  2. Override manuale — coppia (codice_ipa → dominio) hand-curata.
  3. PA-shared NAZIONALE — il candidato è un dominio di infrastruttura PA cross-territoriale (garr.it, sogei.it, consorzi ASMEL); accettato per qualsiasi ente.
  4. PA-shared REGIONALE — il candidato è una piattaforma di una specifica regione (lepida.it = Emilia-Romagna, regione.vda.it = Valle d'Aosta, ruparpiemonte.it = Piemonte, ecc.). Accettato solo se: (a) l'ente è una PA locale (presenza di marker comune., provincia., codice di provincia, ecc.); (b) e l'ente è geograficamente in quella regione, verificato tramite mappatura province (110 codici) + capoluoghi (107 nomi). Rigetto automatico per ministeri/PA centrali (dominio *.gov.it).
  5. Relazione di sottodominio — il candidato è ancestor o discendente del dominio seed.
  6. Intersezione di label significativi — dopo aver rimosso TLD comuni, codici provincia e prefissi strutturali (comune., mail., www., ecc.), le label residue dei due domini condividono almeno un elemento (≥3 caratteri).
  7. Fuzzy match Damerau-Levenshtein ≤ 1 (rule 6.5) — esiste una coppia di label significativi (uno per parte, entrambi di lunghezza ≥ 6 caratteri) la cui distanza di edit è al massimo 1. Cattura typo singoli reali del dataset come consorfarm.itconsofarm.it, consorziolagodibracciano.itconsorziolagodibraciano.it, hyphenation come aslroma1.itasl-roma1.it. La soglia DL=1 + min-length 6 esclude false positive su parole brevi (es. roma/noma).
  8. Label concatenation (rule 6.6) — un singolo label del candidato (≥ 5 caratteri) contiene come substring 2 o più label dell'ente (ciascuno ≥ 3 caratteri), con copertura non-sovrapposta ≥ 80% del label candidato. Cattura il pattern molto comune dell'IndicePA dove l'ente è registrato come {città}.aci.it ma il sito reale è aci{città}.it: ad esempio aciarezzo.itarezzo.aci.it dove aciarezzo = arezzo + aci (copertura 100%). Recupera ~16 enti del cluster ACI provinciali + ordini professionali.
  9. Rigetto PEC — domini di provider PEC (legalmail.it, arubapec.it, postecert.it, ecc.) sono sempre rigettati come base di classificazione MX (per design: la PEC non è considerata posta "operativa" per scopo di sovranità).

Codice: src/mail_sovereignty/scrape_validator.py. Test: scripts/_test_scrape_validator.py.

Metodi di scoperta — taxonomia completa

Ogni voce mostra: la tag stabile (citabile come anchor di questa pagina), una descrizione estesa, la regola formale che la attiva, le sorgenti di evidenza, e il comportamento in caso di fallimento.

1. seed_primary_mx — Dominio IndicePA (diretto)

Il dominio dichiarato come Sito_istituzionale nel record IndicePA dell'ente ha record MX validi. Nessuna correzione, nessun recupero. È il caso ideale e il più frequente per i Comuni con presenza digitale matura.

Regola: lookup_mx(seed.domain) restituisce almeno un MX.
Esempio: comune.milano.itp-milano-mx-001.it-milano.local (su infrastruttura proprietaria).

2. domain_guess — Dominio dedotto dal nome

L'ente non ha dominio in IndicePA. Il sistema genera candidati dal nome (rimozione diacritici, prefissi comune-/provincia-, TLD nazionale .it) e accetta il primo con MX risolvibile.

Regola: generatore deterministico in preprocess.guess_domains() + verifica MX.
Failure: nessun candidato genera MX → entry passa allo stadio successivo.

3. manual_override — Override manuale

Dominio corretto a mano dai mantenitori del dataset. Usato per fixare typo IndicePA (castefrancocastelfranco), domini *.gov.it defunti migrati a comune.{nome}.{prov}.it, o casi che la pipeline automatica non riesce a recuperare correttamente.

Regola: presenza in IT_MANUAL_DOMAIN_OVERRIDES (dict codice_ipa → dominio) in scripts/fetch_indicepa.py. Ogni voce ha un commento di giustificazione.

Numero limitato (≪100). Crescita lenta, una voce per bug confermato.

4. manual_llm_enrichment — Curatela LLM (revisione umana)

Per gli enti che IndicePA segnala con sola PEC e dove né Wikidata né Wikipedia hanno informazioni, generiamo un prompt strutturato (lista di codici IPA + nomi ufficiali) e lo sottomettiamo a una sessione LLM. Le risposte vengono validate a mano e committate in data/manual_llm_enrichment.json.

Regola: presenza in data/manual_llm_enrichment.json + verifica sintattica del hostname + MX risolvibile a runtime.

Generazione non riproducibile (LLM-mediata), consumo deterministico. Vedi scripts/generate_llm_enrichment_prompt.py.

5. pec_only_enrichment — Enrichment PEC-only (Wikidata + Wikipedia)

Quando l'ente IndicePA espone solo indirizzi PEC, lo script enrich_pec_only.py tenta automaticamente di recuperare il dominio istituzionale tramite proprietà P856 di Wikidata indicizzata sul codice ISTAT o sul nome, con fallback al titolo Wikipedia. La verifica MX è obbligatoria prima di accettare il dominio.

Regola: Wikidata P856 valido → hostname normalizzato → MX risolvibile. In assenza di MX la voce è scartata, non promossa a fallback.

6. aoo_uo_tier6 — AOO/UO IndicePA (Tier-6)

IndicePA pubblica oltre al dataset enti anche due dataset ausiliari: Aree Organizzative Omogenee (AOO) e Unità Organizzative (UO). Questi record contengono email non-PEC dei responsabili (mail_resp, mail1..3 con tipo_mail* ≠ Pec). Per ministeri e PA centrali è spesso l'unica fonte di domini reali (il record principale ha solo PEC).

Regola: per ogni codice IPA, raccogliamo tutti i domini non-PEC dalle AOO+UO collegate; ogni dominio passa per is_legit_email_domain(dominio, seed.domain, codice_ipa=...); vengono accettati solo i superstiti. I rigetti vengono loggati per audit (vedi data/indicepa_extended_emails.jsonfiltered_out).
Caso m_it (Min. Interno): 283 record AOO. Domini scoperti: interno.it ✓, vigilfuoco.it ✗ (rigettato — ente separato), regione.vda.it ✗ (rigettato — piattaforma regionale fuori scope per un ministero nazionale).

7. domain_fallback — Mail IndicePA non-PEC (fallback ente)

Il record enti stesso espone fino a 5 campi Mail*. Il fetch ne estrae i domini non-PEC distinti dal primario e li salva in seed.domain_fallbacks. Se il dominio primario non risolve, recover_it_unknowns.py prova ciascun fallback in ordine.

Regola: per ogni fallback fb, is_legit_email_domain(fb, seed.domain, codice_ipa=...) deve restituire True. Solo allora si esegue la classificazione DNS. Rigetti loggati in data/reports/recover_it_unknowns_rejections.json.
Failure: il fallback più frequente che fallisce questa regola è istruzione.it per scuole — gestito tramite la regola specifica istruzione_miur_tenant (sotto).

8. istruzione_miur_tenant — Tenant centrale MIM (istruzione.it)

Le scuole statali italiane (categoria IndicePA L33) non hanno un proprio tenant Microsoft 365: la posta dei dirigenti scolastici, segreterie e docenti abilitati è ospitata sul tenant centrale del Ministero dell'Istruzione e del Merito (MIM) all'indirizzo istruzione.it. Questa dipendenza è una realtà istituzionale, non una misattribuzione — ma è importante segnalarla distintamente da una scuola che gestisce un proprio tenant.

Regole (TUTTE E TRE obbligatorie):
Failure di anche una sola regola: entry rigettata, marcata come miur_tenant_unverified nel report, ente resta unknown. Nessuna assunzione "questo sembra una scuola, sarà MIM".
Caso iccaroberlingieri.edu.it (I.C. Caro-Berlingieri): categoria L33 ✓, fallback istruzione.it ✓, DKIM selector1-istruzione-it._domainkey.miuristruzione.onmicrosoft.com ✓ → tag istruzione_miur_tenant.

9. wikidata_p856 — Wikidata sito ufficiale

Per i comuni che fallirebbero tutti i tentativi precedenti, una query SPARQL batch su Wikidata recupera la proprietà P856 (sito ufficiale) indicizzata sul codice ISTAT 6-cifre del comune. Cattura tipici typo IndicePA e migrazioni *.gov.itcomune.{nome}.{prov}.it.

Regola: SELECT ?web WHERE { ?city wdt:P3829 "{istat6}"; wdt:P856 ?web } + verifica MX. Solo se il dominio Wikidata differisce dal primario IndicePA viene considerato (idempotenza).

10. public_pec_inference — Inferenza da PEC pubblica

Alcuni enti utilizzano come PEC ufficiale infrastrutture pubblicamente operate (non provider commerciali): cert.ruparpiemonte.it (CSI Piemonte, ICT regionale sovrano), asmepec.it (consorzio comuni ASMEL). Quando il record IndicePA mostra solo PEC di questo tipo, classifichiamo l'ente come regional-public — anche senza un MX proprio risolto.

Regola: presenza di Mail* su uno dei provider PEC pubblici whitelistati, e assenza di MX validi sul dominio primario.

11. homepage_scrape — Scraping sito istituzionale

Quando i metodi precedenti falliscono, scarichiamo l'homepage (e alcune sotto-pagine: /contatti, /amministrazione-trasparente, ecc.) ed estraiamo gli indirizzi email visibili (incluso il decifrato dei mailto TYPO3 cifrati con Caesar). Ogni dominio estratto passa per il validatore.

Regola: per ogni email scrapata, is_legit_email_domain(email_host, ente_domain) deve restituire True. Se rigettata, la coppia (host, ragione) viene loggata in data/reports/cleanup_invalid_mx_attributions.json.
Failure storico (ora bloccato): molti siti comunali pubblicano in homepage email di terze parti (eventi, partner, scuole ospitate); l'attribuzione automatica del loro MX al comune era la causa del bug "Min. Interno → MX Comune di Roma" che ha motivato l'introduzione del validatore.

12. search_engine_scrape — Ricerca DDG + scraping

Ultima risorsa per enti con dominio primario defunto e nessuna voce Wikidata. Query DuckDuckGo HTML (no JS, no rate limit aggressivo) per il nome dell'ente; candidati URL filtrati per somiglianza al nome e dominio plausibile; scraping dell'homepage del candidato; validazione is_legit del dominio mail estratto.

Regola: stessa di homepage_scrape (validatore is_legit obbligatorio).

13. smtp_banner — Banner SMTP

Per gli enti già classificati come independent (MX proprio, nessun match con i grandi provider), apriamo una connessione SMTP al primo MX e leggiamo il banner EHLO. Stringhe come "Postfix (Ubuntu)", "Exchange 2019", "Plesk SMTP server" arricchiscono il record con il software identificato.

Regola: match keyword del banner contro SMTP_BANNER_KEYWORDS in constants.py. Concorrenza limitata (5) per non sembrare scanner.

14. unknown — Non risolto

Nessuno dei 13 metodi sopra ha prodotto un MX accettabile. L'ente resta unknown e non viene assegnato a un provider. Le motivazioni di rigetto del validatore sono comunque archiviate per consentire revisione manuale futura.

Promemoria del principio: non classifichiamo mai per riempire la riga. Il numero di unknown è una metrica di onestà del dataset, non un fallimento.

Statistiche di scoperta — distribuzione corrente

Caricamento statistiche…

Aggiornato automaticamente a ogni build del frontend (vedi scripts/build_frontend.py).

Cosa significa "non risolto"

Un ente classificato come unknown può esserlo per varie ragioni — tutte riconducibili al principio "meglio onesto che fuorviante":

Le regole 6.5 (fuzzy Damerau-Levenshtein ≤ 1) e 6.6 (label concatenation) sono già attive — catturano typo singoli e pattern del tipo aciarezzo.itarezzo.aci.it. Restano unknown i casi che richiedono override manuale (mismatch semantici non risolvibili automaticamente senza assunzioni rischiose).

Affidabilità della classificazione (confidence)

Ogni ente porta un punteggio di affidabilità della classificazione (0–100%): quanto è solida l'evidenza DNS che sostiene il provider attribuito. Il modello è un port fedele del classificatore ESORICS 2026 di David Huser e colleghi (citazione completa nei Riferimenti), adattato alla nostra architettura: da noi il provider è già deciso per keyword sui record DNS, qui calcoliamo quanto fidarsene e la giurisdizione dell'infrastruttura di posta.

Il punteggio nasce da un set di 7 regole: si scorre dall'evidenza più forte alla più debole e si prende la prima che combacia. Solo i tre segnali di routing/autorizzazione (MX, SPF, DKIM) determinano la regola di base; gli altri (tenant MS365, autodiscover) contribuiscono solo come boost (+0,02 ciascuno, cap a 1,0). Razionale upstream: avere un tenant Microsoft 365 (es. per Teams) non prova che la posta sia ospitata lì.

regolasegnali richiestigatewaybase
mx_spfMX + SPF0,90
mx_onlyMX0,80
spf_gwSPF0,70
dkim_gwDKIM0,65
dkim_spfDKIM + SPF0,60
spf_onlySPF0,50
fallback— (catch-all)0,40

Sovranità: domestic / foreign / mixed

Per gli enti senza un backend cloud riconosciuto (posta self-hosted o provider minore) non basta dire "indipendente": li qualifichiamo per giurisdizione dell'IP del server MX (paese dell'ASN via Team Cymru). La confidenza usa tabelle dedicate a base piatta (nessun boost, perché i segnali cloud sono irrilevanti alla classificazione per paese):

caso🇮🇹 domestico (IT)🌍 estero
MX + SPFdom_mx_spf 0,80frgn_mx_spf 0,60
solo MXdom_mx_only 0,70frgn_mx_only 0,50
solo evidenza secondariadom_secondary 0,20frgn_secondary 0,10
nientedom_none 0,00frgn_none 0,00

L'etichetta di sovranità nel popup (🇮🇹 MX sovrano / 🌍 MX estero / misto) deriva da mx_countries: domestic se tutti gli MX sono in IT, foreign se nessuno, mixed se alcuni.

domestic MX override — Teams ≠ posta

Caso insidioso: un ente risulta microsoft/google per via del tenant o del DKIM, ma il suo record MX punta a un server self-hosted in Italia (non a *.protection.outlook.com). Significa che il cloud serve Teams/SharePoint, mentre la posta in entrata resta sovrana. In questi casi riclassifichiamo l'ente per giurisdizione invece che come cloud estero.

Regola (come ESORICS): scatta solo se non c'è gateway e l'MX non combacia coi pattern cloud del provider. Dietro un gateway antispam il verdetto cloud viene dal look-through (il DKIM prova il backend reale) ed è legittimo → nessun override.

Anticipazione: validazione via bounce

La confidenza è anche una mappa di dove verificare. Gli enti a confidenza bassa (< 0,60) pur essendo classificati sono i candidati prioritari per la futura validazione attiva via bounce-probing (invio a indirizzo inesistente + analisi del messaggio di ritorno NDR, che rivela il MTA reale del backend). Il report completo — distribuzione aggregata, confidenza per provider, regole attivate, sovranità e lista dei candidati bounce — è rigenerato a ogni run:

Report: confidence_report.md (leggibile) · confidence_report.json (machine-readable). Codice: src/mail_sovereignty/classification_confidence.py (port + test di fedeltà), scripts/compute_confidence.py, scripts/report_confidence.py.

Riferimenti

Questo osservatorio è un fork del lavoro di David Huser e colleghi, a cui va il credito per il metodo di classificazione e per il modello di confidence qui adottato.


Osservatorio Sovranità Digitale PA — codice e dati su GitHub · Licenza dati: ODbL-1.0 + CC-BY-4.0 · Codice: MIT · Metodo: mxmap/ESORICS 2026