GapFill Flowee

This commit is contained in:
2026-06-18 11:24:05 +02:00
parent 64442af182
commit c68daf4313
25 changed files with 2965 additions and 48 deletions
+49 -13
View File
@@ -39,7 +39,8 @@ from app.jobs.dispatch_notification import evaluate_and_enqueue_notifications
from app.jobs.index_message import index_message
from app.models import Attachment, Mailbox, Message
from app.parsers.eml_parser import parse_eml
from app.parsers.pec_parser import apply_outbound_transition, classify_pec_message
from app.parsers.pec_parser import apply_outbound_transition, classify_pec_message, detect_protocol
from app.parsers.rem_parser import classify_rem_message
from app.storage.minio_client import upload_attachment, upload_eml
logger = logging.getLogger(__name__)
@@ -543,15 +544,47 @@ async def _save_message(
logger.debug(f"[{mailbox.email_address}] UID {uid} in {imap_folder!r} già in DB, skip")
return False
# ── Classificazione PEC da header (veloce, senza body) ───────────────────
# ── Classificazione tipo messaggio da header (veloce, senza body) ────────
# La classificazione avviene PRIMA del parsing completo perche' il parser
# deve sapere se il messaggio e' una ricevuta per evitare di sovrascrivere
# il body_text (testo della ricevuta) con il contenuto di postacert.eml.
#
# Strategia dual-protocol (Feature N8 REM europea):
# 1. Rileva automaticamente il protocollo dagli header del messaggio.
# 2. Se header X-REM-* presenti: usa classify_rem_message (ETSI EN 319 532-4)
# 3. Se header PEC italiani (X-Ricevuta/X-TipoRicevuta): usa classify_pec_message
# 4. Default fallback: PEC italiana
#
# Il protocollo configurato sulla casella (mailbox.protocol_type) viene
# usato come hint, ma il rilevamento automatico degli header ha priorita'.
quick_msg = email.message_from_bytes(raw_eml)
pec_class = classify_pec_message(quick_msg)
mailbox_protocol = getattr(mailbox, "protocol_type", "pec_it") or "pec_it"
detected_protocol = detect_protocol(quick_msg)
# Il rilevamento automatico ha priorita': una casella PEC-IT che riceve un
# messaggio REM da partner europeo viene classificata correttamente.
_protocol_type = detected_protocol if detected_protocol == "rem_eu" else mailbox_protocol
if _protocol_type == "rem_eu":
rem_class = classify_rem_message(quick_msg)
_pec_type = rem_class.pec_type
_is_receipt = rem_class.is_receipt
_riferimento_message_id = rem_class.riferimento_message_id
_rem_evidence_type = rem_class.rem_evidence_type
logger.debug(
f"[{mailbox.email_address}] REM UID={uid}: "
f"evidence={_rem_evidence_type!r} → pec_type={_pec_type!r}"
)
else:
pec_class = classify_pec_message(quick_msg)
_pec_type = pec_class.pec_type
_is_receipt = pec_class.is_receipt
_riferimento_message_id = pec_class.riferimento_message_id
_rem_evidence_type = None
_protocol_type = "pec_it"
# ── Parsing completo EML (con is_receipt per proteggere il body) ──────────
parsed = parse_eml(raw_eml, is_receipt=pec_class.is_receipt)
parsed = parse_eml(raw_eml, is_receipt=_is_receipt)
received_at = datetime.now(UTC)
# ── Dedup outbound: upsert sul record send_pec invece di creare duplicato ─
@@ -605,18 +638,18 @@ async def _save_message(
# Solo per messaggi inbound che sono ricevute PEC (non per posta inviata)
parent_message_id: uuid.UUID | None = None
if direction == "inbound" and pec_class.is_receipt and pec_class.riferimento_message_id:
if direction == "inbound" and _is_receipt and _riferimento_message_id:
try:
parent_message_id = await _apply_outbound_state_machine(
riferimento_message_id=pec_class.riferimento_message_id,
pec_type=pec_class.pec_type,
riferimento_message_id=_riferimento_message_id,
pec_type=_pec_type,
tenant_id=mailbox.tenant_id,
db=db,
)
except Exception as bind_err:
logger.error(
f"[{mailbox.email_address}] [receipt-binding] Errore aggiornamento stato "
f"outbound per ricevuta UID={uid} tipo={pec_class.pec_type!r}: {bind_err}",
f"outbound per ricevuta UID={uid} tipo={_pec_type!r}: {bind_err}",
exc_info=True,
)
# Non interrompere il salvataggio della ricevuta: il record viene
@@ -648,7 +681,9 @@ async def _save_message(
imap_folder=imap_folder,
direction=direction,
state=state,
pec_type=pec_class.pec_type,
pec_type=_pec_type,
protocol_type=_protocol_type,
rem_evidence_type=_rem_evidence_type,
subject=parsed.subject,
from_address=parsed.from_address,
to_addresses=parsed.to_addresses if parsed.to_addresses else None,
@@ -687,7 +722,7 @@ async def _save_message(
"from_address": message.from_address or "",
"pec_type": message.pec_type,
"direction": direction,
"is_receipt": pec_class.is_receipt,
"is_receipt": _is_receipt,
"received_at": received_at.isoformat(),
}
await redis_client.publish(f"ws:tenant:{mailbox.tenant_id}", json.dumps(event))
@@ -696,7 +731,7 @@ async def _save_message(
logger.info(
f"[{mailbox.email_address}] Nuovo messaggio: UID={uid} folder={imap_folder!r} "
f"direction={direction!r} pec_type={pec_class.pec_type!r} "
f"direction={direction!r} pec_type={_pec_type!r} protocol={_protocol_type!r} "
f"subject={message.subject!r} allegati={len(parsed.attachments)}"
)
@@ -719,7 +754,8 @@ async def _save_message(
# ── Regole di smistamento automatico (Feature 2) ──────────────────────────
# Solo per messaggi inbound posta_certificata (non ricevute di sistema).
if direction == "inbound" and pec_class.pec_type == "posta_certificata":
# Valido anche per messaggi REM (REMDispatch e' equivalente a posta_certificata).
if direction == "inbound" and _pec_type == "posta_certificata":
try:
await redis_client.enqueue_job("apply_routing_rules", str(message.id))
except Exception as e:
@@ -728,7 +764,7 @@ async def _save_message(
# ── Auto-save mittente nella rubrica (Feature 6) ──────────────────────────
# Per messaggi inbound di tipo posta_certificata, salva automaticamente
# il mittente nella rubrica pec_contacts del tenant (upsert idempotente).
if direction == "inbound" and pec_class.pec_type == "posta_certificata" and message.from_address:
if direction == "inbound" and _pec_type == "posta_certificata" and message.from_address:
try:
from sqlalchemy import text as _text
await db.execute(