Задача пришла в одну строку: письма от конкретного внешнего адреса — партнёра, скажем partner@example.org — приходят нашему сотруднику, но стабильно попадают в «Спам». Нужно, чтобы шли во «Входящие».

Я взялся и час разбирал SPF, DKIM, репутацию адреса и прогрев IP.

Всё это вполне осмысленная работа. Только она отвечала на другой вопрос.

Коротко

  • Я понял задачу как «наши исходящие попадают в спам у получателя». Она была про входящие: наш собственный rspamd считает чужие письма спамом.
  • SPF, DKIM и репутация нашего IP к этому не имеют отношения вообще.
  • Разбор начинается с заголовков письма и истории в /rspamd/, а не с догадок.
  • Вайтлистить домен отправителя целиком — рискованно, его легко подделать.

Как я прочитал задачу задом наперёд

Фраза «письма попадают в спам» имеет два прочтения, и они зеркальны.

Первое: наши письма попадают в спам у получателей. Тогда виноваты наши настройки отправки — SPF, DKIM, репутация нашего адреса. Именно это я и пошёл чинить.

Второе: чужие письма попадают в спам у нас. Тогда виноват наш антиспам, и ни одна из перечисленных настроек ни при чём.

Правильным было второе. Час работы оказался безупречным и бесполезным одновременно — я аккуратно проверял вещи, которые не могли быть причиной.

Урок формулируется в одну строку: сначала уточни направление потока. Кто отправитель, кто получатель, чей антиспам выносит решение. Пока это не сказано вслух, любая техническая работа рискует оказаться работой над другой задачей.

Я записал это себе как правило, потому что случай не уникальный. Формулировки вида «не работает почта», «не приходят письма», «попадает в спам» симметричны по смыслу, и человек, который приносит задачу, обычно описывает её со своей стороны, не уточняя направление — для него оно очевидно.

С чего начинается настоящий разбор

С фактов, а не с гипотез. У письма, попавшего в спам, причина записана прямо в нём.

Заголовки письмаX-Spamd-Result и X-Spam. Там виден итоговый score и то, из чего он сложился. Выглядит это примерно так:

X-Spamd-Result: default: False [8.10 / 15.00];
    BAYES_SPAM(5.10)[99.9%];
    R_SPF_SOFTFAIL(0.00)[~all];
    MIME_HTML_ONLY(0.20)[];
X-Spam: Yes

Читается просто: смотрим, какое правило дало больше всего баллов — оно и есть причина. В этом примере из 8,10 балла пять с лишним принёс BAYES_SPAM, то есть статистический фильтр, обученный на предыдущей почте. А R_SPF_SOFTFAIL, на который глаз цепляется первым делом, дал ноль.

Это очень наглядная иллюстрация того, почему нельзя чинить по названию правила. Увидев в заголовке слово SPF, легко пойти настраивать SPF — и потратить час на строку, которая не добавила ни балла.

История в веб-интерфейсе rspamd, по адресу /rspamd/. Там видно, какие именно правила сработали на конкретном письме.

Это и есть ответ на вопрос «почему», а не предположение о нём. Разница принципиальная: без этих данных вы будете гадать, какое из десятков правил сработало, и почти наверняка начнёте с неверного.

Варианты вайтлиста, по возрастанию охвата

Когда причина известна, дальше решается, насколько широко открывать дверь. Три уровня:

  • на уровне конкретного ящика — самый узкий;
  • на уровне домена в mailcow — шире;
  • глобальная карта вайтлиста rspamd — самый широкий.

Правило выбора простое: берите самый узкий, который решает задачу. Широкий вайтлист решит её тоже, но заодно снимет защиту там, где её никто не просил снимать.

Два узких уровня настраиваются мышкой: в настройках спам-фильтра домена в админке либо в пользовательской панели самого ящика. Самый широкий — правкой карты и перезапуском rspamd:

cd /opt/mailcow-dockerized
echo 'partner@example.org' >> data/conf/rspamd/custom/global_smtp_from_whitelist.map
docker compose restart rspamd-mailcow

Первая строка подразумевает стандартный путь установки; у меня mailcow в /opt/mailcow_data/mailcow-dockerized, так что cd правится под себя.

Отдельное предостережение: вайтлистить домен отправителя целиком — рискованно. Домен в письме подделывается легко, и такой вайтлист открывает дверь ровно тем письмам, от которых антиспам и защищает. Адрес, которому вы доверяете, становится удобной маской.

Подвох на другом конце

Бывает так: score низкий, правила не сработали, а письмо всё равно лежит в Junk.

Тогда антиспам ни при чём, и дальше копать в нём бесполезно. Смотреть надо sieve-фильтры SOGo или правила в самом почтовом клиенте. Симптом тот же — письмо в «Спаме», — а слой совсем другой.

Это второй раз за одну задачу, когда одинаковый симптом имеет разные причины. И второй раз ответ находится не рассуждением, а проверкой фактов: если score низкий, значит решение принял не антиспам, значит ищем того, кто его принял.

Длинная мера, которая работает лучше точечных

Дообучение Bayes: переносить ложные срабатывания из Junk во «Входящие».

Можно и явно, конкретным письмом:

docker compose exec -T rspamd-mailcow rspamc learn_ham < letter.eml

Это не мгновенно и не даёт немедленного эффекта на конкретное письмо. Но это единственный вариант, который учит фильтр, а не обходит его. Вайтлист — исключение из правила, дообучение — исправление самого правила.

И если вернуться к заголовку из примера выше: раз основную часть баллов дал именно BAYES_SPAM, то дообучение бьёт ровно в причину, а вайтлист — мимо неё.

Чем закончилось

Порядок действий подготовлен. Какой именно вариант в итоге применили, я утверждать не буду — у меня это не зафиксировано, а память в таких вещах не свидетель. Написать «сделали вайтлист на ящик» было бы правдоподобно и, возможно, неправдой.

Лучше честно: разобрано, порядок есть, конкретика не записана.