SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-21

Casella email usa e getta su VPS con Mailpit

Crea una casella email usa e getta su un VPS con Mailpit: intercetta ogni messaggio di test, leggilo nel browser e impedisci invii accidentali ai clienti reali.

Che cos’è una casella email usa e getta

Una casella email usa e getta è un piccolo server SMTP (simple mail transfer protocol) che accetta i messaggi per qualsiasi indirizzo e non ne consegna nessuno. L’applicazione di staging invia i messaggi a questo server invece che a un provider email reale, e ogni messaggio si ferma lì. Puoi leggere i messaggi ricevuti in un’interfaccia web, quindi un elenco di destinatari errato o un template non valido non comporta costi, perché il messaggio non lascia mai la casella.

Questa guida crea una casella di questo tipo su un singolo VPS con Docker Compose. Mailpit è il sink catch-all. Il suo listener SMTP è associato a un indirizzo raggiungibile soltanto dall’applicazione; l’interfaccia web è pubblicata tramite nginx con transport layer security (TLS) e password; un limite di conservazione impedisce alla casella di riempire il disco. Se Compose è una novità, le basi di Compose per un VPS descrive la struttura dei file utilizzata da questa guida.

Il risultato è uno strumento di test, non un server email. Non gestisce account, non effettua consegne e non applica filtri antispam. Le caselle reali per persone reali richiedono un server email completo come Mailcow e un lavoro molto più ampio.

Mailpit vs Inbucket vs MailHog: quale sink eseguire

Tre strumenti svolgono questo compito. Le differenze riguardano lo stato della manutenzione, le porte su cui restano in ascolto e le operazioni che possono eseguire su un messaggio dopo averlo accettato. Le versioni riportate di seguito sono state verificate ad agosto 2026.

MailHog (mailhog/mailhog) resta in ascolto sulla porta 1025 per SMTP e rende disponibile la propria interfaccia sulla porta 8025. Funziona ancora. Il branch predefinito non riceve commit da agosto 2022 e il tracker contiene più di 250 issue aperte; di conseguenza, nel percorso di test verrebbero eseguite dipendenze senza patch. Non usatelo per nuovi progetti.

Inbucket (inbucket/inbucket) resta in ascolto sulla porta 2500 per SMTP, sulla porta 9000 per l'interfaccia Web e sulla porta 1100 per POP3 (post office protocol version 3). La versione 3.1.1 è stata rilasciata a dicembre 2025. Salva i messaggi come file in /storage e li elimina automaticamente secondo una policy di pruning: l'immagine imposta INBUCKET_STORAGE_RETENTIONPERIOD=72h e INBUCKET_STORAGE_MAILBOXMSGCAP=300. Sceglietelo quando un test deve raccogliere la posta tramite una libreria client POP3 invece di una chiamata HTTP.

Mailpit (axllent/mailpit) usa le stesse porte di MailHog, 1025 e 8025, quindi lo sostituisce senza modificare la configurazione dell'applicazione. La versione 1.30.7 è stata rilasciata l'8 agosto 2026. Include nel binario le funzioni necessarie a questa guida: un file delle password per l'interfaccia Web e l'API (application programming interface), un limite al numero di messaggi, un limite di età e un filtro per i destinatari. Il resto della guida esegue Mailpit.

Come funziona il catch-all e perché il DNS non è coinvolto

In questo caso l'applicazione non cerca dove effettuare la consegna. Le si forniscono un host e una porta, apre una connessione TCP e annuncia RCPT TO:<anyone@example.test>. Mailpit accetta il destinatario indicato, memorizza il messaggio e non inoltra nulla. Il dominio non viene mai risolto, quindi example.test funziona anche se .test è un nome riservato che non esiste nel Domain Name System (DNS).

Questo è l'intero meccanismo e spiega perché la posta in arrivo è protetta per impostazione predefinita. Non viene utilizzato alcun record MX (mail exchanger), non viene tentata alcuna consegna e nessun messaggio può raggiungere una persona reale.

Punta l'applicazione di staging al sink

