Héberger une boîte e-mail jetable sur un VPS
Avec Mailpit, capturez chaque e-mail de test sur un domaine jetable, consultez-le dans une interface web et empêchez votre staging de contacter un client réel.
Fonctionnement d’une boîte de réception e-mail jetable
Une boîte de réception e-mail jetable est un petit serveur SMTP (simple mail transfer protocol) qui accepte les messages destinés à n’importe quelle adresse, mais n’en remet aucun. Votre application de staging lui envoie les messages au lieu de les envoyer à un fournisseur de messagerie réel, et chaque message s’arrête là. Vous consultez les messages reçus dans une interface web. Une liste de destinataires incorrecte ou un modèle défectueux ne vous coûte donc rien, car le message ne quitte jamais la boîte.
Ce guide en déploie une sur un VPS unique avec Docker Compose. Mailpit sert de puits catch-all. Son écouteur SMTP est lié à une adresse accessible uniquement par votre application. Son interface web est placée derrière nginx, avec la sécurité de la couche transport (TLS) et un mot de passe. Une limite de rétention empêche la boîte aux lettres de remplir le disque. Si Compose est nouveau pour vous, les bases de Compose pour un VPS présentent l’organisation des fichiers utilisée dans ce guide.
Le résultat est un outil de test, pas un serveur de messagerie. Il ne possède ni comptes, ni remise des messages, ni filtrage anti-spam. Les boîtes aux lettres réelles destinées à de vraies personnes nécessitent un serveur de messagerie complet tel que Mailcow et un travail nettement plus important.
Mailpit, Inbucket ou MailHog : quel sink exécuter
Trois outils remplissent ce rôle. Ils se distinguent par leur état de maintenance, les ports sur lesquels ils sont en écoute et les actions possibles sur un message après sa réception. Les versions ci-dessous ont été vérifiées en août 2026.
MailHog (mailhog/mailhog) est en écoute sur le port 1025 pour SMTP et fournit son interface sur le port 8025. Il fonctionne toujours. Sa branche par défaut n’a reçu aucun commit depuis août 2022 et son tracker contient plus de 250 issues ouvertes. Vous exécuteriez donc des dépendances non corrigées dans votre chaîne de test. Ne démarrez pas de nouveau projet avec cet outil.
Inbucket (inbucket/inbucket) est en écoute sur le port 2500 pour SMTP, sur le port 9000 pour l’interface web et sur le port 1100 pour POP3 (post office protocol version 3). La version 3.1.1 est sortie en décembre 2025. Elle stocke les messages sous forme de fichiers dans /storage et les supprime automatiquement selon leur ancienneté : l’image définit INBUCKET_STORAGE_RETENTIONPERIOD=72h et INBUCKET_STORAGE_MAILBOXMSGCAP=300. Choisissez cet outil lorsqu’un test doit récupérer les e-mails avec une bibliothèque cliente POP3 plutôt qu’avec un appel HTTP.
Mailpit (axllent/mailpit) utilise les mêmes ports que MailHog, 1025 et 8025. Il remplace donc MailHog sans modifier la configuration de l’application. La version 1.30.7 est sortie le 8 août 2026. Elle intègre dans le binaire les fonctions nécessaires à ce guide : un fichier de mots de passe pour l’interface web et l’API (application programming interface), une limite sur le nombre de messages, une limite d’ancienneté et un filtre de destinataires. La suite de ce guide utilise Mailpit.
Fonctionnement du catch-all et absence d’intervention du DNS
Votre application ne recherche pas ici où remettre le message. Vous lui fournissez un hôte et un port. Elle ouvre une connexion TCP et annonce RCPT TO:<anyone@example.test>. Mailpit accepte ce destinataire quelle que soit sa valeur, stocke le message et ne le transmet nulle part. Le domaine n’est jamais résolu. example.test fonctionne donc même si .test est un nom réservé qui n’existe nulle part dans le système de noms de domaine (DNS).
C’est tout le mécanisme. C’est pourquoi la boîte de réception est sûre par défaut. Aucun enregistrement MX (mail exchanger) n’est utilisé. Aucune remise n’est tentée. Aucun message ne peut parvenir à une personne réelle.
Configurer l’application de staging pour utiliser le sink
Définissez l’hôte SMTP de l’application sur mailpit lorsqu’elle s’exécute dans un conteneur du même projet Compose, ou sur 127.0.0.1 lorsqu’elle s’exécute sur l’hôte. Définissez le port sur 1025, désactivez TLS et laissez le nom d’utilisateur et le mot de passe vides. Mailpit accepte les messages sans authentification.
Certains frameworks refusent d’envoyer des messages sans identifiants. MP_SMTP_AUTH_ACCEPT_ANY=1 permet à Mailpit d’accepter n’importe quel nom d’utilisateur et n’importe quel mot de passe, tandis que MP_SMTP_AUTH_ALLOW_INSECURE=1 autorise les mécanismes PLAIN et LOGIN sur une connexion non chiffrée. Ces deux paramètres sont sûrs ici uniquement parce que le listener est inaccessible depuis Internet, ce que le déploiement ci-dessous garantit.
Il est préférable de définir MP_SMTP_ALLOWED_RECIPIENTS dès le premier jour. Ce paramètre prend une expression régulière et rejette tous les destinataires qui ne correspondent pas à celle-ci. Configurez-le avec votre domaine de test. Ainsi, si une base de données de staging contient encore l’adresse d’un vrai client, l’application inscrit un échec visible dans son journal au lieu de laisser discrètement le message arriver dans le sink.
Le fichier Docker Compose
Créez d’abord le répertoire et un fichier contenant le mot de passe de l’interface web. htpasswd -B écrit un hash bcrypt, et Mailpit accepte également bcrypt et le texte en clair.
mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qaÉcrivez 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$$'Le double signe dollar n’est pas une erreur. Compose interprète un seul $ comme le début d’une variable à développer. $$ permet donc de transmettre un seul signe dollar littéral au conteneur. L’expression régulière parvient à Mailpit sous la forme @example\.test$.
Démarrez le service et vérifiez son état de santé :
docker compose up -d
docker compose psLa colonne STATUS doit afficher Up ... (healthy). L’image fournit son propre healthcheck, qui exécute /mailpit readyz toutes les 15 secondes. Un conteneur qui reste à l’état starting ou passe à l’état unhealthy ne fournit pas le service sur le port 8025 à l’intérieur du conteneur. Consultez docker compose logs mailpit avant de modifier quoi que ce soit d’autre.
Les deux ports publiés comportent une adresse, et cette adresse constitue le contrôle de sécurité. Dans le conteneur, Mailpit écoute sur 0.0.0.0, ce qui convient, car le conteneur possède son propre espace de noms réseau. Le côté gauche du mapping détermine qui peut y accéder depuis l’extérieur. Écrivez 8025:8025 et Docker lie toutes les adresses de l’hôte, y compris l’adresse publique.
Si votre application de staging est un service défini dans ce même fichier, supprimez complètement le mapping 1025 et configurez l’application pour utiliser le nom d’hôte mailpit sur le port 1025. Les conteneurs d’un réseau Compose partagé communiquent directement entre eux. Le port SMTP n’est donc jamais exposé sur l’hôte. Résolution des noms de service sur les réseaux Compose explique cette résolution.
Envoyer un message et vérifier sa réception
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)
EOFLe script n’affiche rien lorsqu’il réussit. Vérifiez que le message est bien stocké via l’API :
curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messagesCette commande renvoie un JSON qui liste les messages stockés. Si vous supprimez l’option -u, la même requête est refusée, car MP_UI_AUTH_FILE protège à la fois l’API et l’interface web. Tout test qui lit la boîte de réception doit également transmettre ces identifiants.
Un ConnectionRefusedError dans le script Python signifie qu’aucun service n’écoute sur 127.0.0.1:1025. C’est le résultat attendu si vous avez supprimé le mapping SMTP. Le test doit alors être exécuté depuis un conteneur connecté au même réseau Compose.
Publier l’interface web avec nginx et un mot de passe
L’interface répond actuellement uniquement sur l’adresse loopback. nginx termine TLS et demande un mot de passe avant de lui transmettre la moindre requête.
sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qaserver {
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;
}
}Rechargez la configuration après avoir vérifié sa syntaxe avec sudo nginx -t && sudo systemctl reload nginx. Rôle de chaque directive dans un bloc de reverse proxy mérite d’être lu une fois si vous utilisez un proxy pour la première fois.
Utilisez le même nom d’utilisateur et le même mot de passe dans le fichier nginx et dans data/ui-auth. nginx transmet l’en-tête Authorization du navigateur au backend. Les identifiants correspondants satisfont donc les deux contrôles avec une seule demande de mot de passe. Avec des identifiants différents, le navigateur conserve un jeu d’identifiants que le second contrôle rejette.
Les en-têtes Upgrade et Connection ne sont pas décoratifs. Mailpit transmet les nouveaux messages à une page ouverte via un WebSocket, et un proxy exécutant HTTP/1.1 sans ces en-têtes ne peut pas mettre à niveau la connexion. La page se charge alors correctement, mais ne se met jamais à jour : les messages arrivent, l’API les affiche, mais la liste reste figée jusqu’à son rechargement.
Conservez les deux protections. Le mot de passe nginx protège l’adresse publique, tandis que MP_UI_AUTH_FILE protège directement le port 8025. C’est important, car tous les liens de réinitialisation de mot de passe générés par votre application de staging sont lisibles dans cette interface.
Ne laissez jamais le sink devenir un open relay
Un open relay est un serveur SMTP qui accepte un message de n’importe qui et le transfère vers n’importe quelle destination. Les spammeurs les recherchent en permanence. En trouver un sur votre adresse IP entraîne des signalements pour abus et la suspension du compte.
Mailpit n’est pas un open relay par défaut, car il ne transfère jamais les messages. Le relais reste désactivé tant que vous n’indiquez pas à MP_SMTP_RELAY_CONFIG un fichier de configuration du relais. L’action de libération dans l’interface ne fait rien tant que ce fichier n’est pas configuré. Laisser ce paramètre vide est un choix délibéré.
Deux situations peuvent faire perdre cette protection. Configurez un relais pour activer le bouton de libération, puis exposez le port SMTP sur Internet : vous aurez créé un open relay fonctionnel. Exposez le port sans configurer de relais : les tiers ne pourront pas envoyer de messages via votre serveur, mais ils pourront remplir votre espace de stockage et injecter du contenu dans l’interface à laquelle votre équipe fait confiance.
Sur un hôte Docker, le piège vient du firewall. La publication d’un port amène Docker à ajouter ses propres règles dans la table nat. Le trafic destiné au conteneur y est traité avant que les règles d’ufw (uncomplicated firewall) puissent s’appliquer. sudo ufw deny 1025/tcp signale une réussite, mais ne change rien. Pourquoi Docker publie les ports en contournant directement ufw détaille l’ordre de traitement des chaînes.
La correction se fait au niveau de l’adresse dans le mapping, pas avec une règle de firewall. Vérifiez ce qui est réellement bindé :
sudo ss -ltnp | grep -E ':(1025|8025)'Une sortie correcte affiche 127.0.0.1:1025 et 127.0.0.1:8025. Une ligne contenant 0.0.0.0:1025 signifie que le mapping a perdu son adresse et que le sink écoute sur Internet. Depuis une autre machine, nc -vz mail-test.example.com 1025 doit expirer ou être refusé.
Lorsque l’application se trouve sur un autre serveur, n’ouvrez pas le port 1025 pour relier les deux machines. Placez-les sur un réseau privé ou dans un tunnel VPN, puis liez le mapping à l’adresse de cette interface.
Ne publiez des enregistrements MX que si vous voulez recevoir de vrais e-mails
Un enregistrement MX (mail exchanger) indique aux autres serveurs de messagerie quel hôte accepte les e-mails destinés à un domaine. Si votre domaine temporaire ne possède aucun enregistrement MX, aucun e-mail provenant d’Internet ne peut arriver, car les serveurs expéditeurs ne savent pas où le remettre. La boîte de réception contient uniquement les messages envoyés par vos propres applications, ce qui correspond à l’usage d’une boîte de test.
Recevoir de vrais e-mails nécessite un enregistrement MX pointant vers le serveur, Mailpit à l’écoute sur le port 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25), ainsi que l’ouverture de ce port. Vous exploitez alors un catch-all public pour toutes les adresses du domaine. Vous devez bien mesurer les conséquences.
- Le spam commence quelques jours après la publication de l’enregistrement, car les harvesters lisent le DNS. Des attaques par dictionnaire parcourent ensuite les noms courants et stockent un message pour chaque tentative.
- Les pièces jointes provenant d’inconnus arrivent sur votre disque et y restent. Rien ne les filtre : une archive envoyée par un expéditeur inconnu se retrouve à côté de vos propres e-mails de test.
- Toute personne qui connaît le domaine peut s’inscrire à des services tiers avec une adresse de ce domaine, et l’e-mail de confirmation est remis à votre serveur. Si la protection par mot de passe est un jour contournée, ces comptes appartiennent à la personne qui lit la boîte de réception.
- Les limites de rétention cessent d’être une simple tâche d’administration et deviennent essentielles au fonctionnement, car vous ne contrôlez plus le volume.
Si vous avez besoin de recevoir de vrais e-mails pour vérifier la délivrabilité, utilisez un sous-domaine dédié, gardez MP_MAX_AGE court et considérez tout son contenu comme public. Si vous avez besoin de boîtes aux lettres sur lesquelles des utilisateurs comptent, déployez plutôt un véritable serveur de messagerie avec filtrage et sauvegardes.
Rétention : comment un catch-all sans limite remplit le disque
Mailpit conserve 500 messages par défaut et supprime périodiquement ceux qui dépassent cette limite, en commençant par les plus anciens. MP_MAX_MESSAGES: 0 désactive complètement la suppression automatique. Ce seul changement suffit à permettre à un catch-all de remplir un disque sans que personne ne s’en aperçoive. MP_MAX_AGE ajoute une limite de temps, exprimée en heures ou en jours, sous la forme 36h ou 14d.
MP_DATABASE détermine si ces données sont conservées. Sans ce paramètre, Mailpit écrit dans un fichier temporaire supprimé à l’arrêt du processus. Chaque redémarrage vide donc la boîte de réception. Avec ce paramètre, les messages survivent aux redémarrages et le fichier grossit.
Les pièces jointes consomment l’espace disque. Une tâche nocturne qui envoie un rapport PDF de 2 MB à 300 adresses de test représente 600 MB par nuit. Une simple limite sur le nombre de messages ne réagira pas assez vite. Prévoyez cette croissance en tenant compte des autres services qui partagent le volume. Un voisin consommateur d’espace, comme PhotoPrism ou Immich, aura déjà occupé la majeure partie du disque d’un petit VPS.
du -h ~/mailpit/data/mailpit.db
df -h /Videz le stockage entre les exécutions de la CI au lieu d’attendre qu’une limite soit atteinte :
curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messagesInbucket traite le même problème avec INBUCKET_STORAGE_RETENTIONPERIOD (72h dans l’image) et INBUCKET_STORAGE_MAILBOXMSGCAP (300). Quel que soit le logiciel utilisé, définissez la limite avant que la première suite de tests ne l’utilise.
Lecture de la boîte de réception depuis votre suite de tests
GET /api/v1/messages liste les éléments stockés, GET /api/v1/message/{ID} renvoie un message avec ses composants et ses en-têtes, GET /api/v1/search applique des filtres et DELETE /api/v1/messages vide le stockage. La documentation interactive de la version utilisée est disponible à l’adresse http://127.0.0.1:8025/api/v1/.
Un test utile envoie un message, interroge le stockage jusqu’à son apparition, vérifie l’objet et le lien qu’il contient, puis supprime tout. Utilisez une courte boucle de nouvelle tentative plutôt qu’une seule requête, car une application qui met les e-mails en file d’attente dans un worker en arrière-plan termine l’appel d’envoi avant que Mailpit ne reçoive le message. On retrouve le même principe avec les outils auto-hébergés de test et de mock d’API, qui constituent généralement l’autre volet d’un environnement de staging ne touchant jamais la production.
FAQ
Une boîte mail jetable auto-hébergée est-elle un open relay ?
Pas tant que le relais reste désactivé. Mailpit stocke les messages et ne les transfère jamais tant que vous ne configurez pas MP_SMTP_RELAY_CONFIG avec un relais. Une personne qui atteint le port 1025 ne peut donc pas envoyer de messages via votre serveur. Elle peut toutefois remplir votre espace de stockage. Liez donc le port SMTP à une adresse que seule votre application peut atteindre. Le publier sous la forme 1025:1025 dans Compose lie toutes les adresses de l’hôte. sudo ufw deny 1025/tcp ne le fermera pas, car les règles nat de Docker sont traitées en premier.
Ai-je besoin d’un enregistrement MX pour mon domaine de test ?
Uniquement si vous voulez recevoir des messages depuis Internet. Sans enregistrement MX, les serveurs expéditeurs n’ont aucun endroit où remettre les messages. La boîte ne contient donc que les messages envoyés par vos propres applications via SMTP. Publiez l’enregistrement et ouvrez le port 25 : vous exploitez alors un catch-all public. Vous recevrez du spam en quelques jours, des attaques par dictionnaire qui stockent un message à chaque tentative et des pièces jointes envoyées par des inconnus sur votre disque, sans aucun filtrage.
Pourquoi la liste des messages ne se met-elle à jour que lorsque je recharge la page ?
Mailpit transmet les nouveaux messages à une page ouverte via un WebSocket. Un bloc location Nginx qui ne contient pas proxy_http_version 1.1 ni les en-têtes Upgrade et Connection ne peut pas mettre cette connexion à niveau. La page se charge donc normalement, puis se fige. Les messages arrivent toujours et l’API les renvoie toujours. C’est pourquoi la boîte semble obsolète plutôt qu’en panne. Ajoutez ces lignes, rechargez Nginx, puis rechargez la page.
Comment empêcher la boîte de remplir le disque ?
Conservez MP_MAX_MESSAGES avec une valeur réelle et ajoutez MP_MAX_AGE. La limite par défaut est de 500 messages. La valeur 0 désactive entièrement la suppression. C’est ainsi qu’un catch-all contenant des pièces jointes peut grossir discrètement. MP_MAX_AGE accepte des heures ou des jours, par exemple 36h ou 14d. Videz le stockage lors du teardown de la CI avec curl -X DELETE http://127.0.0.1:8025/api/v1/messages. Inbucket fournit la même fonction avec INBUCKET_STORAGE_RETENTIONPERIOD (72h) et INBUCKET_STORAGE_MAILBOXMSGCAP (300).
Dois-je utiliser Mailpit, Inbucket ou MailHog ?
Utilisez Mailpit pour les nouveaux projets, en août 2026. MailHog fonctionne encore, mais sa branche par défaut n’a reçu aucun commit depuis août 2022. Il distribue donc des dépendances non corrigées. Inbucket est activement maintenu (3.1.1, décembre 2025). C’est le meilleur choix lorsqu’un test nécessite POP3, car le serveur POP3 de Mailpit ne démarre qu’après la fourniture d’un fichier de mots de passe. Mailpit utilise les mêmes ports que MailHog, 1025 et 8025. Remplacer MailHog ne demande donc de modifier que le nom de l’image dans votre fichier Compose.