uCheckeruChecker
14 мин чтения

Email-валидация на уровне инфраструктуры: milter, Postfix и входящая фильтрация

Большинство руководств по валидации email описывают проверку на стороне приложения: форма регистрации, API-вызов, cron-задача по списку. Но есть другой подход - перехватывать и фильтровать адреса прямо на уровне почтового сервера, ещё до того как письмо попадёт в очередь доставки. Postfix, milter-протокол и policy-сервисы позволяют это сделать.


Зачем фильтровать на уровне MTA

Postfix обрабатывает SMTP-сессию до того, как письмо попадёт в приложение. Это принципиальная разница. Если вы отклоняете письмо на этапе RCPT TO, отправляющий сервер получает 5xx-ответ и сам генерирует bounce. Ваш сервер не тратит ресурсы на приём тела сообщения, не записывает его на диск, не передаёт в Dovecot или приложение.

Для исходящей почты логика та же. Если ваше приложение передаёт Postfix адрес, который заведомо невалиден, milter может отклонить сообщение до отправки. Bounce rate не растёт, репутация домена не страдает, очередь не забивается undeliverable-письмами.

Три инструмента, которые это обеспечивают: smtpd_recipient_restrictions (встроенные проверки Postfix), policy server (внешний демон, к которому Postfix обращается по протоколу запрос-ответ) и milter (mail filter - полноценный фильтр, работающий на любом этапе SMTP-сессии). Разберём каждый.

Встроенные restriction-классы Postfix

Postfix из коробки умеет отклонять письма по ряду критериев. Минимальная конфигурация, которая отсекает значительную часть мусора:

# /etc/postfix/main.cf

smtpd_recipient_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_unauth_destination,
    reject_invalid_helo_hostname,
    reject_non_fqdn_sender,
    reject_non_fqdn_recipient,
    reject_unknown_sender_domain,
    reject_unknown_recipient_domain,
    reject_rbl_client zen.spamhaus.org,
    permit

Что здесь происходит:

  • reject_invalid_helo_hostname - отклоняет соединения, где HELO/EHLO содержит синтаксически невалидное имя хоста. Отсекает ботнеты, которые не утруждают себя корректным приветствием.
  • reject_non_fqdn_sender и reject_non_fqdn_recipient - требуют полностью квалифицированный домен в адресах отправителя и получателя. Адрес без домена или с точкой в конце - отклонён.
  • reject_unknown_sender_domain - проверяет, что у домена отправителя есть MX или A-запись. Если домен не существует, письмо отклоняется.
  • reject_rbl_client zen.spamhaus.org - проверяет IP отправителя по DNSBL. Spamhaus ZEN объединяет SBL, XBL, PBL и CSS - это самый широко используемый блеклист.

Этот набор правил работает на этапе SMTP-конверта, до приёма тела сообщения. Быстро, дёшево, эффективно. Но у него есть потолок: restriction-классы оперируют только данными SMTP-сессии - IP, HELO, MAIL FROM, RCPT TO. Они не видят заголовки и тело письма, не могут вызвать внешний API, не умеют принимать решения на основе сложной логики.

Postfix policy server: внешняя логика проверки

Policy server - это отдельный процесс, который Postfix вызывает через Unix-сокет или TCP-соединение. Протокол текстовый: Postfix отправляет набор атрибутов (sender, recipient, client_address, helo_name и др.), policy server отвечает одним из действий: DUNNO (не знаю, пусть решает следующее правило), OK (принять), REJECT (отклонить) или DEFER_IF_PERMIT (отложить).

Подключение в main.cf:

# /etc/postfix/main.cf

smtpd_recipient_restrictions =
    ...
    check_policy_service unix:private/policy-spf,
    check_policy_service inet:127.0.0.1:10040,
    ...

Первая строка подключает SPF-проверку через Unix-сокет (например, pypolicyd-spf). Вторая - произвольный policy server на TCP-порту 10040. Это может быть ваш собственный демон, который проверяет адрес получателя по внешней базе или API.

