""" Modelli Message, Attachment, SendJob. """ import uuid from datetime import datetime from typing import Any from sqlalchemy import ( ARRAY, BigInteger, Boolean, DateTime, Enum, ForeignKey, Index, Integer, String, Text, func, ) from sqlalchemy.dialects.postgresql import TSVECTOR, UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base PecDirection = Enum("inbound", "outbound", name="pec_direction", create_type=False) PecState = Enum( "draft", "queued", "sent", "accepted", "delivered", "anomaly", "failed", "received", name="pec_state", create_type=False, ) PecMsgType = Enum( "posta_certificata", "accettazione", "non_accettazione", "presa_in_carico", "avvenuta_consegna", "mancata_consegna", "errore_consegna", "preavviso_mancata_consegna", "rilevazione_virus", "unknown", name="pec_msg_type", create_type=False, ) SendJobStatus = Enum( "pending", "sending", "sent", "failed", "retrying", name="send_job_status", create_type=False, ) class Message(Base): __tablename__ = "messages" 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 ) mailbox_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("mailboxes.id", ondelete="CASCADE"), nullable=False ) # Identificatori message_id_header: Mapped[str | None] = mapped_column(Text, nullable=True) imap_uid: Mapped[int | None] = mapped_column(BigInteger, nullable=True) imap_folder: Mapped[str] = mapped_column(String(255), nullable=False, default="INBOX") direction: Mapped[str] = mapped_column(PecDirection, nullable=False) pec_type: Mapped[str] = mapped_column( PecMsgType, nullable=False, default="posta_certificata" ) state: Mapped[str] = mapped_column(PecState, nullable=False) # Busta PEC subject: Mapped[str | None] = mapped_column(Text, nullable=True) from_address: Mapped[str | None] = mapped_column(String(255), nullable=True) to_addresses: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True) cc_addresses: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True) sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) received_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) size_bytes: Mapped[int | None] = mapped_column(BigInteger, nullable=True) # Corpo body_text: Mapped[str | None] = mapped_column(Text, nullable=True) body_html: Mapped[str | None] = mapped_column(Text, nullable=True) has_attachments: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) # Collegamento ricevute parent_message_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("messages.id"), nullable=True ) # Flag operativi is_read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_starred: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 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) is_conserved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) conserved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) raw_eml_path: Mapped[str | None] = mapped_column(Text, nullable=True) # Full-text search vector (aggiornato da trigger DB + worker per allegati) search_vector: Mapped[Any | None] = mapped_column(TSVECTOR(), 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 attachments: Mapped[list["Attachment"]] = relationship( "Attachment", back_populates="message", cascade="all, delete-orphan" ) children: Mapped[list["Message"]] = relationship( "Message", foreign_keys=[parent_message_id] ) labels: Mapped[list["Label"]] = relationship( # type: ignore[name-defined] "Label", secondary="message_labels", lazy="select", ) __table_args__ = ( Index("idx_messages_tenant", "tenant_id"), Index("idx_messages_mailbox", "mailbox_id"), Index("idx_messages_state", "state"), Index("idx_messages_received_at", "received_at", postgresql_ops={"received_at": "DESC"}), Index( "idx_messages_parent", "parent_message_id", postgresql_where="parent_message_id IS NOT NULL", ), Index("idx_messages_imap_uid", "mailbox_id", "imap_uid"), Index("idx_messages_fts", "search_vector", postgresql_using="gin"), ) def __repr__(self) -> str: return f"" class Attachment(Base): __tablename__ = "attachments" 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 ) message_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("messages.id", ondelete="CASCADE"), nullable=False ) filename: Mapped[str] = mapped_column(String(512), nullable=False) content_type: Mapped[str | None] = mapped_column(String(255), nullable=True) size_bytes: Mapped[int | None] = mapped_column(BigInteger, nullable=True) storage_path: Mapped[str] = mapped_column(Text, nullable=False) checksum_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True) # Testo estratto dal worker (solo PDF e DOCX) per la ricerca full-text extracted_text: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) # Relazioni message: Mapped["Message"] = relationship("Message", back_populates="attachments") __table_args__ = ( Index("idx_attachments_message", "message_id"), ) class SendJob(Base): __tablename__ = "send_jobs" 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 ) mailbox_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("mailboxes.id"), nullable=False ) message_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("messages.id"), nullable=True ) status: Mapped[str] = mapped_column(SendJobStatus, nullable=False, default="pending") attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) 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() ) sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_by: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id"), nullable=True ) __table_args__ = ( Index("idx_sendjobs_tenant", "tenant_id"), Index( "idx_sendjobs_status", "status", "next_retry_at", postgresql_where="status IN ('pending', 'retrying')", ), )