mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Modifiche varie
This commit is contained in:
+105
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user