Минимальный policy server на Python, который отклоняет одноразовые адреса:

#!/usr/bin/env python3
"""Postfix policy server: отклоняет disposable-домены."""

import socketserver
import sys

DISPOSABLE_DOMAINS = {
    "tempmail.com", "guerrillamail.com", "throwaway.email",
    "mailinator.com", "yopmail.com", "trashmail.com",
    # ... загружайте из файла или БД
}

class PolicyHandler(socketserver.StreamRequestHandler):
    def handle(self):
        attrs = {}
        while True:
            line = self.rfile.readline().decode().strip()
            if not line:
                break
            if "=" in line:
                key, value = line.split("=", 1)
                attrs[key] = value

        recipient = attrs.get("recipient", "")
        domain = recipient.rsplit("@", 1)[-1].lower() if "@" in recipient else ""

        if domain in DISPOSABLE_DOMAINS:
            action = f"REJECT disposable address rejected: {domain}"
        else:
            action = "DUNNO"

        self.wfile.write(f"action={action}\n\n".encode())

if __name__ == "__main__":
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 10040), PolicyHandler)
    server.serve_forever()

Policy server вызывается на каждый RCPT TO. Это значит, что для письма с десятью получателями Postfix сделает десять запросов. Если ваш policy server обращается к внешнему API - обязательно кэшируйте результаты. Без кэша при всплеске трафика policy server станет узким местом, и Postfix начнёт отвечать 4xx (временная ошибка) на все входящие соединения.

Ограничение: policy server работает только с данными SMTP-конверта. Он не видит заголовки, тело, вложения. Для полноценной фильтрации по содержимому нужен milter.

Milter: полный контроль над сообщением

Milter (mail filter) - протокол, изначально разработанный для Sendmail и поддерживаемый Postfix начиная с версии 2.3. В отличие от policy server, milter получает доступ к каждому этапу SMTP-сессии: CONNECT, HELO, MAIL FROM, RCPT TO, заголовки, тело сообщения. Может модифицировать заголовки, менять получателей, добавлять тело - или отклонить сообщение целиком.

OpenDKIM, OpenDMARC, SpamAssassin (через spamass-milter), ClamAV (через clamav-milter) - всё это milter-приложения. Вы можете написать своё.

Подключение milter в Postfix

# /etc/postfix/main.cf

# Milter для входящей почты (smtpd)
smtpd_milters =
    unix:opendkim/opendkim.sock,
    unix:opendmarc/opendmarc.sock,
    inet:127.0.0.1:10050

# Milter для исходящей почты (не-SMTP, например sendmail -t)
non_smtpd_milters =
    unix:opendkim/opendkim.sock,
    inet:127.0.0.1:10050

# Что делать, если milter недоступен
milter_default_action = tempfail

# Версия протокола (6 - текущая)
milter_protocol = 6

Параметр milter_default_action критически важен. Если milter упал или не отвечает, tempfail вернёт отправителю 4xx - временную ошибку. Отправляющий сервер повторит попытку позже. Если поставить accept, при падении milter вся почта пойдёт без фильтрации. На продакшене это неприемлемо.

Пример milter на Python (pymilter)

Библиотека pymilter предоставляет Python-обёртку над libmilter. Пример: milter, который проверяет RCPT TO через HTTP API и отклоняет невалидные адреса.

#!/usr/bin/env python3
"""Milter для валидации получателей через внешний API."""

import Milter
import requests
from functools import lru_cache

API_URL = "https://api.uchecker.net/api/v1/validate/single"
API_KEY = "YOUR_API_KEY"

@lru_cache(maxsize=10000)
def check_recipient(email: str) -> bool:
    """Возвращает True, если адрес стоит принимать."""
    try:
        resp = requests.post(
            API_URL,
            json={"email": email},
            headers={"x-api-key": API_KEY},
            timeout=2,
        )
        data = resp.json()
        # result "invalid" или risk_score выше порога -> отклонить
        if data.get("result") == "invalid":
            return False
        if data.get("risk_score", 0) > 0.8:
            return False
        return True
    except Exception:
        # При ошибке API - пропускаем (fail open)
        return True

