SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor

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

Existem três ferramentas para este fim. 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 aceitarem. As versões abaixo foram verificadas em outubro de 2026.

MailHog (mailhog/mailhog) escuta na porta 1025 para SMTP e disponibiliza a interface na porta 8025. Continua a funcionar. O seu branch predefinido não recebe commits desde agosto de 2022 e o tracker tem mais de 200 issues abertas. Por isso, estaria a executar dependências sem patches no caminho de testes. Não comece 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.2.0 foi lançada em 10 de outubro de 2026. Armazena as mensagens como ficheiros em /storage e elimina-as automaticamente: a image 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) utiliza as mesmas portas que o MailHog, 1025 e 8025. Assim, substitui o MailHog sem alterar a configuração da aplicação. A versão 1.31.6 foi lançada em 15 de outubro de 2026; a linha 1.30 terminou na versão 1.30.7, em agosto. 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 arquivo Docker Compose

Crie primeiro o diretório e um ficheiro de palavras-passe para a interface Web. htpasswd -B grava um hash bcrypt, e o Mailpit também lê 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 qa

Escreva 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$$'

O cifrão duplicado não é um erro. O Compose interpreta um único $ como o início de uma variável a expandir, por isso $$ é a forma de passar um cifrão 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 ps

A coluna STATUS deve indicar Up ... (healthy). A imagem inclui o seu próprio healthcheck, que executa /mailpit readyz a cada 15 segundos. Por isso, um contentor que permanece em starting ou passa a unhealthy normalmente não está a disponibilizar o serviço na porta 8025 dentro do contentor. A verificação obtém as definições das variáveis de ambiente MP_, mas não dos flags da linha de comandos. Esta é uma das razões pelas quais este guia define tudo em environment:. Se passar as opções como flags, a verificação pode indicar unhealthy enquanto o Mailpit funciona normalmente. 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 associa a porta a todos os endereços do 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 configure a aplicação para usar o hostname mailpit na porta 1025. Os contentores numa rede Compose partilhada comunicam diretamente entre si, por isso a porta SMTP não chega a tocar 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)
EOF

O 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/messages

Isto 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

Atualmente, a interface responde apenas no endereço de loopback. O nginx termina o TLS e pede uma palavra-passe antes de qualquer pedido lhe chegar.

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

Faça o reload depois de validar a sintaxe com sudo nginx -t && sudo systemctl reload nginx. Vale a pena ler uma vez O que faz cada diretiva num bloco de reverse proxy 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, pelo que credenciais iguais satisfazem ambas as verificações com um único pedido de autenticaçã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 mostra-as e a lista permanece igual até fazer reload.

Mantenha ambas as 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 geradas pela aplicação de staging ficam legíveis nessa interface durante todo o período de retenção.

Nunca permita que o sink se torne um relay aberto

Um relay aberto é um servidor SMTP que recebe uma mensagem de qualquer pessoa e a encaminha para qualquer destino. Os spammers procuram-nos continuamente. Encontrar um no seu endereço termina em denúncias de abuso e na suspensão da conta.

O Mailpit não é um relay aberto por predefinição, porque não envia nada para fora. Tem duas funcionalidades de saída, e ambas permanecem desativadas até serem configuradas. MP_SMTP_RELAY_CONFIG aponta para um ficheiro de configuração do relay e ativa a ação de libertação na interface. Essa ação envia a mensagem escolhida através desse servidor. O relay automático requer uma opção adicional, MP_SMTP_RELAY_ALL (com o valor predefinido false) ou MP_SMTP_RELAY_MATCHING. O encaminhamento, configurado com MP_SMTP_FORWARD_CONFIG ou com as variáveis MP_SMTP_FORWARD_*, copia cada mensagem recebida para endereços fixos à sua escolha. Deixar todas estas opções sem definição é uma escolha deliberada.

Há duas formas de perder essa propriedade. Ative MP_SMTP_RELAY_ALL, que envia cada mensagem recebida para os destinatários originais e ignora MP_SMTP_ALLOWED_RECIPIENTS. Depois exponha a porta SMTP à internet e terá criado um relay aberto funcional. MP_SMTP_RELAY_MATCHING faz o mesmo para qualquer destinatário correspondente ao seu padrão. Se expuser a porta sem ativar o relay, pessoas externas não poderão enviar mensagens através do servidor, mas poderão encher o armazenamento e inserir conteúdo na interface em que a sua equipa confia. Com o encaminhamento ativo, as mensagens também chegarão à caixa de entrada que recebe as cópias.

O problema num host Docker é a firewall. Publicar uma porta faz com que o Docker escreva as próprias regras na tabela nat. O tráfego destinado ao contentor é correspondido nessa tabela antes de as regras do ufw (uncomplicated firewall) serem aplicadas. sudo ufw deny 1025/tcp indica sucesso, mas não altera nada. Porque é 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 da firewall. Verifique o endereço efetivamente associado:

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

Um resultado correto 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 ligar 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 registos MX apenas se quiser receber correio real

