5.9 KiB
SMTP-инцидент 2026-08-12..14: письма форм snolla-сайтов молча не доставлялись
TL;DR
Яндекс отключил протокольный доступ к noreply@snolla.com (доменная почта = платный Яндекс 360) → формы отвечали «успешно», а письма заказов 2 дня не уходили (mailer глотает ошибку catch { console.log }). Заказы НЕ терялись (лежат в FormSubmissions, MSSQL). Переехали на новый ящик e-16513832@yandex.ru (app-password) + mailer переведён на per-recipient отправку (@snolla/mailer@0.9.0). Поставлен монитор SMTP-отправки на VDS.
Три слоя root cause (каждый чинился отдельно)
1. 525 SMTP disabled (12–14.08, инцидент)
Yandex: 525 5.7.13 Error: SMTP disabled на auth — протокольный доступ к ящику отключён (домен на платном 360, «you need a Yandex 360 for Business»). Код сайтов не менялся (git-чисто), старый пароль давал 535 invalid login (пароль тоже сменили когда-то), новый — 525 (ящик жив, но SMTP выключен).
Фикс: новый ящик e-16513832@yandex.ru + пароль приложения «Почта» (создан 14.08 12:10 МСК). Прокатка кредов по 8 стекам.
2. 554 SPAM на мульти-получателях (холодный ящик)
После переезда: письмо с 2+ получателями от нового (холодного) ящика → 554 5.7.1 Message rejected under suspicion of SPAM; с 1 получателем — проходит. Проверено изоляционно (msmtp): 1→250, 2→554 стабильно. Старый ящик был прогрет → мульти-получатели проходили. В БД 11 форм с 2–3 админами — все такие письма падали.
Фикс: @snolla/mailer@0.9.0 — цикл по получателям (по одному письму на каждого; dedupe; per-recipient try/catch; Cc/Bcc/Reply-To сохранены). Потребители перевыпущены: forms-api@0.2.0 (^0.9.0), web@0.44.0 (^0.9.0) — важно: старые пины (^0.8.3, exact 0.8.3) НЕ пускали 0.9.0, деплой был заблокирован до бампов.
3. 550 Sender not owned (запечённый from)
У env-сайтов в config/production.json был запечён mailSettings.from: noreply@snolla.com (мёртвый ящик) → 550 5.7.0 Sender address rejected: not owned by authorized user (auth-юзер e-16513832@yandex.ru не владеет From). Шаблон pilorama98 pilorama98checkoutconfirm тоже имел жёсткий From: noreply@snolla.com.
Фикс: mailSettings.from → e-16513832@yandex.ru во всех 7 production.json + UPDATE EmailTemplates SET From='e-16513832@yandex.ru' WHERE LoweredName='pilorama98checkoutconfirm' (единственный шаблон с From; остальные NULL → fallback на mailSettings).
Монитор (живёт на VDS)
/root/snolla-smtp-monitor/monitor.py (python3 stdlib) + cron 0 8 * * *:
- smtp-auth тест (ловит 525/535/connection) — ежедневно;
- weekly real-send на
OPS_NOTIFY_EMAIL; - parity-чек: SMTP_USER/SMTP_PASSWORD в env Portainer-стеков 17–23 == канон (
pass snolla-smtp/full-env). Алерт: ntfyvds-ops(basic auth vitya). Негатив-тест пройден. Копия:.admin/scripts/snolla-smtp-monitor/.
Рецепт деплоя (что запомнить)
- Креды — только через
pass snolla-smtp/full-env(канон) + root-only файл на VDS/root/.snolla-smtp-monitor.env(600). - pilonuxt (pilorama98): конфиг запечён в образ (сборка с workstation, тег = git-sha,
--secret id=verdaccio_token,env=VERDACCIO_TOKEN). - env-сайты (7): конфиг production.json запечён, секреты через Portainer stack env (${VAR} + Env-массив). Сборка на VDS из gitea-архива по sha (
deploy/Dockerfile,--build-arg VERDACCIO_TOKEN=<books-ci>). - Portainer API: JWT через
/api/auth(admin password, API-ключ давал 401). Обновление стека:GET /api/stacks/{id}/file→ regex-заменаimage:→PUT /api/stacks/{id}?endpointId=1сenvpreserve +pullImage. ⚠️ oldtag парсить regex'ом, неsplit(':')(в строке image идут комментарии). - Гоча: контейнер, созданный ad-hoc (
/tmp/<name>-deploycompose), НЕ принадлежит Portainer-стеку → PUT падает 500 «container name already in use» → перед деплоем снести старый контейнер. - yarn bump по тиражу: после релиза snolla-пакета проверить, что пины потребителей (caret/exact) пускают новую версию; data дедюпить
yarn up -R @snolla/data(две копии = два sequelize-инстанса).
Ссылки
- Таска:
snolla-mailer-per-recipient-send(🟢),snolla-smtp-send-monitor-vds(🟢), pilorama98pilorama-forms-email-down(🟢). - Креды:
pass snolla-smtp/full-env. - Код:
@snolla/mailer@0.9.0(emailSender.js, цикл по получателям, коммит bb8599d).