Imposta l'host SMTP dell'applicazione su mailpit quando l'applicazione viene eseguita come container nello stesso progetto Compose, oppure su 127.0.0.1 quando viene eseguita sull'host. Imposta la porta su 1025, disabilita TLS e lascia vuoti il nome utente e la password. Mailpit accetta messaggi di posta anonimi.

Alcuni framework rifiutano di inviare messaggi senza credenziali. MP_SMTP_AUTH_ACCEPT_ANY=1 fa sì che Mailpit accetti qualsiasi nome utente e password, mentre MP_SMTP_AUTH_ALLOW_INSECURE=1 abilita i meccanismi PLAIN e LOGIN su una connessione non crittografata. Queste due impostazioni sono sicure in questo caso solo perché il listener non è raggiungibile da Internet, come garantisce la configurazione di deployment riportata di seguito.

È consigliabile impostare MP_SMTP_ALLOWED_RECIPIENTS fin dal primo giorno. Accetta un'espressione regolare e rifiuta ogni destinatario che non corrisponde a tale espressione. Indicala per il tuo dominio di test: se un database di staging contiene ancora un indirizzo reale di un cliente, nel log dell'applicazione comparirà un errore evidente invece di un messaggio che finisce silenziosamente nel sink.

Il file Docker Compose

Creare prima la directory e un file delle password per l’interfaccia web. htpasswd -B scrive un hash bcrypt e Mailpit supporta sia bcrypt sia il testo in chiaro.

mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qa

Scrivere compose.yaml:

services:
  mailpit:
    image: axllent/mailpit:v1.30
    container_name: mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"
      - "127.0.0.1:1025:1025"
    volumes:
      - ./data:/data
    environment:
      MP_DATABASE: /data/mailpit.db
      MP_MAX_MESSAGES: 2000
      MP_MAX_AGE: 14d
      MP_UI_AUTH_FILE: /data/ui-auth
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
      MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'

Il doppio simbolo del dollaro non è un errore di battitura. Compose interpreta un singolo $ come l’inizio di una variabile da espandere. Per passare un solo simbolo del dollaro letterale al container, usare quindi $$. L’espressione regolare arriva a Mailpit come @example\.test$.

Avviarlo e controllare lo stato di salute:

docker compose up -d
docker compose ps

La colonna STATUS dovrebbe riportare Up ... (healthy). L’immagine include un proprio healthcheck, che esegue /mailpit readyz ogni 15 secondi. Un container che rimane nello stato starting o passa a unhealthy non sta fornendo il servizio sulla porta 8025 all’interno del container. Consultare docker compose logs mailpit prima di modificare altro.

Entrambe le porte pubblicate includono un indirizzo, che determina il controllo di sicurezza. All’interno del container Mailpit è in ascolto su 0.0.0.0, ed è corretto perché il container dispone di un proprio spazio dei nomi di rete. Il lato sinistro della mappatura determina chi può raggiungere il servizio dall’esterno. Scrivere 8025:8025 fa sì che Docker esegua il binding su ogni indirizzo dell’host, compreso quello pubblico.

Se l’applicazione di staging è un servizio definito nello stesso file, eliminare completamente la mappatura 1025 e configurare l’applicazione per usare il nome host mailpit sulla porta 1025. I container collegati alla stessa rete Compose comunicano direttamente tra loro, quindi la porta SMTP non passa mai dall’host. Come le reti Compose risolvono i nomi dei servizi descrive questa risoluzione.

Invia un messaggio e verifica che sia stato recapitato

python3 - <<'EOF'
import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
EOF

In caso di successo, lo script non visualizza alcun output. Verifica tramite API che il messaggio sia stato memorizzato:

curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messages

Il comando restituisce un JSON con l’elenco dei messaggi memorizzati. Se rimuovi il flag -u, la stessa richiesta viene rifiutata, perché MP_UI_AUTH_FILE protegge sia l’API sia l’interfaccia web. Qualsiasi test che legga la posta in arrivo deve inviare anche queste credenziali.

