П/ВИН

Email рассылка на 200 000 контактов без спама

·9 мин чтения

У меня на руках база примерно из 200 000 контактов, и мне нужно, чтобы email рассылка по ней доходила до людей, а не отлетала в спам, и чтобы каждую отправку можно было отследить - кто открыл, кто кликнул, кто отписался. Звучит просто, пока не начинаешь копать. Потому что доставляемость на 95% определяется НЕ софтом, который шлёт письма, а тем, что вокруг него: репутацией домена и IP, аутентификацией, прогревом и - самое главное - качеством самой базы. Поэтому я взялся проектировать отдельный микросервис, набил шишки в боевых условиях и убедился, что «просто поставить готовое решение с GitHub» - это только 5% работы.

Контекст: почему нельзя просто взять и разослать

Первый и главный соблазн при такой задаче — найти готовое OSS-решение, поднять его в Docker и нажать «Отправить всем». Готовых движков и правда хватает: listmonk, Mautic, Postal, Keila. Но если вы возьмёте смешанную базу на 200k (а у меня именно такая — часть тёплая, часть холодная и непонятного происхождения) и зальёте её одним заходом, вы с высокой вероятностью сожжёте репутацию домена за один запуск. После этого письма не дойдут даже до тёплых подписчиков, которые вас реально ждут.

Почему так? Потому что почтовики — Gmail, Mail.ru, Яндекс — принимают решение «спам или не спам» на основе поведения получателей. Высокий процент bounce'ов (несуществующие адреса) и жалоб на спам — это прямой сигнал «этот домен рассылает мусор». Один плохой запуск по холодной части базы, и репутация домена падает в пол. Восстанавливать её потом — недели прогрева.

Из этого следует первый архитектурный принцип: сегментация — это не фича, а фундамент. Тёплые контакты (подписчики, активные клиенты) должны быть физически отделены от холодных, и рассылаться они должны по-разному, в идеале — с разных доменов/поддоменов, чтобы репутация одного сегмента не тащила за собой другой.

Архитектура: control plane поверх движка

Ключевое прозрение пришло, когда я сформулировал, чего хочу на самом деле. Мне не нужен «ещё один listmonk». Мне нужна обвязка (control plane) поверх движка рассылок, которая ведёт меня по доставляемости полу-автоматически и не даёт выстрелить себе в ногу. Сам движок (listmonk) — это готовый MIT-компонент, его переписывать незачем. А вот слой управления — это и есть мой настоящий микросервис.

Стоит сразу понять важную вещь про то, как вообще устроена отправка: сам движок рассылок не отправляет письма напрямую — он подключается к SMTP-релею. То есть канал доставки (через какой ESP реально уходят письма) можно поменять, не трогая сам сервис. Это развязывает руки: логику рассылок пишем один раз, а провайдера отправки выбираем под аудиторию.

Доставляемость я разложил на четыре слоя, и каждый из них control plane должен по возможности автоматизировать:

Слой 1 — аутентификация домена. Без неё спам гарантирован, это не обсуждается:

  • SPF — TXT-запись в DNS, которая разрешает IP релея слать письма от имени вашего домена.
  • DKIM — криптографическая подпись писем. Ключ выдаёт ESP, вы прописываете публичную часть в DNS. Без DKIM Mail.ru и Gmail режут письма почти сразу.
  • DMARC — политика обработки писем, не прошедших проверку. Начинаем с p=quarantine, потом ужесточаем до p=reject. Плюс rua= для получения отчётов о том, кто шлёт от вашего имени.
  • Отдельный поддомен для рассылок — чтобы маркетинговые письма не влияли на репутацию транзакционной почты с основного домена.

Подробнее про эти стандарты — на dmarc.org. Это must-read, если хотите понимать, что происходит под капотом.

