- CHANGELOG.md and CHANGELOG.pt-BR.md, from the project history. - .env.example now serves development and production; scripts/init-env.sh (make env) creates .env with random secrets. .env.production.example is gone. - docs/RELEASING.md: the pipeline, how a version is cut and deployed, the runners. - DEPLOYMENT.md and both READMEs updated for Gitea and the new env flow. - Service versions set to 1.0.0. - CI: the proxy network step is idempotent and the stack is torn down always, since the runner's Docker daemon now outlives a job. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
24 KiB
LambdaMail
Seu próprio servidor de e-mail, com um webmail que dá gosto de usar.
SMTP, IMAP e POP3 · SPF, DKIM, DMARC e ARC · Webmail e console de administração · Um único docker-compose.yaml, feito para o Coolify.
Webmail · Implantação · Operação · Núcleo da caixa · Lançamentos · Mudanças · Licença
English · Português

A interface real rodando numa caixa inventada: nomes, endereços e mensagens são fictícios.
O LambdaMail é um servidor de e-mail próprio, com webmail e console de
administração integrados, feito para rodar no Coolify a
partir de um único docker-compose.yaml.
SMTP, IMAP, POP3 e ManageSieve, com SPF, DKIM, DMARC, ARC, MTA-STS e TLS-RPT, automação de DNS pela Cloudflare e filtro de spam e de vírus. Sem planos pagos e sem limites artificiais de domínios ou caixas.
Versão 1.0.0 (mudanças). O caminho do e-mail, a postura de segurança e as interfaces web funcionam de ponta a ponta, e há uma suíte de testes automatizados por trás. Leia O que não está pronto antes de colocar e-mail de verdade nele.
O que ele faz
| Área | |
|---|---|
| Protocolos | SMTP MX (25), Submission (587 STARTTLS, 465 TLS implícito), IMAP (143/993) com UID, flags, IDLE e CONDSTORE, POP3 (110/995), ManageSieve (4190) |
| Autenticação | SPF, DKIM (assinatura dupla Ed25519 + RSA), DMARC, selo ARC, DANE/TLSA e MTA-STS na entrega, ingestão de TLS-RPT |
| Filtragem | Pontuação do Rspamd com greylisting, RBLs e limite de taxa; ClamAV; Sieve |
| Entrega | Fila de saída durável no Postgres, com a escada de tentativas e os retornos da RFC 3464; relay (smarthost) para hospedagens com a porta 25 bloqueada |
| DNS | Reconciliação na Cloudflare dos registros esperados de cada domínio ativo, idempotente, com verificação de cada registro em resolvedores públicos |
| Organização | Regras Sieve criadas na interface, pastas próprias e um respondedor de férias (RFC 5230) que responde dentro da conversa e uma vez por remetente por semana |
| Identidade | Foto de perfil por usuário, com editor de recorte circular, exibida no webmail; hospedagem e validação do logo BIMI, a marca que os clientes externos exibem |
| Web | Webmail em /user/* e console de administração em /admin/*, isolados pela audiência da sessão, com dois fatores TOTP, códigos de recuperação, senhas de aplicativo, em inglês, português e espanhol |
| Sessões | As sessões do webmail duram 30 dias e se renovam enquanto são usadas; o console continua em 8 horas. Várias contas podem estar abertas ao mesmo tempo e se alternam pela barra lateral, sem sair |
| Remetentes confiáveis | Confiar num remetente tira o e-mail dele do spam e mostra sempre as imagens, guardado no servidor por caixa. "Não é spam" na pasta Spam faz as duas coisas num clique. Um From forjado não é resgatado: a entrega exige DMARC, ou SPF do domínio do From |
| Conversas | O e-mail é listado em uma linha por conversa, com quem escreveu, quantas mensagens e se você já respondeu. As mensagens se empilham no leitor e a resposta é escrita embaixo delas. Uma chave desliga |
| Leitura | Uma mensagem escrita à mão usa as cores do próprio app; uma desenhada (newsletter, recibo) mantém a folha branca. O histórico citado se recolhe, imagens remotas ficam bloqueadas até você permitir, e dá para ver uma mensagem simples como foi enviada |
| Aparência | Temas escuro (padrão) e claro, menu de comandos em Ctrl K / Cmd K, atalhos c e /, e um aplicativo instalável com ícone próprio. As fontes são servidas pelo próprio app |
| Console | Paginação e filtros no servidor, edição de usuário (papel, cota, idioma, senha, segundo fator), aliases por usuário e uma página de diagnóstico em /admin/tests |
Arquitetura
Dois ambientes de execução, divididos pelo que cada um faz melhor:
protocols(Go) — todos os listeners de protocolo, o worker de entrega, o tratamento dos certificados TLS, a reconciliação de DNS e a API de e-mail do webmail (mensagens, conversas, rascunhos). É dono da semântica de pastas, UIDs, flags e blobs, e decide, na entrega, a qual conversa uma mensagem pertence e se o e-mail de um remetente confiável pula o spam.auth(TypeScript) — logins, sessões e as contas abertas num navegador, dois fatores, remetentes confiáveis, preferências e a API do console de administração.webmail(Next.js) — as duas superfícies web e o proxy que dá ao navegador uma origem única.storage(TypeScript) — só a verificação de saúde, por enquanto.
O Postgres é a fonte da verdade de tudo, inclusive da fila de saída. O Redis é cache e índice de despacho; perdê-lo não perde nenhum e-mail.
┌──────────┐
navegador ───────▶│ webmail │──┬─▶ auth (sessões, contas, remetentes confiáveis, admin)
└──────────┘ └─▶ protocols (ler/enviar e-mail, conversas, DKIM, TLS)
│
internet ──25/587/465/143/993──────────┘
│
Postgres ─┴─ Redis ─ Rspamd ─ ClamAV
Usando o webmail
A referência é o docs/WEBMAIL.md (em inglês). Em resumo:
- Continue conectado. Uma sessão dura 30 dias e se renova a cada dia de uso. Adicione outra conta pelo menu no pé da barra lateral e alterne entre elas; sair deixa as outras abertas.
- Confie num remetente. O escudo ao lado do nome do remetente confia nele: o e-mail dele deixa de ir para o spam e as imagens carregam. Na pasta Spam, Não é spam move a mensagem para a caixa de entrada e confia no remetente.
- Leia em conversas. Uma linha por conversa; abra e responda embaixo das mensagens. Em Configurações → Idioma dá para desligar.
- Aperte
Ctrl K(ouCmd K). Pastas, Escrever, Configurações, contas e aparência, digitando.cescreve uma mensagem e/busca. - Escolha um tema (escuro, claro ou do sistema) no mesmo menu do pé da barra lateral. O escuro é o padrão.
| Tecla | |
|---|---|
Ctrl K / Cmd K |
Menu de comandos |
c |
Nova mensagem |
/ |
Buscar na lista |
O tema claro, e uma mensagem desenhada (uma newsletter, um recibo) que mantém a folha para a qual foi feita. Uma mensagem escrita à mão usa as cores do próprio app.

O menu de comandos, em Ctrl K ou Cmd K.
Implantação
Para uma VPS de verdade, siga o docs/DEPLOYMENT.md (em inglês). Ele cobre as três coisas que nenhum código faz por você — DNS reverso, a delegação DNSSEC e conseguir a porta 25 liberada — e a ordem em que o resto precisa acontecer.
A versão curta:
scripts/init-env.sh --production # .env com segredos aleatórios; lista o que definir
make preflight # confere porta 25, PTR, RBLs, escopo do token
docker compose up -d
make create-admin EMAIL=voce@exemplo.com PASSWORD='...'
make create-admin não é opcional: uma instalação nova não tem nenhuma caixa, e
todo caminho para o console precisa de uma.
Portas
Tudo abaixo é TCP. "Entrada" é o que o firewall do host e o grupo de segurança do provedor precisam permitir; "saída" é o que o host precisa conseguir alcançar.
| Porta | Direção | Quem conecta | Obrigatória? | Observações |
|---|---|---|---|---|
| 25 | entrada | Qualquer MTA remetente | Sim, para receber | Precisa estar aberta ao mundo. Tem limite de taxa e não é autenticada — é assim que o SMTP funciona. |
| 25 | saída | Este host → MTAs remotos | Sim, para enviar | A que a maioria dos provedores bloqueia. Sem ela, configure um relay (RELAY_HOST); veja docs/DEPLOYMENT.md. |
| 80 | entrada | Let's Encrypt, navegadores | Sim | Desafio ACME HTTP-01 e o redirecionamento para HTTPS. Fechá-la significa não ter certificado. |
| 443 | entrada | Navegadores | Sim | Webmail, console, política MTA-STS, autodiscover. |
| 587 | entrada | Clientes de e-mail | Recomendada | Submission com STARTTLS. A porta que os clientes devem usar. |
| 465 | entrada | Clientes de e-mail | Opcional | Submission com TLS implícito, para clientes que insistem. |
| 143 | entrada | Clientes de e-mail | Opcional | IMAP com STARTTLS. Prefira a 993. |
| 993 | entrada | Clientes de e-mail | Recomendada | IMAPS. |
| 110 / 995 | entrada | Clientes de e-mail | Opcional | POP3 / POP3S. Deixe fechada, a menos que algo precise de POP3. |
| 4190 | entrada | Clientes de e-mail | Opcional | ManageSieve, para editar filtros num cliente de desktop. |
| 53 | saída | Este host → resolvedor | Sim | Consultas de MX, SPF, DKIM, DANE e MTA-STS. UDP e TCP — respostas DNSSEC estouram o UDP. |
| 443 | saída | Este host → APIs | Se usado | Automação de DNS da Cloudflare, busca da política MTA-STS, atualização de assinaturas do ClamAV. |
As portas 110, 995 e 4190 são publicadas pelo compose, mas vale fechá-las no firewall a menos que algum cliente precise delas — cada porta aberta é mais uma coisa para manter atualizada.
O que o host precisa permitir
- DNS reverso (PTR) do IPv4 público resolvendo para
PRIMARY_MAIL_HOST, e esse nome resolvendo de volta para o mesmo endereço. Quem configura é o dono do bloco de IPs — o painel do seu provedor, nunca a Cloudflare. Gmail e Outlook mandam para o spam ou recusam e-mail sem isso. Se o host tem IPv6 público e publica um registro AAAA, ele também precisa de PTR: remetentes que conectam por IPv6 são julgados pelo reverso do IPv6. - Porta 25 de saída liberada. Peça ao provedor; um timeout, e não uma recusa, ao conectar num MX remoto é a assinatura de um bloqueio.
- Docker com o plugin compose, e um usuário no grupo
dockerou root. - Portas abaixo de 1024 são abertas pelo proxy do Docker no host, então o daemon precisa do privilégio — o padrão numa instalação normal. Os contêineres em si rodam sem privilégios.
- O diretório de certificados do proxy, só com
TLS_MODE=traefik— veja Certificados abaixo, e prefiraTLS_MODE=acmese puder. - Disco para o spool de e-mail (
protocols_spool) e o Postgres. O e-mail é guardado uma vez, com contagem de referências, mas planeje o crescimento. - Um firewall que não limite a porta 25 até a inutilidade. Alguns provedores aplicam limites de SYN agressivos que parecem falhas de entrega intermitentes.
Rodando localmente
make env # .env a partir do .env.example, com segredos aleatórios
make gen-dev-cert # certificado autoassinado para os listeners
make up # compila a partir do código pelo overlay de build
make seed # uma caixa postmaster@example.test de teste
O webmail fica em http://localhost:3000. A senha do seed é dev-password-only
— é um dado de teste, não uma credencial.
Para ver ou trabalhar na interface sem nenhum serviço de e-mail, rode a caixa
inventada de scripts/screenshots/ e aponte o
webmail para ela; o mesmo servidor de demonstração gerou as imagens acima.
Testes
make test-db-up # um Postgres descartável na :55432; imprime as duas variáveis a exportar
make test # suíte do Go, do auth e do webmail
make lint # go vet, tsc e o lint de idioma
As suítes do Go e do auth incluem testes de integração que precisam de Postgres.
Eles são pulados sem TEST_DATABASE_URL e rodam contra um banco de verdade no
CI, que é onde as regressões de segurança são garantidas — senha errada, código
TOTP errado, revogação de sessão, isolamento entre superfícies, leitura entre
caixas e, agora, sessões que se renovam, contas que se alternam, remetentes
confiáveis e a qual conversa uma resposta pertence. Uma execução verde sem a
variável pula tudo isso, por isso o make test avisa em voz alta; rode com o
banco antes de confiar nela.
A suíte do webmail é de arquivos e lógica (vitest): ela protege o sistema de
cores, o sanitizador, os modos do leitor, o desfazer e o descartar do editor, e
que toda frase exista nos três idiomas. As telas em si são conferidas num
navegador de verdade; veja docs/WEBMAIL.md.
Configuração
Cada variável está documentada em linha em
.env.example, o arquivo único para desenvolvimento e produção.
As que não têm padrão seguro (o scripts/init-env.sh gera os segredos):
| Variável | |
|---|---|
JWT_SECRET |
Assina as sessões. No mínimo 32 caracteres; o serviço se recusa a iniciar abaixo disso, em vez de cair numa constante. |
LAMBDAMAIL_MASTER_KEY |
Cifra as chaves DKIM, os segredos TOTP e os tokens de provedores. Faça backup dela separado do banco — perdê-la torna irrecuperável todo segredo guardado. |
POSTGRES_PASSWORD, REDIS_PASSWORD |
Sem padrão; o compose falha sem elas. |
MAIL_DOMAIN, PRIMARY_MAIL_HOST, PUBLIC_IPV4 |
Identidade. PRIMARY_MAIL_HOST precisa bater com o PTR de PUBLIC_IPV4. |
Certificados
Dois modos, e a escolha importa mais do que parece.
TLS_MODE=acme (recomendado). O LambdaMail obtém o próprio certificado para
PRIMARY_MAIL_HOST, mta-sts.<domínio> e autoconfig.<domínio> por DNS-01,
usando o token da Cloudflare já configurado para o DNS. Precisa de
LAMBDAMAIL_MASTER_KEY para selar as chaves da conta e do certificado, e de
ACME_EMAIL. É o único modo em que o DANE funciona, porque é o único em que a
chave do próximo certificado existe antes do próprio certificado.
TLS_MODE=traefik. Os listeners de e-mail leem o certificado que o proxy
reverso já obteve, em COOLIFY_PROXY_DIR, montado só para leitura. Duas coisas
precisam ser verdade, e nenhuma é óbvia quando não são:
- o proxy precisa ter uma rota para
PRIMARY_MAIL_HOST, ou esse nome nunca aparece noacme.jsone não existe certificado para ler; - o
acme.jsonprecisa ser legível pelo usuário não root do contêiner. Deixado no modo600de propriedade do root — como o proxy o grava — ele não é.
As duas falhas são silenciosas do mesmo jeito: o HTTPS continua funcionando, porque o proxy o serve, enquanto os listeners de e-mail caem para um certificado autoassinado e todo cliente que verifica recusa SMTP, IMAP e POP3. Parece problema de certificado e quase sempre é problema de permissão.
/admin/security informa o emissor e diz de forma direta quando o certificado é
autoassinado, que é o jeito mais rápido de distinguir os dois casos.
Fotos de perfil e BIMI
São duas coisas diferentes, e a diferença é a história toda.
Uma foto de perfil é por usuário e aparece dentro do LambdaMail. Nenhum cliente de e-mail externo a lê, e não existe protocolo que permita isso: o avatar que o Gmail ou o Outlook mostra ao lado de uma mensagem vem dos contatos do destinatário, ou da conta Google ou Microsoft do remetente. Um servidor próprio não consegue pôr uma imagem ali.
O BIMI é o que os clientes externos leem, e é por domínio: uma marca para
todos que enviam dele. Publicar uma exige três coisas, e o console
(/admin/domains, dentro do domínio) cuida de cada uma:
- O logo. Envie um PNG ou um SVG. O estúdio o converte para o que o BIMI aceita, SVG Tiny Portable/Secure numa tela quadrada de no máximo 32 KB, sem nada ativo ou externo dentro, e deixa você posicioná-lo como no editor da foto de perfil: arrastar, ampliar, cor de fundo, com uma prévia dentro do círculo ou do quadrado arredondado que os clientes desenham. De um SVG é removido o que o formato proíbe (scripts, links, bitmaps embutidos, metadados de editor) e avisa o que foi; um PNG é vetorizado, o que perde algum detalhe, então envie o SVG quando tiver. O servidor valida o resultado de novo contra todas as regras de uma vez, porque um receptor que rejeita a marca simplesmente não mostra nada.
- DMARC em
p=quarantineoup=rejectcompct=100, o que este servidor já configura. - Um certificado, para o Gmail e o Apple Mail exibirem. Um Verified Mark
Certificate (VMC, para marca registrada) ou um Common Mark Certificate (CMC,
para marca em uso) é emitido por uma autoridade certificadora (DigiCert,
Entrust, SSL.com e outras) depois que ela verifica a organização. Ele não pode
ser gerado aqui a partir de uma imagem, e nada assinado aqui seria confiável.
O que o console faz: entrega o SVG exato para enviar à autoridade, recebe o
.pemque ela devolver e recusa o que não funcionaria (o logo dentro dele precisa ser, byte a byte, o publicado; precisa cobrir o domínio, estar na validade e nunca conter uma chave privada). Depois o serve emhttps://<host de e-mail>/.well-known/bimi/vmc.peme aponta a taga=para lá. Sem certificado o registro continua válido e alguns clientes o usam; o Gmail e o Apple Mail, não.
"Verificar configuração" no painel olha o que um receptor olharia: o arquivo do logo, o DMARC, o registro BIMI no DNS, a URL pública do logo, o certificado e a URL pública dele, e aponta o que está errado.
O logo é servido em https://<host de e-mail>/.well-known/bimi/<domínio>/default.svg
e o certificado em .../<domínio>/vmc.pem: um host de e-mail, um caminho por
domínio, para que cada domínio hospedado possa publicar o seu. (O antigo
/.well-known/bimi/default.svg escolhia o domínio pelo nome do host, o que só
conseguia nomear o domínio principal; o registro de todo outro domínio apontava
para um 404. Ele ainda responde, para os registros já publicados.)
O registro de DNS acompanha a imagem. Com um token da Cloudflare configurado,
o registro TXT default._bimi.<domínio> faz parte do que o reconciliador de DNS
publica, e salvar ou remover o logo ou o certificado no console dispara essa
reconciliação sozinho ("Publicar no DNS agora" faz o mesmo sob demanda). O
registro é criado quando há um logo, atualizado quando o certificado é
adicionado e removido quando o logo é removido, mas só um registro que este
reconciliador publicou: um feito à mão é deixado em paz.
Um certificado guardado só é anunciado (a=) enquanto for válido para o logo
servido. O certificado atesta um SVG exato, então trocar o logo o encerra, e um
registro que continuasse apontando para ele faria todo receptor rejeitar o BIMI,
inclusive os que mostrariam o logo sem certificado. O registro então volta a só
l=, e o console diz por que o certificado foi retido. O painel, a verificação e
o reconciliador tomam essa decisão da mesma função (bimi.Publish), então não
podem discordar sobre ela.
Uma foto de perfil alterada aparece na hora em todo o webmail: cada foto carrega uma versão que é incrementada ao salvar, nesta aba e nas outras. O servidor deixa o navegador reaproveitar uma por um minuto e depois revalida.
DANE
Desligado por padrão, e vale entender antes de ligar: um registro TLSA que não bate com a chave em uso não degrada a entrega, ele a interrompe em todo remetente que valida. Duas coisas precisam ser verdade primeiro.
- DNSSEC na zona, com o registro DS publicado no registrador. O DANE confia no registro TLSA porque o DNSSEC o assina; sem a cadeia não há o que confiar, e os validadores tratam o registro como ausente.
TLS_MODE=acme. O LambdaMail precisa ser dono da chave para publicar a associação da próxima antes de trocar para ela. ComTLS_MODE=traefiko proxy renova no próprio ritmo e o registro deixaria de bater em silêncio.
Então defina OUTBOUND_DANE_ENABLED=true. O gerenciador publica 3 1 1 —
DANE-EE sobre o SubjectPublicKeyInfo, o único uso que um MTA receptor deveria
oferecer — e cuida da troca publicando a associação atual e a próxima juntas,
esperando a propagação antes de emitir contra a chave nova.
/admin/security mostra o digest do certificado que está de fato em uso. Esse
número e o registro TLSA no DNS precisam ser iguais; quando divergem, o e-mail é
recusado e mais nada informa o motivo:
dig +short TLSA _25._tcp.mail.exemplo.com
Operação
make preflight |
Saída pela porta 25, PTR e FCrDNS, listagens em RBLs, escopo do token da Cloudflare, presença do certificado |
make create-admin EMAIL=… PASSWORD='…' |
Primeiro administrador numa instalação nova |
make reset-password EMAIL=… PASSWORD='…' |
Recupera uma conta bloqueada e encerra as sessões dela |
docker compose logs -f protocols |
Entrega, TLS e reconciliação de DNS |
GET /health no protocols |
200 saudável, 200 com X-LambdaMail-Health: degraded quando um certificado precisa de atenção, 503 quando uma dependência caiu |
O docs/OPERATIONS.md cobre backups, rotação de certificados, o que significam os alertas e como ler uma falha de entrega.
O que não está pronto
Dito com franqueza, porque um servidor de e-mail que funciona pela metade perde e-mail:
- Painéis do console somente de leitura. O checklist guiado de entrada de domínio relata o estado em vez de conduzi-lo.
- Anexos do editor. O seletor lista os arquivos, mas nada é enviado, e o app avisa isso na tela. Ler e baixar os anexos recebidos funciona.
- Aprendizado do filtro de spam. "Não é spam" para de condenar aquele remetente, mas o Rspamd não aprende com isso, então e-mails parecidos de desconhecidos são julgados como antes.
- As conversas são agrupadas na entrega. E-mails antigos são agrupados por
assunto, e só onde existe uma resposta; uma conversa cujas respostas perderam o
Referencese o assunto pode aparecer em linhas separadas. - Não implementado. IMAP
APPEND, JMAP, CalDAV/CardDAV, multi-nó. Os backups estão documentados, mas não automatizados. - DANE vem desligado. Veja DANE para ligá-lo; com
TLS_MODE=traefikele fica desligado de propósito, porque a chave do proxy muda a cada renovação e o registro TLSA publicado deixa de bater.
Contribuindo
- Todo o código, os comentários e os identificadores são em inglês. O
make lintgarante isso (e, no código, só ASCII: escrevaé, não uma letra acentuada). - As cores vêm das variáveis do tema em
globals.css, nunca de um valor literal, e uma nova é definida nos dois temas; veja docs/WEBMAIL.md. O logo é gerado porscripts/brand/. - Migrations novas são numeradas em
migrations/e rodam no deploy. O código de entrega não pode depender de uma que talvez não tenha rodado: os testes de ponta a ponta aplicam só a primeira. - Os testes acompanham a mudança que precisa deles; as suítes acima precisam continuar verdes.
- Os commits citam a issue:
#N: o que mudou e por quê. - Uma mudança que alguém vai notar entra em Não lançado no CHANGELOG.pt-BR.md (e no equivalente em inglês). Como se lança uma versão: docs/RELEASING.md.
Licença
MIT — veja LICENSE.

