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]
|
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(
|
@router.get(
|
||||||
"/{vbox_id}",
|
"/{vbox_id}",
|
||||||
response_model=VirtualBoxResponse,
|
response_model=VirtualBoxResponse,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class MailboxBriefResponse(BaseModel):
|
|||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
email_address: str
|
email_address: str
|
||||||
display_name: str | None
|
display_name: str | None
|
||||||
|
status: str = "active"
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|||||||
@@ -61,12 +61,23 @@ class PermissionService:
|
|||||||
async def check_can_send(
|
async def check_can_send(
|
||||||
self, user: User, mailbox_id: uuid.UUID
|
self, user: User, mailbox_id: uuid.UUID
|
||||||
) -> bool:
|
) -> 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"):
|
if user.role in ("super_admin", "admin"):
|
||||||
return await self._mailbox_belongs_to_tenant(mailbox_id, user.tenant_id)
|
return await self._mailbox_belongs_to_tenant(mailbox_id, user.tenant_id)
|
||||||
|
|
||||||
perm = await self._get_permission(user.id, mailbox_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(
|
async def check_can_manage(
|
||||||
self, user: User, mailbox_id: uuid.UUID
|
self, user: User, mailbox_id: uuid.UUID
|
||||||
@@ -234,3 +245,40 @@ class PermissionService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none() is not None
|
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())
|
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 ─────────────────────────────────────────────────────────────
|
# ─── Private ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def _load_full(self, vbox_id: uuid.UUID) -> VirtualBox | None:
|
async def _load_full(self, vbox_id: uuid.UUID) -> VirtualBox | None:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
VirtualBoxUpdate,
|
VirtualBoxUpdate,
|
||||||
} from '@/types/api.types'
|
} from '@/types/api.types'
|
||||||
|
|
||||||
|
|
||||||
export const virtualBoxesApi = {
|
export const virtualBoxesApi = {
|
||||||
/** Crea una nuova Virtual Box. */
|
/** Crea una nuova Virtual Box. */
|
||||||
create: (data: VirtualBoxCreate) =>
|
create: (data: VirtualBoxCreate) =>
|
||||||
@@ -27,6 +28,15 @@ export const virtualBoxesApi = {
|
|||||||
myVirtualBoxes: () =>
|
myVirtualBoxes: () =>
|
||||||
apiClient.get<VirtualBoxResponse[]>('/virtual-boxes/my').then((r) => r.data),
|
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. */
|
/** Dettaglio Virtual Box. */
|
||||||
get: (id: string) =>
|
get: (id: string) =>
|
||||||
apiClient.get<VirtualBoxResponse>(`/virtual-boxes/${id}`).then((r) => r.data),
|
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 { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useForm, useFieldArray } from 'react-hook-form'
|
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 { useQuery, useMutation } from '@tanstack/react-query'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
@@ -10,9 +10,19 @@ import { Label } from '@/components/ui/Label'
|
|||||||
import { RichTextEditor } from '@/components/RichTextEditor/RichTextEditor'
|
import { RichTextEditor } from '@/components/RichTextEditor/RichTextEditor'
|
||||||
import { sendApi } from '@/api/send.api'
|
import { sendApi } from '@/api/send.api'
|
||||||
import { mailboxesApi } from '@/api/mailboxes.api'
|
import { mailboxesApi } from '@/api/mailboxes.api'
|
||||||
|
import { virtualBoxesApi } from '@/api/virtual_boxes.api'
|
||||||
import { getErrorMessage } from '@/api/client'
|
import { getErrorMessage } from '@/api/client'
|
||||||
import type { MessageResponse } from '@/types/api.types'
|
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 {
|
interface ComposeFormValues {
|
||||||
mailbox_id: string
|
mailbox_id: string
|
||||||
to_addresses: { value: string }[]
|
to_addresses: { value: string }[]
|
||||||
@@ -91,12 +101,18 @@ export function ComposePage() {
|
|||||||
remove: removeCc,
|
remove: removeCc,
|
||||||
} = useFieldArray({ control, name: 'cc_addresses' })
|
} = 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({
|
const { data: mailboxesData, isLoading: mailboxesLoading } = useQuery({
|
||||||
queryKey: ['mailboxes'],
|
queryKey: ['mailboxes'],
|
||||||
queryFn: () => mailboxesApi.list(),
|
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({
|
const sendMutation = useMutation({
|
||||||
mutationFn: (args: {
|
mutationFn: (args: {
|
||||||
data: Parameters<typeof sendApi.sendMultipart>[0]
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@@ -197,13 +242,14 @@ export function ComposePage() {
|
|||||||
{/* Casella mittente */}
|
{/* Casella mittente */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
||||||
{mailboxesLoading ? (
|
{isLoadingMailboxes ? (
|
||||||
<div className="h-10 rounded-md border bg-muted animate-pulse" />
|
<div className="h-10 rounded-md border bg-muted animate-pulse" />
|
||||||
) : activeCaselle.length === 0 ? (
|
) : activeCaselle.length === 0 ? (
|
||||||
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
<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.
|
Nessuna casella PEC attiva disponibile. Contatta l'amministratore.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<select
|
<select
|
||||||
id="mailbox_id"
|
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"
|
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>
|
<option value="">Seleziona casella...</option>
|
||||||
{activeCaselle.map((mb) => (
|
{activeCaselle.map((mb) => (
|
||||||
<option key={mb.id} value={mb.id}>
|
<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>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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 && (
|
{errors.mailbox_id && (
|
||||||
<p className="text-xs text-destructive">{errors.mailbox_id.message}</p>
|
<p className="text-xs text-destructive">{errors.mailbox_id.message}</p>
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ export interface MailboxBriefResponse {
|
|||||||
id: string
|
id: string
|
||||||
email_address: string
|
email_address: string
|
||||||
display_name: string | null
|
display_name: string | null
|
||||||
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VirtualBoxCreate {
|
export interface VirtualBoxCreate {
|
||||||
|
|||||||
+9
-6
@@ -49,6 +49,9 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Inizializzazione worker all'avvio.
|
Inizializzazione worker all'avvio.
|
||||||
Avvia il MailboxPool con tutte le caselle attive.
|
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
|
global _mailbox_pool
|
||||||
|
|
||||||
@@ -63,9 +66,10 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"MinIO non disponibile al startup: {e}")
|
logger.warning(f"MinIO non disponibile al startup: {e}")
|
||||||
|
|
||||||
# Crea client Redis condiviso
|
# Usa il client Redis ArqRedis già presente nel contesto (messo da arq).
|
||||||
redis_client = aioredis.from_url(settings.redis_url, decode_responses=True)
|
# ArqRedis estende Redis, quindi funziona sia per MailboxPool sia per
|
||||||
ctx["redis"] = redis_client
|
# enqueue_job() nei job asincroni (send_pec, watch_receipt, ecc.).
|
||||||
|
redis_client = ctx["redis"]
|
||||||
|
|
||||||
# Avvia MailboxPool
|
# Avvia MailboxPool
|
||||||
_mailbox_pool = MailboxPool(redis_client=redis_client)
|
_mailbox_pool = MailboxPool(redis_client=redis_client)
|
||||||
@@ -87,9 +91,8 @@ async def on_shutdown(ctx: dict[str, Any]) -> None:
|
|||||||
if pool:
|
if pool:
|
||||||
await pool.stop()
|
await pool.stop()
|
||||||
|
|
||||||
redis_client = ctx.get("redis")
|
# NON chiudere ctx["redis"]: è l'ArqRedis gestito da arq,
|
||||||
if redis_client:
|
# che ne gestisce il ciclo di vita autonomamente.
|
||||||
await redis_client.aclose()
|
|
||||||
|
|
||||||
logger.info("🛑 Worker fermato")
|
logger.info("🛑 Worker fermato")
|
||||||
|
|
||||||
|
|||||||
@@ -232,9 +232,9 @@ class SmtpSender:
|
|||||||
await smtp.connect()
|
await smtp.connect()
|
||||||
await smtp.login(creds["user"], creds["password"])
|
await smtp.login(creds["user"], creds["password"])
|
||||||
errors, response = await smtp.sendmail(
|
errors, response = await smtp.sendmail(
|
||||||
sender=self.mailbox.email_address,
|
self.mailbox.email_address,
|
||||||
recipients=all_recipients,
|
all_recipients,
|
||||||
message=raw_eml,
|
raw_eml,
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
failed = ", ".join(f"{addr}: {err}" for addr, err in errors.items())
|
failed = ", ".join(f"{addr}: {err}" for addr, err in errors.items())
|
||||||
|
|||||||
Reference in New Issue
Block a user