mailcow: smarthost для одного домена, остальные напрямую
АйТи Фреш
Linux, Docker и DevOps

Один домен mailcow отправляю через smarthost, остальные — напрямую: как не перепутать маршрут

Автор: , директор ООО «АйТи-Фреш» · · ~14 мин чтения
Один домен mailcow отправляет почту через smarthost-релей, два других домена — напрямую, без промежуточного узла
Один домен — через контрольный пункт партнёра, остальные — напрямую, без лишних задержек.

Test в разделе Routing показал «250 2.0.0 Ok: queued as» — и администратор решил, что smarthost для домена настроен. А письма из ящиков этого домена продолжали идти напрямую, как раньше. В mailcow relayhost и назначение его домену — два разных шага, и первый без второго ничего не меняет. Разбираю, как настроить sender-dependent transport на один домен из нескольких, и как проверить не тест, а реальный маршрут писем.

Зачем одному домену из нескольких нужен отдельный smarthost

У digital-агентства «Цифровой формат» (50 рабочих мест) на одном mailcow-сервере живут три домена: основной корпоративный, домен для рассылок клиентским подрядчикам и домен под конкретный проект с внешним заказчиком. Мы обслуживаем их корпоративную почту, и когда заказчик проекта потребовал, чтобы вся переписка по контракту шла через его корпоративный SMTP-релей (так у него настроен DLP и архивирование), возникла задача: один домен — через внешний smarthost с авторизацией, два других — напрямую через собственный Postfix mailcow, как раньше.

На первый взгляд кажется, что это глобальная настройка relay_host в Postfix — но так релеился бы вообще весь исходящий трафик сервера, включая два домена, которым внешний relay не нужен и не разрешён (у заказчика лимит на отправителей). mailcow для этого держит отдельный механизм — sender-dependent transport, который просто не документирован так подробно, как хотелось бы, из-за чего его настраивают наполовину.

Наполовину — это когда создали транспорт, прогнали Test, увидели зелёный ответ сервера и посчитали дело сделанным. Разница между «транспорт создан» и «транспорт применяется к домену» — отдельный шаг, который легко пропустить, если не знать, что он вообще существует.

Была и альтернатива, которую мы отклонили сразу: попросить заказчика переключить его релей на приём почты вообще с любого IP по домену-отправителю. Это переложило бы риск на партнёра и усложнило бы диагностику при инцидентах — если бы почта вдруг перестала доходить, разбираться пришлось бы в чужой инфраструктуре, а не в своей. Точечный transport на стороне mailcow — тот случай, когда решение проще держать под своим контролем, даже если конфигурация занимает на один шаг больше.

Стоило также учесть, что почтовые домены на одном сервере mailcow физически используют общий Postfix и общий исходящий IP по умолчанию. Это значит, что smarthost для одного домена не меняет репутацию отправки для остальных — но и наоборот, если бы мы настраивали relay неправильно и он начал перехватывать письма чужого домена, это могло бы сказаться на репутации и доставляемости писем, которые вообще не должны были идти через партнёрский сервер. Отсюда и важность именно точечной, а не глобальной настройки.

Где в mailcow создаётся relayhost и что в него вводить

