evgen@spb: ~/2026/07/chast-2-relay-dlya-iredmail-pochemu-rassulochniy-server-ne-podhodit-dlya-korporativnoy-pochty/

[Часть 2] Релей для iRedMail: почему рассылочный сервис не подходит для корпоративной почты

В предыдущей статье я выяснил, что весь /17 моего провайдера лежит в Spamhaus SBL и прямая отправка с сервера невозможна. Значит, нужен релей.

Казалось бы, задача на полчаса: прописать relayhost, добавить пароль, перезапустить. На практике всплыли два неочевидных момента — один про архитектуру Postfix, другой про то, чем «сервис отправки почты» отличается от «сервиса рассылок».

Задача

Есть iRedMail на Debian 12, три домена, полноценный почтовый сервер: принимает входящую, держит ящики, отдаёт по IMAP. Ломать это нельзя. Нужно только исходящую наружу отдавать на релей.

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

Sender-dependent transport

Правильный инструмент — sender_dependent_default_transport_maps. Он переопределяет только default_transport, то есть маршрут «наружу», и не трогает локальную доставку в virtual_mailbox_domains.

# /etc/postfix/sender_transport
@example.com          postbox:[postbox.cloud.yandex.net]:587
@example.net    postbox:[postbox.cloud.yandex.net]:587
@example.org         postbox:[postbox.cloud.yandex.net]:587
postmap hash:/etc/postfix/sender_transport
postconf -e 'sender_dependent_default_transport_maps = hash:/etc/postfix/sender_transport'

Транспорт postbox описывается в master.cf:

postbox   unix  -       -       n       -       -       smtp
  -o syslog_name=postfix/postbox
  -o smtp_sasl_auth_enable=yes
  -o smtp_sasl_password_maps=hash:/etc/postfix/sasl/postbox_passwd
  -o smtp_sasl_security_options=noanonymous
  -o smtp_sasl_mechanism_filter=login,plain
  -o smtp_tls_security_level=encrypt

Отдельный syslog_name — мелочь, но очень удобная: в логе сразу видно, что письмо ушло именно этим маршрутом.

Пароль:

# /etc/postfix/sasl/postbox_passwd
[postbox.cloud.yandex.net]:587	<KEY_ID>:<KEY_SECRET>
chmod 600 /etc/postfix/sasl/postbox_passwd
postmap hash:/etc/postfix/sasl/postbox_passwd

Грабля с проверкой

Естественное желание проверить, что таблица работает:

postmap -q "noreply@example.com" hash:/etc/postfix/sender_transport
# пусто

Пусто — и первая мысль «не работает». На самом деле postmap -q делает точный поиск ключа, а в таблице лежит @example.com. Postfix при резолве идёт по цепочке user@domaindomain@domain и находит на третьем шаге. Проверять надо так:

postmap -q "@example.com" hash:/etc/postfix/sender_transport
postbox:[postbox.cloud.yandex.net]:587

chroot

Если транспорт добавлен, а в логе SASL authentication failed; no mechanism available — почти наверняка дело в chroot. Пятая колонка в master.cf должна быть n, иначе smtp-демон не увидит /etc/postfix/sasl/postbox_passwd.db.

postconf -M postbox/unix
postconf -P 'postbox/unix/*'

Системная почта

Cron и logwatch шлют от root@mail.example.net. Этот домен не подключён к релею, и письма получат отказ вида «домен не доверен». Лечится переписыванием отправителя:

# /etc/postfix/canonical
root@mail.example.net     postmaster@example.net
@mail.example.net         postmaster@example.net
postmap hash:/etc/postfix/canonical
postconf -e 'sender_canonical_maps = hash:/etc/postfix/canonical'

Где применяется транспорт

Важная деталь для iRedMail: письмо проходит через content_filter → amavis (10024) → reinject (10025) → и только потом попадает в очередь на отправку. То есть в логе вы увидите два прохода с разными queue-id, и транспорт применяется на втором:

amavis: ... Queue-ID: 4gzG7Q4PdvzymM, ..., queued_as: 4gzG7Q6YtzzynH
postfix/postbox/smtp: 4gzG7Q6YtzzynH: to=<...>, relay=postbox.cloud.yandex.net...

Если грепать по первому id, покажется, что письмо пропало.

Первый релей: рассылочный сервис

