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
|
||||
|
||||
@@ -7,14 +7,17 @@ Questo job viene usato per:
|
||||
- Retry dopo un errore (called dal pool monitor)
|
||||
|
||||
Non sostituisce il loop IMAP continuo (IMAPConnection); è un one-shot job.
|
||||
|
||||
Sincronizza sia INBOX (per rilevare ricevute PEC e messaggi in arrivo)
|
||||
sia la cartella Sent (per aggiornare imap_uid sul record send_pec ed
|
||||
evitare duplicati outbound che rompono il binding delle ricevute).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.imap.reconnect import ExponentialBackoff
|
||||
from app.imap.sync import sync_new_messages
|
||||
from app.imap.sync import sync_new_messages, sync_sent_messages
|
||||
from app.models import Mailbox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -22,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
async def sync_mailbox(ctx: dict[str, Any], mailbox_id: str) -> dict:
|
||||
"""
|
||||
Job arq: sincronizza una singola casella PEC.
|
||||
Job arq: sincronizza una singola casella PEC (INBOX + Sent).
|
||||
|
||||
Args:
|
||||
ctx: contesto arq (contiene redis, pool reference)
|
||||
@@ -50,28 +53,44 @@ async def sync_mailbox(ctx: dict[str, Any], mailbox_id: str) -> dict:
|
||||
creds = IMAPConnection._decrypt_creds(mailbox)
|
||||
|
||||
try:
|
||||
from app.imap.connection import IMAPConnection
|
||||
|
||||
conn = IMAPConnection(mailbox_id=mailbox_id, redis_client=redis_client)
|
||||
client = await conn._connect(creds)
|
||||
|
||||
n = await sync_new_messages(client, mailbox, db, redis_client)
|
||||
# Sync INBOX: ricevute PEC e messaggi in arrivo
|
||||
n_inbox = await sync_new_messages(client, mailbox, db, redis_client)
|
||||
|
||||
# Sync Sent: aggiorna imap_uid sui record send_pec e previene duplicati.
|
||||
# Fondamentale per il corretto binding delle ricevute PEC successive.
|
||||
n_sent = 0
|
||||
try:
|
||||
n_sent = await sync_sent_messages(client, mailbox, db, redis_client)
|
||||
except Exception as sent_err:
|
||||
logger.warning(
|
||||
f"[sync_mailbox] {mailbox.email_address} errore sync Sent "
|
||||
f"(non critico): {sent_err}"
|
||||
)
|
||||
|
||||
try:
|
||||
await client.logout()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
f"[sync_mailbox] {mailbox.email_address}: "
|
||||
f"INBOX={n_inbox} nuovi, Sent={n_sent} nuovi"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"mailbox": mailbox.email_address,
|
||||
"new_messages": n,
|
||||
"new_messages_inbox": n_inbox,
|
||||
"new_messages_sent": n_sent,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[sync_mailbox] {mailbox_id} errore: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"mailbox": mailbox.email_address,
|
||||
"mailbox": mailbox.email_address if mailbox else mailbox_id,
|
||||
"message": str(e),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user