Fix routing rule

This commit is contained in:
2026-06-18 14:10:26 +02:00
parent c1633b72d1
commit e70f188633
6 changed files with 667 additions and 177 deletions
+201 -1
View File
@@ -6,6 +6,7 @@ Il metodo evaluate_rules() viene chiamato dal worker dopo ogni messaggio inbound
import re
import uuid
from datetime import datetime, timedelta, timezone
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -245,6 +246,16 @@ class RoutingRuleService:
return message.risk_level or ""
elif field == "confidentiality":
return message.confidentiality or ""
# Campi aggiuntivi
elif field == "has_attachments":
return "true" if message.has_attachments else "false"
elif field == "direction":
return message.direction or ""
elif field == "protocol_type":
return message.protocol_type or ""
elif field == "body_contains":
# Restituisce il corpo del messaggio per confronto con contains/regex
return (message.body_text or "").lower()
return ""
def _evaluate_condition(
@@ -279,29 +290,58 @@ class RoutingRuleService:
try:
if action.action_type == "apply_label" and action.action_value:
await self._action_apply_label(message, uuid.UUID(action.action_value))
elif action.action_type == "apply_taxonomy" and action.action_value:
# Applica un nodo tassonomico: identico a apply_label
# Il nodo è una Label con parent_id valorizzato (Processo o Classificazione)
await self._action_apply_label(message, uuid.UUID(action.action_value))
elif action.action_type == "assign_vbox" and action.action_value:
await self._action_assign_vbox(message, uuid.UUID(action.action_value))
elif action.action_type == "mark_read":
message.is_read = True
elif action.action_type == "mark_starred":
message.is_starred = True
elif action.action_type == "archive":
message.is_archived = True
message.archived_at = datetime.now(timezone.utc)
elif action.action_type == "mark_for_conservation":
if not message.is_pending_conservation:
message.is_pending_conservation = True
message.pending_conservation_at = datetime.now(timezone.utc)
elif action.action_type == "set_deadline" and action.action_value:
await self._action_set_deadline(message, action.action_value)
elif action.action_type == "add_to_fascicolo" and action.action_value:
await self._action_add_to_fascicolo(message, uuid.UUID(action.action_value))
elif action.action_type == "notify_webhook" and action.action_value:
await self._action_notify_webhook(message, action.action_value)
elif action.action_type == "notify_channel" and action.action_value:
await self._action_notify_channel(message, uuid.UUID(action.action_value))
# Rischio e Riservatezza (N3)
elif action.action_type == "set_risk_level" and action.action_value:
valid_levels = {"low", "medium", "high", "critical"}
if action.action_value in valid_levels:
message.risk_level = action.action_value
elif action.action_type == "set_confidentiality" and action.action_value:
valid_levels = {"public", "internal", "confidential", "secret"}
if action.action_value in valid_levels:
message.confidentiality = action.action_value
except Exception:
# Le azioni non devono interrompere il flusso principale
pass
# ─── Implementazioni azioni ───────────────────────────────────────────────
async def _action_apply_label(
self, message: Message, label_id: uuid.UUID
) -> None:
@@ -325,6 +365,166 @@ class RoutingRuleService:
if not existing.scalar_one_or_none():
self.db.add(MessageLabel(message_id=message.id, label_id=label_id))
async def _action_assign_vbox(
self, message: Message, vbox_id: uuid.UUID
) -> None:
"""
Assegna il messaggio alla Virtual Box trovando la Label associata.
Le Virtual Box sono filtri read-only: per "assegnare" un messaggio a una VBox
si applica la Label il cui nome corrisponde al campo 'label' della VBox.
Se la VBox non ha un campo 'label' o non esiste una Label con quel nome,
l'azione viene saltata silenziosamente.
"""
from app.models.virtual_box import VirtualBox
vbox_result = await self.db.execute(
select(VirtualBox).where(
VirtualBox.id == vbox_id,
VirtualBox.tenant_id == message.tenant_id,
VirtualBox.is_active == True, # noqa: E712
)
)
vbox = vbox_result.scalar_one_or_none()
if not vbox or not vbox.label:
return
# Cerca la Label con lo stesso nome del campo 'label' della VBox
label_result = await self.db.execute(
select(Label).where(
Label.name == vbox.label,
Label.tenant_id == message.tenant_id,
)
)
label = label_result.scalar_one_or_none()
if label:
await self._action_apply_label(message, label.id)
async def _action_set_deadline(
self, message: Message, value: str
) -> None:
"""
Imposta una scadenza relativa al messaggio.
Formati accettati per action_value:
- Numero intero: "30" → +30 giorni
- Suffisso giorni: "+30d" → +30 giorni
- Suffisso settimane:"+4w" → +28 giorni
- Suffisso mesi: "+2m" → +60 giorni
- Suffisso anni: "+1y" → +365 giorni
- JSON: {"days": 30, "note": "Risposta entro 30 giorni"}
Non sovrascrive una scadenza gia' impostata manualmente.
"""
import json as _json
value = value.strip()
days: int | None = None
note: str | None = None
# Prova JSON
try:
parsed = _json.loads(value)
if isinstance(parsed, dict):
days = int(parsed.get("days", 0)) or None
note = parsed.get("note")
except Exception:
pass
# Prova formato stringa: "30", "+30d", "+4w", "+2m", "+1y"
if days is None:
m = re.match(r"^\+?(\d+)([dDwWmMyY]?)$", value)
if m:
n = int(m.group(1))
unit = m.group(2).lower() if m.group(2) else "d"
if unit == "w":
days = n * 7
elif unit == "m":
days = n * 30
elif unit == "y":
days = n * 365
else:
days = n
if days and days > 0:
base = message.received_at or datetime.now(timezone.utc)
message.deadline_at = base + timedelta(days=days)
if note and not message.deadline_note:
message.deadline_note = note
async def _action_add_to_fascicolo(
self, message: Message, fascicolo_id: uuid.UUID
) -> None:
"""
Aggiunge il messaggio a un fascicolo esistente (se non gia' presente).
Verifica che il fascicolo appartenga al tenant e non sia gia' archiviato.
"""
from app.models.fascicolo import Fascicolo, FascicoloMessage
# Verifica che il fascicolo esista, appartenga al tenant e non sia archiviato
fascicolo_result = await self.db.execute(
select(Fascicolo).where(
Fascicolo.id == fascicolo_id,
Fascicolo.tenant_id == message.tenant_id,
Fascicolo.stato != "archiviato",
)
)
if not fascicolo_result.scalar_one_or_none():
return
# Verifica che il messaggio non sia gia' nel fascicolo
existing = await self.db.execute(
select(FascicoloMessage).where(
FascicoloMessage.fascicolo_id == fascicolo_id,
FascicoloMessage.message_id == message.id,
)
)
if not existing.scalar_one_or_none():
self.db.add(FascicoloMessage(
fascicolo_id=fascicolo_id,
message_id=message.id,
))
async def _action_notify_channel(
self, message: Message, channel_id: uuid.UUID
) -> None:
"""
Invia una notifica tramite un canale configurato (webhook/email/telegram/whatsapp).
Crea un NotificationLog con status 'pending' che il worker processerà
tramite il meccanismo di retry esistente.
"""
from app.models.notification import NotificationChannel, NotificationLog
channel_result = await self.db.execute(
select(NotificationChannel).where(
NotificationChannel.id == channel_id,
NotificationChannel.tenant_id == message.tenant_id,
NotificationChannel.is_active == True, # noqa: E712
)
)
channel = channel_result.scalar_one_or_none()
if not channel:
return
# Crea il log per l'invio asincrono da parte del worker
self.db.add(NotificationLog(
tenant_id=message.tenant_id,
channel_id=channel_id,
event_type="routing_rule_match",
event_payload={
"message_id": str(message.id),
"subject": message.subject,
"from_address": message.from_address,
"pec_type": message.pec_type,
"mailbox_id": str(message.mailbox_id),
"direction": message.direction,
},
status="pending",
max_attempts=3,
))
async def _action_notify_webhook(self, message: Message, url: str) -> None:
"""Invia una notifica webhook per il messaggio."""
import aiohttp