fix parsing ricevute
This commit is contained in:
@@ -364,6 +364,32 @@ async def sync_sent_messages(
|
||||
f"[{mailbox.email_address}] Errore fetch {sent_folder!r} seq {seq}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if synced_count > 0:
|
||||
logger.info(
|
||||
f"[{mailbox.email_address}] Sincronizzati {synced_count} messaggi nuovi da {sent_folder!r}"
|
||||
)
|
||||
|
||||
# Aggiorna sent_last_sync_uid e torna in INBOX
|
||||
if max_uid_synced > last_uid:
|
||||
mailbox.sent_last_sync_uid = max_uid_synced
|
||||
mailbox.last_sync_at = datetime.now(UTC)
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
|
||||
try:
|
||||
await imap_client.select("INBOX")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return synced_count
|
||||
|
||||
|
||||
async def _fetch_and_save_message_by_seq(
|
||||
imap_client: aioimaplib.IMAP4 | aioimaplib.IMAP4_SSL,
|
||||
seq: str,
|
||||
last_uid: int,
|
||||
mailbox: Mailbox,
|
||||
db: AsyncSession,
|
||||
redis_client: aioredis.Redis,
|
||||
imap_folder: str = "INBOX",
|
||||
@@ -711,6 +737,10 @@ async def _save_message(
|
||||
body_html=parsed.body_html,
|
||||
has_attachments=parsed.has_attachments,
|
||||
parent_message_id=parent_message_id,
|
||||
# Salva il X-Riferimento-Message-ID per il binding retroattivo.
|
||||
# Permette allo script rebind_receipts.py di ricollegare ricevute orfane
|
||||
# senza dover ri-leggere l'EML da MinIO.
|
||||
riferimento_message_id=_riferimento_message_id if _is_receipt else None,
|
||||
raw_eml_path=eml_path,
|
||||
# Messaggi outbound (Sent) sono già stati letti dal mittente
|
||||
is_read=(direction == "outbound"),
|
||||
|
||||
@@ -24,6 +24,7 @@ import json
|
||||
import logging
|
||||
import uuid as uuid_module
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import make_msgid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -135,6 +136,35 @@ async def send_pec(ctx: dict[str, Any], send_job_id: str) -> dict:
|
||||
)
|
||||
# Continua senza questo allegato
|
||||
|
||||
# ── Pre-genera Message-ID e commita PRIMA dell'invio SMTP ─────────────
|
||||
# RACE CONDITION FIX: il Message-ID viene scritto nel DB e committato
|
||||
# PRIMA dell'handshake SMTP, in modo che, se il ciclo IMAP sync del
|
||||
# worker gira durante l'invio (finestra di secondi), trovi gia' il
|
||||
# record con message_id_header valorizzato e possa bindare correttamente
|
||||
# le ricevute di accettazione (da Aruba: entro 1-2s dalla ricezione).
|
||||
# Senza questo fix la ricevuta di accettazione veniva sempre processata
|
||||
# con message_id_header=NULL nel DB → binding fallisce → parent_message_id
|
||||
# non impostato sulla ricevuta → ricevuta compare in inbox come messaggio
|
||||
# ordinario e lo stato outbound non avanza ad "accepted".
|
||||
if not msg.message_id_header:
|
||||
# Primo tentativo: genera e persiste un nuovo Message-ID
|
||||
pre_generated_id = make_msgid(domain="pechub.local")
|
||||
msg.message_id_header = pre_generated_id
|
||||
await db.commit()
|
||||
logger.info(
|
||||
f"[send_pec] Message-ID pre-committato prima dell'SMTP: "
|
||||
f"{pre_generated_id}"
|
||||
)
|
||||
else:
|
||||
# Retry successivo: riusa il Message-ID gia' persistito dal
|
||||
# tentativo precedente. In questo modo eventuali ricevute gia'
|
||||
# salvate (ma non bindato per la race) possono essere ricollegate.
|
||||
pre_generated_id = msg.message_id_header
|
||||
logger.debug(
|
||||
f"[send_pec] Retry - riutilizzo Message-ID esistente: "
|
||||
f"{pre_generated_id}"
|
||||
)
|
||||
|
||||
# ── Tenta invio SMTP ──────────────────────────────────────────────────
|
||||
try:
|
||||
sender = SmtpSender(mailbox)
|
||||
@@ -145,11 +175,14 @@ async def send_pec(ctx: dict[str, Any], send_job_id: str) -> dict:
|
||||
body_text=msg.body_text or "",
|
||||
body_html=msg.body_html,
|
||||
attachments=attachments_data,
|
||||
preset_message_id=pre_generated_id,
|
||||
)
|
||||
|
||||
# ── Successo: aggiorna DB ─────────────────────────────────────────
|
||||
# msg.message_id_header e' gia' impostato e committato sopra.
|
||||
# message_id_header restituito dal sender deve essere identico a
|
||||
# pre_generated_id (stessa stringa che abbiamo passato).
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
msg.message_id_header = message_id_header
|
||||
msg.state = "sent"
|
||||
msg.sent_at = now
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@ class Message(Base):
|
||||
parent_message_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("messages.id"), nullable=True
|
||||
)
|
||||
# X-Riferimento-Message-ID estratto dalle ricevute inbound PEC.
|
||||
# Usato dal binding retroattivo per ricollegare ricevute orfane.
|
||||
riferimento_message_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
is_read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
is_starred: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
@@ -174,23 +174,38 @@ def detect_protocol(msg: email.message.Message) -> str:
|
||||
"""
|
||||
Determina il protocollo di un messaggio in arrivo.
|
||||
|
||||
Logica di rilevamento automatico:
|
||||
- Se il messaggio contiene almeno un header X-REM-*, il protocollo e' REM europea
|
||||
- Altrimenti e' PEC italiana (default)
|
||||
Logica di rilevamento automatico (priorita' decrescente):
|
||||
1. Se presenti header PEC-IT specifici (X-Ricevuta, X-TipoRicevuta, X-Trasporto)
|
||||
→ protocollo e' PEC italiana, indipendentemente da altri header.
|
||||
2. Se presenti header X-REM-* SENZA header PEC-IT
|
||||
→ protocollo e' REM europea (ETSI EN 319 532-4)
|
||||
3. Default → PEC italiana
|
||||
|
||||
Questo permette al worker di usare il parser corretto (classify_rem_message vs
|
||||
classify_pec_message) anche per caselle configurate come 'pec_it' che potrebbero
|
||||
ricevere messaggi REM da partner europei (caso edge).
|
||||
IMPORTANTE: Aruba PEC aggiunge header X-REM-* (es. X-REM-Subject) anche ai
|
||||
messaggi PEC-IT standard, come parte di una migrazione verso lo standard ETSI
|
||||
REM. Questi header non devono essere interpretati come protocollo REM europeo
|
||||
quando gli header PEC-IT (X-Ricevuta, X-TipoRicevuta) sono anch'essi presenti.
|
||||
In tal caso la classificazione PEC-IT ha priorita'.
|
||||
|
||||
Args:
|
||||
msg: oggetto email.message.Message gia' parsato dagli header
|
||||
|
||||
Returns:
|
||||
'rem_eu' se header X-REM-* rilevati, 'pec_it' altrimenti.
|
||||
'pec_it' se header PEC-IT specifici presenti (o nessun header REM),
|
||||
'rem_eu' se header X-REM-* presenti senza header PEC-IT specifici.
|
||||
"""
|
||||
# Controlla prima gli header PEC-IT specifici: hanno priorita' assoluta.
|
||||
# Questi header identificano in modo univoco un messaggio PEC italiana.
|
||||
_PEC_IT_SPECIFIC = {"X-RICEVUTA", "X-TIPORICEVUTA", "X-TRASPORTO"}
|
||||
for header_name in msg.keys():
|
||||
if header_name.upper() in _PEC_IT_SPECIFIC:
|
||||
return "pec_it"
|
||||
|
||||
# Nessun header PEC-IT trovato: verifica header REM europea
|
||||
for header_name in msg.keys():
|
||||
if header_name.upper().startswith("X-REM-"):
|
||||
return "rem_eu"
|
||||
|
||||
return "pec_it"
|
||||
|
||||
|
||||
|
||||
@@ -86,17 +86,23 @@ class SmtpSender:
|
||||
body_text: str,
|
||||
body_html: str | None = None,
|
||||
attachments: list[dict] | None = None,
|
||||
preset_message_id: str | None = None,
|
||||
) -> tuple[MIMEMultipart, str]:
|
||||
"""
|
||||
Costruisce il messaggio MIME per la PEC.
|
||||
|
||||
Args:
|
||||
to_addresses: destinatari principali
|
||||
cc_addresses: destinatari in copia (può essere vuoto)
|
||||
subject: oggetto del messaggio
|
||||
body_text: corpo in testo semplice
|
||||
body_html: corpo HTML opzionale
|
||||
attachments: lista di dict {filename, content: bytes, content_type}
|
||||
to_addresses: destinatari principali
|
||||
cc_addresses: destinatari in copia (può essere vuoto)
|
||||
subject: oggetto del messaggio
|
||||
body_text: corpo in testo semplice
|
||||
body_html: corpo HTML opzionale
|
||||
attachments: lista di dict {filename, content: bytes, content_type}
|
||||
preset_message_id: Message-ID pre-generato (opzionale). Se fornito viene
|
||||
usato direttamente invece di generarne uno nuovo con
|
||||
make_msgid(). Permette di settare message_id_header
|
||||
nel DB PRIMA dell'invio SMTP, eliminando la race
|
||||
condition con il binding delle ricevute di accettazione.
|
||||
|
||||
Returns:
|
||||
(msg MIME, message_id_header)
|
||||
@@ -115,7 +121,9 @@ class SmtpSender:
|
||||
body_container = msg
|
||||
|
||||
# Headers obbligatori
|
||||
message_id = make_msgid(domain="pechub.local")
|
||||
# Se il chiamante ha pre-generato il Message-ID (per committarlo nel DB prima
|
||||
# dell'invio SMTP), lo usiamo direttamente. Altrimenti ne generiamo uno nuovo.
|
||||
message_id = preset_message_id if preset_message_id else make_msgid(domain="pechub.local")
|
||||
msg["From"] = self.mailbox.email_address
|
||||
msg["To"] = ", ".join(to_addresses)
|
||||
if cc_addresses:
|
||||
@@ -171,6 +179,7 @@ class SmtpSender:
|
||||
body_text: str,
|
||||
body_html: str | None = None,
|
||||
attachments: list[dict] | None = None,
|
||||
preset_message_id: str | None = None,
|
||||
) -> tuple[str, bytes]:
|
||||
"""
|
||||
Invia la PEC via SMTP.
|
||||
@@ -180,6 +189,12 @@ class SmtpSender:
|
||||
- Porta 587 con STARTTLS (use_tls=False, porta 587)
|
||||
- Porta 25 plain (uso sconsigliato)
|
||||
|
||||
Args:
|
||||
preset_message_id: Message-ID pre-generato da usare nell'header Message-ID.
|
||||
Deve essere gia' stato committato nel DB su msg.message_id_header
|
||||
prima di chiamare questo metodo, per eliminare la race condition
|
||||
con il binding delle ricevute PEC.
|
||||
|
||||
Returns:
|
||||
(message_id_header, raw_eml_bytes)
|
||||
|
||||
@@ -194,6 +209,7 @@ class SmtpSender:
|
||||
body_text=body_text,
|
||||
body_html=body_html,
|
||||
attachments=attachments,
|
||||
preset_message_id=preset_message_id,
|
||||
)
|
||||
|
||||
raw_eml: bytes = msg.as_bytes()
|
||||
|
||||
Reference in New Issue
Block a user