- 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>
110 lines
4.9 KiB
Java
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();
|
|
}
|
|
}
|