mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 20:55:41 +02:00
198 lines
8.2 KiB
Python
198 lines
8.2 KiB
Python
"""
|
|
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
|