class ValidateRcptMilter(Milter.Base):
    def __init__(self):
        self.id = Milter.uniqueID()

    def envrcpt(self, to, *params):
        recipient = to.strip("<>")
        if not check_recipient(recipient):
            self.setreply("550", "5.1.1", f"{recipient}: recipient rejected")
            return Milter.REJECT
        return Milter.CONTINUE

if __name__ == "__main__":
    Milter.factory = ValidateRcptMilter
    Milter.runmilter("validate-rcpt", "inet:127.0.0.1:10050", timeout=10)

Обратите внимание на несколько вещей. Первое: lru_cache на 10 000 записей. Без кэша каждый RCPT TO порождает HTTP-запрос, а при высокой нагрузке это десятки тысяч запросов в минуту. Кэш можно заменить на Redis или memcached для распределённых систем.

Второе: тайм-аут 2 секунды на API-вызов. Если milter не ответит Postfix в течение milter_connect_timeout (по умолчанию 30s), соединение будет разорвано. Держите тайм-ауты короткими. Если API не отвечает за 2 секунды, лучше пропустить письмо, чем блокировать SMTP-сессию.

Третье: Milter.REJECT vs Milter.TEMPFAIL. REJECT возвращает 5xx - постоянный отказ, отправитель не будет повторять. TEMPFAIL возвращает 4xx - отправитель повторит попытку. Для невалидных адресов используйте REJECT. Для временных сбоев (API недоступен) - TEMPFAIL или CONTINUE (fail open, зависит от вашей стратегии).

Архитектура: как это работает вместе

На практике milter и policy server используются одновременно. Каждый решает свою задачу.

Типичная цепочка для входящей почты:

  1. smtpd_recipient_restrictions - отсекает явный мусор: несуществующие домены, невалидный синтаксис, IP из блеклистов. Работает до приёма тела письма.
  2. Policy server (SPF) - проверяет, авторизован ли отправляющий IP для этого домена. pypolicyd-spf или postfix-policyd-spf-python.
  3. Milter: OpenDKIM - проверяет DKIM-подпись. Добавляет заголовок Authentication-Results.
  4. Milter: OpenDMARC - проверяет DMARC-политику на основе результатов SPF и DKIM.
  5. Milter: валидация получателя - ваш кастомный milter, который проверяет адрес получателя по внешнему сервису или локальной базе.
  6. Milter: антиспам - SpamAssassin или rspamd, анализирует содержимое.

Для исходящей почты цепочка короче, но принцип тот же:

# Submission (порт 587) - отдельный milter-набор
# /etc/postfix/master.cf

submission inet n       -       y       -       -       smtpd
  -o smtpd_milters=unix:opendkim/opendkim.sock,inet:127.0.0.1:10050
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

Здесь milter на порту 10050 - валидатор получателей исходящих писем. Приложение отправляет письмо через submission, milter проверяет адрес до отправки. Невалидный адрес отклоняется с 550, приложение получает ошибку и может обработать её - пометить контакт, уведомить пользователя, записать в лог.

Access maps: статические списки

Если не нужна динамическая проверка, Postfix поддерживает статические таблицы для блокировки доменов и адресов:

# /etc/postfix/main.cf
smtpd_recipient_restrictions =
    ...
    check_recipient_access hash:/etc/postfix/recipient_reject,
    ...
# /etc/postfix/recipient_reject
# Формат: паттерн    действие

tempmail.com          REJECT disposable domain
guerrillamail.com     REJECT disposable domain
mailinator.com        REJECT disposable domain
throwaway.email       REJECT disposable domain

После редактирования файла:

postmap /etc/postfix/recipient_reject
postfix reload

Access maps работают мгновенно - это хеш-таблица в памяти. Подходит для чёрных списков disposable-доменов, блокировки конкретных адресов, white-листов VIP-отправителей. Но у подхода есть потолок: список нужно обновлять вручную (или скриптом по cron), и он не отличит живой адрес от мёртвого внутри легитимного домена.

