""" Parser specifico per header PEC (Fase 3). Classifica il tipo di messaggio PEC leggendo gli header: - X-Ricevuta → tipo ricevuta (Aruba, Legalmail) - X-TipoRicevuta → tipo ricevuta alternativo (Namirial) - X-Riferimento-Message-ID → Message-ID del messaggio originale - X-Trasporto → indicatore trasporto PEC (presenza indica PEC) Il risultato della classificazione determina: - `pec_type`: valore enum DB (posta_certificata, accettazione, ...) - `is_receipt`: True se il messaggio è una ricevuta (non l'originale) - `riferimento_message_id`: usato per collegare parent_message_id State machine messaggi outbound: sent/queued → accepted (ricevuta di accettazione o presa in carico) sent/queued → delivered (ricevuta di consegna arrivata prima dell'accettazione) accepted → delivered (ricevuta di avvenuta consegna) any valid → anomaly (non-accettazione, mancata consegna, errore, virus) """ import email.message import logging from dataclasses import dataclass logger = logging.getLogger(__name__) # ─── Mapping header PEC → enum DB ──────────────────────────────────────────── # # I diversi provider PEC usano varianti nei valori degli header: # Aruba PEC: X-Ricevuta con trattini (es. "avvenuta-consegna") # Namirial: X-TipoRicevuta (stessa sintassi di Aruba) # Legalmail: X-Ricevuta, talvolta con underscore o spazi # InfoCert: X-Ricevuta, stessa sintassi Aruba _PEC_TYPE_MAP: dict[str, str] = { # ── Standard con trattini (Aruba, Namirial, InfoCert) ────────────────── "accettazione": "accettazione", "non-accettazione": "non_accettazione", "presa-in-carico": "presa_in_carico", "avvenuta-consegna": "avvenuta_consegna", "mancata-consegna": "mancata_consegna", "errore-consegna": "errore_consegna", "preavviso-mancata-consegna": "preavviso_mancata_consegna", "rilevazione-virus": "rilevazione_virus", "posta-certificata": "posta_certificata", # ── Con underscore (variante Legalmail/Poste) ─────────────────────────── "non_accettazione": "non_accettazione", "presa_in_carico": "presa_in_carico", "avvenuta_consegna": "avvenuta_consegna", "mancata_consegna": "mancata_consegna", "errore_consegna": "errore_consegna", "preavviso_mancata_consegna": "preavviso_mancata_consegna", "rilevazione_virus": "rilevazione_virus", "posta_certificata": "posta_certificata", # ── Con spazio (variante Legalmail rara) ──────────────────────────────── "non accettazione": "non_accettazione", "presa in carico": "presa_in_carico", "avvenuta consegna": "avvenuta_consegna", "mancata consegna": "mancata_consegna", "errore consegna": "errore_consegna", "preavviso mancata consegna": "preavviso_mancata_consegna", "rilevazione virus": "rilevazione_virus", # ── Abbreviazioni usate da alcuni provider ────────────────────────────── "consegna": "avvenuta_consegna", # Legalmail abbreviazione "no-consegna": "mancata_consegna", } @dataclass class PecClassification: """Risultato della classificazione di un messaggio PEC.""" pec_type: str # valore enum DB riferimento_message_id: str | None # X-Riferimento-Message-ID x_ricevuta: str | None # valore raw dell'header X-Ricevuta x_tipo_ricevuta: str | None # valore raw dell'header X-TipoRicevuta is_receipt: bool # True se è ricevuta (non messaggio originale) def classify_pec_message(msg: email.message.Message) -> PecClassification: """ Classifica il tipo di messaggio PEC analizzando gli header specifici. Header letti (in ordine di priorità): 1. X-TipoRicevuta (Namirial, standard più recente) 2. X-Ricevuta (Aruba, Legalmail, InfoCert) Se nessuno dei due è presente, il messaggio è classificato come 'posta_certificata' (messaggio PEC originale). Per la correlazione con il messaggio originale (outbound): - X-Riferimento-Message-ID contiene il Message-ID del messaggio inviato """ x_ricevuta = _clean_header(msg.get("X-Ricevuta")) x_tipo = _clean_header(msg.get("X-TipoRicevuta")) x_riferimento = _clean_header(msg.get("X-Riferimento-Message-ID")) # X-TipoRicevuta ha precedenza raw_type = (x_tipo or x_ricevuta or "").lower() pec_type = _PEC_TYPE_MAP.get(raw_type, "posta_certificata") is_receipt = pec_type != "posta_certificata" return PecClassification( pec_type=pec_type, riferimento_message_id=x_riferimento, x_ricevuta=x_ricevuta, x_tipo_ricevuta=x_tipo, is_receipt=is_receipt, ) def _clean_header(value: str | None) -> str | None: """Pulisce e normalizza il valore di un header PEC.""" if not value: return None return value.strip() # ─── State machine messaggi outbound ───────────────────────────────────────── # # Quando il worker riceve una ricevuta PEC (inbound), deve aggiornare # lo stato del messaggio outbound corrispondente (trovato tramite # X-Riferimento-Message-ID → message_id_header). # # Transizioni valide: # (current_state, receipt_type) → new_state # # Stati terminali: "delivered", "anomaly", "failed" # Non si deve retrocedere di stato (es. delivered → anomaly non è valido) # Tipo ricevuta → nuovo stato da applicare al messaggio outbound _RECEIPT_TO_STATE: dict[str, str] = { "accettazione": "accepted", "presa_in_carico": "accepted", # equivalente funzionale ad accepted "avvenuta_consegna": "delivered", "non_accettazione": "anomaly", "mancata_consegna": "anomaly", "errore_consegna": "anomaly", "preavviso_mancata_consegna": "anomaly", "rilevazione_virus": "anomaly", } # Transizioni di stato valide per ciascuno stato corrente # Solo le transizioni in avanti sono permesse (no retrocessioni) # NOTA: "delivered" e' ammesso anche da "sent"/"queued" perche' i gestori PEC # possono inviare la ricevuta di consegna prima di quella di accettazione # (o in assenza della ricevuta di accettazione). Se arriva prima la consegna # lo stato deve diventare "delivered" indipendentemente dall'ordine di arrivo. VALID_OUTBOUND_TRANSITIONS: dict[str, set[str]] = { "queued": {"accepted", "delivered", "anomaly"}, "sent": {"accepted", "delivered", "anomaly"}, "accepted": {"delivered", "anomaly"}, # delivered e anomaly sono terminali: nessuna transizione } def get_state_transition(pec_type: str) -> str | None: """ Restituisce il nuovo stato da applicare al messaggio outbound quando arriva una ricevuta di questo tipo. Ritorna None se il tipo non provoca transizioni di stato (es. 'posta_certificata' non è una ricevuta). """ return _RECEIPT_TO_STATE.get(pec_type) def apply_outbound_transition(current_state: str, pec_type: str) -> str | None: """ Applica la state machine al messaggio outbound. Args: current_state: stato corrente del messaggio outbound pec_type: tipo della ricevuta arrivata Returns: Il nuovo stato se la transizione è valida, None altrimenti. Ritorna None anche se lo stato è già terminale. """ new_state = get_state_transition(pec_type) if new_state is None: return None # Non è una ricevuta con effetto sullo stato valid_targets = VALID_OUTBOUND_TRANSITIONS.get(current_state, set()) if new_state in valid_targets: return new_state logger.debug( f"Transizione stato ignorata: {current_state!r} → {new_state!r} " f"(ricevuta: {pec_type!r})" ) return None