Безопасность, 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:
@@ -0,0 +1,74 @@
|
||||
package com.mailserver.service;
|
||||
|
||||
import com.mailserver.dto.MessageResponse;
|
||||
import com.mailserver.dto.SendRequest;
|
||||
import com.mailserver.repository.UserRepository;
|
||||
import com.mailserver.smtp.MailRouter;
|
||||
import com.mailserver.smtp.SmtpBackend;
|
||||
import jakarta.mail.Message;
|
||||
import jakarta.mail.Session;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
/** "Send" in the web client: builds the MIME message and routes it like an authenticated SMTP submission. */
|
||||
@Service
|
||||
public class SubmissionService {
|
||||
|
||||
private final UserRepository users;
|
||||
private final MailService mailService;
|
||||
private final MailRouter router;
|
||||
private final String hostname;
|
||||
|
||||
public SubmissionService(UserRepository users, MailService mailService, MailRouter router,
|
||||
@Value("${mailserver.smtp.hostname:localhost}") String hostname) {
|
||||
this.users = users;
|
||||
this.mailService = mailService;
|
||||
this.router = router;
|
||||
this.hostname = hostname;
|
||||
}
|
||||
|
||||
public Mono<MessageResponse> send(String fromEmail, SendRequest request) {
|
||||
String to = request.getTo().trim().toLowerCase(Locale.ROOT);
|
||||
return users.findByEmail(fromEmail)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED)))
|
||||
.flatMap(user -> Mono.fromCallable(() -> {
|
||||
SmtpBackend.Route route = router.route(to, user.getEmail());
|
||||
switch (route) {
|
||||
case LOCAL, OUTBOUND -> { }
|
||||
case UNKNOWN_USER -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No such mailbox: " + to);
|
||||
case OUTBOUND_DISABLED -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Sending outside is disabled on this server");
|
||||
case RELAY_DENIED -> throw new IllegalStateException("logged-in user denied relay");
|
||||
}
|
||||
String messageId = UUID.randomUUID() + "@" + hostname;
|
||||
MimeMessage mime = new MimeMessage(Session.getInstance(new Properties()));
|
||||
mime.setFrom(new InternetAddress(user.getEmail(), user.getDisplayName(), "UTF-8"));
|
||||
mime.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
|
||||
mime.setSubject(request.getSubject(), "UTF-8");
|
||||
mime.setText(request.getBody(), "UTF-8", request.isHtml() ? "html" : "plain");
|
||||
mime.setSentDate(new Date());
|
||||
mime.saveChanges();
|
||||
mime.setHeader("Message-ID", "<" + messageId + ">");
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
mime.writeTo(bytes);
|
||||
router.accept(new SmtpBackend.Envelope("127.0.0.1", hostname, user.getEmail(), user.getEmail(),
|
||||
List.of(new SmtpBackend.Recipient(to, route))), bytes.toByteArray());
|
||||
return messageId;
|
||||
}).subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(messageId -> mailService.saveSent(user, to, request.getSubject(), request.getBody(),
|
||||
request.isHtml(), messageId)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user