Mailpit: caixa de email descartável numa VPS
Configure o Mailpit numa VPS com Docker Compose para capturar emails de teste, vê-los na interface web e impedir que o staging envie mensagens a clientes reais.
O que é uma caixa de entrada de email descartável
Uma caixa de entrada de email descartável é um pequeno servidor SMTP (simple mail transfer protocol) que aceita mensagens para qualquer endereço e não entrega nenhuma. A aplicação de staging envia as mensagens para esse servidor em vez de usar um fornecedor de email real, e todas ficam retidas nele. Pode ler o conteúdo recebido numa interface web. Assim, uma lista de destinatários incorreta ou um template com problemas não causa custos, porque o email nunca sai da caixa.
Este guia cria uma numa única VPS com Docker Compose. O Mailpit funciona como recetor de todas as mensagens. O seu listener SMTP fica ligado apenas num endereço acessível pela aplicação. A interface web fica atrás do nginx, com transport layer security (TLS) e uma password. Um limite de retenção impede que a caixa de correio encha o disco. Se o Compose ainda for novidade, os conceitos básicos do Compose para uma VPS explicam a organização de ficheiros usada neste guia.
O resultado é uma ferramenta de testes, não um servidor de email. Não tem contas, entrega de mensagens nem filtragem de spam. Caixas de correio reais para pessoas reais exigem um servidor de email completo, como o Mailcow e um trabalho muito maior.
Mailpit vs Inbucket vs MailHog: qual sink executar
Três ferramentas fazem este trabalho. O que as distingue é o estado da manutenção, as portas em que escutam e o que conseguem fazer com uma mensagem depois de a aceitar. As versões abaixo foram verificadas em agosto de 2026.
MailHog (mailhog/mailhog) escuta na porta 1025 para SMTP e disponibiliza a interface na porta 8025. Continua a funcionar. O branch predefinido não recebe commits desde agosto de 2022 e o tracker tem mais de 250 issues abertas, pelo que executaria dependências sem correções no caminho de testes. Não inicie trabalho novo com esta ferramenta.
Inbucket (inbucket/inbucket) escuta na porta 2500 para SMTP, na porta 9000 para a interface web e na porta 1100 para POP3 (post office protocol version 3). A versão 3.1.1 foi lançada em dezembro de 2025. Armazena as mensagens como ficheiros em /storage e remove-as automaticamente: a imagem define INBUCKET_STORAGE_RETENTIONPERIOD=72h e INBUCKET_STORAGE_MAILBOXMSGCAP=300. Escolha esta ferramenta quando um teste precisar de recolher correio com uma biblioteca cliente POP3 em vez de uma chamada HTTP.
Mailpit (axllent/mailpit) usa as mesmas portas que o MailHog, 1025 e 8025, pelo que o substitui sem alterar a configuração da aplicação. A versão 1.30.7 foi lançada em 8 de agosto de 2026. Inclui no binário tudo o que este guia precisa: um ficheiro de palavras-passe para a interface web e a API (application programming interface), um limite de mensagens, um limite de idade e um filtro de destinatários. O resto deste guia executa o Mailpit.
Como o catch-all funciona e por que o DNS não está envolvido
A aplicação não consulta um destino para entregar a mensagem. Ela recebe um host e uma porta, abre uma ligação TCP e anuncia RCPT TO:<anyone@example.test>. O Mailpit aceita esse destinatário, independentemente do valor informado, armazena a mensagem e não encaminha nada. O domínio nunca é resolvido, portanto example.test funciona mesmo que .test seja um nome reservado que não existe no sistema de nomes de domínio (DNS).
Esse é todo o mecanismo. Por isso, a caixa de entrada é segura por padrão. Nenhum registo MX (mail exchanger) é utilizado, nenhuma entrega é tentada e nenhuma mensagem pode chegar a uma pessoa real.
Aponte a aplicação de staging para o sink
Defina o host SMTP da aplicação como mailpit quando a aplicação for executada como um contentor no mesmo projeto Compose, ou como 127.0.0.1 quando for executada no host. Defina a porta como 1025, desative o TLS e deixe o nome de utilizador e a palavra-passe vazios. O Mailpit aceita correio anónimo.
Alguns frameworks recusam enviar mensagens sem credenciais. MP_SMTP_AUTH_ACCEPT_ANY=1 faz o Mailpit aceitar qualquer nome de utilizador e palavra-passe, e MP_SMTP_AUTH_ALLOW_INSECURE=1 permite os mecanismos PLAIN e LOGIN numa ligação não cifrada. Estas duas definições só são seguras neste caso porque o listener não é acessível a partir da internet, como a implementação abaixo garante.
Vale a pena definir MP_SMTP_ALLOWED_RECIPIENTS logo no primeiro dia. Esta opção recebe uma expressão regular e rejeita todos os destinatários que não correspondam a ela. Aponte-a para o seu domínio de teste. Assim, se uma base de dados de staging ainda contiver um endereço real de cliente, a aplicação registará uma falha visível no log em vez de uma mensagem que chegue silenciosamente ao sink.
O ficheiro do Docker Compose
Crie primeiro o diretório e um ficheiro de palavra-passe para a interface Web. htpasswd -B escreve um hash bcrypt, e o Mailpit também aceita bcrypt e texto simples.
mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qaEscreva 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$$'O sinal de dólar duplicado não é um erro de digitação. O Compose interpreta um único $ como o início de uma variável a expandir, por isso $$ é a forma de passar um sinal de dólar literal para o contentor. A expressão regular chega ao Mailpit como @example\.test$.
Inicie-o e verifique o estado de saúde:
docker compose up -d
docker compose psA coluna STATUS deve apresentar Up ... (healthy). A imagem inclui o seu próprio healthcheck, que executa /mailpit readyz a cada 15 segundos. Por isso, um contentor que permaneça em starting ou passe para unhealthy não está a servir na porta 8025 dentro do contentor. Consulte docker compose logs mailpit antes de alterar qualquer outra coisa.
Ambas as portas publicadas incluem um endereço, e esse endereço é o controlo de segurança. Dentro do contentor, o Mailpit escuta em 0.0.0.0, o que é adequado porque o contentor tem o seu próprio namespace de rede. O lado esquerdo do mapeamento determina quem lhe pode aceder a partir do exterior. Escreva 8025:8025 e o Docker fará bind de todos os endereços no host, incluindo o endereço público.
Se a sua aplicação de staging for um serviço neste mesmo ficheiro, elimine completamente o mapeamento 1025 e aponte a aplicação para o hostname mailpit na porta 1025. Os contentores numa rede Compose partilhada comunicam diretamente entre si, pelo que a porta SMTP nunca toca no host. Como as redes Compose resolvem nomes de serviços explica essa resolução.
Enviar uma mensagem e confirmar que chegou
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)
EOFO script não apresenta qualquer saída quando é bem-sucedido. Confirme através da API que a mensagem foi armazenada:
curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messagesIsto devolve JSON com a lista das mensagens armazenadas. Se remover a flag -u, o mesmo pedido é recusado, porque MP_UI_AUTH_FILE protege a API e a interface Web em conjunto. Qualquer teste que leia a caixa de entrada também tem de enviar essas credenciais.
Um ConnectionRefusedError do script Python significa que não há nenhum serviço a escutar em 127.0.0.1:1025. Esse é o resultado esperado se removeu o mapeamento SMTP. Nesse caso, a verificação tem de ser executada a partir de um contentor na mesma rede Compose.
Publique a interface web através do nginx com uma palavra-passe
A interface responde atualmente apenas no endereço de loopback. O nginx termina o TLS e solicita uma palavra-passe antes de encaminhar qualquer pedido para a interface.
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;
}
}Depois de verificar a sintaxe, recarregue com sudo nginx -t && sudo systemctl reload nginx. Vale a pena ler uma vez O que cada diretiva num bloco de reverse proxy faz se esta for a sua primeira configuração de proxy.
Use o mesmo nome de utilizador e a mesma palavra-passe no ficheiro do nginx e em data/ui-auth. O nginx encaminha o cabeçalho Authorization do browser para o upstream, por isso credenciais correspondentes satisfazem ambas as verificações com uma única solicitação. Credenciais diferentes deixam o browser com um conjunto que a segunda verificação rejeita.
Os cabeçalhos Upgrade e Connection não são decorativos. O Mailpit envia mensagens novas para uma página aberta através de um WebSocket, e um proxy que execute HTTP/1.1 sem esses cabeçalhos não consegue atualizar a ligação. A página carrega corretamente, mas nunca muda: as mensagens chegam, a API apresenta-as e a lista permanece igual até recarregar a página.
Mantenha as duas proteções. A palavra-passe do nginx protege o endereço público, e MP_UI_AUTH_FILE protege diretamente a porta 8025. Isto é importante porque todas as ligações de reposição de palavra-passe que a sua aplicação de staging já gerou podem ser lidas nessa interface.
Nunca permita que o sink se torne um open relay
Um open relay é um servidor SMTP que aceita uma mensagem de qualquer remetente e a encaminha para qualquer destino. Os spammers procuram-nos constantemente. Encontrar um no seu endereço termina em relatórios de abuso e numa conta suspensa.
O Mailpit não é um open relay por predefinição, porque nunca encaminha mensagens. O relaying permanece desativado até indicar MP_SMTP_RELAY_CONFIG para um ficheiro de configuração do relay. A ação de release na interface também não faz nada até isso ser configurado. Deixar essa opção sem valor é uma decisão deliberada.
Há duas formas de perder essa propriedade. Configure um relay para ativar o botão de release e, em seguida, exponha a porta SMTP à Internet. Terá criado um open relay funcional. Exponha a porta sem configurar um relay. Nesse caso, terceiros não poderão enviar mensagens através do servidor, mas poderão ocupar o armazenamento e inserir conteúdo na interface em que a sua equipa confia.
Num host Docker, a armadilha é a firewall. A publicação de uma porta faz com que o Docker escreva as suas próprias regras na tabela nat. O tráfego destinado ao contentor é correspondido nessa tabela antes de as regras do ufw (uncomplicated firewall) poderem ser aplicadas. sudo ufw deny 1025/tcp comunica sucesso e não altera nada. Por que o Docker publica portas diretamente através do ufw explica a ordem da cadeia.
A correção está no endereço do mapeamento, não numa regra de firewall. Verifique o que está efetivamente associado:
sudo ss -ltnp | grep -E ':(1025|8025)'Uma saída correta mostra 127.0.0.1:1025 e 127.0.0.1:8025. Uma linha com 0.0.0.0:1025 significa que o mapeamento perdeu o endereço e que o sink está a escutar na Internet. A partir de outra máquina, nc -vz mail-test.example.com 1025 deve atingir o tempo limite ou ser recusado.
Quando a aplicação está noutro servidor, não abra a porta 1025 para interligar os dois. Coloque ambas as máquinas numa rede privada ou num túnel VPN e associe o mapeamento ao endereço dessa interface.
Publique registros MX apenas se quiser receber correio real
Um registro MX (mail exchanger) informa aos outros servidores de correio qual host aceita mensagens para um domínio. Sem um registro MX no seu domínio descartável, nenhuma mensagem da Internet pode chegar, porque os servidores remetentes não têm para onde entregá-la. A caixa de entrada contém apenas o que as suas próprias aplicações enviaram, que é precisamente a finalidade de uma caixa de correio de teste.
Receber correio real exige um registro MX apontado para o servidor, o Mailpit escutando na porta 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) e essa porta aberta. A partir desse momento, você está executando um catch-all público para todos os endereços do domínio. Tenha clareza sobre o que isso implica.
- O spam começa em poucos dias depois que o registro aparece, porque os harvesters consultam o DNS. Em seguida, ataques de dicionário percorrem nomes comuns e armazenam uma mensagem para cada tentativa.
- Anexos de remetentes desconhecidos chegam ao disco e permanecem lá. Nada os filtra, então um arquivo de um remetente desconhecido fica ao lado das suas próprias mensagens de teste.
- Qualquer pessoa que descubra o domínio pode se cadastrar em serviços de terceiros usando um endereço desse domínio, e a mensagem de confirmação será entregue ao seu servidor. Se a proteção por senha falhar algum dia, essas contas pertencerão a quem estiver lendo a caixa de entrada.
- Os limites de retenção deixam de ser apenas uma tarefa administrativa e passam a ser essenciais para a capacidade do sistema, porque o volume já não está sob o seu controle.
Se precisar de correio de entrada real para verificar a entregabilidade, use um subdomínio dedicado, mantenha MP_MAX_AGE curto e trate tudo o que houver nele como público. Se precisar de caixas de correio das quais as pessoas dependam, execute um servidor de correio real, com filtragem e backups.
Retenção: como um catch-all sem limites enche o disco
O Mailpit mantém 500 mensagens por predefinição e elimina periodicamente as mais antigas que excedam esse limite. MP_MAX_MESSAGES: 0 desativa completamente a eliminação automática, e essa alteração é suficiente para que um catch-all encha o disco sem que ninguém perceba. MP_MAX_AGE adiciona um limite de tempo, que pode ser de horas ou dias, escrito como 36h ou 14d.
MP_DATABASE determina se estes dados persistem. Sem esta opção, o Mailpit escreve num ficheiro temporário que é eliminado quando o processo termina, por isso cada reinício esvazia a caixa de entrada. Com ela, as mensagens sobrevivem aos reinícios e o ficheiro cresce.
Os anexos são os que consomem espaço. Um job noturno que envia um relatório PDF de 2 MB para 300 endereços de teste ocupa 600 MB por noite, e um limite baseado apenas na quantidade de mensagens não reage a tempo. Considere esse crescimento em conjunto com o espaço usado pelos restantes serviços no volume, porque um vizinho com uso intensivo de media, como PhotoPrism ou Immich, já pode ter ocupado a maior parte do disco de uma VPS pequena.
du -h ~/mailpit/data/mailpit.db
df -h /Esvazie o armazenamento entre as execuções de CI em vez de esperar que um limite seja atingido:
curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messagesO Inbucket trata o mesmo problema com INBUCKET_STORAGE_RETENTIONPERIOD (72h na imagem) e INBUCKET_STORAGE_MAILBOXMSGCAP (300). Independentemente da ferramenta escolhida, defina o limite antes de a primeira suite de testes apontar para ela.
Leitura da caixa de entrada a partir da sua suite de testes
GET /api/v1/messages lista o que está armazenado, GET /api/v1/message/{ID} devolve uma mensagem com as respetivas partes e cabeçalhos, GET /api/v1/search aplica filtros e DELETE /api/v1/messages limpa o armazenamento. A documentação interativa da versão em execução está disponível em http://127.0.0.1:8025/api/v1/.
Um teste útil envia uma mensagem, consulta repetidamente até ela aparecer, verifica o assunto e a ligação nela contida e, depois, elimina tudo. Faça as consultas num ciclo curto de novas tentativas, em vez de enviar um único pedido, porque uma aplicação que coloca o correio numa fila através de um worker em segundo plano termina a chamada de envio antes de o Mailpit receber a mensagem. O mesmo padrão aparece em ferramentas de teste e simulação de APIs autoalojadas, que normalmente são a outra parte de um ambiente de staging que nunca toca na produção.
FAQ
Uma caixa de entrada descartável autoalojada é um relay aberto?
Não enquanto o relay permanecer desativado. O Mailpit armazena as mensagens e nunca as reencaminha até configurar MP_SMTP_RELAY_CONFIG com um relay. Por isso, um estranho que aceda à porta 1025 não consegue enviar mensagens através do seu servidor. Ainda assim, pode ocupar o seu armazenamento. Associe a porta SMTP a um endereço que apenas a sua aplicação consiga alcançar. Publicá-la como 1025:1025 no Compose associa-a a todos os endereços do host. sudo ufw deny 1025/tcp não a fecha, porque as regras nat do próprio Docker são aplicadas primeiro.
Preciso de um registo MX para o meu domínio de teste?
Apenas se quiser receber mensagens da Internet. Sem um registo MX, os servidores de envio não têm para onde entregar as mensagens. Por isso, a caixa de entrada contém apenas o que as suas próprias aplicações enviam por SMTP. Publique o registo e abra a porta 25, e estará a executar um catch-all público: spam em poucos dias, ataques de dicionário que armazenam uma mensagem por tentativa e anexos de desconhecidos no disco, sem filtragem.
Por que motivo a lista de mensagens só é atualizada quando recarrego a página?
O Mailpit envia novas mensagens para uma página aberta através de um WebSocket. Um bloco location do nginx sem proxy_http_version 1.1 e sem os cabeçalhos Upgrade e Connection não consegue atualizar essa ligação. A página é carregada normalmente e depois fica bloqueada. As mensagens continuam a chegar e a API continua a devolvê-las. Por isso, a caixa de entrada parece desatualizada, e não avariada. Adicione essas linhas, recarregue o nginx e depois recarregue a página.
Como impeço que a caixa de entrada encha o disco?
Mantenha MP_MAX_MESSAGES definido com um número válido e adicione MP_MAX_AGE. O limite predefinido é de 500 mensagens. Defini-lo como 0 desativa completamente a eliminação. É assim que um catch-all com anexos cresce silenciosamente. MP_MAX_AGE aceita horas ou dias, como 36h ou 14d. Limpe o armazenamento na fase de teardown do CI com curl -X DELETE http://127.0.0.1:8025/api/v1/messages. O Inbucket faz o mesmo com INBUCKET_STORAGE_RETENTIONPERIOD (72h) e INBUCKET_STORAGE_MAILBOXMSGCAP (300).
Devo executar Mailpit, Inbucket ou MailHog?
Escolha Mailpit para trabalho novo, em agosto de 2026. O MailHog continua a funcionar, mas o seu branch predefinido não recebe commits desde agosto de 2022. Por isso, inclui dependências sem correções. O Inbucket é mantido ativamente (3.1.1, dezembro de 2025) e é a melhor opção quando um teste precisa de POP3, porque o servidor POP3 do Mailpit só inicia depois de lhe fornecer um ficheiro de palavras-passe. O Mailpit usa as mesmas portas que o MailHog, 1025 e 8025. Substituir o MailHog requer apenas alterar o nome da imagem no ficheiro Compose.