Application:
- IEmailSender.SendHtmlAsync(html, textFallback) - multipart/alternative
с plain-text для клиентов без HTML;
- EmailTemplateRenderer - минимальный mustache-light:
{{key}} (HTML-escape), {{{raw}}} (без escape), {{#key}}...{{/key}}
(условный блок, truthy если не-null/не-пусто/не-"0"/не-"false");
- EmailTemplates - загрузчик embedded HTML-шаблонов из
Resources/EmailTemplates/*.html, кеш after-first-read, parse
"Subject: <тема>" из первой строки, плюс plain-text strip для fallback.
Шаблоны (embedded в food-market.api.dll):
- invite.html - приглашение сотрудника с временным паролем и кнопкой
«Открыть Food Market».
- weekly-summary.html - выручка/чеки/средний чек за неделю + топ-товары.
- low-stock.html - таблица товаров с stock < MinStock.
EmployeesController.Create принимает SendInvite (требует CreateAccount):
формирует payload из orgName/loginUrl/role и шлёт через SendHtmlAsync.
SMTP-ошибки логируются warning'ом, не блокируют создание (showOnce
tempPassword фронту всё равно отдаётся).
Hangfire recurring jobs (EmailNotificationJobs):
- weekly-summary: cron "0 7 * * 1" (понедельник 07:00 UTC) - по каждой
активной орге считает revenue/tx/avgTicket/top-5, шлёт Admin'ам;
- low-stock-alert: cron "0 8 * * *" - товары с sum(stock)<MinStock,
шлёт Admin'ам. AsyncLocal tenant override на каждую орг чтобы
query-filter работал корректно.
Тесты: 8 unit на EmailTemplateRenderer + EmailTemplates (escape, raw,
условные блоки, invite/low-stock-шаблон-loaders). Все 35 unit
зелёные (27 + 8 новых).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Пункт 2 + 3 пакета SMTP-настроек.
Backend:
- IEmailSender (Application/Common/Email) — общий интерфейс отправки
одного письма; EmailNotConfiguredException — для контроллеров чтобы
ловить и отдавать понятный 400 вместо 500.
- MailKitEmailSender (Infrastructure/Email) — реализация:
· регистрируется Singleton, на каждой отправке открывает scope для
свежего AppDbContext (конфиг перечитывается без рестарта);
· читает PlatformSettings из БД, расшифровывает пароль через
IDataProtector("foodmarket.smtp");
· поддержка SmtpUseSsl (implicit TLS / 465) и SmtpStartTls (587);
оба false → открытое соединение (для dev/MailHog);
· бросает EmailNotConfiguredException если host или from-email пусты,
или если расшифровка пароля падает (ключ DataProtection ротировался).
API:
- PlatformSettingsController:
GET /api/super-admin/platform-settings — все поля КРОМЕ пароля
(только has-password флаг + updatedAt).
PUT — принимает Reason (≥10) + все поля + опциональный
NewSmtpPassword. Спец-значение "__clear__" снимает пароль.
Пароль шифруется через DataProtection при записи. Audit-log.
POST /test-send — реальная отправка через текущие настройки;
ловит EmailNotConfiguredException → 400, остальные → 500
с message (SuperAdmin-only, diagnostic-info нужна).
DI:
- AddSingleton<IEmailSender, MailKitEmailSender>;
- AddDataProtection (default file-system key store ASP.NET Core).
Пакеты:
- MailKit 4.10.0 (4.8 имел moderate-severity advisory).
- Microsoft.AspNetCore.DataProtection 8.0.11 (transitive в API уже
был через OpenIddict, но Infrastructure нужен явный reference).