Skip to content

feat(bounces): захват DSN / фидбека о доставке#55

Merged
friench merged 3 commits into
mainfrom
feat/bounce-dsn
Jul 6, 2026
Merged

feat(bounces): захват DSN / фидбека о доставке#55
friench merged 3 commits into
mainfrom
feat/bounce-dsn

Conversation

@friench

@friench friench commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Что это

Захват уведомлений о недоставке (bounce/DSN) и связывание их с задачами отправки, с выводом в дашборд и через вебхуки (закрывает #21). Вместо #16 (отложен — см. комментарий в issue).

Surfaces: REST API + dashboard + MCP.

Как работает

  • POST /admin/api/bounces/ingest принимает сырое DSN-письмо (text/* / message/rfc822, либо JSON { raw }).
  • Чистый lib/dsn-parser (RFC 3464, без зависимостей) вытаскивает по каждому получателю recipient/action/status/diagnostic (склеивает свёрнутые строки) + оригинальный Message-ID.
  • Message-ID коррелируется с send_jobs по полю messageId → в bounce_events пишется sendJobId.
  • Классификация: 5.x.x → hard, 4.x.x → soft, иначе unknown.
  • На каждый получатель — событие send.bounced через существующий webhook-диспетчер.
  • GET /admin/api/bounces — список.

Изменения

  • Схема: таблица bounce_events + миграция 0017; SendJobRepository.findByMessageId.
  • domain/bounces: repository + service (parse→classify→correlate→store→dispatch) + serialize; BounceEventDTO.
  • Роуты admin/bounces.ts (ingest + list), admin-scoped.
  • UI: страница «Bounces» (i18n en/ru) + навигация.
  • MCP: list_bounces / ingest_bounce.
  • Тесты: unit парсера (hard/soft, свёрнутая диагностика, мульти-получатели, Original-Message-ID, не-DSN→null) + интеграционные (ingest, классификация, корреляция по message-id, JSON-пейлоад, событие send.bounced, 422 на не-DSN, list).

Проверки

typecheck · lint · format:check · vitest (509 ✓) · ui typecheck/build · mcp typecheck/build — всё зелёное.

Заметка по интеграции

Ingest-эндпоинт — это API захвата. Как доводить bounce-почту до него — деплой-шаг (Postfix pipe-транспорт / внешний форвардер на POST /admin/api/bounces/ingest); встроенного поллера почтового ящика в этом PR нет намеренно (он был бы deploy-fragile и завязан на DMS). Синхронные SMTP-фейлы по-прежнему видны в Send log (dead-задачи); этот PR добавляет именно асинхронный DSN-захват.

Closes #21

🤖 Generated with Claude Code

friench and others added 3 commits July 6, 2026 08:52
Dependency-free i18n for the dashboard with an en/ru locale and a
language switcher (persisted in localStorage; defaults from navigator).

- i18n/: I18nProvider + useT()/useI18n(); dictionaries composed from
  per-namespace `ns/*.ts` modules via import.meta.glob (so adding a
  page's translations is a new file — conflict-free parallel work)
- ns/common, ns/nav, ns/login, ns/dashboard (en + ru)
- Layout: translated nav + language selector; Login + Dashboard translated
- t(key) falls back active-locale → English → key, with {var} interpolation

Stage 1 of 3 (framework + core); remaining pages translated next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the dashboard internationalization: every page and shared
component now renders through `t()` with an English + Russian locale.
23 new namespace modules under i18n/ns/ (one per page/component),
auto-discovered by the glob-composed dictionary — no shared registry.

Translation of the remaining pages was fanned out across a team of
subagents (one batch each: domains/mailboxes, spam+onboarding,
observability/ops, sending/config, and the rest incl. ResourceTable),
then validated by the lead:
- en/ru key parity verified across all 29 namespaces (~470 keys)
- every page wires useT(); protected core i18n files untouched
- ui typecheck + build + prettier clean; backend suite unaffected (499)

Technical tokens (DTO enum values, API paths, feature-flag keys,
protocol names, example placeholders) are intentionally left untranslated.

Closes #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture delivery-status notifications (bounces) and correlate them to
send jobs, surfaced in the dashboard and via webhooks.

- lib/dsn-parser: pure, dependency-free RFC 3464 parser — per-recipient
  recipient/action/status/diagnostic (folded lines joined) + the original
  Message-ID; classifies 5.x.x=hard / 4.x.x=soft (unit-tested)
- schema: bounce_events (sendJobId FK, recipient, type, classification,
  statusCode, diagnostic, originalMessageId) + migration 0017
- SendJobRepository.findByMessageId — correlate a bounce to its send job
- domain/bounces: repository + service (ingest: parse → classify →
  correlate → store → dispatch `send.bounced`) + serialize
- routes: POST /admin/api/bounces/ingest (raw text/* / message/rfc822 or
  JSON {raw}) + GET /admin/api/bounces; admin-scoped; BounceEventDTO
- UI: Bounces page (en/ru i18n) + nav; MCP: list_bounces / ingest_bounce
- tests: dsn-parser unit + bounces integration (ingest, classify, correlate
  by message id, JSON payload, send.bounced event, 422 non-DSN, list)

The ingest endpoint is the capture API; routing bounce mail to it (Postfix
pipe / forwarder) is a deployment step — no in-app mailbox poller here.

Closes #21

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@friench friench merged commit 90a29f4 into main Jul 6, 2026
1 check passed
@friench friench deleted the feat/bounce-dsn branch July 6, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bounce / delivery feedback (DSN capture)

1 participant