Files
reactive-mail-server/README.md
T
jambotron α 165edf1a1e Безопасность, SMTP, очередь отправки, DKIM, деплой на Jambotron (Jambotron v0.2.3 75.1–75.3)
- SMTP-сервер переписан: разбор по строкам, AUTH PLAIN/LOGIN, нет открытого релея,
  MAIL FROM только свой после входа, 250 только после сохранения (иначе 451), лимит размера
- отправка наружу: очередь outbound_queue (V2) с повторами ~22 ч и уведомлением о недоставке;
  Jakarta Mail, режимы off / mx / smarthost, байты письма не меняются (DKIM)
- DKIM: подпись utils-mail-dkim, проверка Apache jDKIM; ключ PKCS#8, старый PKCS#1 читается
- права: письма, папки, вложения — только владельцу (было: любой по номеру); имена вложений без ../
- секреты без значений по умолчанию (DB_PASSWORD, JWT_SECRET), регистрация закрыта,
  admin123 и /api/simulate/receive — только профиль dev
- веб-клиент внутри jar (относительный /api), Dockerfile в 3 этапа, не root, health;
  docker-compose.jambotron.yml: секреты создаются при первом запуске, наружу только 8080
- gradle-wrapper.jar был в .gitignore — сборка не запускалась
- тесты: 26 (SMTP реальным клиентом и сокетом, GreenMail, DKIM подпись+проверка, очередь)

α
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:37:43 +01:00

7.8 KiB

Reactive Mail Server

Почтовый сервер на Spring Boot WebFlux + R2DBC (PostgreSQL) + Angular: свой SMTP-сервер на Reactor Netty, веб-почта, очередь отправки, DKIM/SPF/DMARC.

Назначение — наши личные цели (v0.2.3, раздел 75 чек-листа Jambotron): тестовый проект на Jambotron, ловушка почты для dev/staging. Для писем пользователям Jambotron (регистрация, сброс пароля) остаётся Resend.

Как устроено

 SMTP-клиент / чужой сервер ──► SmtpServer (Netty, порт 3025) ──┐
                                                               ├─► MailRouter ─┬─► LocalDelivery → свой ящик (PostgreSQL)
 Веб-почта (Angular) ──► REST /api ──► SubmissionService ──────┘               └─► OutboundQueue → DeliveryWorker → SMTP получателя
  • Приём (SMTP). Команды разбираются по строкам (LineBasedFrameDecoder): работает и пачка команд в одном пакете, и строка, разорванная на два пакета. Письмо сохраняется или ставится в очередь до ответа 250; если сохранить не удалось — 451, и отправитель повторит. Работа с базой и DNS идёт не в сетевом потоке.
  • Не открытый релей.
    • Письмо на свой домен принимается от кого угодно.
    • Письмо на чужой домен — только после AUTH, иначе 550 5.7.1 Relaying denied.
    • Вошедший пользователь пишет только от своего адреса, иначе 553.
  • Маршрут (MailRouter) один для SMTP и веб-почты:
    • наш домен → наш ящик, а если ящика нет — «нет такого пользователя»;
    • задан MAIL_CATCH_ALL → всё в этот ящик;
    • чужой домен → очередь.
  • Отправка наружу (OUTBOUND_MODE):
    • off — ничего не уходит;
    • mx — напрямую на MX получателя, порт 25, STARTTLS по возможности;
    • smarthost — через провайдера (Resend, SES) с логином и проверкой TLS.
    • Очередь — таблица outbound_queue. Временная ошибка — повтор через 1 мин, 5 мин, 15 мин, 1 ч, 3 ч, 6 ч, 12 ч. Постоянная ошибка или исчерпанные повторы — уведомление отправителю во входящие.
  • DKIM. Подпись — utils-mail-dkim (rsa-sha256, relaxed/relaxed), проверка входящих — Apache James jDKIM по ключу из DNS отправителя. Ключ создаётся один раз в DKIM_KEYS_DIR, запись для DNS печатается в лог при старте: TXT <selector>._domainkey.<domain>.
  • Права. Письма, папки и вложения отдаются только владельцу (чужие — 404). Регистрация по умолчанию закрыта, ящики — только в своих доменах.