Um registo MX (mail exchanger) informa os outros servidores de correio sobre qual host aceita correio para um domínio. A sua ausência, por si só, não fecha a porta: quando um domínio não tem registo MX, os servidores remetentes usam o registo de endereço (A ou AAAA), conforme especificado na secção 5.1 da RFC 5321, e o nome de host que aponta para esta máquina para a interface Web tem um desses registos. O que impede a entrada de correio da Internet é não existir nada nesta máquina a escutar na porta 25. 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 significa ter um registo DNS que encaminha os servidores remetentes para esta máquina (um registo MX ou o registo de endereço usado como alternativa), o Mailpit a escutar na porta 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) e essa porta aberta. Nesse momento, está a executar um catch-all público para todos os endereços do domínio. Tenha em atenção as consequências.

  • O spam começa poucos dias depois de o registo aparecer, porque os harvesters consultam o DNS. Depois, ataques de dicionário percorrem nomes comuns e armazenam uma mensagem para cada tentativa.
  • Os anexos de desconhecidos são gravados no disco e permanecem lá. Nada os filtra, por isso um arquivo enviado por um remetente desconhecido fica junto do seu próprio correio de teste.
  • Qualquer pessoa que descubra o domínio pode inscrever-se em serviços de terceiros usando um endereço desse domínio, e a mensagem de confirmação é entregue no seu servidor. Se a proteção por palavra-passe falhar, essas contas ficam acessíveis a quem estiver a ler a caixa de entrada.
  • Os limites de retenção deixam de ser apenas tarefas de manutenção e passam a ser essenciais para a operação, porque o volume já não está sob o seu controlo.

Se precisar de receber correio real para verificar a entregabilidade, atribua-lhe um subdomínio dedicado, mantenha MP_MAX_AGE curto e trate todo o conteúdo como público. Se precisar de caixas de correio das quais as pessoas dependam, execute antes um servidor de correio real, com filtragem e cópias de segurança.

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 esse limite. Sem definir MP_MAX_AGE, nada é eliminado automaticamente. É assim que um catch-all enche o disco sem que ninguém se aperceba. MP_MAX_AGE adiciona um limite de tempo. Pode ser usado com ou sem o limite de mensagens. O valor é expresso em horas ou dias, 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. Cada reinício esvazia a caixa de entrada. Com esta opção, as mensagens sobrevivem aos reinícios e o ficheiro aumenta de tamanho.

São os anexos que consomem espaço. Uma tarefa noturna que envia um relatório PDF de 2 MB para 300 endereços de teste representa 600 MB por noite. Um limite baseado apenas no número de mensagens não reage a tempo. Tenha esse crescimento em conta face aos restantes dados que partilham o volume. Um vizinho que consuma muitos dados multimédia, 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 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/messages

O Inbucket resolve o mesmo problema com INBUCKET_STORAGE_RETENTIONPERIOD (72h na imagem) e INBUCKET_STORAGE_MAILBOXMSGCAP (300). Independentemente da ferramenta utilizada, 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 de e-mail descartável auto-hospedada é um open relay?

Não enquanto o relay estiver desativado. Por padrão, o Mailpit não encaminha nada: MP_SMTP_RELAY_CONFIG apenas permite libertar manualmente as mensagens que selecionar, e o relay automático precisa de MP_SMTP_RELAY_ALL ou MP_SMTP_RELAY_MATCHING adicionalmente. Deixe essas opções sem definir, e um utilizador externo que alcance a porta 1025 não poderá enviar e-mail através do seu servidor. Ainda poderá encher o armazenamento, por isso 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, e sudo ufw deny 1025/tcp não a fechará, porque as regras nat do próprio Docker são avaliadas primeiro.

Preciso de um registo MX para o meu domínio de teste?

Apenas se quiser que e-mails da internet cheguem ao domínio. Deixá-lo de fora, por si só, não bloqueia o acesso. Sem um registo MX, os servidores remetentes recorrem ao registo A ou AAAA do domínio. Portanto, o que efetivamente impede a entrada de e-mail da internet é não haver nada a escutar na porta 25. A caixa de entrada conterá apenas as mensagens que as suas próprias aplicações enviarem por SMTP. Aponte o DNS para o servidor e abra a porta 25, e terá 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.

Porque é que 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 nem os cabeçalhos Upgrade e Connection não consegue atualizar essa ligação. A página é carregada normalmente e depois deixa de ser atualizada. O e-mail continua a chegar e a API continua a devolvê-lo. 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 real e adicione MP_MAX_AGE. O limite predefinido é de 500 mensagens. Defini-lo como 0 desativa a eliminação baseada na quantidade. Assim, se MP_MAX_AGE não estiver definido, nada é eliminado. É dessa forma que um catch-all com anexos cresce silenciosamente. MP_MAX_AGE aceita horas ou dias, como 36h ou 14d. Limpe o armazenamento no teardown do CI com curl -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages, porque o endpoint de eliminação usa as mesmas credenciais da interface web. Apenas a Send API (POST /api/v1/send) pode receber credenciais próprias, e isso é opcional. O Inbucket faz o mesmo com INBUCKET_STORAGE_RETENTIONPERIOD (72h) e INBUCKET_STORAGE_MAILBOXMSGCAP (300).

Devo executar Mailpit, Inbucket ou MailHog?

Mailpit para trabalho novo, em outubro de 2026. O MailHog continua a funcionar, mas o seu branch predefinido não recebe nenhum commit desde agosto de 2022 e, por isso, inclui dependências sem correções. O Inbucket é mantido ativamente (3.2.0, outubro de 2026) e é a melhor opção quando um teste precisa de POP3, porque o servidor POP3 do Mailpit só é iniciado depois de fornecer credenciais, num ficheiro de palavras-passe ou na variável MP_POP3_AUTH. O Mailpit usa as mesmas portas que o MailHog, 1025 e 8025. Assim, substituir o MailHog requer apenas alterar o nome da imagem no ficheiro Compose.