# DD-036 — Notifiche email via Gmail, da un worker Docker sull'host di casa **Data:** 29 settembre 2026 **Stato:** Accettata **Codice:** `mailer/mailer-core.ts`, `mailer/mailer.mjs`, `src/lib/preferenze-utente.ts`, `supabase/migrations/20260929120000_m22_notifiche_email.sql` **Contesto** Le notifiche di CrAPP arrivano solo con la push (che dipende da Chrome vivo in background, vedi i limiti in `docs/modules/notifiche.md`) e nel centro notifiche in-app, che si vede solo aprendo l'app. Chi non ha attivato la push o ha un telefono aggressivo sul risparmio energetico non riceve nulla ad app chiusa. Serve un canale che arrivi comunque, **gratis**, ai giocatori registrati, che sono tutti su Gmail perché l'accesso è con Google. L'app gira su Vercel, che non esegue container né processi sempre attivi. **Decisione** Le email partono da un **worker Docker sull'host di casa** che legge una coda su Supabase e invia tramite `smtp.gmail.com` con un account Gmail dedicato e una password per app. - La coda è `notifiche_email_coda`, riempita da un trigger su `notifiche_utente`: ogni notifica già esistente, di qualunque tipo e per qualunque destinatario (admin compresi, con la stessa logica degli altri), diventa anche una mail. Le sorgenti di M17 non cambiano, non ne nascono di nuove e l'app non conosce le email. Il certificato in scadenza resta fuori: è una card in Home, non una notifica (DD-035). - Il worker fa solo polling **in uscita** con la service role: nessuna porta aperta sull'host. - La coda è una tabella a parte, con RLS senza policy, perché la policy `UPDATE` di `notifiche_utente` lascerebbe a un client la possibilità di rimettere in coda una mail. - Canale **opt-out**: acceso di default per tutti, con un interruttore per account in Profilo → Opzioni. La preferenza sta in una tabella nuova, `preferenze_utente`, una riga per account (non per slot); l'assenza di riga vale «acceso». - Specifica completa in [notifiche-email.md](../modules/notifiche-email.md); installazione e gestione in [WORKER_EMAIL.md](../WORKER_EMAIL.md). **Alternative scartate** - Inviare direttamente dal proprio IP con un MTA (Postfix) → scartata: porta 25 bloccata dai provider residenziali, IP nelle blocklist, niente reverse DNS; le mail finirebbero in spam o sarebbero rifiutate. - Relay Brevo o Resend → scartati per ora: il piano gratuito funziona ma per una buona consegna richiede un dominio con SPF/DKIM/DMARC, che oggi non c'è. Restano l'alternativa se servirà un mittente della squadra. - Supabase Edge Function con `pg_cron` e un relay → scartata su richiesta esplicita di un servizio dockerizzato sull'host; resta la strada senza host acceso, ma dipende comunque da un relay esterno. - Vercel Cron → scartato: sul piano gratuito gira al massimo una volta al giorno, troppo poco per i promemoria a 24h e 3h. - Chiamare l'host dall'app (webhook verso casa) → scartato: richiederebbe un IP pubblico o un tunnel e una superficie d'attacco in più; il polling in uscita non ne ha. - Colonne di stato sulla riga di `notifiche_utente` → scartate per la RLS spiegata sopra. **Conseguenze** - Costo zero, ma il canale email dipende da un host acceso: se è spento le mail arrivano in ritardo (mai perse). Consegna _almeno una volta_: in caso di crash tra invio e registrazione può arrivare un doppione. - Il tetto di circa 500 destinatari al giorno di Gmail è largo per una squadra, ma è un limite reale: con molte squadre o riepiloghi frequenti andrebbe rivisto. - Nuova migration (`m22_notifiche_email`), nuova cartella `mailer/` (worker bun con la sola dipendenza `nodemailer`, separato dall'app) e un segreto in più (la service role sull'host). L'app non guadagna nessuna dipendenza e le route di M17 non cambiano. - Il certificato in scadenza resta fuori dalle email: non è una notifica (DD-035), quindi non passa da `notifiche_utente`. Renderlo una notifica sarebbe una decisione a parte. - Il tetto giornaliero è una finestra mobile di 24 ore, `MAIL_LIMITE_GIORNO` (400 di default, sotto i circa 500 di Gmail). - Le email dei giocatori si usano per un secondo scopo oltre al login: da qui l'opt-out e l'indicazione in ogni mail su come disattivarlo. - Una tabella in più, `preferenze_utente`, pensata per accogliere in futuro anche le preferenze per tipo di notifica senza toccare `giocatori_squadra`. - In produzione dal 29/09/2026 (migration e worker su un Raspberry Pi). Dalla prova reale: la prima mail, da un account Gmail nuovo, è arrivata ma nella cartella spam, e la password per app non era disponibile finché l'account aveva le sole passkey come secondo passaggio (dettagli in [WORKER_EMAIL.md](../WORKER_EMAIL.md#account-gmail-e-password-per-app)). **Riesame** Se serve un mittente col dominio della squadra, se le mail finiscono in spam anche dopo qualche giorno di uso, se si supera il tetto giornaliero di Gmail, o se l'host di casa si rivela poco affidabile: passare a un relay con dominio e, in quel caso, valutare Edge Function più `pg_cron` al posto del container.