Плагин Discourse Bounce Guard

Репозиторий: GitHub - overgrow/discourse-bounce-guard · GitHub
Лицензия: MIT

&tldr; Что вы получаете: более чистый журнал ошибок (ежечасные повторные попытки отправки на несуществующие адреса прекращаются). Discourse перестаёт отправлять почту, которая не может быть доставлена. И довольные пользователи: они могут вернуться в свой аккаунт, потому что мёртвый адрес вовремя заменяется на рабочий.

Если исходящая почта вашего сайта проходит через собственный ретранслятор (Postfix, Exim, большинство self-hosted конфигураций), вы, вероятно, видели, как /logs заполняется парами записей такого вида:

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

Домена больше нет, и он не вернётся. Но ретранслятор отвечает временным кодом ошибки (450), потому что теоретически поиск в DNS может временно не удалиться. Discourse интерпретирует любую временную ошибку как «повторить через час», и Sidekiq продолжает попытки в течение недель. Встроенное обнаружение отскоков (bounce detection) в ядре никогда не срабатывает. Оно реагирует только тогда, когда возвращается сообщение об отскоке — либо в виде электронной почты (VERP), либо в виде вебхука от вашего почтового провайдера. Здесь же сообщение отклоняется на месте, поэтому сообщение об отскоке никогда не существует. Балл отскока остаётся нулевым, повторные попытки продолжаются, и пользователь сохраняет адрес, который не может принимать ничего, включая сброс пароля.

Это поднималось здесь несколько раз без встроенного решения, например в темах Handling emails to non-existent domain, Deactivate user with hard bounce и How to deactivate accounts of users who are not receiving emails. У нас на форуме была та же проблема, поэтому мы создали плагин.

Что он делает

Bounce Guard подключается к самому пути отправки и классифицирует каждый SMTP-отказ:

  • Ответы 5xx с расширенным статусом «неверный получатель» (5.1.1, 5.1.2, 5.2.1 и подобные) считаются жёсткими сбоями.
  • Любой ответ, соответствующий настраиваемому списку фраз («domain not found», «user unknown» и т. д.), считается жёстким сбоем независимо от кода. Это ловит ретрансляторы, которые помечают постоянные условия как временные с кодом 450.
  • Greylisting, полные почтовые ящики и ограничения частоты остаются без изменений. Поведение ядра при повторных попытках не затрагивается.

Затем зафиксированные жёсткие сбои проходят по ступеням:

  1. Каждый из них добавляется в балл отскока ядра как жёсткий отскок (включено по умолчанию). После двух срабатывает собственный порог ядра, и Discourse перестаёт отправлять письма этому пользователю. Шум в журнале прекращается здесь, даже если вы ничего дополнительно не включите.
  2. После достижимого количества сбоев (по умолчанию 3) в течение настраиваемого периода (по умолчанию 48 часов, чтобы кратковременный сбой не выключал никого), плагин действует. Действие по умолчанию — log_only: в журнале действий персонала записывается, что пользователь был бы деактивирован. Переключитесь на deactivate, когда будете уверены в этом.
  3. Деактивация направляет пользователя через стандартный процесс активации при следующем входе, где смена адреса встроена в процесс. Они подтверждают рабочий адрес и продолжают пользоваться аккаунтом без изменений. Смысл в возможности восстановления: активный аккаунт, единственным каналом восстановления для которого является мёртвый почтовый ящик, — это заманивание в ловушку (lockout), готовое случиться.

Если ваш сайт получает отскоки (VERP или вебхуки), вы также можете позволить плагину действовать, когда балл отскока ядра превысит уровень, который вы выберете (выше того уровня, на котором ядро перестаёт отправлять). Везде действуют несколько правил безопасности. Персонал и боты никогда не затрагиваются. Одна застрявшая почта, которая повторяется каждый час, считается одним сбоем благодаря окну охлаждения (cooldown window). Сбой учитывается для пользователя только в том случае, если адрес, отклонённый сервером, является текущим адресом этого пользователя. И каждый зафиксированный сбой хранится в течение 90 дней, чтобы вы могли проверить, что произошло (запрос Data Explorer описан в README).

Установка

Стандартная установка плагина:

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

Включите bounce_guard_enabled, оставьте bounce_guard_action на log_only на одну-две недели, просмотрите, что он помечает, а затем решите вопрос о деактивации. Справочник по настройкам находится в README.

Только серверная часть, без тем или JS-компонентов. Собран и протестирован против текущего ядра (2026.8), 31 спецификация, CI запускает стандартный workflow discourse-plugin. Будем рады обратной связи, особенно формулировкам отказов от других ретрансляторов, которых нет в списке фраз по умолчанию.