Feedback Loop от Mail.ru и других: как замкнуть петлю жалоб на спам и автоматически гасить жалобщиков
Зарегистрировать FBL в postmaster.mail.ru — это 5% работы. Остальные 95% — вшить токен получателя при отправке, принять ARF-отчёт через Postfix pipe, распарсить multipart/report на Python и необратимо заблокировать жалобщика до следующей рассылки. Разбираем инженерию замкнутой петли, а не клики в веб-панели.
EvilMail Team13 июля 2026 г.12 мин чтения
Большинство гайдов по Feedback Loop заканчиваются на фразе «зайдите в postmaster.mail.ru, добавьте домен и укажите адрес для жалоб». Это ровно та часть, которая ничего не решает. FBL начинает приносить пользу только когда петля замкнута: каждая жалоба на спам за секунды и необратимо гасит конкретного получателя в вашем send path. Всё, что лежит между регистрацией и блокировкой — приём ARF, парсинг multipart/report, восстановление адреса жалобщика и запись в suppression-лист — и есть настоящая работа.
Здесь два подводных камня, которые обходят стороной почти все статьи. Первый: Mail.ru, как и остальные провайдеры, вырезает адрес жалобщика из отчёта ради приватности. Без заранее вшитого токена вы узнаёте только «кто-то пожаловался», но не кто именно. Второй: приём и парсинг ARF должны быть автоматическими и идемпотентными, иначе вы будете разгребать жалобы руками и всё равно долбить тех же людей повторной рассылкой.
Разберём, как поднять это в проде — от вшивания токена при отправке до Postfix pipe и парсера на Python.
Зачем нужен FBL и что происходит с доменом без него
Complaint Feedback Loop — механизм, по которому почтовый провайдер сообщает отправителю: «пользователь нажал *Это спам* на вашем письме». Формат сообщения стандартизирован — ARF, Abuse Reporting Format (RFC 5965). Рекомендации по построению самих петель описаны в
Feedback Loop Mail.ru: приём ARF-жалоб и авто-отписка жалобщиков — EvilMail Blog
RFC 6449
.
Поштучные ARF-отчёты присылают:
Mail.ru — через postmaster.mail.ru, привязка к домену из DKIM-подписи;
Outlook/Hotmail — через JMRP (Junk Mail Reporting Program);
Yahoo/AOL — Complaint Feedback Loop с регистрацией по DKIM d=.
А вот Gmail поштучных ARF не шлёт вообще. У него только агрегированный FBL внутри Postmaster Tools — графики spam rate плюс заголовок Feedback-ID. Никаких «вот этот адрес пожаловался» от Google вы не получите.
Почему это критично. Нормальный complaint rate — ниже 0.10%. У Gmail порог, за которым включается фильтрация и блокировка, — 0.30% (виден в Postmaster Tools). Без FBL жалобщики остаются в базе, каждая новая рассылка снова летит им, они снова жмут «спам», rate растёт, репутация домена и IP деградирует лавинообразно. Один незамкнутый цикл — и вы уже в папке «Спам» у всех.
Подключение FBL Mail.ru через postmaster.mail.ru
Порядок действий в панели postmaster.mail.ru:
1.Добавить домен (например evilmail.pro) и подтвердить владение. Верификация на выбор: TXT-запись вида mailru-verification: <код>, HTML-файл в корне сайта или meta-тег на главной. Для инфраструктуры удобнее TXT:
1.Настроить SPF и DKIM до включения FBL. Это жёсткое предусловие: Mail.ru привязывает петлю к домену d= из DKIM-подписи, и без валидной подписи жалобы просто не будут вам приходить. Проверьте селектор:
bash
dig +short TXT mail._domainkey.evilmail.pro
opendkim-testkey -d evilmail.pro -s mail -vvv
1.Указать адрес-приёмник жалоб. Он должен жить на верифицированном домене — заведите отдельный [email protected]. Не сваливайте жалобы в общий ящик поддержки: ARF нужно обрабатывать программно, а не глазами.
После активации Mail.ru начинает слать ARF-отчёты на указанный адрес, а в панели появляется агрегированный процент жалоб. Панель полезна для мониторинга тренда, но реальная автоматика строится на входящих ARF.
Главная проблема: в ARF нет адреса жалобщика
Вот где ломается 90% самодельных FBL. Mail.ru, Outlook и Yahoo вырезают Original-Rcpt-To из отчёта — по требованиям приватности они не имеют права раскрывать, кто именно нажал «спам». В ARF приходит Original-Mail-From (ваш bounce-адрес), Source-IP, дата — но не адрес получателя.
Значит, идентификатор получателя надо вшить самому при отправке, так, чтобы потом восстановить его из вернувшихся заголовков. Три рабочих способа, лучше применять их вместе.
1. Кастомный X-заголовок с непубличным токеном. Не кладите сырой email в заголовки — это утечка. Кладите HMAC-SHA256 от subscriber_id, ключ хранится на сервере:
3. Feedback-ID для Gmail. Единственный способ получить от Google хоть какую-то сегментацию в Postmaster Tools. Формат — campaignid:userid:mailtype:senderid:
Feedback-ID: promo2026:list42:marketing:evilmail
Токен в X-заголовке — основной канал восстановления для Mail.ru, Yahoo и Outlook, потому что эти провайдеры возвращают исходные заголовки письма целиком.
Анатомия ARF-отчёта: разбираем изнутри
ARF — это письмо с Content-Type: multipart/report; report-type="feedback-report", состоящее из трёх MIME-частей. Понимание структуры критично, иначе парсер будет искать токен не там.
Часть message/rfc822 (иногда text/rfc822-headers, если провайдер отдаёт только заголовки) — это и есть место, где физически лежит ваш X-EvilMail-Sub. Именно её парсит скрипт.
Приём и парсинг: Postfix pipe + Python
Ключевое решение: fbl@ доставляется не в Maildir, а в скрипт. Самый короткий путь — pipe-алиас в /etc/aliases, тогда master.cf трогать не нужно:
fbl: "|/opt/fbl/process_arf.py"
После правки — newaliases. Если нужен отдельный транспорт под своим uid (чище с точки зрения прав), объявите pipe в master.cf:
fbl unix - n n - - pipe
flags=DRhu user=fbl argv=/opt/fbl/process_arf.py
Сам парсер — на стандартной библиотеке email, без внешних зависимостей. Обходим все части через walk(), вытаскиваем токен из вложенных заголовков, верифицируем и пишем в suppression:
python
#!/opt/fbl/venv/bin/python3
import sys, email, psycopg2, syslog
def valid_token(tok: str) -> bool:
# 64 hex-символа = наш HMAC-SHA256
return bool(tok) and len(tok) == 64 and all(c in '0123456789abcdef' for c in tok)
def suppress(token: str):
conn = psycopg2.connect("dbname=maildb user=mailadmin")
with conn, conn.cursor() as cur:
cur.execute(
"INSERT INTO suppression(sub_token, reason, created_at) "
"VALUES (%s,'fbl_complaint', now()) ON CONFLICT DO NOTHING",
(token,))
conn.close()
def main():
raw = sys.stdin.buffer.read()
msg = email.message_from_bytes(raw)
token = None
for part in msg.walk():
if part.get_content_type() in ('message/rfc822', 'text/rfc822-headers'):
payload = part.get_payload(decode=True)
if payload is None and part.is_multipart():
payload = part.get_payload(0).as_bytes()
inner = email.message_from_bytes(payload or b'')
token = inner.get('X-EvilMail-Sub')
if token:
break
if not valid_token(token):
syslog.syslog(syslog.LOG_WARNING, "ARF без валидного токена")
sys.exit(0) # не 75 — не хотим бесконечных повторов
suppress(token.strip())
syslog.syslog(syslog.LOG_INFO, f"FBL: подавлен {token[:12]}...")
if __name__ == '__main__':
main()
Два принципа, которые спасут вас от боли. Выходите с кодом 0 даже на мусорных, не-ARF письмах — иначе Postfix зациклит доставку (код 75, EX_TEMPFAIL, означает «повторить позже») и завалит очередь. И никогда не доверяйте `X-EvilMail-Sub` слепо: токен приходит из внешнего мира, проверяйте формат перед записью в БД.
Замыкание петли: suppression + one-click отписка
Suppression-таблица — сердце всей конструкции. Правила:
Храните хеш email (sha256) или HMAC-токен, а не сырой адрес — по требованиям приватности и на случай утечки БД.
Вставка идемпотентна: ON CONFLICT DO NOTHING (нужен уникальный индекс на sub_token). Две жалобы = одна запись.
Перед каждой отправкой — LEFT JOIN suppression и отсев совпадений. Отписанного никогда не воскрешать автоматически.
sql
SELECT r.email FROM recipients r
LEFT JOIN suppression s ON s.sub_token = r.sub_token
WHERE r.campaign_id = %s AND s.sub_token IS NULL;
И критично: жалоба через FBL, ручная отписка и хардбаунс должны падать в один и тот же механизм. Если у вас три разных списка, вы гарантированно кого-нибудь пропустите.
Отдельно — one-click отписка по RFC 8058. С 2024 года Gmail и Yahoo требуют её от bulk-отправителей. Два заголовка:
POST на URL из List-Unsubscribe должен отписывать без единого подтверждения — сразу писать токен в ту же suppression-таблицу. Многие пользователи жмут «отписаться» вместо «спам»; если отписка работает мгновенно, вы гасите будущую жалобу ещё до того, как она случится.
Complaint Feedback Loop, регистрация по DKIM d= (senders.yahooinc.com)
Поштучные ARF
Mail.ru
postmaster.mail.ru, привязка к DKIM
Поштучные ARF + агрегат в панели
Gmail
Postmaster Tools, Feedback-ID, объём ~5000+/день
Только графики spam rate, ARF нет
Практический вывод: поимённую отписку вам дают Mail.ru, Outlook и Yahoo. Gmail — ранний детектор проблемы по графику, реагировать на который приходится изменением контента и сегментации, а не подавлением конкретных адресов.
Чеклист внедрения
[ ] SPF и DKIM валидны, opendkim-testkey без ошибок — до регистрации FBL.
[ ] Домен верифицирован в postmaster.mail.ru (TXT mailru-verification).
[ ] X-EvilMail-Sub: <HMAC> вшивается в каждое исходящее письмо.
[ ] Feedback-ID добавлен для Gmail-трафика.
[ ] VERP в MAIL FROM как резервный канал восстановления.
[ ] fbl@ заведён на верифицированном домене и указан в панели.
[ ] Postfix pipe → process_arf.py, скрипт выходит с кодом 0 на мусоре.
[ ] Парсер верифицирует токен перед записью, логирует нераспознанные отчёты.
[ ] Suppression-таблица: хеши, уникальный индекс, ON CONFLICT DO NOTHING, проверка перед отправкой.
[ ] FBL, one-click отписка и хардбаунсы пишут в один список.
[ ] JMRP (Outlook) и CFL (Yahoo) зарегистрированы, ARF летят в тот же process_arf.py.
Что мониторить ежедневно: complaint rate отдельно по каждому провайдеру (цель — ниже 0.1%, тревога — приближение к 0.3%), размер suppression-листа, задержку обработки ARF и — самое важное — долю нераспознанных отчётов, где токен не нашёлся. Растущий процент «токен не найден» означает, что где-то в send path письма уходят без X-EvilMail-Sub, и вы снова слепнете. Именно эта метрика первой показывает, что петля начала размыкаться.