203 lines
8.6 KiB
Python
203 lines
8.6 KiB
Python
"""
|
||
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()
|