Files
PecHub/worker/app/parsers/rem_parser.py
T
2026-06-18 11:24:05 +02:00

203 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Parser specifico per messaggi REM (Registered Electronic Mail europea).
Standard di riferimento: ETSI EN 319 532-4
"Electronic Registered Delivery Services REMDispatch and Evidence"
La REM europea e' il protocollo transfrontaliero equivalente alla PEC italiana.
Usa header X-REM-* invece degli header X-Ricevuta / X-TipoRicevuta della PEC italiana.
Header REM principali letti:
X-REM-Evidence-Type → tipo evidenza (SubmissionAcceptance, DeliveryInformation, ecc.)
X-REM-Type → alias usato da alcuni provider (stesso significato)
X-REM-Orig-Message-Id → Message-ID del messaggio originale (correlazione)
X-REM-Message-Id → alias per il Message-ID di riferimento
X-REM-Delivery-Status → success/failure per DeliveryInformation
X-REM-Timestamp → timestamp ISO 8601 (informativo)
X-REM-Provider → nome provider REM (informativo)
Strategia di mapping:
I tipi evidenza REM vengono mappati agli stessi enum DB della PEC italiana
(pec_type enum: posta_certificata, accettazione, presa_in_carico, ecc.)
senza aggiungere nuovi tipi DB. Il valore originale viene conservato in
rem_evidence_type per trasparenza.
"""
import email.message
import logging
from dataclasses import dataclass
logger = logging.getLogger(__name__)
# ─── Mapping evidence type REM → enum DB (riusa enum PEC italiana) ───────────
#
# ETSI EN 319 532-4 evidence types e loro equivalenti PEC italiani:
#
# REMDispatch → posta_certificata (il messaggio inviato)
# SubmissionAcceptance → accettazione (accettato dal provider mittente)
# RelayAcceptance → presa_in_carico (accettato per inoltro al provider destinatario)
# DeliveryInformation → avvenuta_consegna (consegnato, se X-REM-Delivery-Status = success)
# → errore_consegna (se X-REM-Delivery-Status = failed/error)
# DeliveryNonAcceptance → non_accettazione (consegna rifiutata dal destinatario)
# DeliveryExpiry → mancata_consegna (scadenza senza consegna)
# RetrievalNonAcceptance → mancata_consegna (non recuperato dal destinatario)
# ReceivedByNonREM → presa_in_carico (consegnato a sistema non-REM)
# SubmissionRejection → non_accettazione (rifiutato dal provider mittente)
# RelayRejection → non_accettazione (rifiutato durante relay)
_REM_TYPE_MAP: dict[str, str] = {
# ── Nomi standard ETSI (PascalCase) ──────────────────────────────────────
"remdispatch": "posta_certificata",
"submissionacceptance": "accettazione",
"relayacceptance": "presa_in_carico",
"deliveryinformation": "avvenuta_consegna", # rifinito con X-REM-Delivery-Status
"deliverynonacceptance": "non_accettazione",
"deliveryexpiry": "mancata_consegna",
"retrievalnonacceptance": "mancata_consegna",
"receivedbynonrem": "presa_in_carico",
"submissionrejection": "non_accettazione",
"relayrejection": "non_accettazione",
# ── Varianti con trattini (usate da alcuni provider europei) ─────────────
"rem-dispatch": "posta_certificata",
"submission-acceptance": "accettazione",
"relay-acceptance": "presa_in_carico",
"delivery-information": "avvenuta_consegna",
"delivery-non-acceptance": "non_accettazione",
"delivery-expiry": "mancata_consegna",
"retrieval-non-acceptance": "mancata_consegna",
"received-by-non-rem": "presa_in_carico",
"submission-rejection": "non_accettazione",
"relay-rejection": "non_accettazione",
# ── Varianti con underscore ───────────────────────────────────────────────
"rem_dispatch": "posta_certificata",
"submission_acceptance": "accettazione",
"relay_acceptance": "presa_in_carico",
"delivery_information": "avvenuta_consegna",
"delivery_non_acceptance": "non_accettazione",
"delivery_expiry": "mancata_consegna",
"retrieval_non_acceptance": "mancata_consegna",
"received_by_non_rem": "presa_in_carico",
"submission_rejection": "non_accettazione",
"relay_rejection": "non_accettazione",
}
# Valori X-REM-Delivery-Status che indicano fallimento della consegna
# (usati per distinguere DeliveryInformation success da failure)
_DELIVERY_FAILURE_STATUSES: frozenset[str] = frozenset({
"failed", "failure", "error", "rejected", "undeliverable",
"not-delivered", "not_delivered", "ko",
})
@dataclass
class RemClassification:
"""Risultato della classificazione di un messaggio REM."""
pec_type: str # valore enum DB (riusa enum PEC italiana)
rem_evidence_type: str | None # valore raw dell'header X-REM-Evidence-Type
riferimento_message_id: str | None # X-REM-Orig-Message-Id (correlazione outbound)
is_receipt: bool # True se e' un'evidenza (non il messaggio originale)
def classify_rem_message(msg: email.message.Message) -> RemClassification:
"""
Classifica il tipo di messaggio REM analizzando gli header X-REM-*.
Header letti (in ordine di priorita'):
1. X-REM-Evidence-Type (tipo evidenza principale, ETSI EN 319 532-4)
2. X-REM-Type (alias usato da alcuni provider)
3. X-REM-Orig-Message-Id (correlazione con il messaggio originale)
4. X-REM-Message-Id (alias per il Message-ID di riferimento)
5. X-REM-Delivery-Status (per DeliveryInformation: distingue success/failure)
Se X-REM-Evidence-Type non e' presente, il messaggio viene classificato
come 'posta_certificata' (REMDispatch: il messaggio originale).
Returns:
RemClassification con pec_type mappato all'enum DB esistente.
"""
# Header principali
raw_evidence = _clean(
msg.get("X-REM-Evidence-Type") or msg.get("X-REM-Type")
)
x_ref = _clean(
msg.get("X-REM-Orig-Message-Id") or msg.get("X-REM-Message-Id")
)
delivery_status = _clean(msg.get("X-REM-Delivery-Status"))
# Normalizza la chiave per il lookup nella mappa
evidence_key = (raw_evidence or "").lower().strip()
pec_type = _REM_TYPE_MAP.get(evidence_key, "posta_certificata")
# Gestione speciale DeliveryInformation:
# Se X-REM-Delivery-Status indica un fallimento, ri-mappa su errore_consegna
if pec_type == "avvenuta_consegna" and delivery_status:
if delivery_status.lower().strip() in _DELIVERY_FAILURE_STATUSES:
pec_type = "errore_consegna"
logger.debug(
f"REM DeliveryInformation con status={delivery_status!r}: "
f"rimappato a errore_consegna"
)
is_receipt = pec_type != "posta_certificata"
logger.debug(
f"REM classify: evidence={raw_evidence!r} → pec_type={pec_type!r} "
f"is_receipt={is_receipt} ref={x_ref!r}"
)
return RemClassification(
pec_type=pec_type,
rem_evidence_type=raw_evidence,
riferimento_message_id=x_ref,
is_receipt=is_receipt,
)
def detect_rem_headers(msg: email.message.Message) -> bool:
"""
Verifica se un messaggio contiene header REM (X-REM-*).
Usato da detect_protocol() in pec_parser.py per determinare automaticamente
se un messaggio e' REM europea o PEC italiana durante la sincronizzazione.
Returns:
True se almeno un header X-REM-* e' presente, False altrimenti.
"""
for header_name in msg.keys():
if header_name.upper().startswith("X-REM-"):
return True
return False
# ─── Verifica file di sistema REM ─────────────────────────────────────────────
# Nomi file usati dall'infrastruttura REM (non allegati utente)
# Analoghi a daticert.xml / postacert.eml della PEC italiana
REM_SYSTEM_FILENAMES: frozenset[str] = frozenset({
"remevidence.xml",
"rem-evidence.xml",
"rem_evidence.xml",
"remreceipt.xml",
"rem-receipt.xml",
"remdispatch.xml",
"rem-dispatch.xml",
"remdelivery.xml",
"rem-delivery.xml",
"remdispatch.eml",
"rem-dispatch.eml",
"remsignature.p7s",
"rem-signature.p7s",
"signed-rem-dispatch.p7m",
})
def _clean(value: str | None) -> str | None:
"""Pulisce e normalizza il valore di un header REM."""
if not value:
return None
return value.strip()