This commit is contained in:
2026-03-25 17:49:13 +01:00
parent c3ef6465d6
commit 03be5d0e32
13 changed files with 458 additions and 98 deletions
+36 -10
View File
@@ -4,7 +4,8 @@ Router messaggi PEC.
Fornisce:
- GET /messages lista messaggi con filtri (inbox/sent/search/...)
- GET /messages/{id} singolo messaggio
- PATCH /messages/{id} aggiorna flags (is_read, is_starred, is_archived)
- PATCH /messages/{id} aggiorna flags (is_read, is_starred, is_archived, is_trashed)
- PATCH /messages/bulk aggiorna in blocco piu messaggi
- GET /messages/{id}/attachments lista allegati
- GET /messages/{id}/attachments/{att_id}/download scarica allegato da MinIO
- GET /messages/{id}/receipts ricevute (messaggi figlio)
@@ -58,7 +59,7 @@ def _apply_vbox_rule(q, field: str, operator: str, value: str):
elif field == "from_address":
col = Message.from_address
elif field == "to_address":
# to_addresses è ARRAY(Text) converte in stringa per il confronto
# to_addresses e ARRAY(Text) converte in stringa per il confronto
arr_text = func.array_to_string(Message.to_addresses, ",")
if operator == "contains":
return q.where(arr_text.ilike(f"%{value}%"))
@@ -94,7 +95,7 @@ async def _get_visible_mailbox_ids(
) -> Optional[list[uuid.UUID]]:
"""
Per utenti non-admin restituisce la lista di mailbox_id accessibili.
Restituisce None se l'utente è admin (accesso illimitato al tenant).
Restituisce None se l'utente e admin (accesso illimitato al tenant).
"""
if user.is_admin:
return None # nessun filtro per admin
@@ -111,10 +112,10 @@ async def _resolve_message(
) -> Message:
"""Carica il messaggio e verifica i permessi di accesso.
L'accesso è consentito se:
1. L'utente è admin del tenant, oppure
L'accesso e consentito se:
1. L'utente e admin del tenant, oppure
2. L'utente ha un permesso diretto can_read sulla casella, oppure
3. L'utente è assegnato a una Virtual Box attiva che include la casella.
3. L'utente e assegnato a una Virtual Box attiva che include la casella.
"""
result = await db.execute(
select(Message)
@@ -182,6 +183,7 @@ async def list_messages(
is_read: Optional[bool] = Query(None),
is_starred: Optional[bool] = Query(None),
is_archived: Optional[bool] = Query(False),
is_trashed: Optional[bool] = Query(False),
search: Optional[str] = Query(None, max_length=200),
pec_type: Optional[str] = Query(None),
# Paginazione
@@ -192,6 +194,7 @@ async def list_messages(
Elenca i messaggi PEC con filtri opzionali.
- `is_archived=False` (default) esclude i messaggi archiviati.
- `is_trashed=False` (default) esclude i messaggi nel cestino.
- `search` cerca su subject, from_address, to_addresses.
- `vbox_id` filtra per Virtual Box assegnata all'utente corrente.
"""
@@ -278,6 +281,9 @@ async def list_messages(
if is_archived is not None:
q = q.where(Message.is_archived == is_archived)
if is_trashed is not None:
q = q.where(Message.is_trashed == is_trashed)
if search:
term = f"%{search}%"
q = q.where(
@@ -325,7 +331,7 @@ async def bulk_update_messages(
db: DB,
) -> MessageBulkUpdateResponse:
"""
Aggiorna in blocco i flag operativi (is_starred, is_archived) di più messaggi.
Aggiorna in blocco i flag operativi (is_starred, is_archived, is_trashed) di piu messaggi.
Restituisce il numero di messaggi aggiornati e la lista aggiornata.
I messaggi non trovati o non accessibili vengono silenziosamente ignorati.
@@ -352,6 +358,8 @@ async def bulk_update_messages(
now = datetime.now(timezone.utc)
for message in messages:
if data.is_read is not None:
message.is_read = data.is_read
if data.is_starred is not None:
message.is_starred = data.is_starred
if data.is_archived is not None:
@@ -360,6 +368,12 @@ async def bulk_update_messages(
message.archived_at = now
elif not data.is_archived:
message.archived_at = None
if data.is_trashed is not None:
message.is_trashed = data.is_trashed
if data.is_trashed and not message.trashed_at:
message.trashed_at = now
elif not data.is_trashed:
message.trashed_at = None
await db.commit()
@@ -399,7 +413,7 @@ async def update_message(
) -> MessageResponse:
"""
Aggiorna i flag operativi di un messaggio:
is_read, is_starred, is_archived.
is_read, is_starred, is_archived, is_trashed.
"""
message = await _resolve_message(message_id, current_user, db)
@@ -413,6 +427,12 @@ async def update_message(
message.archived_at = datetime.now(timezone.utc)
elif not data.is_archived:
message.archived_at = None
if data.is_trashed is not None:
message.is_trashed = data.is_trashed
if data.is_trashed and not message.trashed_at:
message.trashed_at = datetime.now(timezone.utc)
elif not data.is_trashed:
message.trashed_at = None
await db.commit()
# Re-query con selectinload per evitare MissingGreenlet sui labels
@@ -422,7 +442,13 @@ async def update_message(
.options(selectinload(Message.labels))
)
message = refreshed.scalar_one()
return MessageResponse.model_validate(messa
return MessageResponse.model_validate(message)
@router.get("/{message_id}/attachments", response_model=list[AttachmentResponse])
async def list_attachments(
message_id: uuid.UUID,
current_user: CurrentUser,
db: DB,
) -> list[AttachmentResponse]:
"""Elenca gli allegati di un messaggio."""
@@ -473,7 +499,7 @@ async def download_attachment(
secure=settings.minio_use_ssl,
)
# storage_path è del tipo "tenant_id/attachments/filename"
# storage_path e del tipo "tenant_id/attachments/filename"
storage_path = attachment.storage_path
response = await client.get_object(settings.minio_bucket, storage_path)
+2
View File
@@ -91,6 +91,8 @@ class Message(Base):
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)
raw_eml_path: Mapped[str | None] = mapped_column(Text, nullable=True)
+5
View File
@@ -49,6 +49,8 @@ class MessageResponse(BaseModel):
is_starred: bool = False
is_archived: bool = False
archived_at: Optional[datetime] = None
is_trashed: bool = False
trashed_at: Optional[datetime] = None
raw_eml_path: Optional[str] = None
created_at: datetime
updated_at: datetime
@@ -84,12 +86,15 @@ class MessageUpdateRequest(BaseModel):
is_read: Optional[bool] = None
is_starred: Optional[bool] = None
is_archived: Optional[bool] = None
is_trashed: Optional[bool] = None
class MessageBulkUpdateRequest(BaseModel):
ids: list[uuid.UUID]
is_read: Optional[bool] = None
is_starred: Optional[bool] = None
is_archived: Optional[bool] = None
is_trashed: Optional[bool] = None
class MessageBulkUpdateResponse(BaseModel):
+1 -1
View File
@@ -1,4 +1,4 @@
is"""
"""
Servizio Notifiche Multi-canale CRUD canali, regole, log.
Nota: la cifratura AES-256-GCM di config_enc avviene qui usando