Atlorium

Рассылка ушла, а письма не дошли: почему домен попадает в спам и что чинить до отправки

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

11 минут чтения email-маркетолог · CRM · e-commerce · разработчик

Пятница, 11:00. Рассылка на 48 тысяч адресов — база копилась четыре года: импорт из старой CRM, участники вебинаров, выгрузка с оффлайн-конференции, где визитки расшифровывали вручную. К часу дня ESP присылает предупреждение: доля отскоков 9%. К вечеру аккаунт отправителя на паузе, а письма, которые всё-таки ушли, лежат в спаме даже у тех, кто ждал скидку.

Через неделю разработчик жалуется, что перестали доходить письма с подтверждением заказа. Это уже не про рассылку — это транзакционная почта с того же домена. Домен теперь другой: не по адресу, а по репутации.

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

Всё, что привело к этому дню, проверяется до нажатия «Отправить». Проверок ровно две группы: что не так со списком адресов и что не так с вашим доменом.

Где именно течёт

Отскок отскоку рознь. «Ящик переполнен» — временная история, провайдер это переживёт. А вот hard bounce — ответ «такого ящика нет» — почтовик читает как признак того, что отправитель не знает, кому пишет. Список, где каждый десятый адрес мёртв, отличает рассыльщика по краденой базе от компании, которая пишет своим клиентам. Ровно на это и настроены фильтры.

Спам-ловушка в списке, купленном три года назад

Что происходит
В базе лежит адрес, который никто никогда не вводил в вашу форму: он взят из списка «лиды по отрасли» или подобран парсером с чужого сайта.
Во что обходится
Спам-ловушка — адрес, который специально размещён для отлова тех, кто не спрашивал разрешения. Одно письмо на такой адрес переводит домен в чёрный список у провайдера или у оператора списков — без предупреждения и без переписки.
Чем закрывается
Проверка почты помечает такой адрес статусом spamtrap (или do_not_mail с уточнением possible_trap), и он выбрасывается из выгрузки до отправки.

Каждый десятый ящик уже не существует

Что происходит
Сотрудник уволился, компания сменила домен, человек бросил ящик на бесплатном почтовике. Адрес в базе остался — база не знает, что он умер.
Во что обходится
Доля отскоков растёт с каждой рассылкой, и в какой-то момент ESP останавливает аккаунт: у большинства провайдеров рассылки такой порог прописан в правилах (часто около 5%). Дальше — разбирательство и пауза в коммуникации.
Чем закрывается
Проверка обращается к почтовому серверу домена и возвращает invalid с уточнением mailbox_not_found. Письмо не отправляется — сервер отвечает на служебный запрос, владелец ящика ничего не видит.

Домен подписан неизвестно кем

Что происходит
MX-запись настроена, письма отправляются, всё выглядит рабочим. При этом в DNS нет DMARC, а SPF содержит десяток include-ов, оставшихся от трёх бывших подрядчиков.
Во что обходится
Массовые отправители без SPF, DKIM и DMARC отсекаются на входе: у Gmail и Yahoo это прямое требование к тем, кто шлёт от нескольких тысяч писем в день. Письмо не «попадает в спам» — оно даже не принимается.
Чем закрывается
Проверка DNS-записей отдаёт MX, TXT (там живёт SPF), а по имени _dmarc.вашдомен — политику DMARC. Разбор занимает два запроса и делается до того, как рассылка ушла.

Сколько это стоит

Цену чистки базы посчитать легко: количество адресов умножается на цену одного запроса (актуальная — на странице тарифов, у нас всё pay-as-you-go, без подписки и минимального платежа). Цену отсутствия чистки считать труднее, потому что она размазана по календарю.

Что происходит Рассылка без проверки Рассылка после проверки
Отскоки Все мёртвые адреса уходят в отправку и возвращаются отказом Адреса со статусом invalid отсеяны на выгрузке
Аккаунт у ESP Риск остановки при превышении порога отскоков Порог не достигается
Спам-ловушки Домен уезжает в чёрные списки после одного письма spamtrap и abuse не попадают в отправку
Транзакционные письма Едут в спам вместе с маркетинговыми — домен-то общий Не задеты
Восстановление Прогрев, разбирательство с провайдерами, иногда — новый домен Не требуется

Честное допущение: доли протухших адресов «в среднем по рынку» не существует, она зависит от того, как база собиралась. Свою цифру вы узнаете ровно одним способом — прогнав выгрузку через проверку. Но сама постановка вопроса «дешевле ли проверить 48 тысяч адресов, чем две недели восстанавливать доставляемость» обычно закрывается ещё до того, как кто-то откроет калькулятор.