Rate limiting через policy server

Отдельная задача - ограничение скорости отправки. Если ваш сервер используется несколькими приложениями, одно из них может внезапно начать рассылку по грязной базе и сжечь репутацию IP для всех остальных.

postfwd2 (Postfix firewall daemon) - policy server, который реализует rate limiting по SASL-пользователю, IP-адресу, домену получателя:

# /etc/postfwd/postfwd.cf

# Не больше 200 писем в час от одного SASL-пользователя
id=RATE_LIMIT_USER;
    sasl_username=~^.+$;
    action=rate(sasl_username/200/3600/REJECT rate limit exceeded)

# Не больше 50 писем в час на один домен получателя
id=RATE_LIMIT_DOMAIN;
    recipient_domain=~^.+$;
    action=rate(recipient_domain/50/3600/DEFER_IF_PERMIT slow down)

Rate limiting не заменяет валидацию адресов, но дополняет её. Даже если невалидный адрес проскочил все проверки, ограничение скорости не даст одному сбойному процессу отправить десять тысяч писем за минуту.

Мониторинг и логирование

Инфраструктурная валидация бесполезна без наблюдаемости. Postfix пишет в syslog (обычно /var/log/mail.log). Ключевые метрики, которые стоит собирать:

  • Количество REJECT по каждому restriction-классу. Если reject_unknown_sender_domain внезапно вырос в десять раз - либо атака, либо проблема с DNS-резолвером.
  • Время ответа milter и policy server. Если milter отвечает дольше секунды - проблема. Дольше пяти секунд - SMTP-сессия будет подвисать, отправители начнут получать тайм-ауты.
  • Количество tempfail от milter. Если milter_default_action=tempfail срабатывает регулярно, значит milter падает. Нужно разбираться с причиной, а не переключать на accept.
  • Bounce rate исходящих. Основная метрика. Если после внедрения валидации bounce rate не снизился - что-то настроено неправильно.

Парсинг mail.log для базовой статистики отклонений:

# Количество реджектов за последний час, по причинам
grep "reject:" /var/log/mail.log \
  | awk -F'reject: ' '{print $2}' \
  | cut -d';' -f1 \
  | sort | uniq -c | sort -rn | head -20

Для продакшена вместо grep по логам используйте pflogsumm (суммаризатор логов Postfix), Prometheus с postfix_exporter или Graylog / Loki для централизованного сбора.

Границы MTA-валидации

MTA-уровень решает инфраструктурные задачи: отклонить несуществующий домен, заблокировать disposable-адрес, проверить аутентификацию отправителя, ограничить скорость. Но у него есть принципиальные ограничения.

MTA не знает, активен ли конкретный ящик на catch-all домене. Не определит, что адрес - спам-ловушка, если домен и MX-записи выглядят нормально. Не оценит вероятность bounce на основе поведенческих паттернов. Всё это задачи для выделенного сервиса валидации, который агрегирует данные из множества источников и применяет ML-модели.

Рабочая архитектура - два уровня. Первый: Postfix с restriction-классами, policy server и milter отсекает очевидный мусор на лету, в реальном времени, без задержки. Второй: перед массовой рассылкой весь список проходит через валидатор, который проверяет каждый адрес глубже - SMTP-хэндшейк, catch-all детекция, risk scoring. Первый уровень не заменяет второй. Второй не заменяет первый. Вместе они закрывают разные классы проблем.

MTA фильтрует трафик. Валидатор чистит базу.

Postfix, milter и policy server решают задачу в реальном времени на уровне соединения. Но перед рассылкой по списку из десяти тысяч адресов нужна предварительная проверка: SMTP-хэндшейк, catch-all, disposable-детекция, risk scoring. Загрузите список в uChecker - результат за минуты. Невалидные адреса, спам-ловушки, рискованные контакты - всё в одном отчёте.

milter Postfixвалидация email серверфильтрация входящей почтыPostfix policy serveremail gateway валидация