Безопасность, 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>
This commit is contained in:
jambotron α
2026-11-05 03:37:43 +01:00
parent b6f6d41813
commit 165edf1a1e
44 changed files with 2626 additions and 1715 deletions
@@ -4,24 +4,46 @@ import com.mailserver.repository.UserRepository;
import com.mailserver.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.security.SecureRandom;
import java.util.Base64;
/**
* First mailbox on an empty database: {@code mailserver.admin.email} (default admin@&lt;first domain&gt;) with
* {@code mailserver.admin.password}; no password given — a random one, printed to the log once.
*/
@Configuration
public class DataInitializer {
private static final Logger log = LoggerFactory.getLogger(DataInitializer.class);
@Bean
public CommandLineRunner initDatabase(UserService userService, UserRepository userRepository) {
public CommandLineRunner initDatabase(UserService userService, UserRepository userRepository, MailDomains domains,
@Value("${mailserver.admin.email:}") String email,
@Value("${mailserver.admin.password:}") String password) {
return args -> {
userRepository.count().block(); // Ждём инициализации
if (userRepository.count().block() == 0) {
log.info("База данных пуста, создаю тестового пользователя...");
userService.register("admin@mailserver.local", "admin123", "Администратор").block();
log.info("✓ Тестовый пользователь создан: admin@mailserver.local / admin123");
if (userRepository.count().block() > 0) {
return;
}
String address = email.isBlank() ? "admin@" + domains.domains().iterator().next() : email.trim().toLowerCase();
boolean generated = password.isBlank();
String secret = generated ? randomPassword() : password;
userService.register(address, secret, "Administrator").block();
if (generated) {
log.warn("First mailbox created: {} — password: {} (shown once, change it)", address, secret);
} else {
log.info("First mailbox created: {}", address);
}
};
}
private static String randomPassword() {
byte[] bytes = new byte[18];
new SecureRandom().nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
}