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>
102 lines
5.2 KiB
Java
102 lines
5.2 KiB
Java
package com.mailserver.smtp;
|
|
|
|
import com.mailserver.config.MailDomains;
|
|
import com.mailserver.delivery.OutboundQueue;
|
|
import com.mailserver.delivery.SmtpDeliveryClient;
|
|
import com.mailserver.domain.User;
|
|
import com.mailserver.repository.UserRepository;
|
|
import com.mailserver.service.DkimService;
|
|
import com.mailserver.service.LocalDelivery;
|
|
import com.mailserver.service.UserService;
|
|
import org.junit.jupiter.api.Test;
|
|
import reactor.core.publisher.Mono;
|
|
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/** Mail trap (75.4): existing mailboxes keep their mail, everything else goes into the catch-all mailbox. */
|
|
class MailRouterTest {
|
|
|
|
private final UserRepository users = mock(UserRepository.class);
|
|
private final LocalDelivery localDelivery = mock(LocalDelivery.class);
|
|
private final OutboundQueue queue = mock(OutboundQueue.class);
|
|
private final SmtpDeliveryClient client = mock(SmtpDeliveryClient.class);
|
|
|
|
private SmtpDeliveryClient.Mode mode = SmtpDeliveryClient.Mode.OFF;
|
|
|
|
private static User user(String email, boolean trap) {
|
|
User user = new User();
|
|
user.setEmail(email);
|
|
user.setTrap(trap);
|
|
return user;
|
|
}
|
|
|
|
private MailRouter router(String catchAll) {
|
|
when(users.existsByEmail(anyString())).thenReturn(Mono.just(false));
|
|
when(users.existsByEmail("bob@mail.test")).thenReturn(Mono.just(true));
|
|
when(users.findByEmail(anyString())).thenReturn(Mono.empty());
|
|
when(users.findByEmail("bob@mail.test")).thenReturn(Mono.just(user("bob@mail.test", false)));
|
|
when(users.findByEmail("staging@mail.test")).thenReturn(Mono.just(user("staging@mail.test", true)));
|
|
when(client.mode()).thenReturn(mode);
|
|
return new MailRouter(new MailDomains("mail.test", catchAll), users, mock(UserService.class), localDelivery,
|
|
queue, mock(DkimService.class), client);
|
|
}
|
|
|
|
@Test
|
|
void withoutCatchAllUnknownAndOutsideAddressesAreRefused() {
|
|
MailRouter router = router("");
|
|
|
|
assertThat(router.route("bob@mail.test", null)).isEqualTo(SmtpBackend.Route.LOCAL);
|
|
assertThat(router.route("nobody@mail.test", null)).isEqualTo(SmtpBackend.Route.UNKNOWN_USER);
|
|
assertThat(router.route("user@gmail.com", null)).isEqualTo(SmtpBackend.Route.RELAY_DENIED);
|
|
assertThat(router.route("user@gmail.com", "bob@mail.test")).isEqualTo(SmtpBackend.Route.OUTBOUND_DISABLED);
|
|
}
|
|
|
|
@Test
|
|
void catchAllTakesEverythingWithoutItsOwnMailboxButExistingMailboxesKeepTheirMail() throws Exception {
|
|
MailRouter router = router("trap@mail.test");
|
|
|
|
assertThat(router.route("nobody@mail.test", null)).isEqualTo(SmtpBackend.Route.LOCAL);
|
|
assertThat(router.route("user@gmail.com", null)).isEqualTo(SmtpBackend.Route.LOCAL);
|
|
assertThat(router.mailbox("bob@mail.test")).isEqualTo("bob@mail.test");
|
|
assertThat(router.mailbox("user@gmail.com")).isEqualTo("trap@mail.test");
|
|
|
|
router.accept(new SmtpBackend.Envelope("10.0.0.5", "staging", "no-reply@jambotron.com", null, List.of(
|
|
new SmtpBackend.Recipient("user@gmail.com", SmtpBackend.Route.LOCAL),
|
|
new SmtpBackend.Recipient("bob@mail.test", SmtpBackend.Route.LOCAL))), "Subject: hi\r\n\r\nbody".getBytes());
|
|
|
|
verify(localDelivery).deliver(any(), eq("trap@mail.test"), eq("10.0.0.5"), eq("no-reply@jambotron.com"), anyBoolean());
|
|
verify(localDelivery).deliver(any(), eq("bob@mail.test"), eq("10.0.0.5"), eq("no-reply@jambotron.com"), anyBoolean());
|
|
verify(queue, never()).enqueue(any(), any(), any());
|
|
}
|
|
|
|
@Test
|
|
void trapMailboxSendsNowhereOneCopyLandsInItsOwnInbox() throws Exception {
|
|
mode = SmtpDeliveryClient.Mode.MX;
|
|
MailRouter router = router("");
|
|
|
|
// an ordinary mailbox still sends outside; the trap one keeps everything
|
|
assertThat(router.route("user@gmail.com", "bob@mail.test")).isEqualTo(SmtpBackend.Route.OUTBOUND);
|
|
assertThat(router.route("user@gmail.com", "staging@mail.test")).isEqualTo(SmtpBackend.Route.LOCAL);
|
|
assertThat(router.route("nobody@mail.test", "Staging@Mail.test")).isEqualTo(SmtpBackend.Route.LOCAL);
|
|
assertThat(router.route("bob@mail.test", "staging@mail.test")).isEqualTo(SmtpBackend.Route.LOCAL);
|
|
|
|
router.accept(new SmtpBackend.Envelope("10.0.0.5", "staging", "staging@mail.test", "staging@mail.test", List.of(
|
|
new SmtpBackend.Recipient("user@gmail.com", SmtpBackend.Route.LOCAL),
|
|
new SmtpBackend.Recipient("bob@mail.test", SmtpBackend.Route.LOCAL))), "Subject: hi\r\n\r\nbody".getBytes());
|
|
|
|
verify(localDelivery).deliver(any(), eq("staging@mail.test"), eq("10.0.0.5"), eq("staging@mail.test"), eq(true));
|
|
verify(localDelivery, never()).deliver(any(), eq("bob@mail.test"), any(), any(), anyBoolean());
|
|
verify(queue, never()).enqueue(any(), any(), any());
|
|
}
|
|
}
|