Un ConnectionRefusedError dallo script Python indica che nessun processo è in ascolto su 127.0.0.1:1025. Questo è il risultato previsto se hai rimosso il mapping SMTP; in tal caso, il controllo deve essere eseguito da un container nella stessa rete Compose.

Pubblicare l'interfaccia web tramite nginx con una password

L'interfaccia risponde attualmente solo sull'indirizzo di loopback. nginx termina TLS e richiede una password prima che una richiesta possa raggiungerla.

sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qa
server {
    listen 443 ssl;
    server_name mail-test.example.com;

    ssl_certificate     /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;

    auth_basic           "mailpit";
    auth_basic_user_file /etc/nginx/mailpit.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:8025;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Ricaricare la configurazione dopo averne verificato la sintassi con sudo nginx -t && sudo systemctl reload nginx. Cosa fa ogni direttiva in un blocco di reverse proxy merita una lettura se è la prima volta che si configura un proxy.

Usare lo stesso nome utente e la stessa password nel file di nginx e in data/ui-auth. nginx inoltra a monte l'header Authorization del browser, quindi credenziali corrispondenti soddisfano entrambi i controlli con una sola richiesta. Con credenziali diverse, il browser conserva un set che il secondo controllo rifiuta.

Gli header Upgrade e Connection non sono decorativi. Mailpit invia i nuovi messaggi a una pagina aperta tramite WebSocket e un proxy che esegue HTTP/1.1 senza questi header non può eseguire l'upgrade della connessione. La pagina viene quindi caricata correttamente, ma non si aggiorna: il messaggio arriva, l'API lo mostra, mentre l'elenco rimane invariato finché non si ricarica la pagina.

Mantenere entrambe le protezioni. La password di nginx protegge l'indirizzo pubblico, mentre MP_UI_AUTH_FILE protegge direttamente la porta 8025. Questo è importante perché nell'interfaccia sono leggibili tutti i link per la reimpostazione delle password generati dall'applicazione di staging.

Non trasformare mai il sink in un open relay

Un open relay è un server SMTP che riceve un messaggio da chiunque e lo inoltra verso qualsiasi destinazione. Gli spammer li cercano continuamente. Trovarne uno sul proprio indirizzo IP porta a segnalazioni di abuso e alla sospensione dell'account.

Mailpit non è un open relay per impostazione predefinita, perché non inoltra mai i messaggi. Il relay resta disattivato finché non si indica a MP_SMTP_RELAY_CONFIG un file di configurazione del relay. Anche l'azione di rilascio nell'interfaccia non produce alcun effetto fino a quel momento. Lasciare questa impostazione non definita è una scelta intenzionale.

Esistono due modi per perdere questa proprietà. Si configura un relay affinché il pulsante di rilascio funzioni e poi si espone la porta SMTP a Internet: in questo modo si crea un open relay funzionante. Se si espone la porta senza configurare un relay, gli utenti esterni non possono inviare messaggi attraverso il server, ma possono riempire lo spazio di archiviazione e inserire contenuti nell'interfaccia considerata affidabile dal team.

Su un host Docker, il problema è il firewall. La pubblicazione di una porta fa sì che Docker aggiunga le proprie regole alla tabella nat. Il traffico destinato al container viene quindi intercettato in quella fase, prima che le regole di ufw (uncomplicated firewall) possano intervenire. sudo ufw deny 1025/tcp segnala un esito positivo, ma non modifica nulla. Perché Docker pubblica le porte bypassando direttamente ufw descrive l'ordine della catena.

La correzione riguarda l'indirizzo nella mappatura, non una regola del firewall. Verificare quale indirizzo è effettivamente associato:

sudo ss -ltnp | grep -E ':(1025|8025)'

Un output corretto mostra 127.0.0.1:1025 e 127.0.0.1:8025. Una riga con 0.0.0.0:1025 indica che la mappatura ha perso il proprio indirizzo e che il sink è in ascolto su Internet. Da un altro computer, nc -vz mail-test.example.com 1025 dovrebbe andare in timeout o essere rifiutato.

Quando l'applicazione si trova su un server diverso, non aprire la porta 1025 per collegare i due sistemi. Inserire entrambi i computer in una rete privata o in un tunnel VPN e associare la mappatura all'indirizzo dell'interfaccia utilizzata.

Pubblica i record MX solo se vuoi ricevere posta reale

Un record MX (mail exchanger) indica agli altri server di posta quale host accetta i messaggi destinati a un dominio. Se il dominio usa e getta non ha un record MX, da Internet non può arrivare alcun messaggio, perché i server mittenti non hanno una destinazione a cui consegnarlo. La casella contiene soltanto i messaggi inviati dalle tue applicazioni, che è proprio lo scopo di una casella di test.

Ricevere posta reale richiede un record MX che punti al server, Mailpit in ascolto sulla porta 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) e la porta aperta. Da quel momento gestisci un catch-all pubblico per ogni indirizzo del dominio. È importante sapere cosa comporta.

  • Lo spam inizia entro pochi giorni dalla pubblicazione del record, perché i bot di raccolta leggono il DNS. Gli attacchi a dizionario passano poi in rassegna i nomi più comuni e archiviano un messaggio per ogni tentativo.
  • Gli allegati provenienti da mittenti sconosciuti arrivano sul disco e vi rimangono. Nulla li filtra, quindi un archivio inviato da un mittente sconosciuto si trova accanto alla tua posta di test.
  • Chiunque venga a conoscenza del dominio può registrarsi a servizi di terze parti usando un indirizzo del dominio, e il messaggio di conferma viene consegnato al tuo server. Se la protezione tramite password dovesse venire meno, quegli account appartengono a chiunque legga la casella.
  • I limiti di conservazione non sono più una semplice attività di manutenzione, ma diventano essenziali per il carico, perché il volume non è più sotto il tuo controllo.

