- 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>
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