Как чинить: две стороны одной проблемы

Сначала домен, потом база. Порядок именно такой: чистая база при сломанном SPF не спасёт, а вот исправленный DNS начинает работать сразу и на весь трафик.

Сторона домена: четыре записи в DNS

  1. MX — куда доставляется входящая почта домена. Если MX-записи нет, домен не принимает почту вовсе, и внешние сервисы это видят.
  2. SPF — TXT-запись на самом домене, начинается с v=spf1. Перечисляет, кому разрешено отправлять почту от вашего имени. Смотреть надо не только на наличие, а на содержимое: висящий с прошлого года include: ушедшего подрядчика — это разрешение слать от вашего имени, выданное неизвестно кому.
  3. DKIM — TXT-запись по имени вида selector._domainkey.вашдомен, где селектор выдаёт почтовая платформа. В ней лежит открытый ключ, которым получатель проверяет подпись письма.
  4. DMARC — TXT-запись по имени _dmarc.вашдомен, начинается с v=DMARC1. Говорит получателю, что делать с письмом, которое не прошло SPF и DKIM: пропустить (p=none), отправить в спам (p=quarantine) или отбросить (p=reject). Начинают всегда с p=none и отчётов, иначе первым же делом отрежете собственную почту.

Все четыре записи достаёт Проверка DNS-записей: она запрашивает A, AAAA, MX, TXT, CNAME и NS и группирует ответ по типам. SPF лежит в TXT самого домена, DMARC и DKIM — в TXT служебных поддоменов, поэтому это отдельные запросы по отдельным именам.

Сторона базы: что выбросить до отправки

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

Код: два запроса, которые нужно сделать до рассылки

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

Проверяем адрес перед добавлением в выгрузку

curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     "https://atlorium.com/api/emailvalidation?email=ivan@example.com"
        

import requests

SAFE = {"valid"}                     # отправляем
RISKY = {"catch-all"}                # отправляем осторожно, отдельным сегментом
BLOCK = {"invalid", "spamtrap", "abuse", "do_not_mail"}

def check(address: str) -> str:
    resp = requests.get(
        "https://atlorium.com/api/emailvalidation",
        params={"email": address},
        headers={"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"},
        timeout=15,
    )
    resp.raise_for_status()
    data = resp.json()

    raw = data["statusRaw"]           # valid | invalid | catch-all | unknown | spamtrap | abuse | do_not_mail
    if data.get("didYouMean"):
        # Опечатка в домене: gmial.com -> gmail.com. Показываем подсказку человеку,
        # а не выбрасываем адрес молча.
        print(f"Возможна опечатка: {data['didYouMean']}")

    if raw in BLOCK:
        return "drop"
    if raw in RISKY:
        return "review"
    if raw == "unknown":
        # Сервер домена не ответил (greylisting). Этот запрос НЕ тарифицируется —
        # ставим адрес в очередь на повтор через несколько минут.
        return "retry"
    return "send"
        

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "ak_sandbox_demo_mockdata_v1");

var url = "https://atlorium.com/api/emailvalidation?email=ivan@example.com";
using var doc = JsonDocument.Parse(await http.GetStringAsync(url));
var root = doc.RootElement;

// deliverable = true только у статуса Valid: catch-all и unknown — это неопределённость,
// а не разрешение отправлять.
var deliverable = root.GetProperty("deliverable").GetBoolean();
var statusRaw = root.GetProperty("statusRaw").GetString();

Console.WriteLine($"{statusRaw}: отправлять — {deliverable}");
        

{
  "email": "ivan@example.com",
  "status": "Valid",
  "statusRaw": "valid",
  "subStatus": null,
  "account": "ivan",
  "domain": "example.com",
  "freeEmail": false,
  "catchAllDomain": false,
  "didYouMean": null,
  "domainAgeDays": 3431,
  "smtpProvider": "outlook",
  "mxFound": true,
  "mxRecord": "mx1.example.com",
  "activeInDays": null,
  "activeFirstSeen": null,
  "processedAtUtc": "2026-07-14T09:12:03.031Z",
  "elapsedMs": 214,
  "deliverable": true
}
        

Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории email-verification-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.

Два поля вместо одного — это не избыточность. status — типизированный вердикт (Valid, Invalid, CatchAll, Unknown, SpamTrap, Abuse, DoNotMail), по нему удобно ветвиться в коде. statusRaw — строка ровно в том виде, в каком её отдал источник. Если источник когда-нибудь заведёт восьмой статус, типизированное поле схлопнет его в Unknown, а сырое сохранит — и вы это увидите, а не потеряете.

