Files
reactive-mail-server/backend/src/test/java/com/mailserver/delivery/SmtpDeliveryClientTest.java
T
jambotron α 165edf1a1e Безопасность, 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>
2026-11-05 03:37:43 +01:00

110 lines
4.9 KiB
Java

package com.mailserver.delivery;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import jakarta.mail.internet.MimeMessage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SmtpDeliveryClientTest {
private static final String MESSAGE = "From: alice@ours.test\r\nTo: bob@theirs.test\r\nSubject: Hi\r\n"
+ "DKIM-Signature: v=1; a=rsa-sha256; d=ours.test; s=mail; h=from; bh=x; b=y\r\n\r\nbody\r\n.dot line\r\n";
private GreenMail greenMail;
@BeforeEach
void start() {
greenMail = new GreenMail(new ServerSetup(0, "127.0.0.1", ServerSetup.PROTOCOL_SMTP));
greenMail.setUser("relay-user", "relay-pass");
greenMail.start();
}
@AfterEach
void stop() {
greenMail.stop();
}
private int port() {
return greenMail.getSmtp().getPort();
}
private SmtpDeliveryClient mxClient(int mxPort, List<String> hosts) {
return new SmtpDeliveryClient(new SmtpDeliveryClient.Settings(SmtpDeliveryClient.Mode.MX, "", 0, "", "",
SmtpDeliveryClient.Tls.NONE, "mx.ours.test", 5000, mxPort), domain -> hosts);
}
@Test
void deliversToTheMxAndKeepsTheBytesUnchanged() throws Exception {
SmtpDeliveryClient.Result result = mxClient(port(), List.of("127.0.0.1"))
.deliver("alice@ours.test", "bob@theirs.test", MESSAGE.getBytes(StandardCharsets.UTF_8));
assertThat(result.outcome()).isEqualTo(SmtpDeliveryClient.Outcome.DELIVERED);
MimeMessage[] received = greenMail.getReceivedMessages();
assertThat(received).hasSize(1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
received[0].writeTo(out);
// GreenMail adds its Received/Return-Path; everything we sent is there byte for byte (DKIM stays valid)
assertThat(out.toString(StandardCharsets.UTF_8)).contains(MESSAGE.stripTrailing());
}
@Test
void triesTheNextMxWhenTheFirstDoesNotAnswer() throws Exception {
int closed;
try (ServerSocket socket = new ServerSocket(0)) {
closed = socket.getLocalPort();
}
// first "host" refuses the connection, second is GreenMail — both on 127.0.0.1, so use two clients' ports:
SmtpDeliveryClient.Result refused = mxClient(closed, List.of("127.0.0.1"))
.deliver("alice@ours.test", "bob@theirs.test", MESSAGE.getBytes(StandardCharsets.UTF_8));
assertThat(refused.outcome()).isEqualTo(SmtpDeliveryClient.Outcome.TEMPORARY);
SmtpDeliveryClient.Result fallback = mxClient(port(), List.of("unreachable.invalid", "127.0.0.1"))
.deliver("alice@ours.test", "bob@theirs.test", MESSAGE.getBytes(StandardCharsets.UTF_8));
assertThat(fallback.outcome()).isEqualTo(SmtpDeliveryClient.Outcome.DELIVERED);
}
@Test
void domainWithoutMailServerIsAPermanentFailure() {
SmtpDeliveryClient.Result result = mxClient(port(), List.of())
.deliver("alice@ours.test", "bob@nowhere.test", MESSAGE.getBytes(StandardCharsets.UTF_8));
assertThat(result.outcome()).isEqualTo(SmtpDeliveryClient.Outcome.PERMANENT);
}
@Test
void dnsTroubleIsTemporary() {
SmtpDeliveryClient client = new SmtpDeliveryClient(new SmtpDeliveryClient.Settings(SmtpDeliveryClient.Mode.MX,
"", 0, "", "", SmtpDeliveryClient.Tls.NONE, "mx.ours.test", 5000, port()),
domain -> { throw new SmtpDeliveryClient.TemporaryDnsException("SERVFAIL"); });
assertThat(client.deliver("a@ours.test", "b@theirs.test", MESSAGE.getBytes()).outcome())
.isEqualTo(SmtpDeliveryClient.Outcome.TEMPORARY);
}
@Test
void sendsThroughASmarthostWithLogin() {
SmtpDeliveryClient client = new SmtpDeliveryClient(new SmtpDeliveryClient.Settings(SmtpDeliveryClient.Mode.SMARTHOST,
"127.0.0.1", port(), "relay-user", "relay-pass", SmtpDeliveryClient.Tls.NONE, "mx.ours.test", 5000, 25));
assertThat(client.deliver("alice@ours.test", "bob@theirs.test", MESSAGE.getBytes()).outcome())
.isEqualTo(SmtpDeliveryClient.Outcome.DELIVERED);
assertThat(greenMail.getReceivedMessages()).hasSize(1);
}
@Test
void offModeSendsNothing() {
SmtpDeliveryClient client = new SmtpDeliveryClient(new SmtpDeliveryClient.Settings(SmtpDeliveryClient.Mode.OFF,
"", 0, "", "", SmtpDeliveryClient.Tls.NONE, "mx.ours.test", 5000, 25));
assertThat(client.deliver("a@ours.test", "b@theirs.test", MESSAGE.getBytes()).outcome())
.isEqualTo(SmtpDeliveryClient.Outcome.PERMANENT);
assertThat(greenMail.getReceivedMessages()).isEmpty();
}
}