18 KiB
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 (рекомендуется)
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
# Запустите 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
Регистрация
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"secret123","displayName":"Test User"}'
Логин
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"secret123"}'
Ответ:
{
"token": "eyJhbGci...",
"email": "test@example.com"
}
Все защищённые эндпоинты требуют заголовок Authorization: Bearer <token>.
Получить папки
curl http://localhost:8080/api/folders \
-H "Authorization: Bearer <token>"
Получить письма из папки
curl "http://localhost:8080/api/messages?folderId=1&page=0&size=20" \
-H "Authorization: Bearer <token>"
Отправить письмо
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"}'
Симулировать входящее письмо (для тестов)
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"
}'
Отметить как прочитанное
curl -X PUT http://localhost:8080/api/messages/1/read \
-H "Authorization: Bearer <token>"
Удалить письмо
curl -X DELETE http://localhost:8080/api/messages/1 \
-H "Authorization: Bearer <token>"
Загрузить вложение к письму
curl -X POST http://localhost:8080/api/messages/1/attachments \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/file.pdf"
Получить список вложений письма
curl http://localhost:8080/api/messages/1/attachments \
-H "Authorization: Bearer <token>"
Скачать вложение
curl http://localhost:8080/api/attachments/1/download \
-H "Authorization: Bearer <token>" \
-o file.pdf
Удалить вложение
curl -X DELETE http://localhost:8080/api/attachments/1 \
-H "Authorization: Bearer <token>"
Поиск по письмам
Поиск поддерживает два режима: простой поиск по ключевому слову и расширенный поиск с отдельными фильтрами.
Простой поиск — ищет по ключевому слову в теме, теле, отправителе и получателе:
curl "http://localhost:8080/api/messages/search?folderId=1&keyword=hello" \
-H "Authorization: Bearer <token>"
Расширенный поиск — используйте отдельные параметры для точной фильтрации:
# Поиск по теме
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. Любое письмо, отправленное на этот порт, будет сохранено в базе данных, если получатель зарегистрирован в системе.
# Отправить письмо через telnet
telnet localhost 3025
Или с помощью curl:
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.
Управление чёрным списком
Добавьте отправителя в чёрный список — письма от него будут автоматически помечаться как спам.
Получить список заблокированных отправителей:
curl http://localhost:8080/api/spam/blacklist \
-H "Authorization: Bearer <token>"
Добавить отправителя в чёрный список:
curl -X POST http://localhost:8080/api/spam/blacklist \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"senderEmail":"spammer@example.com","reason":"Спам"}'
Удалить отправителя из чёрного списка:
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 для тестирования.
# Подключение через 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:
# Клонируйте репозиторий
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
Перед запуском:
- Замените
mail.jambotron.comна ваш домен вdocker-compose.prod.yml - Настройте PTR (Reverse DNS) у хостера — IP →
mail.jambotron.com - Добавьте DNS-записи: A (
mail→ IP), MX (@→mail.jambotron.com), TXT (v=spf1 ip4:<IP> -all) - Откройте порты: 80, 443, 25 (SMTP relay), 3025 (SMTP приём)
TODO
- Хэширование паролей (BCrypt)
- Аутентификация (JWT)
- Приём SMTP (встроенный сервер на WebFlux TcpServer)
- IMAP-сервер (встроенный, порт 143)
- Вложения (загрузка/скачивание)
- Поиск по письмам (по теме, телу, отправителю, получателю)
- Angular фронтенд (v22 + Tailwind CSS + Synthwave дизайн)
- Антиспам (чёрный список отправителей)
- DKIM/SPF/DMARC (подпись, верификация, DNS-проверка)
- SMTP релей (доставка на внешнюю почту)
- Контакты (адресная книга)
- Reply / Forward
- Пагинация писем
- HTML/Markdown режим
- Caddy Docker Proxy (автоматический HTTPS)
- TLS шифрование SMTP/IMAP
Лицензия
MIT