Статус проверки → что делать с адресом

statusRaw Что это значит Решение
valid Ящик существует, сервер домена принимает почту Отправлять. Единственный статус, у которого deliverable = true
invalid Ящика нет или домен не принимает почту (mailbox_not_found, no_dns_entries) Не отправлять. Это будущий hard bounce — удалить из выгрузки
spamtrap Адрес-ловушка Не отправлять никогда. Одно письмо — чёрный список для домена
abuse Владелец адреса раньше жаловался на спам Не отправлять. Жалоба повторится, а доля жалоб — метрика провайдера
do_not_mail Ролевой адрес (role_based), одноразовый ящик (disposable), токсичный адрес (toxic), глобальный стоп-лист (global_suppression) Не отправлять в рассылку. Технически адрес может работать — на info@ и sales@ можно писать письмо руками, но не рассылку
catch-all Домен отвечает «валидно» на любой ящик — проверить конкретный снаружи нельзя ни одним сервисом в мире Осторожно. Отдельный сегмент, маленькая партия, следим за отскоками. Есть ли на домене catch-all — видно в поле catchAllDomain
unknown Проверить не удалось: greylisting, сервер не ответил, анти-спам заблокировал проверку Повторить позже. Это не результат, а его отсутствие — и такой запрос не тарифицируется
Проверяем почтовые записи домена: MX, SPF, DMARC

# MX и SPF лежат на самом домене: MX — в группе MX, SPF — в TXT
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     "https://atlorium.com/api/dns/lookup?domain=example.com"

# DMARC — это TXT служебного поддомена
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     "https://atlorium.com/api/dns/lookup?domain=_dmarc.example.com"
        

import requests

API = "https://atlorium.com/api/dns/lookup"
HEAD = {"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"}

def txt_values(domain: str) -> list[str]:
    """Все TXT-строки домена. records — список групп по типам записей."""
    data = requests.get(API, params={"domain": domain}, headers=HEAD, timeout=15).json()
    for group in data["records"]:
        if group["type"] == "TXT":
            return [r["value"] for r in group["records"]]
    return []

def mx_hosts(domain: str) -> list[str]:
    data = requests.get(API, params={"domain": domain}, headers=HEAD, timeout=15).json()
    for group in data["records"]:
        if group["type"] == "MX":
            # preference — приоритет записи, меньше значит важнее
            return [f"{r['value']} (prio {r['preference']})" for r in group["records"]]
    return []

domain = "example.com"

mx = mx_hosts(domain)
spf = [t for t in txt_values(domain) if t.startswith("v=spf1")]
dmarc = [t for t in txt_values(f"_dmarc.{domain}") if t.startswith("v=DMARC1")]
# Селектор DKIM выдаёт почтовая платформа; для Google это обычно "google"
dkim = txt_values(f"google._domainkey.{domain}")

if not mx:    print("Нет MX — домен не принимает почту")
if not spf:   print("Нет SPF — письма от вашего имени может слать кто угодно")
if not dmarc: print("Нет DMARC — массовые провайдеры отправят рассылку в спам")
if not dkim:  print("Нет DKIM-записи по этому селектору — проверьте селектор у почтовой платформы")
        

{
  "domain": "example.com",
  "resolver": "1.1.1.1 (sandbox)",
  "requestedAtUtc": "2026-07-14T09:12:03Z",
  "records": [
    {
      "type": "A",
      "records": [
        { "name": "example.com", "value": "93.184.216.34", "ttl": 300, "preference": null }
      ]
    },
    {
      "type": "MX",
      "records": [
        { "name": "example.com", "value": "mail1.example.com", "ttl": 3600, "preference": 10 },
        { "name": "example.com", "value": "mail2.example.com", "ttl": 3600, "preference": 20 }
      ]
    },
    {
      "type": "TXT",
      "records": [
        { "name": "example.com", "value": "v=spf1 include:_spf.example.com ~all", "ttl": 3600 }
      ]
    }
  ],
  "errors": []
}
        

Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории dns-lookup-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.

Резолвер по умолчанию — Cloudflare (1.1.1.1), сменить его можно параметром resolver. Если вы только что поправили SPF или добавили DMARC, изменение доходит до резолверов не мгновенно: эндпоинт /api/dns/propagation опрашивает сразу несколько публичных резолверов и показывает, кто уже видит новую запись, а кто ещё отдаёт старую. Тарифицируется он порезолверно — четыре резолвера по умолчанию стоят как четыре обычных запроса, максимум за один вызов — восемь.

