wiki: ingest concepts/snolla-smtp-mail-delivery-2026-08 (postmortem SMTP-инцидента + монитор + рецепт деплоя)

This commit is contained in:
2026-08-14 16:07:26 +03:00
parent 147f771de1
commit 0582adbb4f
3 changed files with 52 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
---
title: snolla-smtp-mail-delivery-2026-08
status: live
tags: [snolla, smtp, yandex, incident, mail, portainer, deploy]
related: [[snolla-mailer-per-recipient-send]], [[portainer-stack-management-vds]], [[vds-kzntsv]]
updated: 2026-08-14
---
# 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 (1214.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 форм с 23 админами** — все такие письма падали.
**Фикс**: `@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-стеков 1723 == канон (`pass snolla-smtp/full-env`).
Алерт: ntfy `vds-ops` (basic auth vitya). Негатив-тест пройден. Копия: `.admin/scripts/snolla-smtp-monitor/`.
## Рецепт деплоя (что запомнить)
1. **Креды** — только через `pass snolla-smtp/full-env` (канон) + root-only файл на VDS `/root/.snolla-smtp-monitor.env` (600).
2. **pilonuxt** (pilorama98): конфиг запечён в образ (сборка с workstation, тег = git-sha, `--secret id=verdaccio_token,env=VERDACCIO_TOKEN`).
3. **env-сайты** (7): конфиг production.json запечён, секреты через Portainer stack env (${VAR} + Env-массив). Сборка на VDS из gitea-архива по sha (`deploy/Dockerfile`, `--build-arg VERDACCIO_TOKEN=<books-ci>`).
4. **Portainer API**: JWT через `/api/auth` (admin password, API-ключ давал 401). Обновление стека: `GET /api/stacks/{id}/file` → regex-замена `image:``PUT /api/stacks/{id}?endpointId=1` с `env` preserve + `pullImage`. ⚠️ oldtag парсить **regex'ом**, не `split(':')` (в строке image идут комментарии).
5. **Гоча**: контейнер, созданный ad-hoc (`/tmp/<name>-deploy` compose), НЕ принадлежит Portainer-стеку → PUT падает 500 «container name already in use» → перед деплоем снести старый контейнер.
6. **yarn bump по тиражу**: после релиза snolla-пакета проверить, что пины потребителей (caret/exact) пускают новую версию; data дедюпить `yarn up -R @snolla/data` (две копии = два sequelize-инстанса).
## Ссылки
- Таска: `snolla-mailer-per-recipient-send` (🟢), `snolla-smtp-send-monitor-vds` (🟢), pilorama98 `pilorama-forms-email-down` (🟢).
- Креды: `pass snolla-smtp/full-env`.
- Код: `@snolla/mailer@0.9.0` (emailSender.js, цикл по получателям, коммит bb8599d).