SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor

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 questa funzione. Le differenze riguardano lo stato della manutenzione, le porte su cui sono in ascolto e le operazioni possibili sui messaggi dopo l'accettazione. Le versioni riportate di seguito sono state verificate a ottobre 2026.

MailHog (mailhog/mailhog) è in ascolto sulla porta 1025 per SMTP e rende disponibile l'interfaccia sulla porta 8025. Funziona ancora. Il branch predefinito non riceve commit da agosto 2022 e il tracker contiene più di 200 issue aperte; nel percorso di test useresti quindi dipendenze non aggiornate con patch. Non avviare nuovi lavori su questo progetto.

Inbucket (inbucket/inbucket) è in ascolto sulla porta 2500 per SMTP, sulla porta 9000 per l'interfaccia web e sulla porta 1100 per POP3 (Post Office Protocol versione 3). La versione 3.2.0 è stata rilasciata il 10 ottobre 2026. Archivia i messaggi come file in /storage e li elimina automaticamente: l'immagine imposta INBUCKET_STORAGE_RETENTIONPERIOD=72h e INBUCKET_STORAGE_MAILBOXMSGCAP=300. Sceglilo quando un test deve raccogliere la posta con 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.31.5 è stata rilasciata l'11 ottobre 2026; il ramo 1.30 si è fermato alla versione 1.30.7 ad agosto. Include nel binario tutto ciò che serve 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 all'età dei messaggi 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

Create 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

Scrivi compose.yaml:

services:
  mailpit:
    image: axllent/mailpit:v1.31
    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. Compose interpreta un singolo $ come l'inizio di una variabile da espandere, quindi $$ è il modo per passare un singolo dollaro letterale al container. L'espressione regolare arriva a Mailpit come @example\.test$.

Avvialo e controlla 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, quindi un container che rimane nello stato starting o passa a unhealthy di solito non sta fornendo il servizio sulla porta 8025 all'interno del container. Il controllo legge le impostazioni dalle variabili d'ambiente MP_, ma non dai flag della riga di comando. Questo è uno dei motivi per cui questa guida imposta tutto sotto environment:. Se passi le opzioni come flag, il controllo può riportare unhealthy mentre Mailpit funziona normalmente. Leggi docker compose logs mailpit prima di modificare altro.

Entrambe le porte pubblicate includono un indirizzo, che costituisce il controllo di sicurezza. All'interno del container Mailpit è in ascolto su 0.0.0.0, e questo va bene perché il container dispone di un proprio spazio dei nomi di rete. Il lato sinistro della mappatura determina chi può raggiungerlo dall'esterno. Scrivi 8025:8025 e Docker associa il servizio a tutti gli indirizzi del server, incluso quello pubblico.

Se l'applicazione di staging è un servizio nello stesso file, elimina completamente la mappatura 1025 e configura l'applicazione per usare il nome host mailpit sulla porta 1025. I container collegati alla stessa rete Compose comunicano direttamente, quindi la porta SMTP non passa mai dal server 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

Attualmente l'interfaccia risponde solo sull'indirizzo di loopback. nginx termina TLS e richiede una password prima di inoltrare qualsiasi richiesta.

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;
    }
}

Esegui un reload dopo il controllo della sintassi con sudo nginx -t && sudo systemctl reload nginx. È utile leggere una volta cosa fa ogni direttiva in un blocco di reverse proxy se è la prima volta che configuri un proxy.

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

Gli header Upgrade e Connection non sono ornamentali. Mailpit invia i nuovi messaggi a una pagina aperta tramite WebSocket e un proxy che usa 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 resta invariato finché non esegui un reload.

Mantieni entrambi i blocchi di protezione. La password di nginx protegge l'indirizzo pubblico, mentre MP_UI_AUTH_FILE protegge direttamente la porta 8025. È importante perché nell'interfaccia sono leggibili tutti i link per la reimpostazione delle password generati dall'applicazione di staging, finché la retention li conserva.

Non trasformare mai il sink in un open relay

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

