442 lines
18 KiB
Markdown
442 lines
18 KiB
Markdown
# Mail Server
|
||
|
||
Прототип почтового сервера на **Spring Boot + Angular** с REST API для управления письмами.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ Angular Frontend │
|
||
│ (веб-почта: список, чтение, написание, контакты) │
|
||
└──────────────────────┬──────────────────────────────┘
|
||
│ REST API (JSON)
|
||
┌──────────────────────▼──────────────────────────────┐
|
||
│ Spring Boot Backend │
|
||
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||
│ │ Users │ │ Messages │ │ Contacts │ │
|
||
│ └────────────┘ └────────────┘ └──────────────┘ │
|
||
│ ┌────────────┐ ┌────────────┐ │
|
||
│ │ Folders │ │Attachments │ │
|
||
│ └────────────┘ └────────────┘ │
|
||
│ │ │
|
||
│ ┌────────▼────────┐ │
|
||
│ │ JavaMailSender │ │
|
||
│ │ (SMTP отправка)│ │
|
||
│ └─────────────────┘ │
|
||
└──────────────────────┬──────────────────────────────┘
|
||
│ JPA
|
||
┌──────────────────────▼──────────────────────────────┐
|
||
│ PostgreSQL 17 │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Запуск
|
||
|
||
### 1. Docker Compose (рекомендуется)
|
||
|
||
```bash
|
||
cd mail-server
|
||
docker compose up --build
|
||
```
|
||
|
||
Сервисы:
|
||
- **REST API**: http://localhost:8080
|
||
- **SMTP (приём)**: localhost:3025
|
||
- **IMAP (доступ к письмам)**: localhost:143
|
||
- **Mailpit UI**: http://localhost:8025 (SMTP на порту 1025)
|
||
- **PostgreSQL**: localhost:5432
|
||
|
||
### 2. Локально
|
||
|
||
Требования:
|
||
- Java 21
|
||
- PostgreSQL 17
|
||
|
||
```bash
|
||
# Запустите PostgreSQL
|
||
docker run -d --name mail-db -p 5432:5432 \
|
||
-e POSTGRES_DB=mailserver \
|
||
-e POSTGRES_USER=mail \
|
||
-e POSTGRES_PASSWORD=secret \
|
||
postgres:17
|
||
|
||
# Запустите Mailpit для тестовой отправки
|
||
docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit
|
||
|
||
# Соберите и запустите бэкенд
|
||
cd backend
|
||
./gradlew bootRun
|
||
```
|
||
|
||
## API
|
||
|
||
### Тестовый пользователь
|
||
|
||
При первом запуске приложения автоматически создаётся тестовый пользователь:
|
||
|
||
- **Email:** `admin@mailserver.local`
|
||
- **Пароль:** `admin123`
|
||
|
||
### Регистрация
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/register \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"email":"test@example.com","password":"secret123","displayName":"Test User"}'
|
||
```
|
||
|
||
### Логин
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/login \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"email":"test@example.com","password":"secret123"}'
|
||
```
|
||
|
||
Ответ:
|
||
|
||
```json
|
||
{
|
||
"token": "eyJhbGci...",
|
||
"email": "test@example.com"
|
||
}
|
||
```
|
||
|
||
Все защищённые эндпоинты требуют заголовок `Authorization: Bearer <token>`.
|
||
|
||
### Получить папки
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/folders \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
### Получить письма из папки
|
||
|
||
```bash
|
||
curl "http://localhost:8080/api/messages?folderId=1&page=0&size=20" \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
### Отправить письмо
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/send \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{"to":"recipient@example.com","subject":"Hello","body":"Test message"}'
|
||
```
|
||
|
||
### Симулировать входящее письмо (для тестов)
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/simulate/receive \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"senderEmail":"sender@example.com",
|
||
"senderName":"Sender",
|
||
"recipientEmail":"test@example.com",
|
||
"subject":"Incoming test",
|
||
"body":"This is a test message"
|
||
}'
|
||
```
|
||
|
||
### Отметить как прочитанное
|
||
|
||
```bash
|
||
curl -X PUT http://localhost:8080/api/messages/1/read \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
### Удалить письмо
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:8080/api/messages/1 \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
### Загрузить вложение к письму
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/messages/1/attachments \
|
||
-H "Authorization: Bearer <token>" \
|
||
-F "file=@/path/to/file.pdf"
|
||
```
|
||
|
||
### Получить список вложений письма
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/messages/1/attachments \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
### Скачать вложение
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/attachments/1/download \
|
||
-H "Authorization: Bearer <token>" \
|
||
-o file.pdf
|
||
```
|
||
|
||
### Удалить вложение
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:8080/api/attachments/1 \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
### Поиск по письмам
|
||
|
||
Поиск поддерживает два режима: простой поиск по ключевому слову и расширенный поиск с отдельными фильтрами.
|
||
|
||
**Простой поиск** — ищет по ключевому слову в теме, теле, отправителе и получателе:
|
||
|
||
```bash
|
||
curl "http://localhost:8080/api/messages/search?folderId=1&keyword=hello" \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
**Расширенный поиск** — используйте отдельные параметры для точной фильтрации:
|
||
|
||
```bash
|
||
# Поиск по теме
|
||
curl "http://localhost:8080/api/messages/search?folderId=1&subject=report" \
|
||
-H "Authorization: Bearer <token>"
|
||
|
||
# Поиск по тексту письма
|
||
curl "http://localhost:8080/api/messages/search?folderId=1&body=invoice" \
|
||
-H "Authorization: Bearer <token>"
|
||
|
||
# Поиск по отправителю
|
||
curl "http://localhost:8080/api/messages/search?folderId=1&sender=company.com" \
|
||
-H "Authorization: Bearer <token>"
|
||
|
||
# Комбинированный поиск (все фильтры применяются одновременно)
|
||
curl "http://localhost:8080/api/messages/search?folderId=1&subject=report&sender=company.com" \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
Все параметры поиска (`keyword`, `subject`, `body`, `sender`, `recipient`) необязательны. Если ни один параметр не указан, возвращаются все письма папки.
|
||
|
||
### Отправить письмо через SMTP-сервер
|
||
|
||
Встроенный SMTP-сервер слушает порт **3025**. Любое письмо, отправленное на этот порт, будет сохранено в базе данных, если получатель зарегистрирован в системе.
|
||
|
||
```bash
|
||
# Отправить письмо через telnet
|
||
telnet localhost 3025
|
||
```
|
||
|
||
Или с помощью curl:
|
||
|
||
```bash
|
||
curl --url 'smtp://localhost:3025' \
|
||
--mail-from 'sender@example.com' \
|
||
--mail-rcpt 'test@example.com' \
|
||
-T <(echo -e 'From: sender@example.com\nTo: test@example.com\nSubject: SMTP Test\n\nHello from SMTP!')
|
||
```
|
||
|
||
Письмо появится во входящих (INBOX) пользователя `test@example.com`.
|
||
|
||
### Управление чёрным списком
|
||
|
||
Добавьте отправителя в чёрный список — письма от него будут автоматически помечаться как спам.
|
||
|
||
**Получить список заблокированных отправителей:**
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/spam/blacklist \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
**Добавить отправителя в чёрный список:**
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/spam/blacklist \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-d '{"senderEmail":"spammer@example.com","reason":"Спам"}'
|
||
```
|
||
|
||
**Удалить отправителя из чёрного списка:**
|
||
|
||
```bash
|
||
curl -X DELETE "http://localhost:8080/api/spam/blacklist?senderEmail=spammer@example.com" \
|
||
-H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
Письма от заблокированных отправителей автоматически получают флаг `spam: true` и могут быть отфильтрованы на стороне клиента.
|
||
|
||
### Подключение через IMAP-сервер
|
||
|
||
Встроенный IMAP-сервер слушает порт **143**. Вы можете подключиться к нему из любого почтового клиента (Thunderbird, Outlook, Apple Mail) или через telnet для тестирования.
|
||
|
||
```bash
|
||
# Подключение через telnet
|
||
telnet localhost 143
|
||
```
|
||
|
||
Пример сессии IMAP:
|
||
|
||
```
|
||
a LOGIN test@example.com secret123
|
||
a OK LOGIN completed
|
||
|
||
a LIST "" "*"
|
||
* LIST (\HasNoChildren) "." "INBOX"
|
||
* LIST (\HasNoChildren) "." "Sent"
|
||
a OK LIST completed
|
||
|
||
a SELECT "INBOX"
|
||
* 5 EXISTS
|
||
* 0 RECENT
|
||
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
|
||
a OK [READ-WRITE] SELECT completed
|
||
|
||
a FETCH 1 (BODY[])
|
||
* 1 FETCH (UID 1 FLAGS (\Seen) BODY[] {123
|
||
Hello, this is a test message.
|
||
))
|
||
a OK FETCH completed
|
||
|
||
a SEARCH ALL
|
||
* SEARCH 1 2 3 4 5
|
||
a OK SEARCH completed
|
||
|
||
a LOGOUT
|
||
* BYE IMAP4rev1 server terminating
|
||
a OK LOGOUT completed
|
||
```
|
||
|
||
Поддерживаемые IMAP-команды:
|
||
- **LOGIN** — аутентификация по email и паролю
|
||
- **SELECT / EXAMINE** — выбор папки для работы
|
||
- **FETCH** — получение заголовков и тела писем
|
||
- **SEARCH** — поиск по письмам
|
||
- **STORE** — изменение флагов (прочитано, помечено, удалено)
|
||
- **EXPUNGE** — удаление помеченных писем
|
||
- **LIST / LSUB** — список папок
|
||
- **STATUS** — информация о папке (количество писем, непрочитанных)
|
||
- **CAPABILITY** — список поддерживаемых возможностей
|
||
- **LOGOUT** — выход из сессии
|
||
|
||
Порт IMAP настраивается через `mailserver.imap.port` в `application.properties` (по умолчанию 143).
|
||
|
||
### DKIM / SPF / DMARC — защита от подделки писем
|
||
|
||
Почтовый сервер поддерживает три стандарта защиты электронной почты:
|
||
|
||
**DKIM (DomainKeys Identified Mail)** — криптографическая подпись писем.
|
||
- При отправке письма автоматически подписываются RSA-ключом (2048 бит)
|
||
- При получении входящих писем проверяется DKIM-подпись отправителя
|
||
- Ключи хранятся в директории `./dkim-keys/` (генерируются автоматически при первом запуске)
|
||
- Публичный ключ публикуется в DNS TXT-записи: `_default._domainkey.<domain>`
|
||
|
||
**SPF (Sender Policy Framework)** — проверка разрешённых серверов отправителя.
|
||
- При получении письма проверяется SPF DNS-запись домена отправителя
|
||
- Поддерживаемые механизмы: `all`, `ip4`, `ip6`, `mx`, `a`, `include`
|
||
- Локальные адреса (127.0.0.1, 192.168.x.x) пропускаются без проверки
|
||
|
||
**DMARC (Domain-based Message Authentication, Reporting & Conformance)** — политика обработки.
|
||
- Объединяет результаты SPF и DKIM с проверкой alignment (совпадение доменов)
|
||
- Поддерживаемые политики: `none` (мониторинг), `quarantine` (спам), `reject` (отклонить)
|
||
- Если SPF и DKIM не пройдены, применяется политика DMARC из DNS
|
||
|
||
**Результат проверки:**
|
||
- Письма, не прошедшие SPF/DKIM/DMARC, автоматически помечаются флагом `spam: true`
|
||
- Логируются результаты всех трёх проверок для каждого входящего письма
|
||
|
||
**Настройки в `application.properties`:**
|
||
```
|
||
mailserver.dkim.enabled=true
|
||
mailserver.dkim.domain=localhost
|
||
mailserver.dkim.keys-dir=./dkim-keys
|
||
mailserver.spf.enabled=true
|
||
mailserver.spf.local-bypass=true
|
||
mailserver.dmarc.enabled=true
|
||
mailserver.dmarc.local-bypass=true
|
||
```
|
||
|
||
**Публикация DKIM-ключа в DNS:**
|
||
При первом запуске сервер выводит в лог публичный ключ DKIM. Добавьте его как TXT-запись:
|
||
```
|
||
_name: _default._domainkey.yourdomain.com
|
||
type: TXT
|
||
value: v=DKIM1; k=rsa; p=MIIBojANBgkqhkiG9w0BAQ...
|
||
```
|
||
|
||
## Модели данных
|
||
|
||
| Сущность | Описание |
|
||
|---|---|
|
||
| **User** | Почтовый ящик, пароль, квота |
|
||
| **Folder** | INBOX, Sent, Drafts, Trash, Spam |
|
||
| **Message** | Заголовки, тело, флаги (read, flagged, spam) |
|
||
| **BlacklistEntry** | Заблокированный отправитель, привязка к пользователю |
|
||
| **Attachment** | Имя, MIME-тип, размер, путь к файлу |
|
||
| **Contact** | Имя, email, телефон, компания |
|
||
|
||
## Деплой на VPS
|
||
|
||
### Caddy Docker Proxy
|
||
|
||
Проект использует **Caddy Docker Proxy** для автоматического HTTPS и reverse proxy. Caddy сам генерирует конфиг из `labels` в `docker-compose.prod.yml` — отдельный Caddyfile не нужен.
|
||
|
||
**Как это работает:**
|
||
- Контейнер `mail-server` имеет `labels` с настройками Caddy
|
||
- `lucaslorentz/caddy-docker-proxy` следит за Docker через socket и автоматически создаёт виртуальные хосты
|
||
- HTTPS-сертификат от Let's Encrypt получается автоматически
|
||
|
||
**Ключевые labels:**
|
||
|
||
| Label | Описание |
|
||
|-------|----------|
|
||
| `caddy: mail.jambotron.com` | Домен, на котором доступен сервис |
|
||
| `caddy.route.api.reverse_proxy` | Проксирует `/api/*` на Spring Boot (порт 8080) |
|
||
| `caddy.route.static.root` | Корневая директория для Angular фронтенда |
|
||
| `caddy.route.static.file_server` | Раздаёт статические файлы Angular |
|
||
|
||
**Запуск на VPS:**
|
||
|
||
```bash
|
||
# Клонируйте репозиторий
|
||
git clone https://gitea.jambotron.com/jambotron_gang/reactive-mail-server.git
|
||
cd reactive-mail-server
|
||
|
||
# Соберите фронтенд
|
||
cd frontend && npm install && npx ng build --configuration=production
|
||
cd ..
|
||
|
||
# Запустите
|
||
docker compose -f docker-compose.prod.yml up -d --build
|
||
```
|
||
|
||
**Перед запуском:**
|
||
1. Замените `mail.jambotron.com` на ваш домен в `docker-compose.prod.yml`
|
||
2. Настройте PTR (Reverse DNS) у хостера — IP → `mail.jambotron.com`
|
||
3. Добавьте DNS-записи: A (`mail` → IP), MX (`@` → `mail.jambotron.com`), TXT (`v=spf1 ip4:<IP> -all`)
|
||
4. Откройте порты: 80, 443, 25 (SMTP relay), 3025 (SMTP приём)
|
||
|
||
## TODO
|
||
|
||
- [x] Хэширование паролей (BCrypt)
|
||
- [x] Аутентификация (JWT)
|
||
- [x] Приём SMTP (встроенный сервер на WebFlux TcpServer)
|
||
- [x] IMAP-сервер (встроенный, порт 143)
|
||
- [x] Вложения (загрузка/скачивание)
|
||
- [x] Поиск по письмам (по теме, телу, отправителю, получателю)
|
||
- [x] Angular фронтенд (v22 + Tailwind CSS + Synthwave дизайн)
|
||
- [x] Антиспам (чёрный список отправителей)
|
||
- [x] DKIM/SPF/DMARC (подпись, верификация, DNS-проверка)
|
||
- [x] SMTP релей (доставка на внешнюю почту)
|
||
- [x] Контакты (адресная книга)
|
||
- [x] Reply / Forward
|
||
- [x] Пагинация писем
|
||
- [x] HTML/Markdown режим
|
||
- [x] Caddy Docker Proxy (автоматический HTTPS)
|
||
- [ ] TLS шифрование SMTP/IMAP
|
||
|
||
## Лицензия
|
||
|
||
MIT
|