mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Fix smtp
This commit is contained in:
@@ -94,6 +94,27 @@ async def my_virtual_boxes(
|
||||
return [_to_response(v) for v in items]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/my/mailboxes",
|
||||
response_model=list[MailboxBriefResponse],
|
||||
summary="Caselle PEC da cui l'utente può inviare tramite Virtual Box",
|
||||
description=(
|
||||
"Restituisce le caselle PEC attive associate alle Virtual Box "
|
||||
"a cui l'utente corrente è assegnato. "
|
||||
"Usato dalla pagina di composizione per mostrare le caselle mittente disponibili."
|
||||
),
|
||||
)
|
||||
async def my_sendable_mailboxes(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> list[MailboxBriefResponse]:
|
||||
service = VirtualBoxService(db)
|
||||
mailboxes = await service.get_user_sendable_mailboxes(
|
||||
current_user.id, current_user.tenant_id
|
||||
)
|
||||
return [MailboxBriefResponse.model_validate(m) for m in mailboxes]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{vbox_id}",
|
||||
response_model=VirtualBoxResponse,
|
||||
|
||||
@@ -42,6 +42,7 @@ class MailboxBriefResponse(BaseModel):
|
||||
id: uuid.UUID
|
||||
email_address: str
|
||||
display_name: str | None
|
||||
status: str = "active"
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@@ -61,12 +61,23 @@ class PermissionService:
|
||||
async def check_can_send(
|
||||
self, user: User, mailbox_id: uuid.UUID
|
||||
) -> bool:
|
||||
"""Verifica se l'utente può inviare dalla casella."""
|
||||
"""
|
||||
Verifica se l'utente può inviare dalla casella.
|
||||
|
||||
L'accesso in invio è concesso se:
|
||||
1. L'utente è admin del tenant, oppure
|
||||
2. L'utente ha un permesso diretto can_send sulla casella, oppure
|
||||
3. L'utente è assegnato a una Virtual Box attiva che include la casella.
|
||||
"""
|
||||
if user.role in ("super_admin", "admin"):
|
||||
return await self._mailbox_belongs_to_tenant(mailbox_id, user.tenant_id)
|
||||
|
||||
perm = await self._get_permission(user.id, mailbox_id)
|
||||
return perm is not None and perm.can_send
|
||||
if perm is not None and perm.can_send:
|
||||
return True
|
||||
|
||||
# Fallback: verifica accesso tramite Virtual Box
|
||||
return await self._check_vbox_mailbox_access(user.id, mailbox_id, user.tenant_id)
|
||||
|
||||
async def check_can_manage(
|
||||
self, user: User, mailbox_id: uuid.UUID
|
||||
@@ -234,3 +245,40 @@ class PermissionService:
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
async def _check_vbox_mailbox_access(
|
||||
self,
|
||||
user_id: uuid.UUID,
|
||||
mailbox_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> bool:
|
||||
"""
|
||||
Verifica se l'utente ha accesso a una casella tramite Virtual Box.
|
||||
Restituisce True se l'utente è assegnato ad almeno una VBox attiva
|
||||
che include la casella specificata.
|
||||
"""
|
||||
from app.models.virtual_box import (
|
||||
VirtualBox,
|
||||
VirtualBoxAssignment,
|
||||
virtual_box_mailboxes,
|
||||
)
|
||||
|
||||
result = await self.db.execute(
|
||||
select(VirtualBox.id)
|
||||
.join(
|
||||
VirtualBoxAssignment,
|
||||
VirtualBox.id == VirtualBoxAssignment.virtual_box_id,
|
||||
)
|
||||
.join(
|
||||
virtual_box_mailboxes,
|
||||
VirtualBox.id == virtual_box_mailboxes.c.virtual_box_id,
|
||||
)
|
||||
.where(
|
||||
VirtualBoxAssignment.user_id == user_id,
|
||||
virtual_box_mailboxes.c.mailbox_id == mailbox_id,
|
||||
VirtualBox.tenant_id == tenant_id,
|
||||
VirtualBox.is_active == True,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@@ -341,6 +341,33 @@ class VirtualBoxService:
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_user_sendable_mailboxes(
|
||||
self,
|
||||
user_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> list[Mailbox]:
|
||||
"""
|
||||
Restituisce le caselle PEC da cui l'utente può inviare tramite VBox.
|
||||
|
||||
Aggregazione delle caselle associate a tutte le VBox attive a cui
|
||||
l'utente è assegnato. Filtra solo caselle in stato 'active'.
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Mailbox)
|
||||
.join(virtual_box_mailboxes, Mailbox.id == virtual_box_mailboxes.c.mailbox_id)
|
||||
.join(VirtualBox, virtual_box_mailboxes.c.virtual_box_id == VirtualBox.id)
|
||||
.join(VirtualBoxAssignment, VirtualBox.id == VirtualBoxAssignment.virtual_box_id)
|
||||
.where(
|
||||
VirtualBoxAssignment.user_id == user_id,
|
||||
VirtualBox.tenant_id == tenant_id,
|
||||
VirtualBox.is_active == True,
|
||||
Mailbox.status == "active",
|
||||
)
|
||||
.distinct()
|
||||
.order_by(Mailbox.email_address)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
# ─── Private ─────────────────────────────────────────────────────────────
|
||||
|
||||
async def _load_full(self, vbox_id: uuid.UUID) -> VirtualBox | None:
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
VirtualBoxUpdate,
|
||||
} from '@/types/api.types'
|
||||
|
||||
|
||||
export const virtualBoxesApi = {
|
||||
/** Crea una nuova Virtual Box. */
|
||||
create: (data: VirtualBoxCreate) =>
|
||||
@@ -27,6 +28,15 @@ export const virtualBoxesApi = {
|
||||
myVirtualBoxes: () =>
|
||||
apiClient.get<VirtualBoxResponse[]>('/virtual-boxes/my').then((r) => r.data),
|
||||
|
||||
/**
|
||||
* Caselle PEC attive da cui l'utente può inviare tramite le sue Virtual Box.
|
||||
* Usato nella pagina di composizione per mostrare le caselle mittente disponibili.
|
||||
*/
|
||||
getMyMailboxes: () =>
|
||||
apiClient
|
||||
.get<MailboxBriefResponse[]>('/virtual-boxes/my/mailboxes')
|
||||
.then((r) => r.data),
|
||||
|
||||
/** Dettaglio Virtual Box. */
|
||||
get: (id: string) =>
|
||||
apiClient.get<VirtualBoxResponse>(`/virtual-boxes/${id}`).then((r) => r.data),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useState, useRef, useMemo } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useForm, useFieldArray } from 'react-hook-form'
|
||||
import { Send, X, Plus, ArrowLeft, AlertCircle, Paperclip, Upload } from 'lucide-react'
|
||||
import { Send, X, Plus, ArrowLeft, AlertCircle, Paperclip, Upload, Filter } from 'lucide-react'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@@ -10,9 +10,19 @@ import { Label } from '@/components/ui/Label'
|
||||
import { RichTextEditor } from '@/components/RichTextEditor/RichTextEditor'
|
||||
import { sendApi } from '@/api/send.api'
|
||||
import { mailboxesApi } from '@/api/mailboxes.api'
|
||||
import { virtualBoxesApi } from '@/api/virtual_boxes.api'
|
||||
import { getErrorMessage } from '@/api/client'
|
||||
import type { MessageResponse } from '@/types/api.types'
|
||||
|
||||
/** Tipo unificato casella mittente (sia da permessi diretti che da Virtual Box) */
|
||||
interface MailboxSelectItem {
|
||||
id: string
|
||||
email_address: string
|
||||
display_name: string | null
|
||||
status: string
|
||||
fromVbox?: boolean
|
||||
}
|
||||
|
||||
interface ComposeFormValues {
|
||||
mailbox_id: string
|
||||
to_addresses: { value: string }[]
|
||||
@@ -91,12 +101,18 @@ export function ComposePage() {
|
||||
remove: removeCc,
|
||||
} = useFieldArray({ control, name: 'cc_addresses' })
|
||||
|
||||
// Carica caselle disponibili per l'invio
|
||||
// Carica caselle disponibili per l'invio (permessi diretti)
|
||||
const { data: mailboxesData, isLoading: mailboxesLoading } = useQuery({
|
||||
queryKey: ['mailboxes'],
|
||||
queryFn: () => mailboxesApi.list(),
|
||||
})
|
||||
|
||||
// Carica caselle disponibili tramite Virtual Box (operatori senza permessi diretti)
|
||||
const { data: vboxMailboxes = [], isLoading: vboxMailboxesLoading } = useQuery({
|
||||
queryKey: ['virtual-boxes', 'my-mailboxes'],
|
||||
queryFn: () => virtualBoxesApi.getMyMailboxes(),
|
||||
})
|
||||
|
||||
const sendMutation = useMutation({
|
||||
mutationFn: (args: {
|
||||
data: Parameters<typeof sendApi.sendMultipart>[0]
|
||||
@@ -158,7 +174,36 @@ export function ComposePage() {
|
||||
})
|
||||
}
|
||||
|
||||
const activeCaselle = mailboxesData?.items.filter((m) => m.status === 'active') || []
|
||||
// ── Lista unificata caselle mittente ──────────────────────────────────────
|
||||
// Combina caselle con permesso diretto + caselle accessibili via VBox
|
||||
const activeCaselle = useMemo((): MailboxSelectItem[] => {
|
||||
const regularActive: MailboxSelectItem[] = (
|
||||
mailboxesData?.items.filter((m) => m.status === 'active') || []
|
||||
).map((m) => ({
|
||||
id: m.id,
|
||||
email_address: m.email_address,
|
||||
display_name: m.display_name,
|
||||
status: m.status,
|
||||
fromVbox: false,
|
||||
}))
|
||||
|
||||
const regularIds = new Set(regularActive.map((m) => m.id))
|
||||
|
||||
// Aggiungi caselle VBox non già presenti nella lista diretta
|
||||
const vboxActive: MailboxSelectItem[] = vboxMailboxes
|
||||
.filter((m) => m.status === 'active' && !regularIds.has(m.id))
|
||||
.map((m) => ({
|
||||
id: m.id,
|
||||
email_address: m.email_address,
|
||||
display_name: m.display_name,
|
||||
status: m.status,
|
||||
fromVbox: true,
|
||||
}))
|
||||
|
||||
return [...regularActive, ...vboxActive]
|
||||
}, [mailboxesData, vboxMailboxes])
|
||||
|
||||
const isLoadingMailboxes = mailboxesLoading || vboxMailboxesLoading
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -197,13 +242,14 @@ export function ComposePage() {
|
||||
{/* Casella mittente */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
||||
{mailboxesLoading ? (
|
||||
{isLoadingMailboxes ? (
|
||||
<div className="h-10 rounded-md border bg-muted animate-pulse" />
|
||||
) : activeCaselle.length === 0 ? (
|
||||
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
Nessuna casella PEC attiva disponibile. Contatta l'amministratore.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
id="mailbox_id"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
@@ -214,10 +260,17 @@ export function ComposePage() {
|
||||
<option value="">Seleziona casella...</option>
|
||||
{activeCaselle.map((mb) => (
|
||||
<option key={mb.id} value={mb.id}>
|
||||
{mb.display_name || mb.email_address} ({mb.email_address})
|
||||
{mb.fromVbox ? '📥 ' : ''}{mb.display_name || mb.email_address} ({mb.email_address})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{activeCaselle.some((m) => m.fromVbox) && (
|
||||
<p className="text-xs text-purple-600 flex items-center gap-1">
|
||||
<Filter className="h-3 w-3" />
|
||||
Le caselle con 📥 sono accessibili tramite Virtual Box
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{errors.mailbox_id && (
|
||||
<p className="text-xs text-destructive">{errors.mailbox_id.message}</p>
|
||||
|
||||
@@ -353,6 +353,7 @@ export interface MailboxBriefResponse {
|
||||
id: string
|
||||
email_address: string
|
||||
display_name: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface VirtualBoxCreate {
|
||||
|
||||
+9
-6
@@ -49,6 +49,9 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
||||
"""
|
||||
Inizializzazione worker all'avvio.
|
||||
Avvia il MailboxPool con tutte le caselle attive.
|
||||
|
||||
NOTA: ctx["redis"] è già un ArqRedis (con enqueue_job) impostato da arq
|
||||
prima di chiamare on_startup – NON sovrascrivere con aioredis standard.
|
||||
"""
|
||||
global _mailbox_pool
|
||||
|
||||
@@ -63,9 +66,10 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
||||
except Exception as e:
|
||||
logger.warning(f"MinIO non disponibile al startup: {e}")
|
||||
|
||||
# Crea client Redis condiviso
|
||||
redis_client = aioredis.from_url(settings.redis_url, decode_responses=True)
|
||||
ctx["redis"] = redis_client
|
||||
# Usa il client Redis ArqRedis già presente nel contesto (messo da arq).
|
||||
# ArqRedis estende Redis, quindi funziona sia per MailboxPool sia per
|
||||
# enqueue_job() nei job asincroni (send_pec, watch_receipt, ecc.).
|
||||
redis_client = ctx["redis"]
|
||||
|
||||
# Avvia MailboxPool
|
||||
_mailbox_pool = MailboxPool(redis_client=redis_client)
|
||||
@@ -87,9 +91,8 @@ async def on_shutdown(ctx: dict[str, Any]) -> None:
|
||||
if pool:
|
||||
await pool.stop()
|
||||
|
||||
redis_client = ctx.get("redis")
|
||||
if redis_client:
|
||||
await redis_client.aclose()
|
||||
# NON chiudere ctx["redis"]: è l'ArqRedis gestito da arq,
|
||||
# che ne gestisce il ciclo di vita autonomamente.
|
||||
|
||||
logger.info("🛑 Worker fermato")
|
||||
|
||||
|
||||
@@ -232,9 +232,9 @@ class SmtpSender:
|
||||
await smtp.connect()
|
||||
await smtp.login(creds["user"], creds["password"])
|
||||
errors, response = await smtp.sendmail(
|
||||
sender=self.mailbox.email_address,
|
||||
recipients=all_recipients,
|
||||
message=raw_eml,
|
||||
self.mailbox.email_address,
|
||||
all_recipients,
|
||||
raw_eml,
|
||||
)
|
||||
if errors:
|
||||
failed = ", ".join(f"{addr}: {err}" for addr, err in errors.items())
|
||||
|
||||
Reference in New Issue
Block a user