Mailpit non è un open relay subito dopo l’installazione, perché per impostazione predefinita non inoltra nulla. Dispone di due funzioni di invio verso l’esterno, entrambe disattivate finché non vengono configurate. MP_SMTP_RELAY_CONFIG indica un file di configurazione del relay e abilita nell’interfaccia l’azione di rilascio, che invia tramite quel server un messaggio scelto dall’utente. L’inoltro automatico richiede un’ulteriore impostazione: MP_SMTP_RELAY_ALL (il valore predefinito è false) oppure MP_SMTP_RELAY_MATCHING. Il forwarding, configurato con MP_SMTP_FORWARD_CONFIG o con le variabili MP_SMTP_FORWARD_*, copia ogni messaggio in ingresso verso indirizzi fissi scelti dall’utente. Lasciare tutte queste impostazioni non configurate è una scelta deliberata.

È possibile perdere questa proprietà in due modi. Abilitate MP_SMTP_RELAY_ALL, che inoltra ogni messaggio in ingresso ai destinatari originali ignorando MP_SMTP_ALLOWED_RECIPIENTS, quindi esponete la porta SMTP a Internet: avrete creato un open relay funzionante. MP_SMTP_RELAY_MATCHING fa lo stesso per qualsiasi destinatario corrispondente al relativo pattern. Se esponete la porta senza abilitare il relay, gli utenti esterni non potranno inviare messaggi attraverso il server, ma potranno riempire lo spazio di archiviazione e inserire contenuti nell’interfaccia considerata attendibile dal team. Con il forwarding attivo, i loro messaggi arriveranno anche a qualunque casella riceva le copie.

Su un host Docker, il problema è il firewall. La pubblicazione di una porta induce Docker a scrivere regole proprie nella tabella nat. Il traffico destinato al container viene associato a queste regole prima che le regole di ufw (uncomplicated firewall) possano intervenire. sudo ufw deny 1025/tcp restituisce un esito positivo senza modificare nulla. Perché Docker pubblica le porte aggirando direttamente ufw descrive l’ordine della catena.

La correzione riguarda l’indirizzo nella mappatura, non una regola del firewall. Verificate cosa è effettivamente in ascolto:

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 oppure essere rifiutato.

Quando l’applicazione risiede su un server diverso, non aprite la porta 1025 per collegare i due server. Inserite entrambe le macchine in una rete privata o in un tunnel VPN e associate la mappatura all’indirizzo dell’interfaccia corrispondente.

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. Ometterlo non chiude automaticamente l’accesso: quando un dominio non ha un record MX, i server mittenti usano il record dell’indirizzo (A o AAAA), come specificato nella sezione 5.1 di RFC 5321, e il nome host che punta a questo server per l’interfaccia web ne ha uno. Ciò che impedisce alla posta proveniente da Internet di entrare è l’assenza di qualsiasi processo in ascolto sulla porta 25. La casella contiene soltanto i messaggi inviati dalle tue applicazioni, che è lo scopo di una casella di test.

Ricevere posta reale richiede un record DNS che indirizzi i server mittenti verso il server (un record MX oppure il record dell’indirizzo usato come fallback), Mailpit in ascolto sulla porta 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) e la porta aperta. Da quel momento stai gestendo un catch-all pubblico per ogni indirizzo del dominio. Considera con chiarezza le conseguenze.

  • Lo spam inizia entro pochi giorni dalla pubblicazione del record, perché i sistemi di raccolta degli indirizzi leggono il DNS. Seguono gli attacchi a dizionario, che provano i nomi più comuni e memorizzano un messaggio per ogni tentativo.
  • Gli allegati provenienti da mittenti sconosciuti vengono salvati sul disco e vi rimangono. Nulla li filtra, quindi un archivio ricevuto da un mittente sconosciuto si trova accanto ai tuoi messaggi 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 il controllo della password dovesse essere aggirato, quegli account apparterrebbero a chiunque legga la casella.
  • I limiti di conservazione smettono di essere una semplice attività amministrativa e diventano essenziali per il carico del sistema, perché il volume non è più sotto il tuo controllo.

Se ti serve ricevere posta reale per verificare la deliverability, 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 server di posta con filtraggio e backup.

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

Mailpit conserva 500 messaggi per impostazione predefinita ed elimina periodicamente quelli più vecchi oltre questo limite. MP_MAX_MESSAGES: 0 disattiva questo limite e, se non viene impostato MP_MAX_AGE, nulla viene eliminato automaticamente. È così che un catch-all riempie il disco senza che nessuno se ne accorga. MP_MAX_AGE aggiunge un limite temporale, può essere usato insieme al limite sul numero di messaggi o al suo posto e accetta ore o giorni, scritti come 36h o 14d.