Создаётся транспорт в **Configuration & Details → Routing**, блок «Add sender-dependent transport». Там три поля: **Host** — адрес внешнего SMTP-сервера (в формате [host]:port, если хотите зафиксировать порт и не отдавать выбор MX-записям), и опционально **Username**/**Password**, если relay требует SMTP AUTH. Документация mailcow прямо предупреждает: некоторые релеи, особенно без авторизации, отклоняют соединение от серверов, IP которых заранее не внесён в whitelist — это стоит уточнить у владельца smarthost до того, как тратить время на диагностику «неработающего» транспорта.

Для проекта «Цифрового формата» это выглядело так: заказчик выдал релей [smtp.example-partner.com]:587 с логином и паролем на отдельную учётную запись. Ввели Host, Username, Password, сохранили — в списке транспортов появилась строка. На этом создание транспорта закончено, но сам по себе он пока ни на что не влияет: ни один домен ещё не знает о его существовании.

Про пароль отдельно: mailcow хранит его в открытом виде в базе конфигурации транспорта (это задокументированное поведение, не баг) — учётку для relay стоит заводить отдельную, с минимальными правами именно на отправку, а не переиспользовать боевой пароль администратора у партнёра.

Ещё один нюанс, который стоит уточнить заранее: если у партнёрского relay несколько MX-записей с разным приоритетом и поведением (например, резервный сервер отвечает медленнее или требует другой авторизации), фиксация хоста в квадратных скобках защищает именно от случайного попадания на резервный узел — Postfix не станет перебирать MX-записи домена в Host, а будет стучаться строго в указанный адрес и порт. И сразу про порт: подсказка в самом интерфейсе mailcow предупреждает, что транспорт всегда работает как smtp: и порт 465 с «обёрнутым» TLS (SMTPS) не поддерживается — нужен порт с STARTTLS, обычно 587. Партнёр сначала выдал нам именно 465, и я попросил переключить учётку на 587 до того, как тратить время на тесты.

Формат Host важен: `smtp.example-partner.com` без квадратных скобок Postfix трактует как домен и сначала ищет его MX-записи, а `[smtp.example-partner.com]:587` отключает MX-lookup и жёстко фиксирует хост и порт — так и просил партнёр, и так безопаснее, если у него несколько MX с разным поведением.
Два обязательных шага настройки sender-dependent transport в mailcow: создание транспорта в Routing и назначение его домену в Domains
Транспорт создаётся отдельно от назначения домену — пропустишь второй шаг, и ничего не поменяется.

Второй шаг, который все забывают: назначить транспорт домену

Транспорт становится активным только после того, как его выбрали в настройках конкретного домена: **Mail setup → Domains → (редактирование домена) → Sender-dependent transports**. Именно здесь домен «подписывается» на созданный relayhost — до этого шага транспорт существует в системе, но ни одно письмо через него не идёт.

Для «Цифрового формата» это означало зайти в домен проекта, открыть редактирование, найти выпадающий список Sender-dependent transports и выбрать там созданный на первом шаге relay. Два других домена — основной корпоративный и рассылочный — в этом списке оставили пустыми: без назначенного транспорта Postfix продолжает отправлять их почту напрямую, штатным способом, через MX получателя.

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

Важная деталь: кроме домена, тот же выпадающий список Sender-dependent transports есть и в настройках отдельного ящика — в актуальных версиях mailcow транспорт можно переопределить для конкретного mailbox (доменному администратору для этого нужно право mailbox_relayhost). Если внутри проектного домена «Цифрового формата» нужно было бы разделить ящики — часть через партнёрский relay, часть напрямую, — я бы сделал это именно так, а не через файлы. В нашем случае вся переписка по проекту шла из одного домена, так что домена как единицы маршрутизации хватило с запасом.

Почему зелёный Test — не доказательство рабочего маршрута

Кнопка Test в списке транспортов запускает отдельную проверку: вы указываете email-адрес отправителя, mailcow подключается к relay и пробует отправить тестовое письмо от этого имени. Успешный ответ в логе выглядит как SMTP-диалог, заканчивающийся строкой 250 2.0.0 Ok: queued as... — сервер relay принял письмо в очередь. Это доказывает только одно: указанные Host/Username/Password рабочие и relay готов принимать почту от этого адреса. По умолчанию тестовое письмо уходит на null@mailcow.email; адрес получателя можно поменять переменной $RELAY_TO в data/web/inc/vars.inc.php, и тогда я ещё и вижу, дошло ли письмо до реального ящика.

Test ничего не говорит о том, назначен ли транспорт домену в принципе. Мы видели конфигурацию, где Test проходил зелёным месяцами, а домен всё это время отправлял письма напрямую — потому что на втором шаге администратор забыл сохранить выбор в Sender-dependent transports (форма молча откатилась из-за истёкшей сессии в панели). Внешне всё выглядело настроенным: транспорт есть, тест зелёный.

Проверка реального маршрута — это отправка обычного письма из ящика нужного домена и просмотр журнала Postfix. В логе строка с этим письмом должна содержать имя relay-хоста как точку доставки, а не MX получателя напрямую.

Полезно проверять маршрут не единожды после настройки, а держать это в регламенте: после любого изменения в разделе Routing (даже если правили другой, не связанный транспорт) стоит повторно отправить контрольное письмо от проектного домена. Мы сталкивались со случаем, когда администратор редактировал транспорт для другого клиента, случайно снял чекбокс не у того домена в списке — и «Цифровой формат» начал бы отправлять почту напрямую, если бы это не поймали при плановой проверке.

Есть и более быстрый способ подтвердить маршрут без ожидания реальной доставки — посмотреть очередь Postfix сразу после отправки командой docker compose exec postfix-mailcow postqueue -p. Если письмо на секунду задержалось в очереди (например, relay временно недоступен), в выводе будет видно, на какой хост Postfix пытается его доставить — это тот же хост, что указан в Host у sender-dependent transport, если маршрутизация настроена верно.

Практика: после назначения транспорта домену отправьте тестовое письмо из реального ящика этого домена на внешний адрес и найдите его в логе по queue ID. Ищите не факт отправки, а конкретный relay-хост в строке `relay=` — если там MX получателя вместо вашего smarthost, транспорт домену не применился.
Дерево диагностики: что проверить, если Test relayhost в mailcow зелёный, а письма домена всё равно не идут через smarthost
Зелёный Test — только первая галочка, финальную ставит запись relay= в логе Postfix.

Когда UI не хватает: custom_transport.pcre и его риски

Если логика нужна сложнее, чем «домен или ящик → один транспорт» — например, маршрутизировать по адресу или домену получателя, чего sender-dependent transport не умеет по определению, — mailcow предусматривает файл data/conf/postfix/custom_transport.pcre. Документация прямо называет его файлом для продвинутых случаев и подчёркивает: это **не обязательный** файл, и прежде чем в него лезть, стоит исчерпать возможности штатного UI.

Синтаксис файла — правила PCRE (Perl-совместимые регулярные выражения), которые Postfix использует как transport map: в main.cf mailcow файл подключён первым в списке transport_maps, раньше внутренних таблиц. Официальное предупреждение звучит жёстко: некорректное содержимое файла может нарушить работу Postfix целиком, то есть ошибка в регулярном выражении — это не «транспорт для одного отправителя не сработал», а потенциально «почта не ходит вообще ни у кого на сервере».

Для сценария «Цифрового формата» custom_transport.pcre не понадобился — sender-dependent transport на уровне домена полностью закрыл задачу. Я рекомендую этот файл только тогда, когда правило нужно строить по адресу получателя или по регулярному выражению, которое не укладывается в «домен/ящик → транспорт», — то есть когда встроенного механизма объективно не хватает, а не как способ сэкономить пару кликов в UI.

Ещё одна причина держаться подальше от custom_transport.pcre без необходимости — этот файл не проходит через ту же валидацию, что формы в UI: mailcow не подскажет, что синтаксис регулярного выражения некорректен, пока Postfix не попытается его применить и не откажется стартовать или перечитывать конфигурацию. Если такое правило действительно нужно, лучше сначала проверить его на тестовом стенде, а не сразу на продакшн-сервере с работающей почтой десятков сотрудников. Минимальная проверка, которую я делаю всегда: docker compose exec postfix-mailcow postmap -q "user@example.com" pcre:/opt/postfix/conf/custom_transport.pcre — команда покажет, какой транспорт вернёт таблица для адреса, или ошибку разбора, если выражение битое.

Сравнение штатного sender-dependent transport в UI mailcow и файла custom_transport.pcre для тонкой маршрутизации
Для маршрута по домену или ящику UI хватает с запасом — файл нужен только для более тонких правил и требует осторожности.

Итог по кейсу «Цифрового формата»

Настройка заняла меньше часа: relay от заказчика, создание транспорта в Routing, назначение его одному домену из трёх, тестовое письмо с проверкой по логу. Два других домена весь процесс не заметили — их почта как шла напрямую, так и идёт.

Главный урок этого кейса — не техническая сложность (она невысокая), а последовательность проверки. Я теперь всегда после настройки smarthost для одного домена в мультидоменной инсталляции просим клиента (или проверяем сами) не Test, а реальное письмо с разбором лога — потому что «настроено по инструкции» и «реально работает» в mailcow-транспортах расходятся именно на шаге назначения транспорта домену, а не на шаге его создания.

Через месяц после настройки заказчик подтвердил: вся переписка по проекту действительно проходит через его релей и попадает в архив DLP, а два других домена агентства ни разу не зафиксировали задержек или сбоев — они по-прежнему отправляют почту напрямую, будто transport для них вообще не существует. Именно так и должна выглядеть точечная настройка: изменения затрагивают ровно то, что требовалось изменить, и не трогают остальное.

Частые вопросы

Можно ли назначить один sender-dependent transport сразу нескольким доменам mailcow?

Да, транспорт создаётся один раз в Routing, а затем выбирается в настройках каждого нужного домена отдельно, в поле Sender-dependent transports. Домены, где это поле оставлено пустым, продолжают отправлять почту напрямую.

Почему Test показывает успех, а письма домена всё равно идут не через relay?

Test проверяет только то, что relay принимает почту от указанного адреса отправителя — это отдельная операция от назначения транспорта домену. Если на втором шаге (Mail setup → Domains → Sender-dependent transports) транспорт не выбран и не сохранён, домен продолжает слать почту напрямую, а Test при этом остаётся зелёным.

Как проверить, что письма реального домена идут через smarthost, а не напрямую?

Отправьте письмо из ящика этого домена и посмотрите журнал Postfix командой `docker compose logs --tail=200 postfix-mailcow` из каталога mailcow-dockerized. В строке с этим сообщением поле `relay=` должно содержать адрес вашего smarthost, а не MX-сервер получателя.

Нужно ли использовать custom_transport.pcre, чтобы направить один домен через smarthost?

Нет, для маршрутизации целого домена достаточно штатного sender-dependent transport в UI. Для отдельного ящика тоже есть штатный выбор транспорта в его настройках. custom_transport.pcre документация рекомендует только тогда, когда UI не справляется, — например, для маршрутизации по получателю, — и предупреждает, что ошибка в PCRE-синтаксисе может нарушить работу Postfix целиком.

Хранит ли mailcow пароль от smarthost в защищённом виде?

Нет, пароль сохраняется в открытом виде в конфигурации транспорта — это задокументированное поведение. Для внешнего relay стоит заводить отдельную учётную запись с минимальными правами на отправку, а не использовать существующий административный пароль.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи