Implementazioni varie

This commit is contained in:
2026-03-27 20:59:06 +01:00
parent 047990811f
commit 46784aca4c
40 changed files with 4090 additions and 34 deletions
+3
View File
@@ -10,3 +10,6 @@ from app.models.permission import MailboxPermission # noqa: F401
from app.models.virtual_box import VirtualBox, VirtualBoxRule, VirtualBoxAssignment # noqa: F401
from app.models.notification import NotificationChannel, NotificationRule, NotificationLog # noqa: F401
from app.models.tenant_settings import TenantSettings # noqa: F401
from app.models.template import MessageTemplate # noqa: F401
from app.models.routing_rule import RoutingRule, RoutingRuleCondition, RoutingRuleAction # noqa: F401
from app.models.pec_contact import PecContact # noqa: F401
+6
View File
@@ -95,6 +95,10 @@ class Message(Base):
is_trashed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
trashed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# Scadenzario (Feature 4)
deadline_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
deadline_note: Mapped[str | None] = mapped_column(Text, nullable=True)
# Conservazione
is_pending_conservation: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
pending_conservation_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
@@ -194,6 +198,8 @@ class SendJob(Base):
max_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
# Invio differito (Feature 5): se impostato, il job non viene processato prima di questa data
scheduled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
queued_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
+49
View File
@@ -0,0 +1,49 @@
"""
Modello PecContact rubrica indirizzi PEC del tenant.
"""
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class PecContact(Base):
__tablename__ = "pec_contacts"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
)
email: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
organization: Mapped[str | None] = mapped_column(String(255), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
is_favorite: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
# auto_saved=True → salvato automaticamente dalla sincronizzazione IMAP
# auto_saved=False → aggiunto manualmente dall'utente
auto_saved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
UniqueConstraint("tenant_id", "email", name="uq_pec_contact_email_tenant"),
Index("idx_pec_contacts_tenant", "tenant_id"),
Index("idx_pec_contacts_email", "tenant_id", "email"),
)
def __repr__(self) -> str:
return f"<PecContact {self.email!r}>"
+114
View File
@@ -0,0 +1,114 @@
"""
Modelli RoutingRule, RoutingRuleCondition, RoutingRuleAction.
Le regole di smistamento automatico vengono valutate a ogni messaggio in arrivo:
1. Si caricano tutte le regole attive del tenant, ordinate per priority ASC
2. Per ogni regola si valutano le condizioni (AND tra le condizioni della stessa regola)
3. Se tutte le condizioni sono soddisfatte, si eseguono le azioni
4. Se stop_processing=True, si ferma l'elaborazione (non vengono valutate regole successive)
"""
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class RoutingRule(Base):
__tablename__ = "routing_rules"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=100)
# Se True, le regole successive non vengono valutate una volta che questa fa match
stop_processing: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
# Relazioni
conditions: Mapped[list["RoutingRuleCondition"]] = relationship(
"RoutingRuleCondition", back_populates="rule", cascade="all, delete-orphan"
)
actions: Mapped[list["RoutingRuleAction"]] = relationship(
"RoutingRuleAction", back_populates="rule", cascade="all, delete-orphan"
)
__table_args__ = (
Index("idx_routing_rules_tenant", "tenant_id"),
Index("idx_routing_rules_active", "tenant_id", "priority"),
)
def __repr__(self) -> str:
return f"<RoutingRule {self.name!r} priority={self.priority}>"
class RoutingRuleCondition(Base):
"""
Singola condizione di una regola.
field : from_address | to_address | subject | mailbox_id | pec_type
operator : contains | equals | starts_with | ends_with | regex | not_contains
value : valore da confrontare (stringa)
"""
__tablename__ = "routing_rule_conditions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
rule_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("routing_rules.id", ondelete="CASCADE"), nullable=False
)
field: Mapped[str] = mapped_column(String(50), nullable=False)
operator: Mapped[str] = mapped_column(String(30), nullable=False, default="contains")
value: Mapped[str] = mapped_column(Text, nullable=False)
rule: Mapped["RoutingRule"] = relationship("RoutingRule", back_populates="conditions")
__table_args__ = (
Index("idx_routing_conditions_rule", "rule_id"),
)
class RoutingRuleAction(Base):
"""
Singola azione di una regola.
action_type : apply_label | assign_vbox | mark_read | mark_starred | notify_webhook
action_value : UUID della label/vbox, o URL del webhook
"""
__tablename__ = "routing_rule_actions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
rule_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("routing_rules.id", ondelete="CASCADE"), nullable=False
)
action_type: Mapped[str] = mapped_column(String(50), nullable=False)
action_value: Mapped[str | None] = mapped_column(Text, nullable=True)
rule: Mapped["RoutingRule"] = relationship("RoutingRule", back_populates="actions")
__table_args__ = (
Index("idx_routing_actions_rule", "rule_id"),
)
+45
View File
@@ -0,0 +1,45 @@
"""
Modello MessageTemplate template riutilizzabili per la composizione PEC.
"""
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class MessageTemplate(Base):
__tablename__ = "message_templates"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
subject: Mapped[str] = mapped_column(Text, nullable=False, default="")
body_text: Mapped[str | None] = mapped_column(Text, nullable=True)
body_html: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
UniqueConstraint("tenant_id", "name", name="uq_template_name_tenant"),
Index("idx_templates_tenant", "tenant_id"),
)
def __repr__(self) -> str:
return f"<MessageTemplate {self.name!r}>"