Descadastro em um clique que funciona de verdade, com List-Unsubscribe
Implemente o descadastro sem login nem perguntas: link assinado, cabeçalho List-Unsubscribe com um clique, lista de supressão e a regra de nunca reinscrever por outro funil.
O link "cancelar inscrição" no rodapé da newsletter abria uma tela de login. A pessoa não tinha senha, porque nunca criou conta: só tinha deixado o e-mail num formulário. Ela fechou a aba e clicou em "denunciar spam". Multiplique isso por algumas centenas e o provedor passa a desconfiar do seu domínio inteiro, inclusive do e-mail de pedido confirmado. A resposta curta: sair da lista é um clique, sem login, sem formulário, com efeito imediato. E quem saiu não volta por nenhum outro caminho.
Descadastro difícil não segura ninguém na lista. Só troca um "sair" educado por uma reclamação de spam, que custa muito mais.
Por que o descadastro precisa ser fácil
Para a pessoa, existem duas maneiras de parar de receber seus e-mails: o link de descadastro ou o botão de spam. O segundo está sempre à mão, no mesmo lugar, em todo cliente de e-mail. Se o primeiro dá trabalho, ela usa o segundo.
Para o provedor, as duas ações são muito diferentes. Descadastro é neutro. Reclamação de spam é um sinal negativo contra o remetente. Desde 2024, Google e Yahoo exigem de quem envia em grande volume o descadastro em um clique pelo cabeçalho padrão, e a orientação do Google para remetentes fala em processar o pedido em até dois dias. Cumprir isso é o mínimo; o ideal é efeito imediato.
Para a lei, a LGPD dá à pessoa o direito de revogar o consentimento, e a revogação precisa ser facilitada. Este texto não substitui orientação jurídica, mas o caminho técnico que atende à lei e ao provedor é o mesmo.
As duas portas de saída
Uma newsletter bem feita oferece duas formas de sair:
- O cabeçalho
List-Unsubscribe. Clientes como o Gmail usam esse cabeçalho para mostrar um botão "cancelar inscrição" ao lado do remetente. Com o cabeçalhoList-Unsubscribe-Post, definido na RFC 8058, o clique faz um POST direto para a sua URL, sem a pessoa sair da caixa de entrada. - O link no rodapé. Para quem procura no corpo da mensagem. Ele abre uma página do seu site.
Um detalhe que pega muita gente: filtros de segurança corporativos costumam abrir os links do e-mail antes da pessoa, para verificar se são maliciosos. Se o GET do link do rodapé já descadastra, o filtro descadastra gente que nem leu a mensagem. Por isso, o link do rodapé abre uma página com um único botão "Confirmar saída", e quem descadastra de verdade é o POST.
Como implementar passo a passo
- Gere um link assinado por inscrito e lista. Sem login: a assinatura prova que o link foi emitido por você.
- Envie os cabeçalhos de descadastro em cada mensagem da lista.
- Atenda o GET com a página de confirmação e o POST com o descadastro.
- Grave na lista de supressão. Ela é consultada antes de qualquer envio e de qualquer importação.
- Confirme na tela, sem pedir motivo. Pesquisa de motivo pode existir, opcional, depois que a saída já foi feita.
O link assinado
import crypto from 'node:crypto';
const SEGREDO = process.env.DESCADASTRO_SEGREDO;
const BASE = 'https://www.exemplo.com.br';
function assinar(inscritoId, lista) {
return crypto.createHmac('sha256', SEGREDO).update(inscritoId + ':' + lista).digest('base64url');
}
export function tokenValido(inscritoId, lista, token) {
const esperado = Buffer.from(assinar(inscritoId, lista));
const recebido = Buffer.from(String(token || ''));
return esperado.length === recebido.length && crypto.timingSafeEqual(esperado, recebido);
}
export function linkDescadastro(inscritoId, lista) {
const params = new URLSearchParams({ i: String(inscritoId), l: lista, t: assinar(inscritoId, lista) });
return BASE + '/descadastro?' + params.toString();
}
O segredo fica no ambiente, fora do repositório. A comparação usa timingSafeEqual para não vazar informação pelo tempo de resposta. O link não expõe o e-mail na URL.
Os cabeçalhos no envio
const link = linkDescadastro(inscrito.id, 'resumo-semanal');
await transporte.sendMail({
from: '"DevBras" <resumo@news.exemplo.com.br>',
to: inscrito.email,
subject: assunto,
text: corpoTexto + '\n\nPara sair desta lista: ' + link,
html: corpoHtml,
headers: {
'List-Unsubscribe': '<' + link + '>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
},
});
A RFC 8058 exige que esses dois cabeçalhos estejam cobertos pela assinatura DKIM da mensagem. Se o seu provedor assina por você, confira na documentação dele se os cabeçalhos personalizados entram na assinatura, ou se ele mesmo gera o descadastro. Muitos serviços de envio de newsletter fazem isso nativamente.
As rotas
app.get('/descadastro', (req, res) => {
const { i, l, t } = req.query;
if (!tokenValido(i, l, t)) return res.status(400).render('descadastro-invalido');
res.render('descadastro-confirmar', { i, l, t });
});
app.post('/descadastro', express.urlencoded({ extended: false }), async (req, res, next) => {
const { i, l, t } = req.query;
if (!tokenValido(i, l, t)) return res.status(400).render('descadastro-invalido');
try {
await pool.execute(
'UPDATE inscricoes SET ativo = 0, saiu_em = COALESCE(saiu_em, NOW()) WHERE inscrito_id = ? AND lista = ?',
[i, l]
);
await pool.execute(
'INSERT IGNORE INTO supressao (inscrito_id, lista, criado_em) VALUES (?, ?, NOW())',
[i, l]
);
res.render('descadastro-feito');
} catch (err) {
next(err);
}
});
A página de confirmação tem um formulário com method="post" apontando para a mesma URL, com os parâmetros na query. Assim, o botão "Confirmar saída" e o POST de um clique do cliente de e-mail caem na mesma rota. A operação é idempotente: clicar duas vezes não dá erro nem cria registro duplicado (o INSERT IGNORE depende de uma chave única em inscrito_id e lista).
Quem saiu não volta por outro funil
O descadastro mais comum de falhar não é o do link, é o do processo. A pessoa sai da newsletter, alguém importa uma planilha do evento do ano passado, e ela volta. Ou outra ferramenta de marketing tem a própria tabela de contatos e nunca soube da saída.
A regra: a lista de supressão vale para todas as ferramentas e todas as importações. Antes de qualquer envio em massa, o sistema remove os endereços suprimidos. Antes de qualquer importação, idem. E um novo comentário ou cadastro não reinscreve ninguém automaticamente, como mostra o guia sobre separar comentário e newsletter.
O e-mail transacional segue um caminho separado. Sair da newsletter não pode desligar o aviso de senha alterada ou de pedido confirmado. São listas diferentes, com regras diferentes, como explica o post sobre e-mail transacional que chega.
Erros comuns
- Exigir login para sair. Quem não tem senha usa o botão de spam, e a reputação do domínio inteiro cai.
- Descadastrar no GET do link. Filtros de segurança que pré-abrem links tiram da lista quem nunca pediu para sair.
- Formulário com motivo obrigatório. Cada campo extra é uma chance de a pessoa desistir e reclamar.
- "Sua saída será processada em até 10 dias úteis". Enquanto isso, ela recebe mais mensagens e marca como spam.
- Reinscrever por importação ou outra ferramenta. Para a pessoa, seu descadastro não funciona, e ela tem razão.
- Link sem assinatura, com o e-mail na URL. Qualquer um descadastra qualquer endereço só trocando o parâmetro.
Checklist
- O link do rodapé abre uma página sem login, com um único botão.
- As mensagens da lista têm
List-UnsubscribeeList-Unsubscribe-Post, cobertos pelo DKIM. - O POST de descadastro tem efeito imediato e é idempotente.
- O token do link é assinado e conferido com comparação de tempo constante.
- A lista de supressão é aplicada em todo envio e em toda importação.
- Sair da newsletter não afeta o e-mail transacional.
Para fechar: abra a última newsletter que o seu sistema enviou, numa janela anônima, e clique em sair. Você consegue completar sem criar conta nem digitar nada?
Referência: https://postmarkapp.com/guides/transactional-email