Email transazionali di Laravel che finiscono in spam o non arrivano: diagnosi e fix su VPS unmanaged
A giugno 2025 ho ricevuto una segnalazione da un cliente che gestisce un e-commerce B2B di prodotti chimici industriali su un VPS Hetzner CPX41 (8 vCPU, 16 GB RAM, 240 GB SSD). L'applicazione Laravel 10 inviava circa 800 email transazionali al giorno: conferme d'ordine, notifiche di spedizione, fatture in PDF, alert di scadenza contratti. Il tutto passava per un'istanza Postfix locale configurata tre anni prima dallo sviluppatore originale - un MAIL_MAILER=smtp con MAIL_HOST=localhost nel .env, e un Postfix con la configurazione di default generata da dpkg-reconfigure. Il titolare mi ha chiamato perché un cliente importante gli aveva detto: "Non ricevo più le conferme d'ordine da tre settimane." Non una, non due - tre settimane di email perse nel silenzio.
La diagnosi ha rivelato un quadro che trovo in almeno la metà dei VPS che prendo in carico per PMI: nessun record SPF nel DNS, nessuna firma DKIM, nessuna policy DMARC, e l'IP del VPS inserito in due blacklist (Spamhaus ZEN e Barracuda BRBL) - probabilmente a causa di un invio massivo di email di marketing che il titolare aveva fatto sei mesi prima dallo stesso IP, senza segmentare il traffico transazionale da quello promozionale. Il risultato: il 40% delle email non superava i filtri di Gmail, il 15% veniva rifiutato silenziosamente da Outlook/Microsoft 365, e il restante 45% arrivava - ma con un ritardo medio di 4-6 ore perché i server destinatari applicavano greylisting a un mittente senza autenticazione.
Perché le email della tua applicazione Laravel finiscono in spam anche se il server funziona?
Il problema non è quasi mai Laravel, e quasi mai il server SMTP. Il problema è l'autenticazione del dominio mittente. Dal febbraio 2024, Google e Yahoo richiedono esplicitamente SPF, DKIM e DMARC per tutti i mittenti che inviano più di 5.000 email al giorno - ma nella pratica, anche chi ne invia 50 al giorno viene penalizzato se manca l'autenticazione. Microsoft 365 ha requisiti analoghi. SPF, DKIM e DMARC non sono più "nice to have" dal 2025 - sono il requisito minimo per non finire in spam.
SPF (Sender Policy Framework) dichiara nel DNS quali server sono autorizzati a inviare email per il tuo dominio. Senza SPF, il server destinatario non ha modo di verificare se l'email proviene da un mittente legittimo o da un server compromesso che sta abusando del tuo dominio.
DKIM (DomainKeys Identified Mail) firma crittograficamente ogni email con una chiave privata presente sul server mittente. Il destinatario verifica la firma usando la chiave pubblica pubblicata nel DNS. Se la firma non corrisponde - o non esiste - l'email perde punti di reputazione.
DMARC (Domain-based Message Authentication, Reporting and Conformance) è la policy che dice ai server destinatari cosa fare quando SPF o DKIM falliscono: niente (none), quarantena (quarantine) o rifiuto (reject). Senza DMARC, ogni server destinatario decide autonomamente - e la tendenza nel 2025-2026 è rifiutare tutto ciò che non è autenticato.
Sul VPS del cliente, nessuno dei tre era configurato. Il DNS del dominio aveva un record MX che puntava a Google Workspace (per la posta aziendale), ma nessun record TXT per SPF, nessun selettore DKIM, nessuna policy DMARC. Per Google, le email che uscivano dal VPS erano indistinguibili dallo spam.
Stai cercando un Consulente Informatico esperto per risolvere problemi di deliverability email sulla tua applicazione Laravel? Nel mio profilo professionale trovi l'esperienza concreta su configurazione SMTP, autenticazione del dominio e gestione email transazionali su VPS Hetzner, OVH, Contabo e Digital Ocean.
Diagnosi: i quattro check che faccio nei primi trenta minuti
Prima di toccare qualunque configurazione, raccolgo i dati. Quattro comandi che danno un quadro completo dello stato dell'invio email:
# 1. Stato di Postfix e coda email
systemctl is-active postfix && postqueue -p | tail -5
# 2. Ultimi errori di invio (bounce, reject, timeout)
grep -E 'bounced|reject|timeout|refused' /var/log/mail.log | tail -20
# 3. Verifica record DNS di autenticazione dall'esterno
dig TXT azienda.it +short # SPF
dig TXT default._domainkey.azienda.it +short # DKIM
dig TXT _dmarc.azienda.it +short # DMARC
# 4. Verifica blacklist dell'IP del server
# (usando un servizio come mxtoolbox.com/blacklists.aspx dall'esterno)Sul VPS del cliente, il primo comando mostrava 347 email in coda - bloccate da 4 giorni perché i server destinatari le rifiutavano con 550 5.7.1 Message rejected due to failed DMARC policy. Il secondo comando mostrava un pattern ripetitivo di Connection timed out verso gli MX di Microsoft - l'IP era in blacklist e i server Microsoft semplicemente chiudevano la connessione. Il terzo comando confermava l'assenza totale di record di autenticazione. Il quarto ha rivelato le due blacklist.
Il test che chiude la diagnosi è l'invio di un'email di prova da riga di comando - non da Laravel, ma direttamente da Postfix - per isolare se il problema è nell'applicazione o nel server:
# Test diretto Postfix (bypassa Laravel completamente)
echo "Test deliverability $(date)" | mail -s "Test SMTP diretto" [email protected]
# Dopo 30 secondi, verifica il log di invio
grep "[email protected]" /var/log/mail.log | tail -5Se il log mostra status=sent, Postfix funziona e il problema è nella configurazione Laravel o nella reputazione del mittente. Se mostra status=bounced o status=deferred, il problema è a livello di rete o di reputazione dell'IP. Sul VPS del cliente, il test diretto mostrava status=deferred con Connection timed out - conferma che il problema era l'IP in blacklist, non l'applicazione.
L'errore che vedo fare più spesso a questo punto è "aggiustiamo Postfix". Non è Postfix il problema. Postfix fa esattamente quello che gli hai chiesto: prende l'email da Laravel e prova a consegnarla. Il problema è che il mondo esterno non si fida del tuo server perché non hai detto al mondo chi sei. La soluzione è su due livelli: autenticazione del dominio (DNS) e - nella maggior parte dei casi per le PMI - migrazione a un provider di email transazionali.
La soluzione: autenticazione DNS e provider transazionale
Autenticazione DNS: il minimo indispensabile
I tre record DNS che ho aggiunto al dominio del cliente:
; SPF: autorizza il VPS Hetzner e Google Workspace
azienda.it. IN TXT "v=spf1 ip4:XXX.XXX.XXX.XXX include:_spf.google.com ~all"
; DKIM: selettore generato da Postfix con opendkim
default._domainkey.azienda.it. IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBg..."
; DMARC: policy iniziale none (monitoring), poi quarantine dopo 30 giorni
_dmarc.azienda.it. IN TXT "v=DMARC1; p=none; rua=mailto:[email protected]; pct=100"Il rollout sicuro di DMARC raccomandato dagli esperti di deliverability è in tre fasi: p=none (solo monitoring, nessun impatto) per 30 giorni, poi p=quarantine (le email sospette vanno in spam) per altri 30 giorni, e infine p=reject (le email non autenticate vengono rifiutate). Saltare direttamente a reject prima di aver analizzato i report DMARC è un errore: potresti scoprire che ci sono servizi legittimi (CRM, newsletter tool, sistema di ticketing) che inviano email per il tuo dominio senza che tu lo sappia - e bloccaresti anche quelli.
Perché Postfix locale non basta (e quando invece va bene)
Dopo aver aggiunto l'autenticazione DNS e configurato OpenDKIM su Postfix, le email avrebbero iniziato ad arrivare. Ma con l'IP in due blacklist, ci sarebbero volute settimane prima che la reputazione si ristabilisse - e nel frattempo le email transazionali (conferme d'ordine, fatture) continuavano a non arrivare. Per il cliente, settimane di attesa non erano un'opzione.
La soluzione più pragmatica per le PMI con volumi medio-bassi (sotto le 10.000 email al giorno) è delegare l'invio a un provider di email transazionali - Amazon SES, Postmark, Mailgun, SendGrid. Questi provider hanno IP con reputazione consolidata, SPF e DKIM preconfigurati, dashboard di deliverability, e gestione automatica dei bounce. Il costo per 800 email al giorno è nell'ordine di 10-30 euro al mese - trascurabile rispetto al costo di un ordine perso perché la conferma non è arrivata.
Sul cliente ho configurato Amazon SES perché il progetto usava già AWS per lo storage S3. La configurazione in Laravel è un cambio nel .env e l'installazione di un pacchetto:
composer require aws/aws-sdk-phpMAIL_MAILER=ses
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-west-1
[email protected]
MAIL_FROM_NAME="Azienda Chimica Srl"In 15 minuti dall'attivazione, le email transazionali venivano consegnate in meno di 3 secondi - contro le 4-6 ore di ritardo di Postfix con greylisting. La dashboard SES mostrava un delivery rate del 99,7% nelle prime 24 ore.
Postfix locale resta la scelta giusta in due scenari: email di sistema (cron, logwatch, alert) che vanno solo a indirizzi interni, e ambienti dove i dati non possono uscire dalla rete aziendale per requisiti di compliance. Per tutto il resto - email transazionali verso clienti, fornitori, utenti - un provider dedicato è la scelta ingegneristicamente corretta nel 2025.
Code asincrone: le email non devono mai bloccare la request
L'ultimo intervento sul progetto del cliente è stato spostare tutte le email dalla modalità sincrona alla coda asincrona. Nel codice originale, le email venivano inviate direttamente nel ciclo request-response del controller - il che significava che se il server SMTP impiegava 2 secondi a rispondere (o andava in timeout), l'utente aspettava 2 secondi (o vedeva un errore 500) prima di ricevere la conferma d'ordine. Con le code, l'email viene serializzata in Redis e processata in background da un worker dedicato:
// Prima: sincrono, blocca la request
Mail::to($order->customer)->send(new OrderConfirmation($order));
// Dopo: asincrono, ritorna in millisecondi
Mail::to($order->customer)->queue(new OrderConfirmation($order));La differenza per l'utente: il checkout impiega 200 ms anziché 2.200 ms. La differenza per l'affidabilità: se SES è temporaneamente irraggiungibile, il job viene riprovato automaticamente con backoff esponenziale anziché fallire con un errore 500. E la differenza per il monitoring: i job falliti finiscono nella tabella failed_jobs dove puoi vederli, analizzarli e ritentarli - mentre un'email sincrona che fallisce durante la request svanisce nel nulla, a meno che non hai un error handler che la cattura e la logga esplicitamente.
Per prevenire l'invio doppio - un rischio reale quando i job vengono riprovati automaticamente - la soluzione è aggiungere un campo confirmation_sent_at sul model Order e controllarlo prima di inviare:
// Nel job OrderConfirmation::handle()
public function handle(): void
{
if ($this->order->confirmation_sent_at !== null) {
return; // Già inviata, skip silenzioso
}
Mail::to($this->order->customer)->send(new OrderConfirmationMail($this->order));
$this->order->update(['confirmation_sent_at' => now()]);
}Questo pattern di idempotenza è banale da implementare ma vitale in produzione: senza di esso, un retry dopo un timeout di rete può generare email duplicate - e un cliente che riceve due conferme d'ordine identiche perde fiducia nel sistema. Per chi gestisce applicazioni Laravel con volumi significativi di email transazionali, la guida completa alla progettazione dell'email delivery in Laravel copre in dettaglio Mailable, code, gestione dei fallimenti e template accessibili.
Un ultimo punto che trovo sistematicamente ignorato: la separazione tra email transazionali e email di marketing. Se invii newsletter, promozioni e email di marketing dallo stesso dominio e dallo stesso IP delle conferme d'ordine, stai mettendo a rischio la deliverability delle email che il tuo business non può permettersi di perdere. Un utente che segnala come spam la tua newsletter abbassa la reputazione del mittente - e la prossima conferma d'ordine finisce in spam insieme alla newsletter. La regola è semplice: transazionale e marketing su flussi separati, idealmente su sottodomini separati ([email protected] vs [email protected]). In Laravel, questo si implementa configurando due mailer distinti in config/mail.php - uno per SES (transazionale) e uno per Mailgun o SendGrid (marketing) - e usando il metodo mailer() sulla facade Mail per scegliere quale usare:
// Email transazionale: usa SES (alta reputazione, bassa latenza)
Mail::mailer('ses')->to($customer)->queue(new OrderConfirmation($order));
// Email marketing: usa Mailgun (gestione bounce/unsubscribe integrata)
Mail::mailer('mailgun')->to($subscriber)->queue(new WeeklyNewsletter($content));Questa separazione non è solo una best practice di deliverability - è un requisito implicito della normativa anti-spam. Se un utente si disiscrive dalla newsletter e continua a ricevere email dallo stesso mittente (perché le transazionali usano lo stesso flusso), hai un problema di compliance oltre che di reputazione. Per la configurazione di Postfix su Debian per le notifiche di sistema - l'altra metà del problema email su un VPS - ho scritto una guida dedicata alla configurazione di Postfix su Debian e Ubuntu per notifiche email su VPS.
Il risultato sul cliente: da un delivery rate sconosciuto (nessuno lo misurava) con il 40% in spam e il 15% non consegnato, a un delivery rate del 99,7% misurato su dashboard SES, con tempo di consegna medio di 2,8 secondi. Il costo mensile di SES per 800 email al giorno: 4 euro. Il costo del problema che ha risolto - tre settimane di conferme d'ordine non ricevute da un cliente chiave - non quantificabile, ma il titolare ha stimato circa 15.000 euro di ordini ritardati o persi nel periodo. Contattami se le email della tua applicazione Laravel non arrivano o finiscono in spam: in una giornata diagnostico il problema, configuro l'autenticazione DNS, migro a un provider transazionale affidabile e imposto il monitoring per sapere in tempo reale quando qualcosa non funziona.