Несколько ящиков у одного логина, общие ящики, кто ответил, вход ящик*логин
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
- V7: users.is_shared, mailbox_access (MEMBER от админа/Jambotron, ATTACHED по паролю ящика), messages.answered_by
- /api/mailboxes: мои ящики с непрочитанными, сессия другого ящика (JWT с claim by), подключить по паролю
(LoginGuard), убрать из меню
- общий ящик: без своего входа (веб/SMTP/IMAP), участники открывают; «прочитано» общее; ответ из веба → answered_by
- SMTP/IMAP: ящик*логин + пароль логина (как master user в Dovecot)
- открытый чужой ящик (общий, подключённый, OPEN админа): профиль и пароль не меняются (403)
- админ: создать shared, участники ящика (PUT /{id}/members); служебное API: PUT /shared/{address};
выключение аккаунта убирает и из общих ящиков
- UI: MAILBOXES в левом меню (переключение, ⊕ подключить, × убрать), shared/участники в настройках,
«Answered by» у письма
- 88 тестов; живой прогон на Postgres 17 (апгрейд V6→V7 на данных), скриншоты UI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,9 @@ class MailboxAdminControllerTest {
|
||||
private final MailAdmins admins = new MailAdmins("", "", domains);
|
||||
private final com.mailserver.util.JwtUtils jwt = mock(com.mailserver.util.JwtUtils.class);
|
||||
private final com.mailserver.service.MailLists lists = mock(com.mailserver.service.MailLists.class);
|
||||
private final com.mailserver.service.MailDirectory directory = new com.mailserver.service.MailDirectory(users, lists, domains);
|
||||
private final MailboxAdminController controller = new MailboxAdminController(users, mail, domains, admins, jwt, directory, lists);
|
||||
private final com.mailserver.service.MailboxAccess access = mock(com.mailserver.service.MailboxAccess.class);
|
||||
private final com.mailserver.service.MailDirectory directory = new com.mailserver.service.MailDirectory(users, lists, domains, access);
|
||||
private final MailboxAdminController controller = new MailboxAdminController(users, mail, domains, admins, jwt, directory, lists, access);
|
||||
|
||||
private final User admin = user(1L, "admin@jambocheck.com");
|
||||
private final User alice = user(2L, "alice@jambocheck.com");
|
||||
@@ -44,6 +45,7 @@ class MailboxAdminControllerTest {
|
||||
MailboxAdminControllerTest() {
|
||||
when(mail.usedBytes(anyLong())).thenReturn(Mono.just(0L));
|
||||
when(lists.exists(anyString())).thenReturn(Mono.just(false));
|
||||
when(access.members(anyLong())).thenReturn(Flux.empty());
|
||||
when(lists.exists("alerts@jambocheck.com")).thenReturn(Mono.just(true));
|
||||
when(users.findOptional(anyString())).thenReturn(Mono.empty());
|
||||
when(users.findOptional("alice@jambocheck.com")).thenReturn(Mono.just(alice));
|
||||
@@ -89,6 +91,26 @@ class MailboxAdminControllerTest {
|
||||
.isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sharedMailboxHasNoPasswordShownMembersAreSet() {
|
||||
when(users.setShared(any(), any(Boolean.class))).thenAnswer(i -> {
|
||||
User u = i.getArgument(0);
|
||||
u.setShared(i.getArgument(1));
|
||||
return Mono.just(u);
|
||||
});
|
||||
var created = controller.create(as("admin@jambocheck.com"),
|
||||
Mono.just(new MailboxAdminController.CreateRequest("support@jambocheck.com", "Support", "", true))).block();
|
||||
assertThat(created.password()).isNull();
|
||||
assertThat(created.mailbox().shared()).isTrue();
|
||||
|
||||
when(access.setMembers(anyLong(), any())).thenReturn(Mono.empty());
|
||||
controller.members(as("admin@jambocheck.com"), 2L,
|
||||
Mono.just(new MailboxAdminController.MembersRequest(List.of("Alice@jambocheck.com")))).block();
|
||||
verify(access).setMembers(eq(2L), eq(List.of(2L)));
|
||||
assertThat(status(() -> controller.members(as("admin@jambocheck.com"), 2L,
|
||||
Mono.just(new MailboxAdminController.MembersRequest(List.of("ghost@jambocheck.com")))).block())).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void administratorSwitchesTrapOthersCanNot() {
|
||||
assertThat(controller.trap(as("admin@jambocheck.com"), 2L, Mono.just(new MailboxAdminController.TrapRequest(true))).block().trap()).isTrue();
|
||||
@@ -117,14 +139,14 @@ class MailboxAdminControllerTest {
|
||||
@Test
|
||||
void createsReservedNamesWithAGeneratedPasswordShownOnce() {
|
||||
var created = controller.create(as("admin@jambocheck.com"),
|
||||
Mono.just(new MailboxAdminController.CreateRequest(" No-Reply@JamboCheck.com ", "Jambotron", ""))).block();
|
||||
Mono.just(new MailboxAdminController.CreateRequest(" No-Reply@JamboCheck.com ", "Jambotron", "", null))).block();
|
||||
|
||||
assertThat(created.mailbox().email()).isEqualTo("no-reply@jambocheck.com");
|
||||
assertThat(created.password()).hasSizeGreaterThanOrEqualTo(20);
|
||||
verify(users).register(eq("no-reply@jambocheck.com"), eq(created.password()), eq("Jambotron"));
|
||||
|
||||
var chosen = controller.create(as("admin@jambocheck.com"),
|
||||
Mono.just(new MailboxAdminController.CreateRequest("monitor@jambocheck.com", null, "long-enough-1"))).block();
|
||||
Mono.just(new MailboxAdminController.CreateRequest("monitor@jambocheck.com", null, "long-enough-1", null))).block();
|
||||
assertThat(chosen.password()).isNull();
|
||||
}
|
||||
|
||||
@@ -153,7 +175,7 @@ class MailboxAdminControllerTest {
|
||||
|
||||
@Test
|
||||
void administratorOpensAnyMailboxOthersCanNot() {
|
||||
when(jwt.generateToken("alice@jambocheck.com")).thenReturn("alice-token");
|
||||
when(jwt.generateToken("alice@jambocheck.com", "admin@jambocheck.com")).thenReturn("alice-token");
|
||||
|
||||
var session = controller.open(as("admin@jambocheck.com"), 2L).block();
|
||||
assertThat(session.getEmail()).isEqualTo("alice@jambocheck.com");
|
||||
@@ -164,7 +186,7 @@ class MailboxAdminControllerTest {
|
||||
}
|
||||
|
||||
private MailboxAdminController.Created create(String me, String email, String password) {
|
||||
return controller.create(as(me), Mono.just(new MailboxAdminController.CreateRequest(email, null, password))).block();
|
||||
return controller.create(as(me), Mono.just(new MailboxAdminController.CreateRequest(email, null, password, null))).block();
|
||||
}
|
||||
|
||||
private static Mono<Authentication> as(String email) {
|
||||
|
||||
@@ -31,7 +31,8 @@ class ServiceControllerTest {
|
||||
private final UserService users = mock(UserService.class);
|
||||
private final MailLists lists = mock(MailLists.class);
|
||||
private final MailDomains domains = new MailDomains("jambocheck.com", "");
|
||||
private final MailDirectory directory = new MailDirectory(users, lists, domains);
|
||||
private final com.mailserver.service.MailboxAccess access = mock(com.mailserver.service.MailboxAccess.class);
|
||||
private final MailDirectory directory = new MailDirectory(users, lists, domains, access);
|
||||
private final User existing = user(7L, "jambotronpw@jambocheck.com");
|
||||
|
||||
ServiceControllerTest() {
|
||||
@@ -40,6 +41,7 @@ class ServiceControllerTest {
|
||||
when(lists.exists(anyString())).thenReturn(Mono.just(false));
|
||||
when(lists.listsOf(anyLong())).thenReturn(Flux.just("alerts-prod@jambocheck.com"));
|
||||
when(lists.removeEverywhere(anyLong())).thenReturn(Mono.just(1L));
|
||||
when(access.removeEverywhere(anyLong())).thenReturn(Mono.just(1L));
|
||||
when(users.register(anyString(), anyString(), any())).thenAnswer(i -> Mono.just(user(8L, i.getArgument(0))));
|
||||
when(users.setActive(any(), any(Boolean.class))).thenAnswer(i -> {
|
||||
User u = i.getArgument(0);
|
||||
@@ -50,7 +52,7 @@ class ServiceControllerTest {
|
||||
}
|
||||
|
||||
private ServiceController api(String token) {
|
||||
return new ServiceController(token, users, lists, directory);
|
||||
return new ServiceController(token, users, lists, directory, access);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,6 +86,7 @@ class ServiceControllerTest {
|
||||
Mono.just(new ServiceController.AccountRequest(null, false))).block();
|
||||
assertThat(off.getBody().account().active()).isFalse();
|
||||
verify(lists).removeEverywhere(7L);
|
||||
verify(access).removeEverywhere(7L);
|
||||
|
||||
var reset = api("s3cret-token").password(AUTH, "jambotronpw@jambocheck.com").block();
|
||||
assertThat(reset.password()).isNotBlank();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.mailserver.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** The login behind a session: own session, or a member who opened a shared mailbox. */
|
||||
class ActorTest {
|
||||
|
||||
@Test
|
||||
void ownAndOpened() {
|
||||
var own = new UsernamePasswordAuthenticationToken("bob@x", null, List.of());
|
||||
assertThat(Actor.login(own)).isEqualTo("bob@x");
|
||||
assertThat(Actor.opened(own)).isFalse();
|
||||
|
||||
var opened = new UsernamePasswordAuthenticationToken("support@x", null, List.of());
|
||||
opened.setDetails("bob@x");
|
||||
assertThat(Actor.login(opened)).isEqualTo("bob@x");
|
||||
assertThat(Actor.opened(opened)).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ class MailRouterTest {
|
||||
when(lists.find("talk@mail.test")).thenReturn(Mono.just(new MailLists.MailList(2L, "talk@mail.test",
|
||||
null, null, true, List.of("bob@mail.test"))));
|
||||
return new MailRouter(new MailDomains("mail.test", catchAll), users, mock(UserService.class), localDelivery,
|
||||
queue, mock(DkimService.class), client, lists);
|
||||
queue, mock(DkimService.class), client, lists, mock(com.mailserver.service.MailboxAccess.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -43,8 +43,8 @@ class SmtpServerTest {
|
||||
volatile boolean failStore;
|
||||
|
||||
@Override
|
||||
public boolean authenticate(String username, String password) {
|
||||
return password.equals(passwords.get(username));
|
||||
public String authenticate(String username, String password) {
|
||||
return password.equals(passwords.get(username)) ? username : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user