MP_DATABASE determina se i messaggi sopravvivono ai riavvii. Senza questa opzione, Mailpit scrive in un file temporaneo che viene eliminato quando il processo termina. Ogni riavvio svuota quindi la posta in arrivo. Con questa opzione, i messaggi restano disponibili dopo i 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. Il solo limite sul numero di messaggi non interviene in tempo. Valuta questa crescita rispetto agli altri dati che condividono il volume, perché un servizio che usa molto spazio multimediale, come PhotoPrism o Immich, potrebbe avere già occupato gran parte del disco di un VPS di piccole dimensioni.

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

Svuota l'archivio tra un'esecuzione CI e l'altra, invece di attendere l'attivazione di un 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 tu scelga, imposta il limite prima di collegarvi la prima suite di test.

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

Un inbox email usa e getta self-hosted è un open relay?

Non finché il relay resta disattivato. Per impostazione predefinita, Mailpit non inoltra nulla: MP_SMTP_RELAY_CONFIG abilita soltanto il rilascio manuale dei messaggi selezionati, mentre il relay automatico richiede anche MP_SMTP_RELAY_ALL o MP_SMTP_RELAY_MATCHING. Lasciate queste variabili non impostate: un estraneo che raggiunge la porta 1025 non può inviare email tramite il server. Può comunque riempire lo spazio di archiviazione, quindi associate la porta SMTP a un indirizzo raggiungibile soltanto dall'applicazione. Pubblicarla come 1025:1025 in Compose associa la porta a tutti gli indirizzi del server; sudo ufw deny 1025/tcp non la chiude, perché le regole NAT gestite direttamente da Docker vengono applicate prima.

Serve un record MX per il dominio di test?

Soltanto se volete ricevere messaggi da Internet; lasciarlo assente, da solo, non impedisce la ricezione. Senza un record MX, i server mittenti usano come fallback il record A o AAAA del dominio. Ciò che impedisce effettivamente la ricezione da Internet è che nessun servizio sia in ascolto sulla porta 25. L'inbox contiene quindi soltanto i messaggi inviati dalle vostre applicazioni tramite SMTP. Se puntate il DNS al server e aprite la porta 25, state eseguendo un catch-all pubblico: spam entro pochi giorni, attacchi a dizionario che archiviano un messaggio per ogni tentativo e allegati di estranei sul disco, senza alcun filtraggio.

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

Mailpit invia i nuovi messaggi a una pagina aperta tramite WebSocket. Un blocco location di nginx privo di proxy_http_version 1.1 e degli header Upgrade e Connection non può aggiornare la 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 l'inbox sembra obsoleta anziché non funzionante. Aggiungete quelle righe, ricaricate nginx e poi ricaricate la pagina.

Come posso impedire che l'inbox riempia il disco?

Mantenete MP_MAX_MESSAGES impostato su un valore effettivo e aggiungete MP_MAX_AGE. Il limite predefinito è di 500 messaggi. Impostarlo su 0 disabilita l'eliminazione basata sul numero di messaggi. Di conseguenza, se non è impostato MP_MAX_AGE, non viene eliminato nulla. È così che un catch-all con allegati può crescere senza attirare l'attenzione. MP_MAX_AGE accetta ore o giorni, ad esempio 36h o 14d. Svuotate lo store nella fase di teardown della CI con curl -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages, poiché l'API richiede le stesse credenziali dell'interfaccia web. Inbucket svolge lo stesso compito con INBUCKET_STORAGE_RETENTIONPERIOD (72h) e INBUCKET_STORAGE_MAILBOXMSGCAP (300).

Devo eseguire Mailpit, Inbucket o MailHog?

Per i progetti nuovi, scegliete Mailpit, considerando lo stato a ottobre 2026. MailHog continua a funzionare, ma il suo branch predefinito non riceve commit da agosto 2022 e quindi distribuisce dipendenze senza patch. Inbucket è gestito attivamente (3.2.0, ottobre 2026) ed è la scelta migliore quando un test richiede POP3, poiché il server POP3 di Mailpit si avvia soltanto dopo aver configurato le credenziali, in un file delle password oppure nella variabile MP_POP3_AUTH. Mailpit usa le stesse porte di MailHog, 1025 e 8025. Sostituire MailHog richiede quindi di modificare soltanto il nome dell'immagine nel file Compose.