Порядок действий перед большой рассылкой

  1. Проверить MX домена-отправителя. Нет MX — дальше идти незачем, чинить надо это.
  2. Достать TXT домена, найти v=spf1. Выкинуть из него include-ы сервисов, которыми вы больше не пользуетесь.
  3. Достать TXT по selector._domainkey.вашдомен — селектор посмотреть в панели почтовой платформы.
  4. Достать TXT по _dmarc.вашдомен. Нет записи — завести хотя бы p=none с адресом для отчётов.
  5. Прогнать всю выгрузку через проверку почты и разложить адреса по таблице выше.
  6. Адреса со статусом unknown отправить на повторную проверку через несколько минут — это бесплатно.
  7. Отправить рассылку по valid. Сегмент catch-all — отдельной маленькой партией, с наблюдением за отскоками.

Чего это не умеет

Список короткий, но прочитать его надо до того, как вы построите на проверке критичный процесс.

  • Не проверяет, читает ли человек ящик. Проверка отвечает на вопрос «примет ли сервер домена письмо на этот адрес». Живой ли за ним подписчик, откроет ли он письмо — это данные, которых у нас нет. Письмо в ходе проверки не отправляется, и владелец адреса о ней не узнаёт.
  • Не собирает персональные данные владельца. Имя, фамилия, пол, геолокация ящика не запрашиваются, не хранятся и не возвращаются — ни в веб-интерфейсе, ни в API. Сервис отвечает только про доставляемость.
  • DKIM проверяется только как наличие TXT-записи по селектору. Мы достаём запись из DNS и показываем её содержимое. Криптографической проверки подписи конкретного письма здесь нет — для неё нужно само письмо, а его у нас нет и не должно быть.
  • Содержимое SPF и DMARC не разбирается на синтаксис. Вы получаете строку записи как она есть. Убедиться, что в SPF нет лишних include-ов, а в DMARC стоит нужная политика, — работа человека или вашего кода; сырьё для неё мы отдаём.
  • catch-all остаётся неопределённостью. На домене, который отвечает «валидно» на любой ящик, существование конкретного адреса снаружи не проверяется в принципе. Никакой сервис — ни наш, ни чужой — этого не может. Мы честно возвращаем catch-all, а не выдаём догадку за вердикт.
  • Проверка не заменяет double opt-in. Чистая база не даёт вам права писать тем, кто не соглашался. Она лишь означает, что письма тем, кто согласился, дойдут.

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

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

Почему письма уходят в спам, хотя SPF настроен?

SPF в одиночку сегодня недостаточен. Крупные провайдеры требуют от массовых отправителей связку SPF, DKIM и DMARC: DMARC говорит получателю, что делать с письмом, не прошедшим проверку. Домен без DMARC-записи выглядит для фильтра как домен, за отправку с которого никто не отвечает.

Чем проверка адреса отличается от письма с кодом подтверждения?

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

Что делать со статусом catch-all?

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

Почему за статус unknown не берут денег?

Unknown означает, что почтовый сервер домена применил greylisting, не ответил или заблокировал проверку анти-спамом. Вердикта нет, есть только его отсутствие. Такой запрос не тарифицируется, а адрес стоит проверить повторно через несколько минут.

Как проверить DMARC и DKIM через API?

DMARC лежит в TXT-записи служебного поддомена _dmarc.вашдомен, DKIM — в TXT по имени selector._domainkey.вашдомен, где селектор выдаёт почтовая платформа. Обе достаются обычным запросом к /api/dns/lookup с этим именем в параметре domain, ответ приходит в группе TXT.

Можно ли проверить базу до регистрации?

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

Сервисы из этой статьи

Проверка почты

Проверка доставляемости адреса: существует ли ящик, MX, catch-all, одноразовый ящик, спам-ловушка, подсказка по опечатке

Проверка DNS-записей

Резолв A/AAAA/MX/TXT и других типов, в т.ч. через DoH

Попробовать прямо сейчас — без регистрации

Демо-ключ ak_sandbox_demo_mockdata_v1 — публичный и общий для всех. С ним API отвечает моками: данные правдоподобные, но сгенерированные, и они не меняются от запроса к запросу — на них удобно писать тесты. Настоящие данные приходят с личным ключом.

curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     https://atlorium.com/openapi/email_ru.json
Полезно? Перешлите коллеге: Telegram VK

Восстанавливаем соединение…

Похоже, связь с сервером ненадолго прервалась. Переподключаемся автоматически — пожалуйста, подождите несколько секунд.

Не удалось переподключиться

Проверьте интернет-соединение. Можно повторить попытку или обновить страницу.

Сессия устарела

Соединение восстановлено, но сессию нужно перезагрузить. Обновите страницу, чтобы продолжить.