Tratamento de rejeições do Mandrill: suportado no núcleo, mas (AFAICT) sem documentação

O Core possui um endpoint funcional para bounces do Mandrill — POST /webhooks/mandrill em WebhooksController — que processa eventos de hard_bounce e soft_bounce, aplica hard_bounce_score / soft_bounce_score e desativa endereços que ultrapassam o bounce_score_threshold. O pareamento depende do cabeçalho message_id em X-MC-Metadata, que o Email::Sender adiciona quando o DISCOURSE_SMTP_ADDRESS é exatamente smtp.mandrillapp.com.

Estive tentando configurar isso, mas encontrei alguns problemas que achei que valem a pena compartilhar, caso ajude outros. Sei que é um caso de uso de nicho, já que o Mandrill deixou de oferecer apenas SMTP há mais de uma década e agora fornece o serviço de SMTP apenas como um complemento para o produto de e-mails transacionais do Mailchimp. No entanto, um dos meus clientes tinha exatamente essa configuração, e é por isso que estamos aqui!

1. Não está documentado em lugar nenhum que eu consiga encontrar. O tópico sobre VERP não menciona isso, o que é razoável, porque o VERP não é como o Mandrill funciona: os próprios documentos do Mailchimp afirmam que eles lidam com os bounces por conta própria, mesmo quando você configura um domínio de Return-Path personalizado, portanto, bounces por mensagem nunca chegam a um receptor de e-mail auto-hospedado. Os webhooks do provedor são o único caminho, e não há nenhuma indicação óbvia para eles para quem está começando pelo tópico sobre VERP.

2. O webhook não pode, na verdade, ser criado. O WebhooksController#mandrill retorna 406 sempre que o mandrill_authentication_key estiver vazio. Esse comportamento é intencional — é a correção para a CVE-2026-26077 (Discourse 2025.12.2 / 2026.1.1 / 2026.2.0), que encerrou a forjagem não autenticada de payloads de bounce nos endpoints do SendGrid, Mailjet, Mandrill, Postmark, SparkPost e Mailpace. Portanto, isso não é um pedido para afrouxar a segurança; é que a correção deixou o Mandrill sem uma maneira de iniciar o processo.

O Mandrill valida um novo webhook com um POST (não o HEAD para o qual o mandrill_head foi escrito), vê o 406 e se recusa a salvar o webhook — portanto, a chave de que você precisa nunca fica visível. O caminho da API falha da mesma forma:

{"status":"error","code":-98,"name":"ValidationError","message":"Unable to validate webhook URL"}

Log de origem para uma tentativa pela interface do usuário:

"POST /webhooks/mandrill HTTP/2.0" "Mandrill-Webhook/1.0" 406

Os outros provedores têm tokens de nível de conta, portanto, um administrador pode definir um segredo arbitrário antes de criar o webhook. A chave do Mandrill é emitida por webhook, apenas depois que o webhook é salvo — portanto, é um dilema (Catch-22).

Tenho certeza de que o fluxo poderia ser corrigido, talvez respondendo com 200 à primeira verificação (que não tem payload de mandrill_events), permitindo a criação no Mandrill, depois o segredo pode ser copiado manualmente para o Discourse, e estamos prontos. Ficarei feliz em abrir um PR se isso for aceitável.

3. Solução alternativa, caso alguém precise hoje. Crie o webhook contra uma URL que já retorne 200, sem gatilhos (payloads de bounce contêm endereços de destinatários, portanto, nunca aponte um webhook com gatilhos para um placeholder), depois instale a chave e mova-o:

# 1. crie contra um placeholder, sem eventos
curl -sS -X POST https://mandrillapp.com/api/1.0/webhooks/add.json \
  -H 'Content-Type: application/json' \
  -d '{"key":"<api-key>","url":"https://httpbin.org/status/200","description":"Discourse bounce handling","events":[]}'

# 2. coloque a auth_key retornada na configuração do site mandrill_authentication_key

# 3. aponte para o fórum e ative os gatilhos — isso agora passa na validação
curl -sS -X POST https://mandrillapp.com/api/1.0/webhooks/update.json \
  -H 'Content-Type: application/json' \
  -d '{"key":"<api-key>","id":<id>,"url":"https://<forum>/webhooks/mandrill","description":"Discourse bounce handling","events":["hard_bounce","soft_bounce"]}'

Testado no 2026.9.0-latest.