mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
fase 3
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
VALID_OUTBOUND_TRANSITIONS: dict[str, set[str]] = {
|
||||
"queued": {"accepted", "anomaly"},
|
||||
"sent": {"accepted", "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
|
||||
Reference in New Issue
Block a user