[Часть 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@domain → domain → @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 по логину и паролю.
Схема настройки:
- Сервисный аккаунт с ролью
postbox.sender. - API-ключ со scope
yc.postbox.send— его ID становится SMTP-логином, секрет паролем. - Identity на каждый домен — обязательно в том же каталоге, что и сервисный аккаунт, иначе отправка упадёт на правах.
- 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 — сервис считает вашу переписку рассылкой, и почтовики получателей будут считать так же.