Настройки (переменные окружения)

Секреты без значений по умолчанию — без них сервер не стартует. Их можно передать переменными или файлами в MAILSERVER_SECRETS_DIR (имя файла = имя переменной).

Переменная Что По умолчанию
DB_PASSWORD пароль PostgreSQL обязательно
JWT_SECRET ключ токенов входа, ≥ 32 байт обязательно
DB_HOST, DB_PORT, DB_NAME, DB_USER база localhost, 5432, mailserver, mail
MAIL_DOMAINS наши домены, через запятую localhost
MAIL_HOSTNAME имя сервера в SMTP localhost
MAIL_ADMIN_EMAIL, MAIL_ADMIN_PASSWORD первый ящик на пустой базе; без пароля — случайный, один раз в логе admin@<первый домен>
MAIL_REGISTRATION открыть регистрацию false
MAIL_CATCH_ALL ящик-ловушка: вся почта сюда, наружу ничего —
SMTP_PORT порт SMTP 3025
SMTP_AUTH_WITHOUT_TLS разрешить AUTH без шифрования — только в закрытой сети (docker, ВМ) false
OUTBOUND_MODE off / mx / smarthost off
OUTBOUND_HOST, OUTBOUND_PORT, OUTBOUND_USERNAME, OUTBOUND_PASSWORD, OUTBOUND_TLS smarthost (starttls / ssl / none) —, 587, —, —, starttls
DKIM_DOMAIN, DKIM_SELECTOR, DKIM_KEYS_DIR DKIM localhost, mail, ./dkim-keys
SPF_ENABLED, DMARC_ENABLED проверки входящих true
MAIL_CORS_ORIGINS другие источники для веб-клиента (только разработка) —

Разработка

docker compose up -d                                               # PostgreSQL на 127.0.0.1:5433
./gradlew :backend:bootRun --args='--spring.profiles.active=dev'   # API :8080, SMTP :3025, admin@mailserver.local / admin123
cd frontend && npm install && npx ng serve                         # http://localhost:4200, /api → :8080
./gradlew :backend:test                                            # тесты (база не нужна)

Профиль dev — только для своего компьютера: известные пароли, открытая регистрация, POST /api/simulate/receive (письмо в ящик без SMTP).

Деплой на Jambotron (тест)

В мастере деплоя выбрать docker-compose.jambotron.yml, домен дать сервису mail, порт 8080.

  • Пароль базы и ключ токенов создаёт контейнер secrets-init при первом запуске (том secrets) — вводить ничего не нужно, в git ничего нет.
  • Первый ящик — admin@mail.test, пароль — в логах сервиса mail при первом запуске.
  • Наружу открыты только веб-почта и API. SMTP (3025) — внутри сети ВМ, наружу ничего не отправляется (OUTBOUND_MODE=off).
  • Домен ящиков — MAIL_DOMAINS (по умолчанию mail.test).

Образ собирается из backend/Dockerfile в три этапа: Angular → Spring Boot (веб-клиент внутри jar) → JRE не от root.

Не сделано

  • TLS: STARTTLS для SMTP и IMAP. Пока AUTH — только в закрытой сети.
  • IMAP выключен: упрощённый, блокирует сетевой поток (.block()). Переписать и проверить с Thunderbird.
  • Вложения входящих писем не сохраняются (берётся только текст или HTML).
  • Письмо нескольким получателям: при ошибке на втором первый может получить его дважды (после повтора отправителя).
  • Своя настоящая почта: отдельная VPS, порт 25, PTR, отдельный домен (чек-лист Jambotron 75.6).

Лицензия

MIT