Ловушка у ящика (TRAP): что ящик отправляет — в его же INBOX, наружу ничего
continuous-integration/drone/pr Build is passing

Для staging@: staging-стенд шлёт через настоящий mail.jambocheck.com,
но тестовые письма не доходят до живых людей.
- V5: users.is_trap
- MailRouter: отправитель-ловушка → все получатели LOCAL, одна копия в INBOX отправителя
- PUT /api/admin/mailboxes/{id}/trap, MailboxView.trap
- MAILBOXES: кнопка outbox/move_to_inbox, метка TRAP
- тесты: router + admin (77 тестов)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 10:45:20 +01:00
co-authored by Claude Opus 5.5
parent 0811dfcf68
commit fc3fe3970d
11 changed files with 117 additions and 3 deletions
@@ -17,6 +17,8 @@ import java.util.Locale;
* 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).
* A logged-in TRAP mailbox (staging@) sends nowhere: every recipient is accepted and ONE copy is stored in the
* sender's own INBOX — a staging stand uses the real server, but its test mail never reaches people.
*/
@Service
public class MailRouter implements SmtpBackend {
@@ -48,6 +50,9 @@ public class MailRouter implements SmtpBackend {
@Override
public Route route(String recipient, String authenticatedUser) {
if (isTrap(authenticatedUser)) {
return Route.LOCAL;
}
if (domains.isLocal(recipient) && mailboxExists(recipient)) {
return Route.LOCAL;
}
@@ -66,6 +71,11 @@ public class MailRouter implements SmtpBackend {
@Override
public void accept(Envelope envelope, byte[] message) throws Exception {
boolean trusted = envelope.authenticatedUser() != null;
if (isTrap(envelope.authenticatedUser())) {
localDelivery.deliver(message, envelope.authenticatedUser().toLowerCase(Locale.ROOT), envelope.remoteIp(),
envelope.mailFrom(), true);
return;
}
byte[] signed = null;
for (Recipient recipient : envelope.recipients()) {
switch (recipient.route()) {
@@ -92,6 +102,15 @@ public class MailRouter implements SmtpBackend {
return domains.catchAll();
}
/** The logged-in sender is an active trap mailbox. */
boolean isTrap(String authenticatedUser) {
if (authenticatedUser == null) {
return false;
}
User user = users.findByEmail(authenticatedUser.toLowerCase(Locale.ROOT)).block();
return user != null && user.isTrap();
}
private boolean mailboxExists(String address) {
return Boolean.TRUE.equals(users.existsByEmail(address.toLowerCase(Locale.ROOT)).block());
}