# PecFlow – Architettura di Sistema

> **Documento redatto il:** 2026-03-18 | **Ultima revisione:** 2026-03-18  
> **Versione:** 2.0  
> **Stato:** Draft – da approvare prima di iniziare la Fase 1

---

## Indice

1. [Struttura Repository](#1-struttura-repository)
2. [Modello Dati](#2-modello-dati)
3. [Piano di Sviluppo a Fasi](#3-piano-di-sviluppo-a-fasi)
4. [Decisioni Architetturali](#4-decisioni-architetturali)
5. [Sistemi Avanzati](#5-sistemi-avanzati)
   - 5.1 [Permessi Utente Granulari](#51-permessi-utente-granulari)
   - 5.2 [Virtual Box](#52-virtual-box)
   - 5.3 [Ricerca Avanzata](#53-ricerca-avanzata)
   - 5.4 [Sistema di Notifiche Multi-canale](#54-sistema-di-notifiche-multi-canale)

---

## 1. Struttura Repository

Monorepo con workspace separati. Il confine di responsabilità è netto: ogni
cartella di primo livello è un deployable (o una libreria condivisa) indipendente.

```
PecFlow/                                  ← root del monorepo
│
├── .github/                              ← CI/CD GitHub Actions
│   ├── workflows/
│   │   ├── ci.yml                        # lint + test su ogni PR
│   │   ├── cd-staging.yml                # deploy automatico su staging
│   │   └── cd-production.yml             # deploy manuale su prod (approval gate)
│   └── CODEOWNERS                        # chi deve approvare le PR per area
│
├── .husky/                               ← pre-commit hooks (lint, secret scan)
├── .env.example                          ← template variabili d'ambiente (NO valori reali)
├── .gitignore
├── .gitattributes
├── docker-compose.yml                    ← stack locale completo (dev)
├── docker-compose.prod.yml               ← override per produzione
├── Makefile                              ← comandi rapidi: make dev, make test, make migrate
├── ARCHITECTURE.md                       ← questo file
├── CONTRIBUTING.md
│
├── backend/                              ═══════════════════════════════
│   │                                     ← FastAPI – API HTTP + WebSocket
│   ├── Dockerfile
│   ├── pyproject.toml                    # dipendenze (uv / poetry)
│   ├── alembic.ini                       # configurazione Alembic migrations
│   │
│   ├── alembic/                          ← migrazioni DB versionate
│   │   ├── env.py
│   │   └── versions/
│   │       └── 0001_initial_schema.py
│   │
│   ├── app/
│   │   ├── main.py                       # entrypoint FastAPI, registra router
│   │   ├── config.py                     # Settings (pydantic-settings, legge .env)
│   │   ├── database.py                   # engine SQLAlchemy, session factory
│   │   ├── dependencies.py               # get_db, get_current_user, ecc.
│   │   │
│   │   ├── api/                          ← router HTTP (versioned)
│   │   │   └── v1/
│   │   │       ├── auth.py               # login, refresh, logout, 2FA
│   │   │       ├── tenants.py            # CRUD tenant (super-admin)
│   │   │       ├── users.py              # CRUD utenti
│   │   │       ├── mailboxes.py          # CRUD caselle PEC
│   │   │       ├── messages.py           # lista, dettaglio, search PEC
│   │   │       ├── search.py             # ricerca avanzata full-text + filtri
│   │   │       ├── send.py               # invio PEC
│   │   │       ├── archival.py           # versamenti, RdV, DIP
│   │   │       ├── reports.py            # export PDF/CSV
│   │   │       ├── permissions.py        # permessi granulari per casella
│   │   │       ├── virtual_boxes.py      # CRUD virtual box + assegnazioni
│   │   │       ├── notifications.py      # CRUD canali e regole notifiche
│   │   │       └── ws.py                 # WebSocket endpoint (Socket.io)
│   │   │
│   │   ├── models/                       ← SQLAlchemy ORM models (1:1 con tabelle)
│   │   │   ├── tenant.py
│   │   │   ├── user.py
│   │   │   ├── mailbox.py
│   │   │   ├── message.py
│   │   │   ├── receipt.py
│   │   │   ├── archival.py
│   │   │   ├── permission.py             # mailbox_permissions
│   │   │   ├── virtual_box.py            # virtual_boxes, rules, assignments
│   │   │   ├── notification.py           # notification_channels, rules, log
│   │   │   └── audit_log.py
│   │   │
│   │   ├── schemas/                      ← Pydantic schemas (request/response DTOs)
│   │   │   ├── auth.py
│   │   │   ├── mailbox.py
│   │   │   ├── message.py
│   │   │   ├── search.py                 # SearchQuery, SearchResult
│   │   │   ├── permission.py
│   │   │   ├── virtual_box.py
│   │   │   ├── notification.py
│   │   │   └── archival.py
│   │   │
│   │   ├── services/                     ← logica di business (nessun DB diretto)
│   │   │   ├── auth_service.py           # JWT, TOTP, password hash
│   │   │   ├── mailbox_service.py        # cifratura credenziali
│   │   │   ├── message_service.py        # ricerca, etichette, stati
│   │   │   ├── search_service.py         # query builder FTS + filtri compositi
│   │   │   ├── permission_service.py     # verifica accesso utente/casella
│   │   │   ├── virtual_box_service.py    # applica filtri VBox a query messaggi
│   │   │   ├── notification_service.py   # valuta regole + dispatcha notifiche
│   │   │   ├── send_service.py           # invio SMTP, salvataggio ricevute
│   │   │   └── archival_service.py       # generazione SIP, chiamate conservatore
│   │   │
│   │   ├── core/                         ← utilities trasversali
│   │   │   ├── security.py               # cifratura AES-256-GCM, hashing
│   │   │   ├── exceptions.py             # eccezioni applicative custom
│   │   │   ├── pagination.py
│   │   │   └── logging.py                # structured logging (JSON)
│   │   │
│   │   └── websocket/
│   │       └── manager.py                # gestione connessioni WS per tenant
│   │
│   └── tests/
│       ├── unit/
│       │   ├── test_auth_service.py
│       │   └── test_security.py
│       ├── integration/
│       │   ├── test_api_auth.py
│       │   ├── test_api_messages.py
│       │   └── conftest.py               # fixture DB in-memory (SQLite / testcontainers)
│       └── e2e/                          # (opzionale, gestiti da Playwright lato frontend)
│
├── worker/                               ═══════════════════════════════
│   │                                     ← BullMQ worker (Node.js / Python Celery alt.)
│   │                                        Scelta: Python (arq) per non aggiungere runtime
│   ├── Dockerfile
│   ├── pyproject.toml
│   │
│   └── app/
│       ├── main.py                       # entrypoint arq / asyncio loop
│       ├── config.py
│       │
│       ├── imap/                         ← IMAP sync engine
│       │   ├── pool.py                   # MailboxPool: gestisce N connessioni IMAP
│       │   ├── connection.py             # IMAPConnection: IDLE + polling fallback
│       │   ├── sync.py                   # sincronizzazione UID, fetch envelope + body
│       │   └── reconnect.py              # strategia backoff esponenziale
│       │
│       ├── parsers/                      ← parser PEC (lib interna, riusata anche da backend)
│       │   ├── pec_parser.py             # estrae tipo messaggio PEC, ricevute EML-in-EML
│       │   ├── eml_parser.py             # parsing MIME generico
│       │   └── receipt_extractor.py      # estrae campi da ricevute (tipo, timestamp, msgid)
│       │
│       ├── smtp/                         ← invio SMTP
│       │   ├── sender.py                 # connessione STARTTLS/SSL, invio, retry
│       │   └── receipt_watcher.py        # attende ricevuta accettazione/consegna post-invio
│       │
│       ├── archival/                     ← archiviazione sostitutiva
│       │   ├── sip_builder.py            # genera pacchetto SIP (UNI SInCRO)
│       │   ├── conservatore_client.py    # client HTTP verso API conservatore AgID
│       │   └── rdv_processor.py          # processa ricevuta di versamento (RdV)
│       │
│       ├── notifications/                ← dispatcher notifiche async
│       │   ├── dispatcher.py             # valuta regole e sceglie canali
│       │   ├── channels/
│       │   │   ├── webhook.py            # HTTP POST con payload JSON + HMAC signature
│       │   │   ├── email_smtp.py         # invio email via SMTP (aiosmtplib)
│       │   │   ├── telegram.py           # Telegram Bot API (sendMessage)
│       │   │   └── whatsapp.py           # Meta Cloud API / Twilio WhatsApp
│       │   └── retry.py                  # retry con backoff per canali falliti
│       │
│       ├── search/                       ← indicizzazione full-text
│       │   ├── indexer.py                # aggiorna search_vector su nuovo messaggio
│       │   └── text_extractor.py         # estrae testo da PDF/DOCX allegati (Tika HTTP)
│       │
│       ├── jobs/                         ← definizione job BullMQ / arq
│       │   ├── sync_mailbox.py           # job: sincronizza singola casella
│       │   ├── send_pec.py               # job: invia PEC con retry
│       │   ├── archive_batch.py          # job: versamento batch verso conservatore
│       │   ├── generate_report.py        # job: genera PDF/CSV report
│       │   ├── dispatch_notification.py  # job: invia notifiche per un evento PEC
│       │   └── index_message.py          # job: estrae testo allegati e aggiorna FTS
│       │
│       └── tests/
│           ├── unit/
│           │   ├── test_pec_parser.py
│           │   ├── test_sip_builder.py
│           │   ├── test_notification_rules.py
│           │   └── test_virtual_box_filter.py
│           └── integration/
│               ├── test_imap_sync.py     # usa GreenMail / Mailhog mock IMAP
│               └── test_notification_dispatch.py
│
├── frontend/                             ═══════════════════════════════
│   │                                     ← React 18 + TypeScript + Vite
│   ├── Dockerfile
│   ├── package.json
│   ├── tsconfig.json
│   ├── vite.config.ts
│   │
│   ├── src/
│   │   ├── main.tsx
│   │   ├── App.tsx
│   │   │
│   │   ├── api/                          ← client HTTP (axios instances per risorsa)
│   │   │   ├── client.ts                 # axios base + interceptor JWT refresh
│   │   │   ├── auth.api.ts
│   │   │   ├── messages.api.ts
│   │   │   ├── search.api.ts             # ricerca avanzata
│   │   │   ├── mailboxes.api.ts
│   │   │   ├── permissions.api.ts
│   │   │   ├── virtual_boxes.api.ts
│   │   │   ├── notifications.api.ts
│   │   │   └── archival.api.ts
│   │   │
│   │   ├── store/                        ← Zustand stores
│   │   │   ├── auth.store.ts
│   │   │   ├── mailbox.store.ts
│   │   │   ├── inbox.store.ts
│   │   │   └── search.store.ts           # stato ricerca (query, filtri, risultati)
│   │   │
│   │   ├── pages/
│   │   │   ├── Login/
│   │   │   ├── Inbox/                    # lista messaggi PEC (filtra per VBox se assegnato)
│   │   │   ├── Search/                   # ricerca avanzata con pannello filtri
│   │   │   ├── MessageDetail/            # dettaglio + ricevute + allegati
│   │   │   ├── Compose/                  # composizione invio PEC
│   │   │   ├── Mailboxes/                # gestione caselle (admin)
│   │   │   ├── Permissions/              # gestione permessi per casella (admin)
│   │   │   ├── VirtualBoxes/             # gestione virtual box (admin)
│   │   │   ├── Users/                    # gestione utenti (admin)
│   │   │   ├── Notifications/            # canali e regole notifiche (admin)
│   │   │   ├── Archival/                 # log versamenti + DIP
│   │   │   └── Reports/                  # dashboard + export
│   │   │
│   │   ├── components/                   ← UI atomici riusabili
│   │   │   ├── PecBadge/                 # badge stato PEC (Inviata/Accettata/Consegnata)
│   │   │   ├── ReceiptTree/              # visualizza gerarchia ricevute EML-in-EML
│   │   │   ├── AttachmentList/
│   │   │   ├── SearchBar/                # barra ricerca con suggerimenti
│   │   │   ├── SearchFilters/            # pannello filtri avanzati (collapsible)
│   │   │   ├── VirtualBoxBadge/          # indica se l'utente è in una VBox
│   │   │   └── NotificationChannelIcon/  # icona canale (webhook/email/telegram/whatsapp)
│   │   │
│   │   ├── hooks/
│   │   │   ├── useWebSocket.ts           # Socket.io hook, aggiorna inbox in real-time
│   │   │   ├── useAuth.ts
│   │   │   ├── useSearch.ts              # debounce, history, highlight risultati
│   │   │   ├── usePermissions.ts         # controlla permessi utente corrente
│   │   │   └── usePagination.ts
│   │   │
│   │   └── types/                        ← SOLO tipi frontend-specific
│   │       └── ui.types.ts
│   │
│   └── tests/
│       ├── unit/                         # Vitest + Testing Library
│       └── e2e/                          # Playwright
│
├── shared/                               ═══════════════════════════════
│   │                                     ← Tipi/contratti condivisi tra frontend e backend
│   │                                        Generati da OpenAPI schema o scritti a mano
│   ├── types/
│   │   ├── api.types.ts                  # DTOs generati dall'OpenAPI spec del backend
│   │   ├── pec.types.ts                  # enumerazioni stati PEC, tipi ricevute
│   │   └── archival.types.ts             # tipi SIP, RdV
│   └── README.md
│
├── database/                             ═══════════════════════════════
│   │                                     ← Script SQL stand-alone, seeds, fixtures
│   ├── init/
│   │   └── 00_extensions.sql             # CREATE EXTENSION pgcrypto, uuid-ossp
│   ├── seeds/
│   │   ├── dev_tenant.sql                # tenant e utente demo per sviluppo locale
│   │   └── test_fixtures.sql
│   └── backups/                          # (gitignored) dump locali
│
├── infra/                                ═══════════════════════════════
│   │                                     ← Infrastructure as Code
│   ├── nginx/
│   │   ├── nginx.conf                    # reverse proxy, rate limiting, TLS termination
│   │   └── conf.d/
│   │       └── pecflow.conf
│   ├── redis/
│   │   └── redis.conf                    # maxmemory, eviction policy
│   ├── prometheus/
│   │   └── prometheus.yml
│   └── grafana/
│       └── dashboards/
│           └── pecflow.json
│
└── docs/                                 ═══════════════════════════════
    ├── api/                              ← OpenAPI spec generata (non commitare auto-gen)
    ├── adr/                              ← Architecture Decision Records
    │   ├── ADR-001-multitenancy.md
    │   ├── ADR-002-credential-encryption.md
    │   └── ADR-003-imap-concurrency.md
    └── diagrams/
        ├── c4-context.png
        ├── c4-container.png
        └── db-schema.png
```

---

## 2. Modello Dati

### Strategia multi-tenancy

Si usa **row-level con `tenant_id`** su ogni tabella applicativa (vedi ADR-001 in
sezione 4 per la motivazione completa). Le policy RLS PostgreSQL vengono attivate
come secondo strato di difesa.

### DDL Completo

```sql
-- ============================================================
-- ESTENSIONI
-- ============================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";    -- gen_random_bytes per nonce AES


-- ============================================================
-- 1. TENANTS
--    Ogni organizzazione cliente del SaaS
-- ============================================================
CREATE TABLE tenants (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    slug            VARCHAR(63)  NOT NULL UNIQUE,   -- usato come subdomain: acme.pecflow.it
    name            VARCHAR(255) NOT NULL,
    plan            VARCHAR(50)  NOT NULL DEFAULT 'starter',  -- starter|pro|enterprise
    is_active       BOOLEAN      NOT NULL DEFAULT TRUE,
    max_mailboxes   INT          NOT NULL DEFAULT 5,
    max_users       INT          NOT NULL DEFAULT 10,
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tenants_slug ON tenants (slug);


-- ============================================================
-- 2. USERS
-- ============================================================
CREATE TYPE user_role AS ENUM ('super_admin', 'admin', 'supervisor', 'operator', 'readonly');

CREATE TABLE users (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id           UUID         NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    email               VARCHAR(255) NOT NULL,
    password_hash       TEXT         NOT NULL,           -- bcrypt, work factor 12
    full_name           VARCHAR(255) NOT NULL,
    role                user_role    NOT NULL DEFAULT 'operator',
    is_active           BOOLEAN      NOT NULL DEFAULT TRUE,
    totp_secret         TEXT,                            -- cifrato con app key
    totp_enabled        BOOLEAN      NOT NULL DEFAULT FALSE,
    last_login_at       TIMESTAMPTZ,
    failed_login_count  INT          NOT NULL DEFAULT 0,
    locked_until        TIMESTAMPTZ,
    created_at          TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ  NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_user_email_tenant UNIQUE (tenant_id, email)
);

CREATE INDEX idx_users_tenant    ON users (tenant_id);
CREATE INDEX idx_users_email     ON users (email);

-- RLS: un utente vede solo gli utenti del proprio tenant
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY users_tenant_isolation ON users
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);


-- ============================================================
-- 3. REFRESH TOKENS
-- ============================================================
CREATE TABLE refresh_tokens (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id     UUID        NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash  TEXT        NOT NULL UNIQUE,   -- SHA-256 del token raw
    issued_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at  TIMESTAMPTZ NOT NULL,
    revoked_at  TIMESTAMPTZ,
    user_agent  TEXT,
    ip_address  INET
);

CREATE INDEX idx_rt_user    ON refresh_tokens (user_id);
CREATE INDEX idx_rt_expires ON refresh_tokens (expires_at) WHERE revoked_at IS NULL;


-- ============================================================
-- 4. MAILBOXES  (caselle PEC)
-- ============================================================
CREATE TYPE mailbox_status AS ENUM ('active', 'paused', 'error', 'deleted');

CREATE TABLE mailboxes (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id           UUID         NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    email_address       VARCHAR(255) NOT NULL,
    display_name        VARCHAR(255),
    provider            VARCHAR(100),                 -- 'aruba', 'namirial', 'legalmail', ecc.

    -- credenziali IMAP/SMTP cifrate (AES-256-GCM)
    -- formato colonna: base64(nonce || ciphertext || tag)
    imap_host_enc       TEXT         NOT NULL,
    imap_port_enc       TEXT         NOT NULL,
    imap_user_enc       TEXT         NOT NULL,
    imap_pass_enc       TEXT         NOT NULL,
    imap_use_ssl        BOOLEAN      NOT NULL DEFAULT TRUE,

    smtp_host_enc       TEXT         NOT NULL,
    smtp_port_enc       TEXT         NOT NULL,
    smtp_user_enc       TEXT         NOT NULL,
    smtp_pass_enc       TEXT         NOT NULL,
    smtp_use_tls        BOOLEAN      NOT NULL DEFAULT TRUE,

    status              mailbox_status NOT NULL DEFAULT 'active',
    last_sync_at        TIMESTAMPTZ,
    last_sync_uid       BIGINT,                       -- ultimo UID IMAP sincronizzato
    sync_error_msg      TEXT,
    sync_error_count    INT          NOT NULL DEFAULT 0,

    created_by          UUID         REFERENCES users(id),
    created_at          TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ  NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_mailbox_email_tenant UNIQUE (tenant_id, email_address)
);

CREATE INDEX idx_mailboxes_tenant ON mailboxes (tenant_id);
CREATE INDEX idx_mailboxes_status ON mailboxes (status) WHERE status = 'active';

ALTER TABLE mailboxes ENABLE ROW LEVEL SECURITY;
CREATE POLICY mailboxes_tenant_isolation ON mailboxes
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);


-- ============================================================
-- 5. MESSAGES  (PEC ricevute e inviate)
-- ============================================================
CREATE TYPE pec_direction  AS ENUM ('inbound', 'outbound');
CREATE TYPE pec_state      AS ENUM (
    -- outbound flow
    'draft', 'queued', 'sent', 'accepted', 'delivered', 'anomaly', 'failed',
    -- inbound
    'received'
);
CREATE TYPE pec_msg_type   AS ENUM (
    'posta_certificata',     -- messaggio originale PEC
    'accettazione',          -- ricevuta di accettazione dal gestore mittente
    'non_accettazione',      -- ricevuta di non accettazione
    'presa_in_carico',       -- ricevuta intermedia alcuni provider
    'avvenuta_consegna',     -- ricevuta di avvenuta consegna dal gestore destinatario
    'mancata_consegna',      -- ricevuta di mancata consegna (timeout / errore)
    'errore_consegna',       -- ricevuta di errore consegna
    'preavviso_mancata_consegna',
    'rilevazione_virus',
    'unknown'
);

CREATE TABLE messages (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id           UUID          NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    mailbox_id          UUID          NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE,

    -- identificatori
    message_id_header   TEXT,                          -- valore header Message-ID
    imap_uid            BIGINT,                        -- UID IMAP nella casella
    imap_folder         VARCHAR(255)  NOT NULL DEFAULT 'INBOX',

    direction           pec_direction NOT NULL,
    pec_type            pec_msg_type  NOT NULL DEFAULT 'posta_certificata',
    state               pec_state     NOT NULL,

    -- busta PEC
    subject             TEXT,
    from_address        VARCHAR(255),
    to_addresses        TEXT[],                        -- array destinatari
    cc_addresses        TEXT[],
    sent_at             TIMESTAMPTZ,                   -- data invio originale
    received_at         TIMESTAMPTZ,                   -- data ricezione IMAP
    size_bytes          BIGINT,

    -- corpo
    body_text           TEXT,                          -- testo plain estratto
    body_html           TEXT,                          -- HTML estratto (solo per visualizzazione)
    has_attachments     BOOLEAN       NOT NULL DEFAULT FALSE,

    -- per messaggi outbound: riferimento alla PEC originale di cui sono ricevuta
    parent_message_id   UUID          REFERENCES messages(id),

    -- flag operativi
    is_read             BOOLEAN       NOT NULL DEFAULT FALSE,
    is_starred          BOOLEAN       NOT NULL DEFAULT FALSE,
    is_archived         BOOLEAN       NOT NULL DEFAULT FALSE,
    archived_at         TIMESTAMPTZ,

    -- raw storage path (allegati e raw EML)
    raw_eml_path        TEXT,                          -- percorso in object storage

    created_at          TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ   NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_messages_tenant       ON messages (tenant_id);
CREATE INDEX idx_messages_mailbox      ON messages (mailbox_id);
CREATE INDEX idx_messages_state        ON messages (state);
CREATE INDEX idx_messages_received_at  ON messages (received_at DESC);
CREATE INDEX idx_messages_parent       ON messages (parent_message_id) WHERE parent_message_id IS NOT NULL;
CREATE INDEX idx_messages_imap_uid     ON messages (mailbox_id, imap_uid);
-- Full-text search sul soggetto
CREATE INDEX idx_messages_subject_fts  ON messages USING GIN (to_tsvector('italian', COALESCE(subject, '')));

ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY messages_tenant_isolation ON messages
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);


-- ============================================================
-- 6. ATTACHMENTS
-- ============================================================
CREATE TABLE attachments (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id       UUID         NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    message_id      UUID         NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    filename        VARCHAR(512) NOT NULL,
    content_type    VARCHAR(255),
    size_bytes      BIGINT,
    storage_path    TEXT         NOT NULL,   -- percorso MinIO/S3: tenants/{tid}/msgs/{mid}/{filename}
    checksum_sha256 CHAR(64),               -- integrità
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_attachments_message ON attachments (message_id);


-- ============================================================
-- 7. SEND_JOBS  (tracciamento invii SMTP)
-- ============================================================
CREATE TYPE send_job_status AS ENUM ('pending', 'sending', 'sent', 'failed', 'retrying');

CREATE TABLE send_jobs (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id       UUID              NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    mailbox_id      UUID              NOT NULL REFERENCES mailboxes(id),
    message_id      UUID              REFERENCES messages(id),  -- NULL fino all'invio riuscito
    status          send_job_status   NOT NULL DEFAULT 'pending',
    attempt_count   INT               NOT NULL DEFAULT 0,
    max_attempts    INT               NOT NULL DEFAULT 5,
    next_retry_at   TIMESTAMPTZ,
    last_error      TEXT,
    queued_at       TIMESTAMPTZ       NOT NULL DEFAULT NOW(),
    sent_at         TIMESTAMPTZ,
    created_by      UUID              REFERENCES users(id)
);

CREATE INDEX idx_sendjobs_tenant  ON send_jobs (tenant_id);
CREATE INDEX idx_sendjobs_status  ON send_jobs (status, next_retry_at) WHERE status IN ('pending','retrying');


-- ============================================================
-- 8. ARCHIVAL_BATCHES  (versamenti verso conservatore AgID)
-- ============================================================
CREATE TYPE archival_status AS ENUM (
    'pending',          -- in coda
    'building_sip',     -- generazione pacchetto SIP
    'uploading',        -- trasferimento al conservatore
    'uploaded',         -- ricevuta di versamento ricevuta (RdV)
    'confirmed',        -- conservatore ha confermato presa in carico
    'rejected',         -- conservatore ha rifiutato il versamento
    'failed'            -- errore tecnico non recuperabile
);

CREATE TABLE archival_batches (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id           UUID             NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    conservatore_id     VARCHAR(100)     NOT NULL,   -- codice conservatore (es. 'aruba-cons', 'docuvision')
    status              archival_status  NOT NULL DEFAULT 'pending',
    
    sip_path            TEXT,                        -- path del pacchetto SIP generato
    sip_checksum        CHAR(64),                    -- SHA-256 del SIP
    
    -- risposta dal conservatore
    versamento_id       TEXT,                        -- ID assegnato dal conservatore
    rdv_received_at     TIMESTAMPTZ,
    rdv_path            TEXT,                        -- path della RdV archiviata
    rdv_checksum        CHAR(64),
    
    attempt_count       INT              NOT NULL DEFAULT 0,
    max_attempts        INT              NOT NULL DEFAULT 3,
    next_retry_at       TIMESTAMPTZ,
    last_error          TEXT,
    
    period_from         DATE             NOT NULL,   -- periodo di riferimento del versamento
    period_to           DATE             NOT NULL,
    
    created_at          TIMESTAMPTZ      NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ      NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_archival_tenant  ON archival_batches (tenant_id);
CREATE INDEX idx_archival_status  ON archival_batches (status, next_retry_at);

-- Tabella pivot: quali messaggi sono inclusi in quale batch
CREATE TABLE archival_batch_messages (
    batch_id    UUID NOT NULL REFERENCES archival_batches(id) ON DELETE CASCADE,
    message_id  UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    PRIMARY KEY (batch_id, message_id)
);

-- DIP (Dissemination Information Package): recupero da conservatore
CREATE TABLE archival_dips (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id       UUID         NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    batch_id        UUID         REFERENCES archival_batches(id),
    requested_by    UUID         REFERENCES users(id),
    dip_path        TEXT,
    status          VARCHAR(50)  NOT NULL DEFAULT 'requested',
    requested_at    TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    received_at     TIMESTAMPTZ
);


-- ============================================================
-- 9. AUDIT_LOG
--    Immutabile (no UPDATE/DELETE via applicazione)
-- ============================================================
CREATE TABLE audit_log (
    id              BIGSERIAL    PRIMARY KEY,
    tenant_id       UUID         REFERENCES tenants(id),
    user_id         UUID         REFERENCES users(id),
    action          VARCHAR(100) NOT NULL,   -- 'message.read', 'mailbox.create', ecc.
    resource_type   VARCHAR(100),
    resource_id     UUID,
    ip_address      INET,
    user_agent      TEXT,
    payload         JSONB,                   -- parametri dell'azione (sanitizzati)
    outcome         VARCHAR(20)  NOT NULL DEFAULT 'success',  -- success|failure
    occurred_at     TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

-- Partizionamento per range (mese) per gestire volumi grandi senza purge
-- In Postgres 14+ si può fare: PARTITION BY RANGE (occurred_at)
-- Per semplicità in v1, si aggiunge un indice composito
CREATE INDEX idx_audit_tenant_date ON audit_log (tenant_id, occurred_at DESC);
CREATE INDEX idx_audit_user        ON audit_log (user_id, occurred_at DESC);
CREATE INDEX idx_audit_action      ON audit_log (action);

-- Blocca UPDATE/DELETE anche per DBA (si può bypassare solo con superuser esplicito)
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY audit_no_delete ON audit_log FOR DELETE USING (FALSE);
CREATE POLICY audit_no_update ON audit_log FOR UPDATE USING (FALSE);
CREATE POLICY audit_tenant_read ON audit_log FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant_id')::UUID);


-- ============================================================
-- 10. LABELS / ETICHETTE  (tagging messaggi)
-- ============================================================
CREATE TABLE labels (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id   UUID         NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name        VARCHAR(100) NOT NULL,
    color       CHAR(7),   -- hex color #RRGGBB
    CONSTRAINT uq_label_name_tenant UNIQUE (tenant_id, name)
);

CREATE TABLE message_labels (
    message_id  UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    label_id    UUID NOT NULL REFERENCES labels(id)   ON DELETE CASCADE,
    PRIMARY KEY (message_id, label_id)
);


-- ============================================================
-- TRIGGER: updated_at automatico
-- ============================================================
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
$$;

DO $$ DECLARE t TEXT; BEGIN
    FOREACH t IN ARRAY ARRAY['tenants','users','mailboxes','messages','archival_batches'] LOOP
        EXECUTE format('CREATE TRIGGER trg_%s_updated_at
            BEFORE UPDATE ON %s FOR EACH ROW EXECUTE FUNCTION set_updated_at()', t, t);
    END LOOP;
END $$;
```

---

## 3. Piano di Sviluppo a Fasi

### Fase 1 – Fondamenta (3 settimane)

**Obiettivo:** infrastruttura funzionante, autenticazione sicura, deploy locale.

**Task:**
- [ ] Inizializzare monorepo con struttura cartelle, `.gitignore`, `Makefile`
- [ ] `docker-compose.yml` con PostgreSQL 16, Redis 7, MinIO (S3 local), Nginx
- [ ] Backend FastAPI: struttura app, config pydantic-settings, health endpoint
- [ ] Alembic: prima migrazione con tutte le tabelle DDL (sezione 2)
- [ ] Modello `tenants` + `users` + ORM SQLAlchemy corrispondente
- [ ] Auth API: `POST /auth/login` (email+password), JWT access token (15 min) + refresh token (30 gg)
- [ ] Middleware RLS: `set_config('app.current_tenant_id', ...)` su ogni request
- [ ] API 2FA TOTP: `POST /auth/totp/setup`, `POST /auth/totp/verify`
- [ ] Rate limiting su endpoint auth (slowapi)
- [ ] CRUD utenti base (admin crea/disabilita utenti dello stesso tenant)
- [ ] CI GitHub Actions: lint (ruff, mypy), test (pytest), build Docker
- [ ] Seed script: tenant demo + utente admin per dev locale

**Definition of Done:**
- Login, refresh token e 2FA funzionanti e coperti da test di integrazione
- `make dev` porta in piedi tutto lo stack locale
- CI verde su PR

---

### Fase 2 – IMAP Sync Engine (4 settimane)

**Obiettivo:** connessione IMAP stabile a N caselle in parallelo, sincronizzazione messaggi.

**Task:**
- [ ] CRUD caselle PEC (`POST /mailboxes`, cifratura credenziali AES-256-GCM)
- [ ] Worker arq: entrypoint, connessione Redis, worker health check
- [ ] `MailboxPool`: avvia un task async per ogni casella `status=active` all'avvio
- [ ] `IMAPConnection`: connessione IMAP via aioimaplib, gestione IDLE con heartbeat 28 min (RFC 2177)
- [ ] Polling fallback: se IDLE non supportato, polling ogni 60 secondi
- [ ] Backoff esponenziale su disconnessione (1s → 2s → 4s → max 5 min)
- [ ] Fetch UID list + envelope per messaggi nuovi (UID > `last_sync_uid`)
- [ ] Salvataggio `messages` con campi base, aggiornamento `last_sync_uid`
- [ ] Download body e raw EML su MinIO: `tenants/{tid}/raw/{mailbox_id}/{uid}.eml`
- [ ] Aggiornamento stato casella: `sync_error_count`, `status=error` dopo N fallimenti
- [ ] Notifica WebSocket su nuovo messaggio: evento `mailbox:new_message` al tenant
- [ ] Test di integrazione con Greenmail (server IMAP mock Java) via Docker

**Definition of Done:**
- Connessione a casella Aruba PEC reale in ambiente di test (sandbox)
- Verifica leggibilità mail 
- Riconnessione automatica verificata (kill connessione di rete e attesa recovery)

---

### Fase 3 – Parser PEC & Tracking Ricevute (3 settimane)

**Obiettivo:** classificare correttamente i messaggi PEC e collegare le ricevute.

**Task:**
- [x] `pec_parser.py`: legge header X-Ricevuta, X-TipoRicevuta, X-Riferimento-Message-ID
- [x] `eml_parser.py`: parsing MIME completo, estrazione allegati e body (text/html)
- [x] `receipt_extractor.py`: estrae EML allegato dentro ricevuta (EML-in-EML annidato)
- [x] Mappatura `pec_msg_type` da header PEC a enum DB
- [x] Collegamento `parent_message_id`: associa ricevuta al messaggio originale via `X-Riferimento-Message-ID`
- [x] State machine messaggi outbound: `sent→accepted→delivered` (o `anomaly`)
- [x] Download e salvataggio allegati su MinIO, inserimento tabella `attachments`
- [x] Test unitari parser con EML reali (fixture anonimizzate) per tutti i tipi di ricevuta
- [x] Test regressione: parsing Aruba / Namirial / Legalmail (formato header leggermente diverso per provider)

**Definition of Done:**
- ✅ 100% dei tipi ricevuta classificati correttamente su un set di 50+ EML reali
- ✅ I messaggi outbound aggiornano stato automaticamente all'arrivo della ricevuta

---

### Fase 4 – Invio SMTP (2 settimane)

**Obiettivo:** invio PEC affidabile con retry e tracking.

**Task:**
- [x] API `POST /send`: validazione, creazione `send_job` e `message` in stato `draft`→`queued`
- [x] Job `send_pec`: connessione SMTP STARTTLS/SSL via aiosmtplib
- [x] Gestione `To`, `Cc` multipli, allegati, headers PEC obbligatori
- [x] Salvataggio raw EML inviato su MinIO
- [x] Retry con backoff: 5 tentativi (1 min → 5 min → 15 min → 1h → 4h)
- [x] Dopo invio: avvia `receipt_watcher` – attende ricevuta di accettazione entro 24h
- [x] Alert: se nessuna accettazione in 24h → stato `anomaly` + notifica WS

**Definition of Done:**
- ✅ API POST /send, GET /send/jobs, DELETE /send/jobs/{id} implementate e testate (13/13 test passati)
- ✅ Job send_pec con retry esponenziale e watch_receipt registrati nel worker
- ✅ SmtpSender con supporto SSL/STARTTLS (porta 465/587) e costruzione MIME corretta
- ✅ Upload raw EML su MinIO (percorso outbound/{message_id}.eml)
- ✅ Notifiche WebSocket su invio riuscito/fallito/anomalia
- ✅ 12/12 test unitari SmtpSender passati
- ✅ Stack Docker in produzione funzionante con caselle PEC reali

---

### Fase 5 – Frontend (5 settimane)

**Obiettivo:** interfaccia utente completa per operatori e admin.

**Task:**
- [x] Setup Vite + React 18 + TypeScript + Tailwind CSS + shadcn/ui
- [x] Layout base: sidebar navigazione, header tenant
- [x] Pagina Login + 2FA TOTP (QR code, OTP input)
- [x] Axios client: interceptor refresh token (refresh silenzioso su 401)
- [x] Hook `useWebSocket` (WebSocket nativo): aggiorna inbox in real-time con backoff esponenziale
- [x] Inbox: lista messaggi paginata, filtri (casella, direzione, stato, testo), badge PEC state
- [x] Dettaglio messaggio: corpo (HTML/testo), allegati scaricabili, tree ricevute (`ReceiptTree`)
- [x] Composizione PEC: form To/Cc multipli/Subject/Body, risposta a thread, invio
- [x] Gestione Caselle (admin): CRUD, test connessione IMAP/SMTP, stato sync
- [x] Gestione Utenti (admin): CRUD, cambio ruolo, reset password, toggle attivo
- [x] Gestione Permessi: matrice utenti × caselle con toggle can_read/can_send/can_manage
- [x] Internazionalizzazione: tutta l'UI in italiano (nessuna stringa in inglese)
- [x] Zustand stores: auth, inbox, mailbox
- [x] React Query: caching, refetch automatico, invalidation
- [x] Dockerfile multi-stage (dev: Vite hot-reload, prod: nginx)

**Definition of Done:**
- ✅ Build TypeScript 0 errori, bundle 467KB (146KB gzip)
- ✅ Stack Docker completo funzionante (HTTP 200, tutti i moduli caricati)
- ✅ Frontend accessibile su http://localhost
- ✅ Backend health confermato via Nginx proxy
- ✅ Tutti i container Up: db, redis, minio, backend, frontend, worker, nginx

---

### Fase 6 – Archiviazione Sostitutiva (4 settimane)

**Obiettivo:** versamento PEC verso conservatore AgID, recupero DIP.

**Task:**
- [ ] `sip_builder.py`: generazione pacchetto SIP conforme UNI SInCRO 11386:2023
    - Indice del pacchetto (XML)
    - Allegati originali + ricevute
    - Calcolo hash SHA-256 per ogni file
- [ ] `conservatore_client.py`: client HTTP per API conservatore (mock locale in dev)
    - `POST /versamento` (upload SIP)
    - `GET /versamento/{id}` (polling stato)
    - `GET /dip/{id}` (richiesta DIP)
- [ ] `rdv_processor.py`: parsing RdV XML, salvataggio in `archival_batches`
- [ ] Job `archive_batch`: seleziona messaggi `archived=false` nel periodo, crea SIP, carica
- [ ] Retry versamenti falliti: 3 tentativi con backoff progressivo
- [ ] API `GET /archival/batches`, `GET /archival/batches/{id}/rdv`
- [ ] API `POST /archival/dip` (richiesta DIP)
- [ ] Pagina frontend Archivio: log versamenti, download RdV, richiesta DIP
- [ ] Configurazione per-tenant del conservatore (endpoint, credenziali cifrate)

**Definition of Done:**
- Versamento completato end-to-end verso conservatore mock
- RdV XML salvata e scaricabile dal frontend

---

### Fase 7 – Dashboard e Reportistica (2 settimane)

**Obiettivo:** visibilità operativa e compliance.

**Task:**
- [ ] API `/reports/summary`: KPI real-time (PEC ricevute/inviate oggi, anomalie, tasso consegna)
- [ ] API `/reports/export`: generazione PDF (WeasyPrint) e CSV
- [ ] Dashboard frontend: grafici (Recharts), filtri per periodo e casella
- [ ] Audit log viewer (admin): ricerca per utente/azione/data
- [ ] Alerting: soglie configurabili (es. >10% anomalie → email notifica admin)

**Definition of Done:**
- Export PDF generato correttamente con dati reali
- Dashboard mostra dati aggiornati in < 2s

---

### Fase 8 – Hardening, Test, Go-Live (3 settimane)

**Obiettivo:** sistema production-ready.

**Task:**
- [ ] Penetration test interno: OWASP Top 10 check (IDOR, injection, broken auth)
- [ ] Secret scan CI: Gitleaks / Trufflesecurity su ogni commit
- [ ] Dependency audit: `pip audit`, `npm audit` → zero critical
- [ ] Copertura test: ≥ 80% backend, ≥ 60% frontend
- [ ] Load test: Locust – 50 utenti concorrenti, 10 caselle sincronizzate
- [ ] Backup automatico PostgreSQL (pg_dump daily → MinIO, retention 30gg)
- [ ] Monitoring: Prometheus + Grafana dashboard (latency API, queue size, IMAP errors)
- [ ] Log aggregation: Loki o ELK stack
- [ ] Runbook operativo: come aggiungere tenant, debug IMAP, recovery da errore conservatore
- [ ] GDPR: endpoint `DELETE /tenants/{id}` (cancellazione dati completa + audit trail)

**Definition of Done:**
- Zero vulnerability critiche
- SLA target: API p95 < 500ms, uptime 99.5%
- Runbook revisionato dal team

---

## 4. Decisioni Architetturali

---

### ADR-001 – Multi-tenancy: Schema-per-tenant vs Row-level con tenant_id

**Opzione A – Schema-per-tenant (un PostgreSQL schema per ogni organizzazione)**

*Pro:*
- Isolamento totale dei dati: impossibile data leak cross-tenant per bug SQL
- Backup e restore per singolo tenant molto semplici
- Possibilità di migrare un tenant su un DB separato senza refactoring

*Contro:*
- Alembic migrations vanno applicate a tutti gli schema (N schema × migrazione)
- Connection pooling complesso: PgBouncer deve gestire schema switching
- Difficile fare query aggregate cross-tenant (es. monitoraggio globale SaaS)
- Overhead operativo significativo oltre i 100 tenant

**Opzione B – Row-level con `tenant_id` + PostgreSQL RLS**

*Pro:*
- Un solo schema, migrations applicate una volta
- Query cross-tenant per operazioni di sistema (monitoring, billing)
- Molto più semplice da gestire con Alembic e ORM
- Scala bene fino a decine di migliaia di tenant

*Contro:*
- Bug nell'impostazione `current_tenant_id` può causare data leak → mitigato con RLS come secondo livello
- Restore di singolo tenant richiede WHERE clause su dump

**Raccomandazione: Opzione B (row-level + RLS)**

Per un SaaS B2B con caselle PEC, il numero di tenant nel medio termine è nell'ordine delle centinaia, non decine di migliaia. Il rischio principale (data leak cross-tenant) è mitigato da due strati indipendenti:
1. Applicativo: ogni query include `WHERE tenant_id = :current_tenant_id`
2. DB: PostgreSQL RLS come safety net, impostata tramite `SET LOCAL app.current_tenant_id = '...'` in ogni transazione

Il vantaggio operativo (migrazioni semplici, query di monitoring, pool unico) supera i rischi, a condizione di avere test di integrazione che verificano il corretto isolamento.

---

### ADR-002 – Cifratura credenziali IMAP/SMTP a riposo

**Opzione A – Cifratura applicativa (AES-256-GCM)**

Le credenziali sono cifrate nel layer applicativo prima di essere scritte in DB.
La chiave master è in `SECRET_KEY` (variabile d'ambiente, ruotabile).
Formato storage: `base64(nonce_12byte || ciphertext || tag_16byte)`.

*Pro:* la chiave non è mai in DB; un dump del DB senza la chiave è inutile.  
*Contro:* rotazione chiave richiede re-cifratura di tutte le righe.

**Opzione B – pgcrypto `pgp_sym_encrypt`**

Cifratura dentro PostgreSQL con passphrase.

*Pro:* semplice da implementare.  
*Contro:* la chiave transita sulla connessione DB; meno controllo sul key management.

**Opzione C – HashiCorp Vault / AWS KMS (envelope encryption)**

Vault genera una data encryption key (DEK) per ogni casella; la DEK è cifrata con key encryption key (KEK) gestita da Vault.

*Pro:* key rotation automatica, audit trail accessi chiave, compliance elevata.  
*Contro:* dipendenza infrastrutturale aggiuntiva, complessità setup per v1.

**Raccomandazione: Opzione A per v1, con migrazione verso C in Fase 8**

In v1 si usa AES-256-GCM con chiave in variabile d'ambiente (gestita via Docker Secret o Kubernetes Secret). L'interfaccia di cifratura è astratta in `core/security.py`, quindi la migrazione a Vault è trasparente al resto del codice. La rotazione della chiave viene gestita con un management command dedicato.

---

### ADR-003 – Concorrenza IMAP su N caselle

**Opzione A – Un processo per casella (multiprocessing)**

Un processo OS per ogni casella PEC attiva.

*Pro:* isolamento totale; crash di una casella non impatta le altre.  
*Contro:* overhead di memoria elevato (N × ~50MB), difficile scalare oltre ~50 caselle per host.

**Opzione B – Thread pool**

Un thread per casella dentro un unico processo Python.

*Contro:* GIL Python limita parallelismo CPU-bound; librerie IMAP non sempre thread-safe.

**Opzione C – Async task pool (asyncio) – RACCOMANDATO**

Un singolo processo async con N coroutine IMAP, una per casella. La libreria `aioimaplib` è nativa async. Ogni casella gira in un loop IDLE / polling come coroutine indipendente.

*Pro:* memory footprint bassissimo (< 10MB per casella), migliaia di caselle per host, codice semplice.  
*Contro:* un bug che blocca l'event loop impatta tutte le caselle → mitigato con timeout su ogni operazione IMAP.

**Implementazione consigliata:**

```
MailboxPool (asyncio)
  ├── asyncio.Task per casella #1  → IMAPConnection (IDLE loop)
  ├── asyncio.Task per casella #2  → IMAPConnection (IDLE loop)
  └── ...
```

Il `MailboxPool` monitora ogni task con `asyncio.shield` + timeout. Se un task muore, viene riavviato con backoff esponenziale. Un semaforo limita il numero di fetch simultanei (es. max 10) per evitare saturazione banda.

---

### ADR-004 – Retry strategy versamenti verso conservatore AgID

Il conservatore AgID può essere temporaneamente non disponibile (manutenzione, overload). Un versamento fallito non è recuperabile in modo autonomo dal conservatore: il sistema deve ritentare attivamente.

**Strategia:**

1. **Backoff esponenziale con jitter:** tentativo 1 subito, poi 1h, 4h, 24h (max 3 retry oltre il primo)
2. **Dead letter queue:** dopo tutti i retry falliti, il batch va in stato `failed` e genera un alert email all'admin del tenant + al team ops PecFlow
3. **Idempotenza:** prima di ogni retry, verificare se il conservatore ha già ricevuto il versamento (`GET /versamento/{id}`) per evitare duplicati
4. **Finestra di versamento:** i versamenti periodici (es. mensili) devono completarsi entro la scadenza normativa; se il sistema prevede che non ce la farà, genera alert anticipato a 72h dalla scadenza
5. **Circuit breaker:** se il conservatore fallisce 5 volte in 1 ora, si sospendono tutti i versamenti verso quel conservatore per 30 minuti (evita tempesta di retry)

---

### ADR-005 – Storage allegati e raw EML

**Opzione A – Filesystem locale**

*Pro:* zero dipendenze, semplicissimo.  
*Contro:* non scala orizzontalmente (worker su host diversi non vedono i file); backup complesso.

**Opzione B – PostgreSQL BYTEA / Large Object**

*Contro:* gonfia il DB, WAL enorme, backup lenti, nessun vantaggio rispetto a object storage.

**Opzione C – Object Storage S3-compatible (MinIO in locale, S3/Wasabi in prod)**

*Pro:* scala infinitamente, separazione DB/oggetti, backup indipendente, URL pre-firmati per download diretto dal browser senza passare per il backend, CDN-ready.  
*Contro:* dipendenza aggiuntiva (MinIO in Docker Compose per dev).

**Raccomandazione: Opzione C**

Percorso oggetto: `{bucket}/tenants/{tenant_id}/mailboxes/{mailbox_id}/messages/{message_id}/{tipo}/{filename}`

Dove `{tipo}` può essere `raw` (EML grezzo), `attachments`, `sip` (pacchetti SIP), `rdv`.

I metadati (filename, content-type, checksum, path) sono in PostgreSQL nella tabella `attachments`. Il frontend riceve URL pre-firmati con scadenza 15 minuti per il download diretto.

In ambiente di sviluppo locale, MinIO è incluso nel `docker-compose.yml` con credenziali fisse. In produzione si sostituisce con la variabile `S3_ENDPOINT_URL` che punta a S3 o a un provider compatibile.

---

### ADR-006 – Strategia Full-Text Search

**Opzione A – PostgreSQL FTS nativo (tsvector/tsquery)**

Colonna `search_vector` di tipo `tsvector` aggiornata tramite trigger ad ogni INSERT/UPDATE su `messages`. Copre: oggetto, corpo, mittente, destinatari, nomi allegati.

*Pro:* zero dipendenze aggiuntive, stesso DB già in stack, transazionale, indice GIN incluso nel backup.  
*Contro:* ricerca su testo estratto dagli allegati (PDF, DOCX) richiede pre-processing separato; non scala quanto Elasticsearch su miliardi di documenti.

**Opzione B – Elasticsearch / OpenSearch**

Servizio dedicato con indici invertiti ottimizzati, supporto nativo a highlighting, fuzzy, suggerimenti.

*Pro:* performance eccellente su grandi volumi, relevance scoring avanzato, analizzatori linguistici.  
*Contro:* servizio aggiuntivo da gestire (heap JVM, snapshot, upgrade), sincronizzazione DB→ES può andare out-of-sync, licenza cambiata (OpenSearch è alternativa open).

**Raccomandazione: Opzione A per v1-v2, revisione a v3**

La ricerca FTS nativa di PostgreSQL è più che sufficiente per volumi tipici di PEC aziendali (< 5 milioni di messaggi). Il `search_vector` copre tutti i campi principali; il testo degli allegati viene estratto in background (Apache Tika via container Docker dedicato) e aggiunto al vettore. La migrazione a Elasticsearch in futuro è possibile senza modificare l'API pubblica, solo il `search_service.py`.

---

### ADR-007 – Architettura notifiche multi-canale

**Problema:** i canali di notifica (webhook, email, Telegram, WhatsApp) hanno latenza, affidabilità e costi di chiamata completamente diversi. Un fallimento su un canale non deve bloccare gli altri.

**Architettura scelta: Fan-out asincrono per canale con retry indipendenti**

```
Evento PEC (es. message.received)
    ↓
notification_service.evaluate_rules()   ← trova regole applicabili
    ↓
Per ogni regola corrispondente:
    → enqueue job dispatch_notification (canale, payload, rule_id)
        ↓
        worker/notifications/dispatcher.py
            ├── webhook.py    → HTTP POST + HMAC-SHA256 signature header
            ├── email_smtp.py → template HTML via aiosmtplib
            ├── telegram.py   → Bot API sendMessage (MarkdownV2)
            └── whatsapp.py   → Meta Cloud API v18 / Twilio fallback
        ↓
        notification_log (outcome: success|failed, attempt_count)
```

Ogni dispatch è un job arq indipendente con retry (max 3, backoff 5 min → 30 min → 2h). Il fallimento di Telegram non ritarda il webhook. Ogni canale ha il proprio circuit breaker (5 fail / 10 min → pausa 1h).

**WhatsApp:** si usa Meta Cloud API (gratuita fino a 1000 conversazioni/mese) con fallback opzionale su Twilio per volumi maggiori. Le credenziali (token, phone_number_id) sono configurate per-tenant in `notification_channels`.

**Telegram:** un bot per tenant (token nel canale) o bot condiviso con `chat_id` per-utente. Si raccomanda bot condiviso per semplicità operativa in v1.

---

### ADR-008 – Strategia permessi e isolamento Virtual Box

**Problema:** il sistema deve supportare sia accesso globale (admin vede tutto) sia accesso ristretto granulare (utente vede solo mail specifiche da casella specifica).

**Strategia a due livelli:**

**Livello 1 – Permessi per casella (`mailbox_permissions`)**
Ogni utente può avere permessi espliciti su una casella: `can_read`, `can_send`, `can_manage`. Gli admin hanno accesso implicito a tutte le caselle del proprio tenant. Gli operatori senza permessi espliciti non vedono nessuna casella.

**Livello 2 – Virtual Box (filtro sui messaggi)**
Una Virtual Box è un filtro configurabile che, se assegnato a un utente, restringe ulteriormente i messaggi visibili *dentro* le caselle permesse. I criteri sono combinati in AND: l'utente vede solo i messaggi che soddisfano TUTTI i criteri della VBox assegnata.

```
Utente pbianchi
  ├── mailbox_permissions: info@ → can_read=TRUE, can_send=FALSE
  └── virtual_box_assignment: "Multe info@"
        └── virtual_box_rules:
              ├── mailbox_id = <info@>
              └── subject_pattern = 'Multa' (ILIKE '%Multa%')

→ pbianchi vede SOLO i messaggi di info@ con oggetto contenente "Multa"
```

L'isolamento è applicato nel `virtual_box_service.py` che inietta le clausole WHERE aggiuntive in ogni query messaggi. La stessa logica si applica alla ricerca avanzata: se l'utente ha una VBox, la ricerca non può uscire dal perimetro della VBox.

---

## 2.2 DDL Aggiuntivo – Sistemi Avanzati

```sql
-- ============================================================
-- A. SEARCH VECTOR su MESSAGES
--    Colonna calcolata per full-text search su tutti i campi
-- ============================================================

-- Aggiunta della colonna search_vector alla tabella messages
ALTER TABLE messages ADD COLUMN IF NOT EXISTS
    search_vector TSVECTOR
    GENERATED ALWAYS AS (
        setweight(to_tsvector('italian', COALESCE(subject, '')),       'A') ||
        setweight(to_tsvector('italian', COALESCE(from_address, '')),  'B') ||
        setweight(to_tsvector('italian', array_to_string(COALESCE(to_addresses, '{}'), ' ')), 'B') ||
        setweight(to_tsvector('italian', COALESCE(body_text, '')),     'C')
    ) STORED;

CREATE INDEX idx_messages_search_vector ON messages USING GIN (search_vector);

-- Aggiunta testo estratto degli allegati (aggiornato dal worker Tika)
ALTER TABLE attachments ADD COLUMN IF NOT EXISTS extracted_text TEXT;
ALTER TABLE attachments ADD COLUMN IF NOT EXISTS extraction_status VARCHAR(20) DEFAULT 'pending';
-- extraction_status: pending | done | skipped (non testuale) | failed

CREATE INDEX idx_attachments_fts
    ON attachments USING GIN (to_tsvector('italian', COALESCE(extracted_text, '')))
    WHERE extracted_text IS NOT NULL;


-- ============================================================
-- B. PERMESSI GRANULARI PER CASELLA
-- ============================================================
-- Matrice permessi: per ogni (user, mailbox) definisce cosa può fare.
-- Un admin di tenant NON ha bisogno di record: accesso implicito.
-- Operatori e readonly DEVONO avere un record per vedere la casella.

CREATE TABLE mailbox_permissions (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id   UUID    NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    user_id     UUID    NOT NULL REFERENCES users(id)   ON DELETE CASCADE,
    mailbox_id  UUID    NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE,

    can_read    BOOLEAN NOT NULL DEFAULT TRUE,    -- legge messaggi
    can_send    BOOLEAN NOT NULL DEFAULT FALSE,   -- invia PEC da questa casella
    can_manage  BOOLEAN NOT NULL DEFAULT FALSE,   -- modifica configurazione casella

    granted_by  UUID    REFERENCES users(id),
    granted_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_perm_user_mailbox UNIQUE (user_id, mailbox_id)
);

CREATE INDEX idx_mbperm_user    ON mailbox_permissions (user_id);
CREATE INDEX idx_mbperm_mailbox ON mailbox_permissions (mailbox_id);
CREATE INDEX idx_mbperm_tenant  ON mailbox_permissions (tenant_id);


-- ============================================================
-- C. VIRTUAL BOX
-- ============================================================

-- Definizione di una Virtual Box (vista filtrata sui messaggi)
CREATE TABLE virtual_boxes (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id   UUID         NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name        VARCHAR(255) NOT NULL,
    description TEXT,
    is_active   BOOLEAN      NOT NULL DEFAULT TRUE,
    created_by  UUID         REFERENCES users(id),
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_vbox_name_tenant UNIQUE (tenant_id, name)
);

CREATE INDEX idx_vbox_tenant ON virtual_boxes (tenant_id);

-- Regole di filtro per una Virtual Box.
-- Tutte le regole valide di una VBox sono combinate in AND.
-- Campi NULL = "non filtrare su questo criterio".
CREATE TABLE virtual_box_rules (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    virtual_box_id  UUID          NOT NULL REFERENCES virtual_boxes(id) ON DELETE CASCADE,
    tenant_id       UUID          NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,

    -- Filtri strutturali (NULL = qualsiasi)
    mailbox_id      UUID          REFERENCES mailboxes(id) ON DELETE CASCADE,
    imap_folder     VARCHAR(255),                          -- es. 'INBOX', 'Sent'
    direction       pec_direction,                         -- inbound | outbound | NULL
    pec_type        pec_msg_type,                          -- filtra per tipo PEC

    -- Filtri testuali (ILIKE '%pattern%' case-insensitive, NULL = qualsiasi)
    subject_pattern     TEXT,                             -- es. 'Multa'
    from_pattern        TEXT,                             -- es. 'comune.roma.it'
    to_pattern          TEXT,                             -- filtra su to_addresses

    -- Filtro temporale
    date_from       DATE,                                 -- messaggi ricevuti da questa data
    date_to         DATE,

    -- Filtro su etichetta
    label_id        UUID          REFERENCES labels(id),

    created_at      TIMESTAMPTZ   NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_vbox_rules_vbox ON virtual_box_rules (virtual_box_id);

-- Assegnazione utenti alle Virtual Box
-- Un utente può essere assegnato a più VBox (in OR tra loro, vede l'unione)
CREATE TABLE virtual_box_assignments (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id       UUID        NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    user_id         UUID        NOT NULL REFERENCES users(id)   ON DELETE CASCADE,
    virtual_box_id  UUID        NOT NULL REFERENCES virtual_boxes(id) ON DELETE CASCADE,
    assigned_by     UUID        REFERENCES users(id),
    assigned_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_vbox_assign UNIQUE (user_id, virtual_box_id)
);

CREATE INDEX idx_vbox_assign_user  ON virtual_box_assignments (user_id);
CREATE INDEX idx_vbox_assign_vbox  ON virtual_box_assignments (virtual_box_id);


-- ============================================================
-- D. NOTIFICHE
-- ============================================================

CREATE TYPE notification_channel_type AS ENUM ('webhook', 'email_smtp', 'telegram', 'whatsapp');
CREATE TYPE notification_event_type AS ENUM (
    'message.received',          -- nuova PEC ricevuta
    'message.delivered',         -- PEC outbound consegnata
    'message.anomaly',           -- PEC anomalia (mancata consegna, virus, ecc.)
    'message.failed',            -- invio fallito definitivamente
    'mailbox.error',             -- casella in stato error
    'archival.completed',        -- versamento conservatore completato
    'archival.failed',           -- versamento fallito
    'mailbox.quota_warning'      -- casella quasi piena (stima da sync)
);

-- Canali di notifica configurati per tenant (o per singolo utente)
CREATE TABLE notification_channels (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id   UUID                        NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    user_id     UUID                        REFERENCES users(id),   -- NULL = canale condiviso del tenant
    name        VARCHAR(255)                NOT NULL,
    type        notification_channel_type   NOT NULL,
    is_active   BOOLEAN                     NOT NULL DEFAULT TRUE,

    -- Configurazione specifica per tipo (cifrata se contiene segreti)
    -- webhook:      { "url": "https://...", "secret": "hmac_key" }
    -- email_smtp:   { "to": ["addr1","addr2"] }
    -- telegram:     { "bot_token_enc": "...", "chat_id": "123456" }
    -- whatsapp:     { "phone_number_id": "...", "access_token_enc": "...", "to": "+39..." }
    config_enc  JSONB                       NOT NULL,   -- AES-256-GCM su campi sensibili

    created_at  TIMESTAMPTZ                 NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ                 NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_notif_ch_tenant ON notification_channels (tenant_id);

-- Regole: quali eventi triggherano quale canale, con filtri opzionali
CREATE TABLE notification_rules (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id       UUID                        NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    channel_id      UUID                        NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
    event_type      notification_event_type     NOT NULL,
    is_active       BOOLEAN                     NOT NULL DEFAULT TRUE,

    -- Filtri opzionali (NULL = qualsiasi)
    mailbox_id      UUID            REFERENCES mailboxes(id),  -- solo per questa casella
    from_pattern    TEXT,                                       -- mittente ILIKE '%pattern%'
    subject_pattern TEXT,                                       -- oggetto ILIKE '%pattern%'
    pec_type        pec_msg_type,                               -- solo questo tipo PEC

    created_at      TIMESTAMPTZ     NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_notif_rule_tenant  ON notification_rules (tenant_id);
CREATE INDEX idx_notif_rule_channel ON notification_rules (channel_id);
CREATE INDEX idx_notif_rule_event   ON notification_rules (event_type);

-- Log delle notifiche inviate
CREATE TABLE notification_log (
    id              BIGSERIAL   PRIMARY KEY,
    tenant_id       UUID        NOT NULL REFERENCES tenants(id),
    rule_id         UUID        REFERENCES notification_rules(id),
    channel_id      UUID        REFERENCES notification_channels(id),
    message_id      UUID        REFERENCES messages(id),   -- messaggio che ha triggerato
    event_type      notification_event_type NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',  -- pending|sent|failed
    attempt_count   INT         NOT NULL DEFAULT 0,
    last_error      TEXT,
    payload         JSONB,       -- payload inviato (per debug)
    sent_at         TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_notif_log_tenant  ON notification_log (tenant_id, created_at DESC);
CREATE INDEX idx_notif_log_status  ON notification_log (status) WHERE status = 'pending';


-- ============================================================
-- Estendi trigger set_updated_at ai nuovi modelli
-- ============================================================
DO $$ DECLARE t TEXT; BEGIN
    FOREACH t IN ARRAY ARRAY['virtual_boxes','notification_channels'] LOOP
        EXECUTE format('CREATE TRIGGER trg_%s_updated_at
            BEFORE UPDATE ON %s FOR EACH ROW EXECUTE FUNCTION set_updated_at()', t, t);
    END LOOP;
END $$;
```

---

## 3.B Piano di Sviluppo – Fasi Aggiuntive (Sistemi Avanzati)

Le seguenti fasi si inseriscono nel piano originale. La **Fase 1-A** è contestuale
alla Fase 1, le altre possono essere sviluppate in parallelo al frontend (Fase 5).

---

### Fase 1-A – Permessi Granulari (inserita in Fase 1, +1 settimana)

**Obiettivo:** implementare il sistema di permessi a due livelli: ruoli globali + permessi per casella.

**Task:**
- [ ] Alembic migration per `mailbox_permissions`
- [ ] `permission_service.py`: `check_user_can_read(user, mailbox)`, `check_user_can_send(user, mailbox)`
- [ ] Middleware FastAPI: ogni endpoint messaggi/invio chiama `permission_service` prima di procedere
- [ ] API `POST /permissions/mailboxes/{mailbox_id}/users/{user_id}` (admin assegna permesso)
- [ ] API `DELETE /permissions/mailboxes/{mailbox_id}/users/{user_id}` (admin revoca)
- [ ] API `GET /permissions/mailboxes/{mailbox_id}/users` (lista permessi casella)
- [ ] API `GET /permissions/users/{user_id}/mailboxes` (caselle accessibili a un utente)
- [ ] Test: utente senza permesso riceve 403, utente con `can_read=False` non vede messaggi
- [ ] Frontend: pagina Permissions (admin) con tabella utenti × caselle con toggle

**Definition of Done:**
- Utente `readonly` senza permessi espliciti non vede nessuna casella
- Admin aggiunge/revoca permesso e l'effetto è immediato (no cache JWT)
- Test di integrazione coprono tutti i casi limite (ruolo vs permesso esplicito)

---

### Fase 5-A – Virtual Box (inserita dopo Fase 5, 2 settimane)

**Obiettivo:** permettere agli admin di creare viste filtrate e assegnarle a utenti specifici.

**Task:**
- [ ] Alembic migration per `virtual_boxes`, `virtual_box_rules`, `virtual_box_assignments`
- [ ] `virtual_box_service.py`: `build_filter_clauses(user_id)` → lista di clausole SQLAlchemy
- [ ] Integrazione in `message_service.py`: se l'utente ha VBox assegnate, applica filtri
- [ ] Stessa integrazione in `search_service.py` (ricerca non esce dal perimetro VBox)
- [ ] API `POST /virtual-boxes` (crea VBox con regole)
- [ ] API `PUT /virtual-boxes/{id}/rules` (aggiorna regole)
- [ ] API `POST /virtual-boxes/{id}/assignments` (assegna utente)
- [ ] API `DELETE /virtual-boxes/{id}/assignments/{user_id}` (rimuovi assegnazione)
- [ ] Frontend: pagina VirtualBoxes – builder visuale delle regole (dropdown campi, input pattern)
- [ ] Frontend: Inbox mostra badge "VBox attiva: [nome]" se l'utente è in modalità filtrata
- [ ] Test: utente pbianchi in VBox "Multe info@" non vede mail con oggetto diverso da "Multa"

**Definition of Done:**
- Scenario di esempio funzionante: admin crea VBox, assegna utente, utente vede solo mail filtrate
- Admin che non ha VBox assegnata vede tutto normalmente

---

### Fase 5-B – Ricerca Avanzata (inserita dopo Fase 5, 2 settimane)

**Obiettivo:** ricerca full-text su tutti i campi del messaggio e sul contenuto degli allegati.

**Task:**
- [ ] Alembic migration: colonna `search_vector` GENERATED + `attachments.extracted_text`
- [ ] `worker/search/text_extractor.py`: chiama Apache Tika (`docker run apache/tika`) via HTTP REST, estrae testo da PDF/DOCX/ODT/TXT/HTML
- [ ] Job `index_message`: avviato dopo ogni sync IMAP, estrae testo allegati e aggiorna vettore
- [ ] `search_service.py`: costruisce `tsquery` da input utente (parole AND, frasi con `""`, NOT con `-`)
- [ ] API `POST /search`: accetta `{ q, mailbox_ids, direction, date_from, date_to, has_attachments, labels, pec_type }`
- [ ] Risposta: `{ results: [...], total, highlights: { message_id: {field: snippet} } }`
- [ ] Frontend: pagina Search con barra + pannello filtri laterale collapsible
- [ ] Hook `useSearch`: debounce 400ms, history ultimi 10 termini (localStorage), highlight parole chiave nei risultati
- [ ] Test: ricerca "Multa" trova messaggi con "Multa" in oggetto, corpo E allegato PDF contenente "Multa"

**Definition of Done:**
- Ricerca restituisce risultati in < 500ms su dataset di 100.000 messaggi
- Evidenziazione (highlight) visibile nel frontend
- I filtri VBox si applicano anche alla ricerca

---

### Fase 5-C – Sistema Notifiche (in parallelo con 5-A e 5-B, 3 settimane)

**Obiettivo:** notifiche granulari su eventi PEC via webhook, email, Telegram e WhatsApp.

**Task:**
- [ ] Alembic migration per `notification_channels`, `notification_rules`, `notification_log`
- [ ] API `POST /notifications/channels` (crea canale, config cifrata a riposo)
- [ ] API `POST /notifications/channels/{id}/test` (invia notifica di test)
- [ ] API `POST /notifications/rules` (crea regola evento → canale + filtri)
- [ ] `notification_service.py`: al salvataggio di ogni messaggio, valuta regole applicabili e accoda job
- [ ] `worker/notifications/dispatcher.py`: smista per tipo canale
- [ ] `webhook.py`: POST JSON con header `X-PecFlow-Signature: sha256=<HMAC>` per verifica autenticità
- [ ] `email_smtp.py`: template HTML notifica (oggetto, mittente, link messaggio)
- [ ] `telegram.py`: messaggio Telegram con MarkdownV2, link deep al messaggio
- [ ] `whatsapp.py`: Meta Cloud API `POST /messages` con template pre-approvato (o freeform in 24h window)
- [ ] Retry: 3 tentativi per canale, log in `notification_log`
- [ ] Frontend: pagina Notifications – gestione canali (form per tipo) + lista regole
- [ ] Test integrazione: mock HTTP server per webhook, verifica HMAC; mock Telegram API

**Definition of Done:**
- Demo completa: nuovo messaggio PEC → webhook riceve POST in < 5 secondi
- Telegram e WhatsApp testati con account sandbox
- Log notifiche visibile in frontend (admin)

---

## 5. Sistemi Avanzati

---

### 5.1 Permessi Utente Granulari

#### Gerarchia ruoli

| Ruolo | Accesso caselle | Invio | Configurazione | Gestione utenti |
|---|---|---|---|---|
| `super_admin` | Tutti i tenant | ✓ | ✓ | ✓ |
| `admin` | Tutte le caselle del tenant | ✓ | ✓ | ✓ |
| `supervisor` | Caselle con permesso esplicito | ✓ | ✗ | ✗ |
| `operator` | Caselle con `can_read=TRUE` | Solo se `can_send=TRUE` | ✗ | ✗ |
| `readonly` | Caselle con `can_read=TRUE` | ✗ | ✗ | ✗ |

#### Logica applicata nel backend

```python
# permission_service.py

def get_visible_mailboxes(user: User, db: Session) -> list[UUID]:
    """Restituisce le caselle visibili all'utente."""
    if user.role in ('super_admin', 'admin'):
        return db.query(Mailbox.id).filter(Mailbox.tenant_id == user.tenant_id).all()
    return (
        db.query(MailboxPermission.mailbox_id)
        .filter(
            MailboxPermission.user_id == user.id,
            MailboxPermission.can_read == True,
        )
        .all()
    )

def check_can_send(user: User, mailbox_id: UUID, db: Session) -> bool:
    if user.role in ('super_admin', 'admin'):
        return True
    perm = db.query(MailboxPermission).filter_by(
        user_id=user.id, mailbox_id=mailbox_id
    ).first()
    return perm is not None and perm.can_send
```

#### Esempi di utilizzo reale

- **Studio legale**: paralegale `plegal` può leggere e inviare da `pec@studiorossi.it`, non da `amministrazione@studiorossi.it`
- **PA**: funzionario `fmarco` ha `can_read=TRUE`, `can_send=FALSE` → può leggere ma non rispondere
- **Call center**: tutti gli operatori hanno `can_read` su `info@azienda.it`, solo il team leader ha `can_send`

---

### 5.2 Virtual Box

#### Concetto

Una **Virtual Box** è una vista nominata e filtrata su un sottoinsieme di messaggi. Quando un utente ha una o più VBox assegnate, l'applicazione applica automaticamente i filtri a *tutte* le sue query messaggi, inclusa la ricerca avanzata.

Un utente può avere **più VBox** assegnate → vede l'**unione** (OR) dei messaggi che soddisfano almeno una VBox. Le regole *dentro* una singola VBox sono in **AND** tra loro.

#### Esempio concreto

**Scenario:** Comune di Esempio ha la casella `protocollo@comune.it`. Vuole che:
- L'operatore `pbianchi` veda solo le PEC con oggetto contenente "Multa" da `protocollo@comune.it`
- L'operatore `gverdi` veda solo le PEC da mittenti `@prefettura.it`

```
VBox "Multe - Protocollo"
  └── Regole:
        mailbox_id   = <protocollo@comune.it>
        subject_pattern = 'Multa'

VBox "Prefettura - Protocollo"
  └── Regole:
        mailbox_id   = <protocollo@comune.it>
        from_pattern = 'prefettura.it'

Assegnazioni:
  pbianchi → "Multe - Protocollo"
  gverdi   → "Prefettura - Protocollo"
```

#### Implementazione SQL query

```sql
-- Query messaggi per utente con VBox attiva
-- Generata da virtual_box_service.py

SELECT m.*
FROM messages m
WHERE m.tenant_id = :tenant_id
  AND m.mailbox_id IN (
      -- solo caselle con permesso can_read
      SELECT mailbox_id FROM mailbox_permissions
      WHERE user_id = :user_id AND can_read = TRUE
  )
  AND (
      -- OR tra le VBox assegnate
      (m.mailbox_id = :vbox1_mailbox AND m.subject ILIKE '%Multa%')
      OR
      (m.mailbox_id = :vbox2_mailbox AND m.from_address ILIKE '%prefettura.it%')
  )
ORDER BY m.received_at DESC;
```

#### API Reference

| Metodo | Endpoint | Descrizione |
|---|---|---|
| `POST` | `/virtual-boxes` | Crea VBox con regole iniziali |
| `GET` | `/virtual-boxes` | Lista VBox del tenant |
| `PUT` | `/virtual-boxes/{id}` | Aggiorna nome/descrizione |
| `PUT` | `/virtual-boxes/{id}/rules` | Sostituisce tutte le regole |
| `POST` | `/virtual-boxes/{id}/assignments` | Assegna utente |
| `DELETE` | `/virtual-boxes/{id}/assignments/{user_id}` | Rimuovi assegnazione |
| `GET` | `/virtual-boxes/my` | VBox dell'utente corrente |

---

### 5.3 Ricerca Avanzata

#### Campi ricercabili

| Campo | Tipo ricerca | Peso FTS | Note |
|---|---|---|---|
| `subject` | FTS + ILIKE | A (massimo) | |
| `from_address` | FTS + ILIKE | B | |
| `to_addresses` | FTS array | B | |
| `body_text` | FTS | C | |
| allegati `.extracted_text` | FTS | D (minimo) | dopo estrazione Tika |
| `received_at` | range date | — | filtro, non FTS |
| `state` | exact | — | filtro |
| `direction` | exact | — | filtro |
| `mailbox_id` | exact | — | filtro |
| label | join | — | filtro |

#### Sintassi query utente

```
multa comune          → entrambe le parole (AND implicito)
"avvenuta consegna"   → frase esatta
multa -cartella       → multa ma NOT cartella
comune OR prefettura  → OR esplicito
```

Il `search_service.py` traduce la query utente in `tsquery` PostgreSQL:
```python
# "multa comune" → to_tsquery('italian', 'multa & comune')
# '"avvenuta consegna"' → phraseto_tsquery('italian', 'avvenuta consegna')
```

#### API

```
POST /search
{
  "q": "multa comune",
  "mailbox_ids": ["uuid1", "uuid2"],     // opzionale
  "direction": "inbound",               // opzionale
  "date_from": "2026-01-01",            // opzionale
  "date_to": "2026-12-31",              // opzionale
  "has_attachments": true,              // opzionale
  "labels": ["uuid_label"],             // opzionale
  "pec_type": "posta_certificata",      // opzionale
  "page": 1,
  "page_size": 25
}

→ 200 OK
{
  "total": 142,
  "results": [
    {
      "id": "...",
      "subject": "Multa n. 12345/2026 del Comune di Roma",
      "from_address": "protocollo@comune.roma.it",
      "received_at": "2026-03-15T10:30:00Z",
      "highlights": {
        "subject": "...del <mark>Comune</mark> di Roma",
        "body_text": "...in merito alla <mark>multa</mark> stradale..."
      }
    }
  ]
}
```

#### Estrazione testo allegati

Il worker `text_extractor.py` chiama Apache Tika REST API:

```python
import httpx

async def extract_text(file_bytes: bytes, content_type: str) -> str | None:
    async with httpx.AsyncClient() as client:
        r = await client.put(
            "http://tika:9998/tika",
            content=file_bytes,
            headers={"Content-Type": content_type, "Accept": "text/plain"},
            timeout=30.0,
        )
    return r.text if r.status_code == 200 else None
```

Apache Tika è incluso nel `docker-compose.yml` come container `tika:latest` (porta 9998). Supporta PDF, DOCX, ODT, XLSX, HTML, XML, TXT e altri 1000+ formati.

---

### 5.4 Sistema di Notifiche Multi-canale

#### Architettura

```
Evento PEC (trigger: salvataggio nuovo messaggio o cambio stato)
    │
    ├── notification_service.evaluate_rules(event_type, message)
    │       ├── filtra regole per event_type
    │       ├── applica filtri (mailbox, from_pattern, subject_pattern)
    │       └── per ogni regola corrispondente:
    │               → accoda job dispatch_notification(rule_id, message_id)
    │
    └── [arq job queue Redis]
            │
            └── dispatcher.py
                    ├── carica canale + configurazione
                    └── chiama channel handler specifico:
                            ├── webhook.py
                            ├── email_smtp.py
                            ├── telegram.py
                            └── whatsapp.py
```

#### Canali supportati

**Webhook**
```json
POST https://your-endpoint.com/pecflow-hook
Headers:
  X-PecFlow-Event: message.received
  X-PecFlow-Signature: sha256=<HMAC-SHA256(secret, body)>
  Content-Type: application/json

Body:
{
  "event": "message.received",
  "tenant_id": "...",
  "message": {
    "id": "...",
    "subject": "PEC ricevuta da comune.roma.it",
    "from_address": "protocollo@comune.roma.it",
    "mailbox": "info@azienda.it",
    "received_at": "2026-03-18T14:00:00Z",
    "state": "received",
    "url": "https://app.pecflow.it/messages/..."
  }
}
```

**Email SMTP**
Notifica in HTML via template configurabile. Il mittente usa un relay SMTP dedicato (non la casella PEC del cliente). Supporta più destinatari nella config del canale.

**Telegram**
```
🔔 Nuova PEC ricevuta

📮 Casella: info@azienda.it
👤 Da: protocollo@comune.roma.it
📋 Oggetto: Convocazione riunione del 20/03/2026
🕐 Ricevuta: 18/03/2026 14:00

🔗 Visualizza: https://app.pecflow.it/messages/...
```

**WhatsApp (Meta Cloud API)**
Template pre-approvato Meta per notifiche al di fuori della finestra 24h. Per messaggi dentro la finestra 24h (es. risposta a notifica) si usa freeform. La configurazione del canale include `phone_number_id`, `access_token` (cifrato) e numero destinatario.

#### Configurazione canale via API

```json
POST /notifications/channels
{
  "name": "Webhook ERP Aziendale",
  "type": "webhook",
  "config": {
    "url": "https://erp.azienda.it/api/pec-webhook",
    "secret": "mysecretkey123"
  }
}

POST /notifications/channels
{
  "name": "Alert Telegram Team",
  "type": "telegram",
  "config": {
    "bot_token": "123456:ABC...",
    "chat_id": "-1001234567890"
  }
}

POST /notifications/rules
{
  "channel_id": "uuid-del-canale",
  "event_type": "message.received",
  "mailbox_id": "uuid-casella-info",    // opzionale
  "from_pattern": "comune.roma.it"      // opzionale
}
```

#### Test canale

```
POST /notifications/channels/{id}/test
→ invia una notifica di test con dati fittizi al canale
→ risponde con: { "status": "sent" | "failed", "error": "..." }
```

#### Gestione errori e retry

| Scenario | Comportamento |
|---|---|
| Endpoint webhook irraggiungibile | Retry 3x (5 min → 30 min → 2h), poi `failed` in `notification_log` |
| Telegram bot bloccato dall'utente | `failed` immediato, no retry, alert admin |
| WhatsApp fuori finestra 24h senza template | Usa template approvato o salta con log warning |
| Email SMTP rifiutata | Retry 3x, poi `failed` |
| Tutti i canali di una regola falliti | Alert WebSocket all'admin del tenant |
