Бэкенд: регистрация со входом сразу и /api/config, профиль и смена пароля, свои папки, PATCH письма (прочитано, флажок, перенос; Спам ставит отметку), корзина и очистка, черновики, несколько адресатов и копия, вложения черновика уходят с письмом, вложения и заголовки To/Cc входящих сохраняются, письма заблокированных — в Спам. Исправлено: загрузка файла (415), колонка content_id (500 на любых вложениях), SPA-маршрут с точкой. Фронтенд переписан: все таблицы — грид, оформление — шаблон synthwave-design-showcase.html. α Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
11 KiB
Markdown
103 lines
11 KiB
Markdown
# 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`). Регистрация по умолчанию закрыта, ящики — только в своих доменах.
|
|
|
|
## Веб-почта
|
|
|
|
Angular 22 в стиле [synthwave-шаблона](frontend/src/assets/synthwave-design-showcase.html): стеклянные панели, неон, движущаяся сетка (выключается в настройках и при `prefers-reduced-motion`), четыре палитры и высокий контраст. Все таблицы — грид [`@jambotron/aria-grid`](https://gitea.jambotron.com/jambotron_gang/jambotron-aria-grid) (сортировка, страницы, выбор строк, массовые действия), пакет — из реестра npm Gitea (`frontend/.npmrc`).
|
|
|
|
- **Вход и регистрация.** Регистрация (если открыта, `MAIL_REGISTRATION`) — имя ящика в одном из `MAIL_DOMAINS`, после неё вход сразу. Служебные имена (`postmaster`, `abuse`, `admin`…) заняты. Пароль — от 8 символов.
|
|
- **Папки.** Входящие, Черновики, Отправленные, Спам, Корзина и свои (создать, переименовать, удалить — письма уходят в Корзину). Счётчики непрочитанных обновляются раз в 30 с.
|
|
- **Список писем.** Поиск по теме, отправителю, получателям и тексту; флажок; прочитано/не прочитано; перенос в папку, в Спам и обратно; удаление (в Корзину, из Корзины — насовсем); очистка Корзины и Спама.
|
|
- **Письмо.** HTML-письма — в изолированном `iframe` без скриптов, внешние картинки — только по кнопке (отправитель не узнаёт, что письмо открыли). Вложения скачиваются. Заблокировать отправителя — его письма сразу идут в Спам.
|
|
- **Написать.** Несколько адресов в «Кому» и «Копия», подсказки из контактов, ответ, ответ всем, пересылка. Черновик сохраняется сам каждые 15 с; файлы (до 10 МБ) прикрепляются к черновику и уходят вместе с письмом.
|
|
- **Контакты, заблокированные отправители, настройки** (имя, пароль, место, оформление).
|
|
|
|
API: `GET /api/config` (без входа), `/api/me`, `/api/me/password`, `/api/folders`, `/api/messages` (`PATCH` — `read`/`flagged`/`folderId`), `/api/drafts`, `/api/send` (`draftId` — с вложениями черновика), `/api/contacts`, `/api/spam/blacklist`.
|
|
|
|
## Настройки (переменные окружения)
|
|
|
|
Секреты без значений по умолчанию — без них сервер не стартует. Их можно передать переменными или файлами в `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` | другие источники для веб-клиента (только разработка) | — |
|
|
|
|
## Разработка
|
|
|
|
```bash
|
|
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` при первом запуске.
|
|
- Регистрация открыта (`MAIL_REGISTRATION=true`): любой может завести ящик в `MAIL_DOMAINS`. Наружу письма не уходят, поэтому для тестового стенда это безопасно; закрыть — `MAIL_REGISTRATION=false`.
|
|
- Наружу открыты только веб-почта и 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-писем (`cid:`) показываются как вложения, а не внутри письма.
|
|
- [ ] Защита регистрации и входа от перебора (лимит попыток, капча).
|
|
- [ ] Письмо нескольким получателям: при ошибке на втором первый может получить его дважды (после повтора отправителя).
|
|
- [ ] Своя настоящая почта: отдельная VPS, порт 25, PTR, отдельный домен (чек-лист Jambotron 75.6).
|
|
|
|
## Лицензия
|
|
|
|
MIT
|