Изначально я настроил отправку через SMTP-сервис своего провайдера. Технически всё завелось, письма пошли. А потом я посмотрел на письмо в Gmail.

Над текстом висела плашка: «ae@example.com — Отказаться от рассылки». И строка «отправлено через relay-provider.example».

Смотрю оригинал письма:

Precedence: bulk
X-MSG-TYPE: bulk
List-id: 6088
List-Unsubscribe: <https://smtp.relay-provider.example/list/unsubscribe/v2?issuen=...>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Feedback-ID: 2126014:16666517:6088:samotpravil

Релей добавляет полный набор маркеров массовой рассылки. Precedence: bulk — это буквально «это не личное письмо» в терминах RFC. Gmail считывает его вместе с List-* и рисует соответствующий интерфейс.

Причём заголовки входят в h= DKIM-подписи релея, то есть добавлены до подписи, на его стороне. Убрать их через header_checks на своём Postfix невозможно — письмо отдаётся раньше.

Обратите внимание и на Feedback-ID: там прямо указано имя платформы рассылок, на которой построен сервис. Это не транзакционный relay, переодетый в рассылочный, — это рассылочный движок как он есть.

Почему это плохо

Можно отмахнуться: ну висит ярлык, письма же доходят. Но:

  • каждое письмо коллеге приходит с кнопкой «отписаться» — выглядит как маркетинговый спам;
  • Gmail и другие охотнее кладут Precedence: bulk в «Промоакции»;
  • лимит скорости у рассылочных сервисов рассчитан на пакетную отправку, а не на интерактивную переписку (в моём случае — 100 писем за 5 минут на все домены сразу, это 20 писем в минуту);
  • стоп-листы: если хоть один адрес в письме на несколько получателей забанен глобально, письмо не уйдёт никому из списка;
  • bounce-и уходят не отправителю, а в панель сервиса.

Для рассылок это всё осмысленно. Для корпоративной переписки — нет.

Второй релей: транзакционный

Переехал на Yandex Cloud Postbox. Ключевое отличие для моего случая: он не требует, чтобы отправляющий сервер находился внутри инфраструктуры провайдера. Мой сервер остался где стоял и просто ходит на их SMTP по логину и паролю.

Схема настройки:

  1. Сервисный аккаунт с ролью postbox.sender.
  2. API-ключ со scope yc.postbox.send — его ID становится SMTP-логином, секрет паролем.
  3. Identity на каждый домен — обязательно в том же каталоге, что и сервисный аккаунт, иначе отправка упадёт на правах.
  4. DNS: подтверждение владения + SPF. DKIM сервис генерирует и публикует сам.

Один ключ обслуживает все домены. Отдельно на каждый нужны только identity и DNS-записи.

Проверка кредов до всякой правки Postfix:

swaks --server postbox.cloud.yandex.net:587 --tls \
  --auth LOGIN --auth-user '<KEY_ID>' --auth-password '<SECRET>' \
  --from ae@example.com --to me@gmail.com --header 'Subject: test'

Если тут 235 Authentication succeeded и 250 OK — можно переключать транспорт. Если нет, дальше идти незачем.

Что изменилось в заголовках

Return-Path: <feedback+XXXXXXXXXXXX@postbox.yandexcloud.net>
Authentication-Results: mx.google.com;
  dkim=pass header.i=@example.com header.s=selector-XXXXXXXX;
  spf=pass smtp.mailfrom=feedback+...@postbox.yandexcloud.net;
  dmarc=pass header.from=example.com

Ни Precedence: bulk, ни List-Unsubscribe, ни List-id. Плашка в Gmail исчезла — письмо выглядит как обычное письмо от человека.

Остались Feedback-ID и X-Mailru-Msgtype, но это трекинг доставки для почтовиков, а не маркер рассылки — интерфейс на них не реагирует.

Чего не избежать

Return-Path всё равно чужой. Так работают все транзакционные релеи — AWS SES, Mailgun, и этот тоже. Следствия:

  • SPF проверяется по домену релея, а не по вашему;
  • DMARC вытягивает исключительно за счёт DKIM-алайнмента — поэтому DKIM с d=вашдомен обязателен;
  • Gmail показывает «via postbox.yandexcloud.net»;
  • bounce-и уходят к релею, а не вашим пользователям.

Часть сервисов позволяет настроить собственный MAIL FROM домен — тогда Return-Path станет bounce@вашдомен и «via» исчезнет. Стоит уточнить, есть ли такая опция.