Se ti serve ricevere posta reale per verificare la recapitabilità, assegnale un sottodominio dedicato, mantieni MP_MAX_AGE breve e considera pubblico tutto ciò che contiene. Se ti servono caselle su cui le persone fanno affidamento, usa invece un vero mail server con filtri e backup.

Conservazione: come un catch-all senza limiti riempie il disco

Per impostazione predefinita, Mailpit conserva 500 messaggi ed elimina periodicamente quelli più vecchi oltre tale limite. MP_MAX_MESSAGES: 0 disattiva completamente l’eliminazione automatica, ed è proprio questa modifica che consente a un catch-all di riempire il disco senza che nessuno se ne accorga. MP_MAX_AGE aggiunge un limite temporale, espresso in ore o giorni, come 36h o 14d.

MP_DATABASE determina se questi dati vengono conservati. Senza questa impostazione, Mailpit scrive in un file temporaneo che viene eliminato quando il processo termina; di conseguenza, ogni riavvio svuota la casella. Con questa impostazione, i messaggi sopravvivono ai riavvii e il file cresce.

Sono gli allegati a consumare spazio. Un job notturno che invia un report PDF da 2 MB a 300 indirizzi di test genera 600 MB per notte, e un semplice limite al numero di messaggi non reagirà abbastanza rapidamente. Considerate questa crescita nel calcolo dello spazio disponibile sul volume, soprattutto se sullo stesso volume è presente un vicino con molti contenuti multimediali, come PhotoPrism o Immich, che potrebbe avere già occupato gran parte del disco di un VPS di piccole dimensioni.

du -h ~/mailpit/data/mailpit.db
df -h /

Svuotate lo store tra un’esecuzione CI e l’altra, invece di attendere l’attivazione del limite:

curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages

Inbucket gestisce lo stesso problema con INBUCKET_STORAGE_RETENTIONPERIOD (72h nell’immagine) e INBUCKET_STORAGE_MAILBOXMSGCAP (300). Qualunque soluzione utilizziate, impostate il limite prima che la prima suite di test inizi a utilizzarla.

Lettura della posta in arrivo dalla suite di test