Слой 2 — репутация и прогрев. Новый домен/поддомен нельзя сразу нагружать 200k отправок. Объём наращивается постепенно: сначала сотни писем в день по самым тёплым, потом тысячи. Параллельно мониторим репутацию через Google Postmaster Tools для Gmail и через постмастеры postmaster.mail.ru и postoffice.yandex.ru для русской аудитории.

Слой 3 — гигиена базы. Перед каждой крупной отправкой — валидация адресов, чтобы отсечь несуществующие ящики до того, как они превратятся в bounce'ы. Автоматическая обработка bounce-вебхуков: жёсткий bounce → адрес моментально выводится из активной рассылки.

Слой 4 — сегментация и engagement. При импорте каждый контакт получает тег источника и engagement_score. Дальше сегменты строятся через SQL прямо в listmonk: тёплые (открывали/кликали недавно) — в один поток, холодные — в отдельный, с осторожным прогревом или вообще в карантин.

Автоматизация DNS через Cloudflare

Самый изящный кусок — превратить слой 1 из ручной возни с DNS-записями в кнопку «Проверить домен» в админке. Для этого я выбрал Cloudflare как DNS-провайдера: у него лучший среди бесплатных API, мгновенная пропагация записей и удобное программное управление.

Логика в control plane простая: код делает DNS-запросы, проверяет наличие и корректность SPF/DKIM/DMARC, а если чего-то не хватает — предлагает создать запись одним кликом через Cloudflare API. Та же механика закрывает TXT-верификацию в постмастерах Mail.ru и Яндекса. В итоге то, что обычно делается руками через панель регистратора с задержкой пропагации в часы, становится полностью автоматическим и проверяемым из коробки.

Концептуально проверка выглядит так:

Кнопка «Проверить домен»
  → DNS lookup (SPF / DKIM / DMARC / TXT-постмастеров)
  → если запись отсутствует или некорректна
      → Cloudflare API: создать/исправить запись
  → повторная проверка через N минут
  → статус в админке: 🟢 готов / 🟡 ждём пропагации / 🔴 ошибка

Мульти-проектность: изоляция репутации

По ходу проектирования всплыло требование, которое серьёзно повлияло на архитектуру: мульти-проектность. Проект 1 — это один наш сервис, проект 2 — другой, и так далее. И здесь ключевой инсайт: репутация домена изолирована по своей природе. Если проект 2 разошлёт спам, это ни в коем случае не должно задеть проект 1.

Поэтому правильная изоляция — не «разные списки в одном общем listmonk», а отдельный инстанс listmonk на каждый проект: свой контейнер, своя база, свой домен и DKIM, свой коннект к ESP. Да, это дороже по ресурсам, но это единственный способ гарантировать, что плохое поведение одного арендатора не утопит остальных.

Для провижна новых инстансов я выбрал raw Docker API (docker service create в Swarm) вместо обёрток над PaaS. Причина прагматичная: это детерминированно и идемпотентно, без quirks с force-pull кэшированных образов, и при этом правило проекта «деплой инфраструктуры — только через выделенный пайплайн» остаётся нетронутым. Сам control plane при этом деплоится обычным CI/CD через Docker и GHCR.

Канал отправки: запад против РФ

Отдельный важный вывод про выбор ESP под русскоязычную аудиторию. Западные провайдеры (SES, Brevo) льют письма с европейских и американских IP, а Mail.ru и Яндекс относятся к таким письмам с большим недоверием — больше грейлистинга, выше шанс попасть в «Промоакции» или спам. Для РФ/СНГ заметно лучше работают либо российские ESP, у которых есть postmaster-договорённости с Mail.ru/Яндексом, либо собственный SMTP на российском IP, зарегистрированный в постмастерах.

Приятный момент: Gmail лоялен практически к любому отправителю с правильной аутентификацией. Так что один грамотно настроенный RU-канал закрывает сразу всю географию — и русские почтовики, и Gmail.

Уроки из боевой эксплуатации

Параллельно с email-частью у меня уже крутится «постовая» часть бота — отложенная публикация контента по расписанию. И именно она дала два болезненных, но показательных урока про надёжность.

