Files
admin/.wiki/concepts/snolla-smtp-mail-delivery-2026-08.md

51 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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).