SPF: две частые ошибки

Ошибка первая — вторая запись. У домена должна быть ровно одна TXT-запись SPF. Добавите вторую вместо правки существующей — получите PermError и сломаете аутентификацию для всех.

v=spf1 include:spf.postbox.yandexcloud.net ~all

Ошибка вторая — добавить IP сервера «на всякий случай».

v=spf1 ip4:203.0.113.10 include:spf.postbox.yandexcloud.net ~all

Выглядит логично, но ip4: тут бесполезен. Сервер вообще не участвует в SMTP-транзакции с внешним миром: письмо уходит с IP релея, Return-Path принадлежит релею, и получатель проверяет SPF домена из Return-Path. Ваша запись при таком раскладе не смотрится вовсе.

Ещё стоит следить за лимитом DNS-лукапов (10 по RFC 7208). У Postbox include внутри себя тянет ещё три — итого четыре из десяти. Запас есть, но пара дополнительных сервисов его съест.

И ?all (neutral) вместо ~all — это фактически отсутствие политики. Раз вся исходящая идёт через релей, ~all безопасен.

DKIM: не подписывайте чужие домены

Тут я налетел на грабли из конфига по умолчанию. В логе amavis:

From: <ae@example.com> ... dkim_new=dkim:example.net

Письмо от одного домена подписано ключом другого. Подпись формально валидна, но не выровнена с From — для DMARC она бесполезна, просто болтается лишним заголовком.

Причина — catch-all в /etc/amavis/conf.d/50-user:

@dkim_signature_options_bysender_maps = ({
    '.' => {d => 'example.net', a => 'rsa-sha256', ...},
});

«Любой домен подписывай как example.net». Ключ-то есть только один.

Правильно:

@dkim_signature_options_bysender_maps = ({
    'example.net' => { d => 'example.net', a => 'rsa-sha256',
                         c => 'relaxed/simple', ttl => 30*24*3600 },
    '.'             => { ttl => 30*24*3600 },   # без d => подпись не ставится
});

Ключевой момент: в правиле '.' нет d =>. Без него amavis не находит ключ и просто не подписывает — вместо того чтобы лепить чужой.

После этого:

systemctl restart amavis
amavisd testkeys

Для доменов, где своего ключа нет, подпись поставит релей — выровненную и валидную. А для домена со своим ключом получится две подписи: своя и релея. Обе валидны, DMARC проходит по любой, конфликта нет.

Свой ключ я оставил сознательно: это независимость от релея. Сменю сервис — DKIM останется.

Мелочь, о которой стоит знать

Postbox отвергает письма без темы:

500 header "Subject" is required

Обычный SMTP такое пропускает, рассылочный сервис тоже пропускал. Чинится только в источнике: always_add_missing_headers добавляет From, To, Date, Message-ID, но не Subject, а header_checks умеет только реагировать на существующие заголовки, а не добавлять отсутствующие.

Так что если у вас есть скрипты, отправляющие уведомления без темы, — их придётся поправить.

Итог

Настройка sender-dependent транспорта — дело на полчаса, и она аккуратно решает задачу «отдать наружу только часть потока, не тронув локальную доставку».

А вот выбор релея стоит делать осознанно. «SMTP-сервис» и «сервис рассылок» звучат похоже, но у второго встроена семантика массовой отправки, которую нельзя выключить со своей стороны — она добавляется после того, как письмо покинуло ваш сервер.

Простой критерий: отправьте тестовое письмо себе и посмотрите оригинал. Если там Precedence: bulk или List-Unsubscribe — сервис считает вашу переписку рассылкой, и почтовики получателей будут считать так же.

evgen@spb: ~/contacts
evgen@spbping -c1 evgen.info
64 bytes from evgen.info: отвечу в течение рабочего дня
evgen@spbcat contacts.txt
mail      ya@evgen.info
telegram  @EvgenOne
github    github.com/onegin
blog      evgen.info/blog
evgen@spbecho $ЗАЧЕМ_ПИСАТЬ
интересная инфраструктурная задача · вопрос по Proxmox или Icinga ·
желание обсудить, почему memtest молчал, а память была битая
evgen@spb
© 2026 Евгений Подолинский · Санкт-Петербург
синий здесь – не тот, о котором вы подумали.