GET /api/v1/messages elenca gli elementi archiviati, GET /api/v1/message/{ID} restituisce un messaggio con le relative parti e intestazioni, GET /api/v1/search applica filtri e DELETE /api/v1/messages svuota l'archivio. La documentazione interattiva per la versione in esecuzione è disponibile all'indirizzo http://127.0.0.1:8025/api/v1/.

Un test utile invia un messaggio, esegue il polling finché il messaggio non compare, verifica l'oggetto e il link al suo interno, quindi elimina tutto. Eseguite il polling in un breve ciclo di tentativi invece di inviare una sola richiesta, perché un'applicazione che accoda la posta in un worker in background restituisce il controllo dalla chiamata di invio prima che Mailpit abbia ricevuto il messaggio. Lo stesso schema viene utilizzato negli strumenti self-hosted per test e mocking delle API, che in genere costituiscono l'altra componente di un ambiente di staging che non interagisce mai con la produzione.

FAQ

Una casella email usa e getta self-hosted è un open relay?

Non finché l'inoltro rimane disabilitato. Mailpit archivia i messaggi e non li inoltra finché non configuri MP_SMTP_RELAY_CONFIG con le impostazioni di un relay. Di conseguenza, un estraneo che raggiunge la porta 1025 non può inviare email attraverso il server. Può comunque riempire lo spazio di archiviazione, quindi associa la porta SMTP a un indirizzo raggiungibile soltanto dalla tua applicazione. In Compose, pubblicarla come 1025:1025 associa la porta a tutti gli indirizzi del host e sudo ufw deny 1025/tcp non la chiude, perché le regole NAT di Docker vengono valutate prima.

Mi serve un record MX per il dominio di test?

Solo se vuoi ricevere email da Internet. Senza un record MX, i server mittenti non hanno una destinazione per la consegna, quindi la casella contiene soltanto i messaggi inviati dalle tue applicazioni tramite SMTP. Se pubblichi il record e apri la porta 25, stai gestendo un catch-all pubblico: spam entro pochi giorni, attacchi dictionary che archiviano un messaggio per ogni tentativo e allegati inviati da estranei sul disco, senza alcun filtro.

Perché l'elenco dei messaggi si aggiorna soltanto quando ricarico la pagina?

Mailpit invia i nuovi messaggi alla pagina aperta tramite WebSocket. Un blocco location di nginx privo di proxy_http_version 1.1 e degli header Upgrade e Connection non può effettuare l'upgrade della connessione. La pagina viene quindi caricata normalmente, ma poi smette di aggiornarsi. I messaggi continuano ad arrivare e l'API continua a restituirli. Per questo la casella sembra non aggiornata, non non funzionante. Aggiungi quelle righe, ricarica nginx e poi ricarica la pagina.

Come posso evitare che la casella riempia il disco?

Mantieni MP_MAX_MESSAGES impostato su un valore reale e aggiungi MP_MAX_AGE. Il limite predefinito è di 500 messaggi. Impostandolo su 0 disabiliti completamente l'eliminazione. È così che un catch-all con allegati può crescere silenziosamente. MP_MAX_AGE accetta ore o giorni, ad esempio 36h o 14d. Svuota l'archivio durante la fase di teardown della CI con curl -X DELETE http://127.0.0.1:8025/api/v1/messages. Inbucket svolge la stessa funzione con INBUCKET_STORAGE_RETENTIONPERIOD (72h) e INBUCKET_STORAGE_MAILBOXMSGCAP (300).

Devo usare Mailpit, Inbucket o MailHog?

Scegli Mailpit per i nuovi progetti, ad agosto 2026. MailHog continua a funzionare, ma il suo branch predefinito non riceve commit da agosto 2022 e quindi distribuisce dipendenze senza patch. Inbucket è mantenuto attivamente (3.1.1, dicembre 2025) ed è la scelta migliore quando un test richiede POP3, perché il server POP3 di Mailpit si avvia soltanto dopo aver fornito un file delle password. Mailpit usa le stesse porte di MailHog, 1025 e 8025, quindi sostituire MailHog richiede di cambiare soltanto il nome dell'immagine nel file Compose.