Garantindo Entrega de E-mails: Deliverability de Ponta a Ponta
SPF, DKIM e DMARC configurados no DNS, Multipart/Alternative obrigatório, separação de streams transacional e marketing, e como monitorar reputação de domínio antes de cair no spam.
Você implementou o fluxo de recuperação de senha, testou localmente, deployou em produção — e então percebeu que nenhum usuário recebe o e-mail. Ou pior: recebem no spam. Isso não é falha do código, é falha de deliverability (entregabilidade): a capacidade que seus e-mails têm de chegar na caixa de entrada e não serem filtrados.
Deliverability é uma disciplina com múltiplas camadas: autenticação de domínio (SPF, DKIM, DMARC), reputação do IP, estrutura do HTML, proporção texto/imagem, separação de streams e monitoramento contínuo. Este artigo cobre cada uma dessas camadas com implementações concretas.
Autenticação de Domínio: SPF, DKIM e DMARC
Esses três registros DNS são a base da reputação de domínio. Sem eles, provedores como Gmail e Outlook tratam seu e-mail como suspeito por padrão.
- SPF (Sender Policy Framework) — registro TXT que lista quais servidores estão autorizados a enviar e-mails em nome do seu domínio. Se um servidor não listado envia, o destinatário pode rejeitar.
- DKIM (DomainKeys Identified Mail) — adiciona uma assinatura criptográfica em cada e-mail. O destinatário verifica que o e-mail não foi adulterado em trânsito usando a chave pública no DNS.
- DMARC (Domain-based Message Authentication) — define a política: o que fazer se SPF ou DKIM falhar (
none,quarantineoureject). Também envia relatórios para você sobre tentativas de uso indevido do domínio.
; SPF: apenas servidores listados podem enviar por este domínio
; 'include:amazonses.com' autoriza o AWS SES
; '-all' rejeita qualquer outro remetente (recomendado)
seuapp.com. TXT "v=spf1 include:amazonses.com -all"
; DKIM: o AWS SES gera as chaves e fornece este registro
; O seletor (ex: 'abc123') é fornecido pelo seu ESP
abc123._domainkey.seuapp.com. TXT "v=DKIM1; k=rsa; p=MIGf..."
; DMARC: política e endereço para relatórios
; p=quarantine envia para spam; p=reject bloqueia
; rua= endereço que recebe relatórios diários em XML
_dmarc.seuapp.com. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@seuapp.com; pct=100"
; Para e-mails transacionais, considere um subdomínio exclusivo:
; auth.seuapp.com — isola a reputação transacional do marketingComece com p=none no DMARC para monitorar sem bloquear nada. Analise os relatórios XML por 2-4 semanas (use o DMARC Analyzer ou dmarcian para visualizar), confirme que todo tráfego legítimo passa, depois migre para p=quarantine e finalmente p=reject.
Multipart/Alternative: A Obrigatoriedade do Plain Text
E-mails enviados apenas com HTML são penalizados pelos filtros de spam. O formato correto é Multipart/Alternative: um único e-mail que contém tanto a versão HTML quanto a versão Plain Text. O cliente de e-mail do destinatário escolhe qual renderizar. Filtros de spam examinam ambas.
import nodemailer, { type Transporter } from 'nodemailer';
import { SES } from '@aws-sdk/client-ses';
import { convert } from 'html-to-text'; // npm install html-to-text
interface SendMailDTO {
to: string | Array<string>;
subject: string;
html: string;
// text é gerado automaticamente a partir do HTML se não fornecido
text?: string;
replyTo?: string;
attachments?: Array<{ filename: string; content: Buffer }>;
}
export class SESMailProvider implements IMailProvider {
private transporter: Transporter;
constructor() {
const ses = new SES({
region: process.env.AWS_REGION ?? 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
},
});
this.transporter = nodemailer.createTransport({
SES: { ses, aws: { SES } },
// Limita a 14 envios/segundo (limite do SES no tier padrão)
// Evita throttling com 429 Too Many Requests
sendingRate: 14,
});
}
async send(dto: SendMailDTO): Promise<void> {
// Gera Plain Text a partir do HTML automaticamente se não fornecido
const textContent = dto.text ?? convert(dto.html, {
wordwrap: 80,
selectors: [
{ selector: 'a', options: { hideLinkHrefIfSameAsText: true } },
{ selector: 'img', format: 'skip' }, // Ignora imagens no text
{ selector: 'h1', options: { uppercase: false } },
{ selector: 'h2', options: { uppercase: false } },
],
});
await this.transporter.sendMail({
from: `"${process.env.MAIL_FROM_NAME}" <${process.env.MAIL_FROM_ADDRESS}>`,
to: Array.isArray(dto.to) ? dto.to.join(', ') : dto.to,
subject: dto.subject,
html: dto.html, // Versão rich HTML
text: textContent, // Fallback Plain Text (obrigatório)
replyTo: dto.replyTo,
attachments: dto.attachments,
headers: {
// X-SES-CONFIGURATION-SET: associa ao configuration set para tracking
'X-SES-CONFIGURATION-SET': process.env.AWS_SES_CONFIG_SET ?? '',
},
});
}
}Estrutura HTML de E-mail: Boas Práticas
HTML de e-mail não é HTML de site. Clientes de e-mail como Outlook 2019 usam o motor de renderização do Word — sem Flexbox, sem Grid, sem variáveis CSS. As regras são:
- Use tabelas para layout —
<table>,<tr>,<td>. Sim, como em 2004. É o único layout que funciona em todos os clientes. - CSS inline — estilos em
style="..."diretamente nos elementos. Muitos clientes removem<style>e<head>. - Largura máxima de 600px — e-mails acima disso quebram em mobile.
- Proporção texto/imagem — e-mails com mais de 60-70% de imagens são suspeitos. Mantenha texto real no HTML.
- Alt text em todas as imagens — muitos clientes bloqueiam imagens por padrão; o
alté o que o usuário vê.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{subject}}</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
<!-- Container principal -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f5;">
<tr>
<td align="center" style="padding: 24px 16px;">
<!-- Card do e-mail -->
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; max-width: 600px; width: 100%;">
<!-- Header com logo -->
<tr>
<td style="padding: 32px 40px 24px; border-bottom: 1px solid #e4e4e7;">
<img src="{{logoUrl}}" alt="Logo {{appName}}" width="120" style="display: block;">
</td>
</tr>
<!-- Conteúdo principal -->
<tr>
<td style="padding: 32px 40px;">
<h1 style="margin: 0 0 16px; font-size: 24px; font-weight: 700; color: #18181b;">{{title}}</h1>
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.6; color: #52525b;">{{body}}</p>
<!-- CTA Button -->
<table cellpadding="0" cellspacing="0">
<tr>
<td style="background-color: #6366f1; border-radius: 6px;">
<a href="{{ctaUrl}}" style="display: inline-block; padding: 14px 28px; color: #ffffff; font-size: 16px; font-weight: 600; text-decoration: none;">{{ctaText}}</a>
</td>
</tr>
</table>
<p style="margin: 24px 0 0; font-size: 13px; color: #a1a1aa;">Se o botão não funcionar, copie e cole este link no navegador:<br>
<a href="{{ctaUrl}}" style="color: #6366f1;">{{ctaUrl}}</a>
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 24px 40px; border-top: 1px solid #e4e4e7;">
<p style="margin: 0; font-size: 13px; color: #a1a1aa;">Este e-mail foi enviado por {{appName}}. Se você não solicitou isso, ignore esta mensagem.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>Separação de Streams: Transacional vs Marketing
Esta é a prática mais ignorada e de maior impacto. E-mails transacionais (confirmação de conta, recuperação de senha, recibos) e e-mails de marketing (newsletters, promoções) devem usar domínios diferentes — ou ao mínimo, subdomínios diferentes:
- `noreply@auth.seuapp.com` — apenas e-mails transacionais. Altíssima prioridade de entrega. Volume baixo e previsível. Nunca deve ter unsubscribe.
- `contato@marketing.seuapp.com` — newsletters e campanhas. Volume alto, taxas de abertura variáveis, reclamações de spam mais comuns.
O motivo é crítico: a reputação é por domínio. Se uma campanha de marketing gerar muitas marcações de spam, a reputação do domínio cai. Se esse domínio é o mesmo que envia recuperações de senha, seus usuários passam a não receber e-mails críticos. A separação protege o stream transacional do variável comportamento do marketing.
Monitoramento de Reputação com AWS SES
O AWS SES oferece Configuration Sets com rastreamento de eventos (bounces, complaints, opens, clicks). Configurar isso corretamente é essencial para não ter sua conta suspensa:
import type { Request, Response } from 'express';
// O SES envia notificações via SNS (Simple Notification Service)
// Configure SNS Topic → HTTP endpoint → sua API
interface SESNotification {
notificationType: 'Bounce' | 'Complaint' | 'Delivery';
bounce?: {
bounceType: 'Permanent' | 'Transient';
bouncedRecipients: Array<{ emailAddress: string }>;
};
complaint?: {
complainedRecipients: Array<{ emailAddress: string }>;
};
}
export class SESWebhookController {
async handle(req: Request, res: Response): Promise<void> {
// SNS envia um JSON com Message como string
const snsMessage = JSON.parse(req.body.Message ?? '{}') as SESNotification;
switch (snsMessage.notificationType) {
case 'Bounce':
if (snsMessage.bounce?.bounceType === 'Permanent') {
// Bounce permanente: e-mail inválido ou inexistente
// CRÍTICO: remova imediatamente da lista de envios
// Continuar enviando para bounces permanentes = suspensão da conta SES
const emails = snsMessage.bounce.bouncedRecipients.map((r) => r.emailAddress);
await this.emailBlocklistService.addMany(emails, 'permanent_bounce');
}
break;
case 'Complaint':
// Usuário marcou o e-mail como spam
// Remova imediatamente e nunca mais envie marketing para este endereço
const complaintEmails = snsMessage.complaint?.complainedRecipients.map(
(r) => r.emailAddress
) ?? [];
await this.emailBlocklistService.addMany(complaintEmails, 'spam_complaint');
break;
case 'Delivery':
// Entregue com sucesso — pode usar para métricas
break;
}
res.status(200).send();
}
}O AWS SES suspende contas automaticamente quando a taxa de bounce permanente supera 5% ou a taxa de reclamação de spam supera 0.1%. Sem o webhook de bounce processado corretamente, uma lista suja pode resultar em suspensão em horas. Isso é inegociável — trate bounces e complaints no dia zero.
Aquecimento de Domínio (IP Warm-up)
Se você acabou de configurar um domínio/IP novo para envio de e-mails, não dispare 100.000 e-mails no primeiro dia. Provedores como Gmail e Outlook desconfiam de IPs/domínios que surgem do nada com alto volume. O processo de aquecimento é gradual:
- Semana 1: 200-500 e-mails/dia — apenas para usuários engajados (abriram e-mails recentemente)
- Semana 2: 1.000-2.000/dia — expanda para usuários ativos dos últimos 30 dias
- Semana 3: 5.000-10.000/dia — continue expandindo mantendo métricas saudáveis
- Semana 4+: volume de produção — se bounces < 2% e complaints < 0.08%
Ferramentas de Teste e Diagnóstico
- Mail-tester.com — envia um e-mail de teste e recebe um score de entregabilidade de 1 a 10, com diagnóstico de cada problema
- MXToolbox — verifica SPF, DKIM, DMARC, blacklists e configuração do servidor
- Google Postmaster Tools — monitora reputação de domínio e IP especificamente no Gmail (gratuito, essencial)
- Litmus ou Email on Acid — renderiza seu e-mail em 90+ clientes de e-mail diferentes para detectar problemas de layout
Conclusão
Deliverability é uma camada de infraestrutura tão importante quanto o banco de dados ou o servidor. SPF + DKIM + DMARC configuram a autenticidade. Multipart/Alternative garante que filtros e clientes variados recebam o conteúdo corretamente. A separação de streams protege o fluxo crítico do transacional. E o monitoramento de bounces e complaints é o que mantém a reputação saudável a longo prazo. Um sistema de e-mail robusto não é construído em um dia, mas os danos de ignorar deliverability aparecem em minutos.