""" 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()