Cita con percorso completo i 28 file di src/ che nessun documento nominava e aggiunge la riga Codice a 21 decisioni, ricavata dai file che le citano. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
78 lines
5.0 KiB
Markdown
78 lines
5.0 KiB
Markdown
# 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.
|