Fix routing rule
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user