Gestión de rebotes de Mandrill: compatible con el núcleo pero (AFAICT) sin documentación

Core tiene un punto de finalización (endpoint) funcional para rebot de Mandrill: POST /webhooks/mandrill en WebhooksController, que procesa eventos de hard_bounce y soft_bounce, aplica hard_bounce_score / soft_bounce_score y deshabilita las direcciones que superan el bounce_score_threshold. El emparejamiento se basa en la cabecera message_id de X-MC-Metadata que Email::Sender añade cuando DISCOURSE_SMTP_ADDRESS es exactamente smtp.mandrillapp.com.

Estaba intentando configurar esto pero me encontré con algunos problemas que pensé que valía la pena compartir por si ayudan a otros. Sé que es un caso de uso de nicho, ya que Mandrill dejó de ofrecer “solo SMTP” hace más de una década, y ahora solo proporciona el servicio SMTP como un complemento al producto de correo transaccional de Mailchimp. Sin embargo, uno de mis clientes tenía exactamente esta configuración, ¡así que aquí estamos!

1. No está documentado en ningún lugar que pueda encontrar. El tema de VERP no lo menciona, lo cual es razonable, porque VERP no es cómo funciona Mandrill: la propia documentación de Mailchimp indica que gestiona los rebotes por sí mismo incluso cuando configuras un dominio Return-Path personalizado, por lo que los rebotes por mensaje nunca llegan a un receptor de correo autoalojado. Los webhooks del proveedor son la única vía, y no hay ninguna referencia obvia a ellos para quien empiece desde el tema de VERP.

2. El webhook no se puede crear realmente. WebhooksController#mandrill devuelve 406 siempre que mandrill_authentication_key esté vacío. Ese comportamiento es intencional: es la corrección para CVE-2026-26077 (Discourse 2025.12.2 / 2026.1.1 / 2026.2.0), que cerró la falsificación no autenticada de cargas de rebote en los puntos de finalización de SendGrid, Mailjet, Mandrill, Postmark, SparkPost y Mailpace. Por lo tanto, esto no es una solicitud para relajar la seguridad; el problema es que la corrección dejó a Mandrill sin forma de inicializarse.

Mandrill valida un webhook nuevo con un POST (no el HEAD para el que estaba escrito mandrill_head), ve el 406 y se niega a guardar el webhook: por lo tanto, la clave que necesitas nunca es visible. La ruta de la API falla de la misma manera:

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

Registro de origen para un intento desde la interfaz de usuario:

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

Los otros proveedores tienen tokens a nivel de cuenta, por lo que un administrador puede establecer un secreto arbitrario antes de crear el webhook. La clave de Mandrill se emite por webhook, solo después de que el webhook se guarda: por lo tanto, es un dilema (Catch-22).

Estoy bastante seguro de que el flujo podría corregirse, quizás respondiendo 200 a la primera verificación (que no tiene carga de mandrill_events), permitiendo la creación en Mandrill, luego la clave se puede copiar manualmente a Discourse, y listo. Estoy dispuesto a abrir una PR si eso fuera aceptable.

3. Solución temporal, si alguien la necesita hoy. Crea el webhook contra una URL que ya devuelva 200, sin disparadores (las cargas de rebote contienen direcciones de destinatarios, por lo que nunca apuntes un webhook con disparadores a un marcador), luego instala la clave en Discourse, y una vez hecho eso mueve la URL del webhook:

# 1. crea contra un marcador, sin 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. coloca la auth_key devuelta en la configuración del sitio mandrill_authentication_key

# 3. redirige al foro y habilita los disparadores - esto ahora pasa la validación
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"]}'

Probado en 2026.9.0-latest.