Voltar para Artigos
Back-end14 min de leitura

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.

12 de agosto de 2026

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, quarantine ou reject). Também envia relatórios para você sobre tentativas de uso indevido do domínio.
Configuração DNS (zona: seuapp.com)
; 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 marketing

Comece 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.

SESMailProvider.ts
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ê.
base-email.html
<!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:

SESWebhookController.ts
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:

  1. Semana 1: 200-500 e-mails/dia — apenas para usuários engajados (abriram e-mails recentemente)
  2. Semana 2: 1.000-2.000/dia — expanda para usuários ativos dos últimos 30 dias
  3. Semana 3: 5.000-10.000/dia — continue expandindo mantendo métricas saudáveis
  4. 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.