Modifiche varie

This commit is contained in:
2026-06-04 20:54:49 +02:00
parent ccc4167e28
commit e31676d22e
31 changed files with 3058 additions and 153 deletions
+105 -13
View File
@@ -528,6 +528,8 @@ async def _save_message(
- Salvataggio allegati su MinIO + tabella attachments
- State machine outbound: solo per messaggi inbound (ricevute PEC)
- Collegamento parent_message_id via X-Riferimento-Message-ID
- Dedup outbound: evita duplicati quando un messaggio inviato via send_pec
viene poi trovato anche nella cartella Sent del server IMAP
"""
# ── Idempotenza: chiave composta (mailbox_id + imap_uid + imap_folder) ────
existing = await db.execute(
@@ -552,17 +554,73 @@ async def _save_message(
parsed = parse_eml(raw_eml, is_receipt=pec_class.is_receipt)
received_at = datetime.now(UTC)
# ── Dedup outbound: upsert sul record send_pec invece di creare duplicato ─
# Problema: send_pec crea un record outbound con imap_uid=NULL e poi
# la sync della cartella Sent trova lo stesso messaggio e vorrebbe creare
# un secondo record con lo stesso message_id_header. I duplicati rompono
# il binding delle ricevute (_apply_outbound_state_machine usava
# scalar_one_or_none() che esplode con MultipleResultsFound).
# Soluzione: se esiste già un record outbound con lo stesso message_id_header
# e imap_uid=NULL (il record canonico di send_pec), aggiorniamo quel record
# con l'imap_uid/imap_folder della Sent folder invece di crearne uno nuovo.
if direction == "outbound" and parsed.message_id:
existing_outbound = await db.execute(
select(Message).where(
Message.mailbox_id == mailbox.id,
Message.message_id_header == parsed.message_id,
Message.direction == "outbound",
Message.imap_uid.is_(None),
)
)
send_pec_record = existing_outbound.scalar_one_or_none()
if send_pec_record:
# Aggiorna il record esistente con i dati IMAP della cartella Sent
send_pec_record.imap_uid = uid
send_pec_record.imap_folder = imap_folder
send_pec_record.updated_at = datetime.now(UTC)
# Aggiorna anche il raw_eml_path se non è già impostato
if not send_pec_record.raw_eml_path:
try:
eml_path = await upload_eml(
tenant_id=str(mailbox.tenant_id),
mailbox_id=str(mailbox.id),
uid=uid,
eml_bytes=raw_eml,
)
send_pec_record.raw_eml_path = eml_path
except Exception as e:
logger.warning(
f"[{mailbox.email_address}] Upload EML MinIO per record send_pec "
f"UID {uid}: {e}"
)
await db.flush()
logger.info(
f"[{mailbox.email_address}] Sent-sync: aggiornato record send_pec "
f"message_id={parsed.message_id!r} con imap_uid={uid} "
f"folder={imap_folder!r} (evitato duplicato outbound)"
)
return True
# ── State machine: trova e aggiorna messaggio outbound ────────────────────
# 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:
parent_message_id = await _apply_outbound_state_machine(
riferimento_message_id=pec_class.riferimento_message_id,
pec_type=pec_class.pec_type,
tenant_id=mailbox.tenant_id,
db=db,
)
try:
parent_message_id = await _apply_outbound_state_machine(
riferimento_message_id=pec_class.riferimento_message_id,
pec_type=pec_class.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}",
exc_info=True,
)
# Non interrompere il salvataggio della ricevuta: il record viene
# comunque inserito, ma senza parent_message_id.
# ── Upload raw EML su MinIO ───────────────────────────────────────────────
eml_path: str | None = None
@@ -700,6 +758,12 @@ async def _apply_outbound_state_machine(
Cerca il messaggio outbound con message_id_header == riferimento_message_id,
applica la transizione di stato se valida.
Gestisce il caso di messaggi outbound duplicati (uno creato da send_pec con
imap_uid=NULL e uno creato dalla sync della cartella Sent): in caso di multipli,
prioritizza quello con imap_uid=NULL (il record canonico creato da send_pec).
Il dedup in _save_message riduce drasticamente la probabilità di multipli,
ma questa funzione gestisce anche i casi residui per robustezza.
Returns:
UUID del messaggio originale se trovato, None altrimenti.
"""
@@ -710,15 +774,37 @@ async def _apply_outbound_state_machine(
Message.direction == "outbound",
)
)
parent_msg = result.scalar_one_or_none()
candidates = result.scalars().all()
if not parent_msg:
logger.debug(
f"Messaggio outbound non trovato per riferimento={riferimento_message_id!r} "
f"(potrebbe essere stato inviato da client diverso)"
if not candidates:
logger.warning(
f"[receipt-binding] Messaggio outbound non trovato per "
f"riferimento={riferimento_message_id!r} (ricevuta: {pec_type!r}). "
f"Potrebbe essere stato inviato da un client esterno o il message_id_header "
f"non e' ancora stato persistito."
)
return None
# In presenza di duplicati (es. record send_pec + record Sent-sync),
# prioritizza il messaggio con imap_uid=NULL (quello canonico di send_pec).
parent_msg: Message | None = None
if len(candidates) == 1:
parent_msg = candidates[0]
else:
logger.warning(
f"[receipt-binding] Trovati {len(candidates)} messaggi outbound con "
f"message_id_header={riferimento_message_id!r}. "
f"Prioritizzo il record con imap_uid=NULL (send_pec)."
)
# Priorità 1: imap_uid IS NULL (creato da send_pec)
for m in candidates:
if m.imap_uid is None:
parent_msg = m
break
# Priorità 2: qualsiasi altro (creato dalla sync Sent)
if parent_msg is None:
parent_msg = candidates[0]
new_state = apply_outbound_transition(parent_msg.state, pec_type)
if new_state:
old_state = parent_msg.state
@@ -726,8 +812,14 @@ async def _apply_outbound_state_machine(
parent_msg.updated_at = datetime.now(UTC)
await db.flush()
logger.info(
f"State machine outbound: {riferimento_message_id!r} "
f"{old_state!r} {new_state!r} (ricevuta: {pec_type!r})"
f"[receipt-binding] State machine outbound: {riferimento_message_id!r} "
f"{old_state!r} -> {new_state!r} (ricevuta: {pec_type!r}, "
f"msg_id={parent_msg.id})"
)
else:
logger.debug(
f"[receipt-binding] Nessuna transizione valida per {riferimento_message_id!r} "
f"state={parent_msg.state!r} ricevuta={pec_type!r}"
)
return parent_msg.id