Plugin Discourse Bounce Guard

Repositório: GitHub - overgrow/discourse-bounce-guard · GitHub
Licença: MIT

&tldr; O que você obtém: logs de erro mais limpos (as tentativas de reenvio horário para endereços inativos cessam). O Discourse para de enviar e-mails que não podem ser entregues. E usuários mais felizes: eles conseguem voltar a acessar suas contas, pois um endereço inativo é substituído por um funcional a tempo.

Se seu e-mail de saída passa por um retransmissor próprio (Postfix, Exim, a maioria das configurações auto-hospedadas), você provavelmente já viu o /logs se encher de pares como este:

SMTP Error Net::SMTPServerBusy with message: 450 4.1.2 <someone@gone-domain.com>: Recipient address rejected: Domain not found
Job exception: Net::SMTPServerBusy

O domínio desapareceu e não vai voltar. Mas o retransmissor responde com um código de erro temporário (450), porque, em teoria, uma consulta DNS pode falhar momentaneamente. O Discourse interpreta qualquer erro temporário como “tente novamente em uma hora”, e o Sidekiq continua tentando por semanas. A detecção de rejeições (bounce) do núcleo nunca é acionada. Ela só reage quando uma mensagem de rejeição volta, seja como um e-mail (VERP) ou como um webhook do seu provedor de e-mail, e aqui a mensagem é recusada na hora, então nenhuma mensagem de rejeição jamais existe. A pontuação de rejeição permanece em zero, as tentativas continuam, e o usuário mantém um endereço que não pode receber nada, incluindo a redefinição de senha.

Isso já surgiu aqui algumas vezes sem uma resposta integrada, por exemplo Handling emails to non-existent domain, Deactivate user with hard bounce e How to deactivate accounts of users who are not receiving emails. Tivemos a mesma dor no nosso fórum, então criamos um plugin.

O que ele faz

O Bounce Guard se integra ao próprio caminho de envio e classifica cada rejeição SMTP:

  • Respostas 5xx com status aprimorado de destinatário inválido (5.1.1, 5.1.2, 5.2.1 e afins) são contadas como falhas definitivas (hard failures).
  • Qualquer resposta que corresponda a uma lista de frases configurável (“domain not found”, “user unknown”, …) é contada como falha definitiva, independentemente do código. Isso pega retransmissores que falham de forma temporária (soft-fail) em condições permanentes com um 450.
  • Greylisting, caixas postais cheias e limites de taxa são deixados de lado. O comportamento de reenvio do núcleo permanece intacto.

As falhas definitivas registradas então sobem uma escada:

  1. Cada uma delas alimenta a pontuação de rejeição do núcleo como uma rejeição definitiva (ativado por padrão). Após duas, o próprio limite do núcleo é atingido e o Discourse para de enviar e-mails para o usuário. O ruído nos logs termina aqui, mesmo que você nunca ative nada além disso.
  2. Após uma quantidade configurável (padrão 3) distribuída ao longo de um período configurável (padrão 48 horas, para que uma interrupção curta não desative ninguém), o plugin age. A ação padrão é log_only: uma entrada no log de ações da equipe registrando que o usuário teria sido desativado. Alterne para deactivate quando você confiar nele.
  3. A desativação direciona o usuário pelo fluxo padrão de ativação no próximo login, onde a alteração do endereço é embutida. Eles verificam um e-mail funcional e continuam com sua conta intacta. O ponto é a recuperabilidade: uma conta ativa cujo único canal de recuperação é uma caixa postal inativa é um bloqueio esperando para acontecer.

Se o seu site recebe rejeições (VERP ou webhooks), você também pode permitir que o plugin atue quando a pontuação de rejeição do núcleo passar por um nível que você escolher, mais alto do que a pontuação na qual o núcleo para de enviar. Algumas regras de segurança se aplicam em todos os lugares. Staff e bots nunca são tocados. Um e-mail travado que é reenviado a cada hora conta como uma única falha, graças a uma janela de resfriamento (cooldown). Uma falha só conta para um usuário quando o endereço rejeitado pelo servidor é o endereço atual daquele usuário. E cada falha registrada é mantida por 90 dias para que você possa verificar o que aconteceu (consulta do Data Explorer no README).

Instalação

Instalação padrão de plugin:

hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git
          - git clone https://github.com/overgrow/discourse-bounce-guard.git

Ative bounce_guard_enabled, deixe bounce_guard_action em log_only por uma ou duas semanas, revise o que ele sinaliza e depois decida sobre a desativação. A referência de configurações está no README.

Apenas no lado do servidor, sem componentes de tema ou JS. Construído e testado contra o núcleo atual (2026.8), 31 especificações, o CI executa o workflow padrão do discourse-plugin. Feedback é bem-vindo, especialmente frases de rejeição de outros retransmissores que a lista de frases padrão não pega.