75.4 Ловушка почты: catch-all только для адресов без своего ящика, файл с паролями

- MailRouter: существующий ящик получает свою почту, всё остальное (и чужие домены) —
  в ящик MAIL_CATCH_ALL; раньше ловушка забирала и письма между ящиками стенда
- DataInitializer: ящик-ловушка создаётся при старте; случайные пароли — в
  MAIL_PASSWORDS_FILE (права 600, в образе /app/data/initial-passwords.txt)
- docker-compose.jambotron.yml: MAIL_CATCH_ALL=trap@mail.test по умолчанию
- README: ловушка, файл паролей, что нужно для staging Jambotron (TCP-проброс SMTP)
- тесты: MailRouterTest, DataInitializerTest (33 всего)

α
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 17:43:49 +01:00
parent db0249eb74
commit 12c2223c15
8 changed files with 192 additions and 27 deletions
@@ -14,8 +14,9 @@ import java.util.Locale;
/**
* Where mail goes — one rule for SMTP and for the web client:
* our domain → our mailbox (or "no such user"); catch-all set → everything into that mailbox;
* someone else's domain → the delivery queue, only for a logged-in sender (never an open relay).
* an existing mailbox of our domain → that mailbox; catch-all set → everything else into the catch-all mailbox
* (nothing goes outside, existing mailboxes still get their own mail); otherwise our domain without such a mailbox →
* "no such user", someone else's domain → the delivery queue, only for a logged-in sender (never an open relay).
*/
@Service
public class MailRouter implements SmtpBackend {
@@ -47,12 +48,14 @@ public class MailRouter implements SmtpBackend {
@Override
public Route route(String recipient, String authenticatedUser) {
if (domains.isLocal(recipient) && mailboxExists(recipient)) {
return Route.LOCAL;
}
if (domains.catchAll() != null) {
return Route.LOCAL;
}
if (domains.isLocal(recipient)) {
return Boolean.TRUE.equals(users.existsByEmail(recipient.toLowerCase(Locale.ROOT)).block())
? Route.LOCAL : Route.UNKNOWN_USER;
return Route.UNKNOWN_USER;
}
if (authenticatedUser == null) {
return Route.RELAY_DENIED;
@@ -67,7 +70,7 @@ public class MailRouter implements SmtpBackend {
for (Recipient recipient : envelope.recipients()) {
switch (recipient.route()) {
case LOCAL -> {
String mailbox = domains.catchAll() != null ? domains.catchAll() : recipient.address();
String mailbox = mailbox(recipient.address());
localDelivery.deliver(message, mailbox, envelope.remoteIp(), envelope.mailFrom(), trusted);
}
case OUTBOUND -> {
@@ -80,4 +83,16 @@ public class MailRouter implements SmtpBackend {
}
}
}
/** Mailbox that stores mail to this address: its own if it exists here, otherwise the catch-all one. */
String mailbox(String address) {
if (domains.catchAll() == null || (domains.isLocal(address) && mailboxExists(address))) {
return address;
}
return domains.catchAll();
}
private boolean mailboxExists(String address) {
return Boolean.TRUE.equals(users.existsByEmail(address.toLowerCase(Locale.ROOT)).block());
}
}