Если вы меняете почтового провайдера у интернет-магазина, а уведомления о заказах уходят письмом, проверьте два места: куда идут письма вам самим и кто читает отчёты о недоставке. В августе мы переезжали сами и наткнулись на обе ловушки. Сервис отправки отчитался «доставлено 1, 100%», а письма не было ни в одной из шести папок ящика. Ни ошибок в логах, ни упавших заказов: обе ловушки молчат.
Почему переезжали
22 августа Яндекс 360 перевёл нашу организацию в режим просмотра за неоплату, и отправка встала: 525 5.7.13 Error: SMTP disabled. Приём при этом работал: IMAP пускал с тем же паролем. Ущерба не было: очередь писем стояла пустой, статусов FAILED не появилось. Мы включили отсрочку на 5 дней и за это время перевели отправку на Yandex Cloud Postbox.
Транспорт магазина целиком на переменных окружения, поэтому код менять не пришлось:
|
Переменная |
Было |
Стало |
|---|---|---|
|
|
||
|
|
адрес ящика |
идентификатор API-ключа |
|
|
пароль приложения |
секрет API-ключа |
Порт 465 и TLS при подключении остались прежними. SPF менять не пришлось: сервис шлёт со своего служебного домена, а DMARC проходит за счёт DKIM нашего домена. Мы проверили это на письме в Gmail: dkim=pass, spf=pass, dmarc=pass.
Первый ключ не заработал: 535 5.7.8 Authentication failed. Мы сверили хеш записанного значения с выданным, прогнали сырой SMTP-диалог, проверили роль сервисного аккаунта и область ключа. Причина оказалась в том, что секрет мы прочитали со скриншота, а в том шрифте 1, l и I неразличимы. Новый ключ, присланный текстом, заработал с первой попытки.
Ловушка 1: письмо на собственный домен пропадает
Проверочное письмо с адреса магазина на адрес того же магазина сервис отметил как доставленное, 100%. В ящике его не было: мы искали во всех шести папках, включая «Спам» и «Удалённые». Наше объяснение: Яндекс принимает такое письмо на уровне SMTP и молча выбрасывает, потому что внешний сервер представился доменом, который Яндекс сам обслуживает. Подтверждения от поддержки у нас нет, это вывод из опыта.
Грозило это не тестовыми письмами. Все админские уведомления шли на адрес CONTACT_TO_EMAIL, а он был ящиком магазина: новый оплаченный заказ, заказ с оплатой при получении, продажа подарочной карты, контактная форма. После переезда они бы исчезали при «доставлено» в отчёте. Мы перевели уведомления на ящик другого провайдера и с тех пор не проверяем канал письмом самому себе: проверка идёт на чужую почту, и читаем заголовки Authentication-Results, а не «пришло или нет».
Эту ловушку мы встретили второй раз, уже в другом виде. В ноябре магазин переехал на другой сервер, окружение собрали заново, и строки CONTACT_TO_EMAIL в нём не оказалось. Код ведёт себя так:
const adminTo = process.env.CONTACT_TO_EMAIL;if (adminTo) { await enqueueOrderEmail({ to: adminTo, template: "admin-new-order", /* ... */ });}
if (adminTo) {
await enqueueOrderEmail({ to: adminTo, template: "admin-new-order", /* ... */ });
}
Пустая переменная означает «письма нет» и ошибки нет: условие молча пропускает постановку. Это уведомление мы добавляли после живой продажи подарочной карты за 5 000 ₽, о которой узнали случайно, и вот оно отключилось само. О заказах могло извещать ещё и сообщение в Telegram, поэтому отсутствие письма легко не заметить. А в форме обратной связи тот же пустой адрес сработал иначе:
export const DEFAULT_CONTACT_TO = process.env.CONTACT_TO_EMAIL ?? STORE_BRAND.email.contact; // адрес на домене магазина
process.env.CONTACT_TO_EMAIL ?? STORE_BRAND.email.contact; // адрес на домене магазина
Пустая переменная здесь не пропускает письмо, а отправляет его на ящик собственного домена, то есть прямо в первую ловушку. Нашли мы это проверкой на боевом сервере, а не по жалобе. Вывод для кода: настройка, от которой зависит канал уведомлений, не должна иметь «молчаливого» значения по умолчанию. Пустая должна быть ошибкой при старте или записью в журнал.
Ловушка 2: отбойники уходят не туда
Отчёты о недоставке приходят письмами. Пока отправка и приём жили в одном Яндекс 360, они ложились в наш ящик, и скрипт разбирал их оттуда: жёсткие отказы закрывают адрес в базе, мягкие (ящик переполнен, временная ошибка) не закрывают. Зачем это нужно, видно из комментария к скрипту: на волне рассылки 11 августа мёртвых адресов было 12,5%, а почтовые провайдеры читают такую долю как признак купленной базы.
Мы увидели в заголовках тестового письма, что у нового сервиса служебный Return-Path вида feedback+...@postbox.yandexcloud.net: отчёты о недоставке будут возвращаться в сервис, а не в ящик. Скрипт при этом продолжит ходить в IMAP, будет находить ноль писем и честно писать «закрыто адресов: 0». Это выглядит как здоровье. Мёртвые адреса перестанут закрываться, а доля отказов вырастет через несколько волн, когда исправлять уже поздно. Мы увидели механизм и закрыли его заранее, а не прожили последствия.
Что мы сделали. Раньше скрипт брал для чтения ящика те же SMTP_USER и SMTP_PASSWORD, что и для отправки. Теперь у чтения свои IMAP_USER и IMAP_PASSWORD, а подстановка прежних идёт с проверкой, что логин похож на адрес почты:
function looksLikeMailbox(value: string): boolean { const at = value.indexOf("@"); return at > 0 && at < value.length - 1 && !value.includes(" ");}
const at = value.indexOf("@");
return at > 0 && at < value.length - 1 && !value.includes(" ");
}
Идентификатор ключа сервиса адресом не является, поэтому подстановка падает с объяснением, а не молчаливо возвращает пустой ящик. Мы проверили это на боевом: прогон с ключом сервиса в SMTP_USER и без IMAP_* даёт отказ с текстом причины.
Чего мы не сделали, и это надо сказать прямо. Полноценная замена скрипту это уведомления самого сервиса, но их мало включить: нужен приёмник, который кладёт отказ в базу. Мы его не написали. Пока рассылки небольшие, а отказов мало, это терпимо, но это долг.
Третья находка: суточная квота общая
18 сентября, на 203-м письме за сутки, сервис начал отвечать 550 5.4.5 Daily sending quota exceeded. Квота по умолчанию была 200 писем в 24 часа, и мы упёрлись в неё почти ровно. Важная деталь: квота общая на аккаунт, поэтому встала вся почта магазина, а не только рассылка: подтверждения заказов и ссылки для входа тоже. Цена маркетинговой волны оказалась больше, чем недоставленные письма кампании.
С 28 октября квота у нас 2000 писем в сутки. Письма менеджеров делят её с письмами о заказах, поэтому, если задан суточный лимит (EMAIL_DAILY_LIMIT), код перед постановкой письма менеджера проверяет остаток и держит запас в 30 писем под письма о заказах. Предел этой защиты назван в самом комментарии к коду: то, что шлёт почту в обход очереди, счётчик не видит.
Тишина в ящике не диагностика
Живой поток во входящие у нас около полутора писем в месяц, поэтому «ничего не пришло» ни о чём не говорит. Мы сделали сторожа приёма: раз в сутки он шлёт пробу на тот ящик, который читает, и проверяет, что вчерашняя проба там лежит. Состояние хранит сам ящик, ни таблицы, ни файла для сторожа нет. У него есть сознательные пределы: он не проверяет пересылку на внешний ящик, а самый первый прогон честно докладывает «набираю историю», а не выдумывает вердикт.
Что проверить до переезда почты
-
Куда уходят админские уведомления. Если на ящик вашего домена, переведите на ящик другого провайдера до переезда.
-
Что делает код, когда адрес уведомлений пуст: письма нет, ошибка или адрес по умолчанию на вашем домене.
-
Канал отправки проверяйте письмом на чужую почту и читайте заголовки
dkim,spf,dmarc. -
Кто читает отчёты о недоставке после переезда и что покажет скрипт, когда писем не станет.
-
Квота сервиса на сутки и общая она или раздельная для рассылки и писем о заказах.
-
Доступы чтения ящика отдельно от доступов отправки.
Что осталось не закрыто: приёмник уведомлений сервиса о недоставке и проверка пересылки.
ссылка на оригинал статьи https://habr.com/ru/articles/1103750/