Урок первый: таймзоны в планировщике. Бот однажды отправил контент в 7:30 утра вместо запланированных 10:30. Ровно три часа разницы — классический признак рассинхрона UTC и MSK (Москва — UTC+3). Я начал диагностику с очевидного: проверил таймзону сервера (timedatectl показал Europe/Moscow — не виновата) и DNS (резолвится корректно). Корень оказался в cron-задаче, где время было прописано в UTC, а не в локальном времени:

# Было — срабатывало в 07:30 MSK
30 7 * * 5 root curl -fsS -X POST https://.../trigger
 
# Стало — корректные 10:30 MSK
30 10 * * 5 root curl -fsS -X POST https://.../trigger

Мораль: всегда явно фиксируй таймзону планировщика и проверяй фактическое время срабатывания, а не предполагай его. Три часа сдвига — почти всегда таймзона, а не «магия».

Урок второй: надёжность доставки вебхуков. Webhook бота шёл через общий прокси-слой, и в условиях, когда трафик из РФ проходит через промежуточный бридж с DPI-троттлингом, доставка апдейтов от внешнего API становилась нестабильной. Решение — выделенный поддомен для вебхука, который маршрутизируется напрямую, в обход бриджа:

# Выделенный блок для вебхука — прямой маршрут, минуя RU-бридж
tg.example.online {
    reverse_proxy 127.0.0.1:9080 {
        header_up Host example.online
    }
}

После переключения вебхука на прямой URL входящие апдейты пошли стабильно, с нулевым количеством зависших обновлений в очереди. Урок: критичные для real-time входящие каналы (вебхуки) нельзя гонять через тот же путь, что и обычный пользовательский трафик — у них разные требования к латентности и стабильности.

Выводы

Главный вывод, ради которого стоило всё это затевать: массовая рассылка — это инфраструктурная задача, а не задача «выбрать софт». Движок (listmonk) — это 5% работы. Остальные 95% — аутентификация домена, прогрев, гигиена базы, сегментация и мониторинг репутации. Любой, кто продаёт вам «волшебную кнопку рассылки», либо не понимает проблему, либо продаёт способ сжечь ваш домен. Правильная ментальная модель — строить control plane, который автоматизирует именно эти 95%, а готовый движок просто подключать как сменный компонент.

Второй урок — про изоляцию. Когда у вас несколько проектов под одной крышей, репутация должна быть изолирована физически, а не логически. Один инстанс на проект дороже, но это страховка от каскадного провала: ошибка одного арендатора не должна стоить вам доставляемости всех остальных. Этот принцип «изолируй то, что имеет общую судьбу» применим далеко за пределами email — это базовый паттерн мульти-арендных систем.

Третий урок пришёл из эксплуатации, а не из проектирования: надёжность планировщиков и входящих каналов нужно проверять эмпирически, а не на доверии. Таймзона, которая «должна быть правильной», и вебхук, который «вроде доходит», — это два места, где предположения стоят дороже всего. Я теперь начинаю диагностику любого сбоя по расписанию с фактической проверки времени срабатывания, а критичные вебхуки развожу на отдельные, максимально короткие сетевые маршруты. Три часа сдвига в отправке контента клиенту — это репутационный удар, которого можно было избежать одной строчкой в cron.

И наконец, про географию. Если ваша аудитория в РФ и СНГ — не копируйте западные best practices слепо. Mail.ru и Яндекс живут по своим правилам, и письмо с идеальным SPF/DKIM с американского IP всё равно может улететь в «Промоакции». Выбор канала под конкретную географию — это не оптимизация, а условие, без которого вся остальная работа по доставляемости обесценивается. Проект ещё в стадии активной сборки control plane, но фундамент — сегментация, изоляция, автоматизация DNS и осознанный выбор канала — уже заложен правильно, и именно он определит, дойдут письма до 200 000 человек или осядут в спам-папках.

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.

Связанный проект

AI Content Aggregator