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.1e 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:
- 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.
- 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 paradeactivatequando você confiar nele. - 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.