This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch 3.9.x in repository https://gitbox.apache.org/repos/asf/james-project.git
commit ba4b8ae71f392643ed1517726c460d0b809be8c1 Author: Benoit TELLIER <[email protected]> AuthorDate: Tue Jun 16 22:03:41 2026 +0200 [FIX] User rename: handle submailbox edge cases when mailbox exist --- .../mailbox/MailboxUsernameChangeTaskStep.java | 21 ++- .../mailbox/MailboxUsernameChangeTaskStepTest.java | 177 +++++++++++++++++++++ 2 files changed, 192 insertions(+), 6 deletions(-) diff --git a/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStep.java b/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStep.java index b23334c786..016e662c93 100644 --- a/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStep.java +++ b/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStep.java @@ -19,6 +19,7 @@ package org.apache.james.adapter.mailbox; +import java.util.Comparator; import java.util.concurrent.ThreadLocalRandom; import jakarta.inject.Inject; @@ -77,14 +78,20 @@ public class MailboxUsernameChangeTaskStep implements UsernameChangeTaskStep { int renameOperationId = ThreadLocalRandom.current().nextInt(1000); return mailboxManager.search(queryUser, MailboxManager.MailboxSearchFetchType.Minimal, fromSession) - // Only keep top level, rename takes care of sub mailboxes - .filter(mailbox -> mailbox.getPath().getHierarchyLevels(fromSession.getPathDelimiter()).size() == 1) + // Process the deepest mailboxes first. As renameMailbox is recursive (it also renames + // sub mailboxes), handling a mailbox only once all of its descendants have already been + // migrated guarantees it is effectively a leaf at that point: the rename then carries no + // sub mailbox along, which lets us migrate every node individually - including when the + // destination mailbox already exists and a plain rename is therefore not possible. + .sort(Comparator.comparingInt( + (MailboxMetaData mailbox) -> mailbox.getPath().getHierarchyLevels(fromSession.getPathDelimiter()).size()) + .reversed()) .concatMap(mailbox -> migrateMailbox(fromSession, toSession, mailbox, renameOperationId)) .doFinally(any -> mailboxManager.endProcessingRequest(fromSession)) .doFinally(any -> mailboxManager.endProcessingRequest(toSession)); } - private Mono<Void> migrateMailbox(MailboxSession fromSession, MailboxSession toSession, org.apache.james.mailbox.model.MailboxMetaData mailbox, int renameOperationId) { + private Mono<Void> migrateMailbox(MailboxSession fromSession, MailboxSession toSession, MailboxMetaData mailbox, int renameOperationId) { MailboxPath renamedPath = mailbox.getPath().withUser(toSession.getUser()); return mailboxManager.mailboxExists(renamedPath, toSession) .flatMap(exist -> { @@ -104,9 +111,11 @@ public class MailboxUsernameChangeTaskStep implements UsernameChangeTaskStep { .then(); } - // The destination mailbox already exists: bring the source mailbox into the destination - // account under a temporary name, MOVE its messages into the destination, then drop the - // emptied temporary mailbox. + // The destination mailbox already exists: a plain rename is impossible, so we bring the source + // mailbox into the destination account under a temporary name, MOVE its messages into the + // destination, then drop the emptied temporary mailbox. Sub mailboxes are not handled here: as + // mailboxes are migrated deepest first, the source mailbox no longer has any descendant when we + // reach this point. private Mono<Void> moveWhenMailboxExist(MailboxSession fromSession, MailboxSession toSession, MailboxMetaData mailbox, MailboxPath renamedPath, int renameOperationId) { MailboxPath temporaryPath = new MailboxPath(renamedPath.getNamespace(), renamedPath.getUser(), renamedPath.getName() + "-tmp-" + renameOperationId); return mailboxManager.renameMailboxReactive(mailbox.getPath(), temporaryPath, diff --git a/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStepTest.java b/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStepTest.java index 18c5187116..439f77d02b 100644 --- a/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStepTest.java +++ b/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/MailboxUsernameChangeTaskStepTest.java @@ -38,6 +38,7 @@ import org.apache.james.mailbox.model.search.MailboxQuery; import org.apache.james.mailbox.store.StoreSubscriptionManager; import org.apache.james.mime4j.dom.Message; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -198,4 +199,180 @@ class MailboxUsernameChangeTaskStepTest { MailboxPath.forUser(BOB, "test.child"), bobInbox); } + + private void appendMessage(MailboxPath path, MailboxSession session, String body) throws Exception { + mailboxManager.getMailbox(path, session) + .appendMessage(MessageManager.AppendCommand.from(Message.Builder.of() + .setSubject("subject") + .setBody(body, StandardCharsets.UTF_8) + .build()), session); + } + + private long messageCount(MailboxPath path, MailboxSession session) throws Exception { + return mailboxManager.getMailbox(path, session).getMessageCount(session); + } + + @Nested + class WhenDestinationParentMailboxAlreadyExists { + @Test + void shouldMigrateSubMailboxesWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(mailboxManager.list(mailboxManager.createSystemSession(BOB))) + .containsOnly(MailboxPath.forUser(BOB, "test"), + MailboxPath.forUser(BOB, "test.child")); + } + + @Test + void shouldNotLeaveTemporaryMailboxesWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(mailboxManager.list(mailboxManager.createSystemSession(BOB))) + .noneMatch(path -> path.getName().contains("-tmp-")); + } + + @Test + void shouldNotLooseMessagesOfSourceOnlyTopLevelMailboxWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + appendMessage(MailboxPath.forUser(ALICE, "test"), aliceSession, "alice"); + appendMessage(MailboxPath.forUser(BOB, "test"), bobSession, "bob"); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(messageCount(MailboxPath.forUser(BOB, "test"), bobSession)).isEqualTo(2); + } + + @Test + void shouldMoveSubMailboxMessagesWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + appendMessage(MailboxPath.forUser(ALICE, "test.child"), aliceSession, "alice-child"); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(messageCount(MailboxPath.forUser(BOB, "test.child"), bobSession)).isEqualTo(1); + } + + @Test + void shouldMergeSubMailboxMessagesWhenBothUsersHaveTheSameSubMailbox() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test.child"), MailboxManager.CreateOption.NONE, bobSession); + appendMessage(MailboxPath.forUser(ALICE, "test"), aliceSession, "alice-parent"); + appendMessage(MailboxPath.forUser(ALICE, "test.child"), aliceSession, "alice-child"); + appendMessage(MailboxPath.forUser(BOB, "test"), bobSession, "bob-parent"); + appendMessage(MailboxPath.forUser(BOB, "test.child"), bobSession, "bob-child"); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(mailboxManager.list(mailboxManager.createSystemSession(BOB))) + .containsOnly(MailboxPath.forUser(BOB, "test"), + MailboxPath.forUser(BOB, "test.child")); + assertThat(messageCount(MailboxPath.forUser(BOB, "test"), bobSession)).isEqualTo(2); + assertThat(messageCount(MailboxPath.forUser(BOB, "test.child"), bobSession)).isEqualTo(2); + } + + @Test + void shouldMigrateDeepHierarchyWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child.grandchild"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(mailboxManager.list(mailboxManager.createSystemSession(BOB))) + .containsOnly(MailboxPath.forUser(BOB, "test"), + MailboxPath.forUser(BOB, "test.child"), + MailboxPath.forUser(BOB, "test.child.grandchild")); + } + + @Test + void shouldMigrateSubMailboxesWhenOnlyDeepestChildConflicts() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.other"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test.child"), MailboxManager.CreateOption.NONE, bobSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(mailboxManager.list(mailboxManager.createSystemSession(BOB))) + .containsOnly(MailboxPath.forUser(BOB, "test"), + MailboxPath.forUser(BOB, "test.child"), + MailboxPath.forUser(BOB, "test.other")); + } + + @Test + void shouldRemoveSourceMailboxesWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(mailboxManager.list(mailboxManager.createSystemSession(BOB))) + .filteredOn(path -> ALICE.equals(path.getUser())) + .isEmpty(); + } + + @Test + void shouldTransferSubscriptionOfSubMailboxWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.CREATE_SUBSCRIPTION, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + assertThat(subscriptionManager.subscriptions(mailboxManager.createSystemSession(BOB))) + .containsOnly(MailboxPath.forUser(BOB, "test.child")); + } + + @Test + void shouldMigrateDelegationAclOfSubMailboxWhenDestinationParentAlreadyExists() throws Exception { + MailboxSession aliceSession = mailboxManager.createSystemSession(ALICE); + MailboxSession bobSession = mailboxManager.createSystemSession(BOB); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(ALICE, "test.child"), MailboxManager.CreateOption.NONE, aliceSession); + mailboxManager.createMailbox(MailboxPath.forUser(BOB, "test"), MailboxManager.CreateOption.NONE, bobSession); + mailboxManager.applyRightsCommand(MailboxPath.forUser(ALICE, "test.child"), + MailboxACL.command().forUser(CEDRIC).rights(MailboxACL.FULL_RIGHTS).asAddition(), + aliceSession); + + Mono.from(testee.changeUsername(ALICE, BOB)).block(); + + MailboxSession cedricSession = mailboxManager.createSystemSession(CEDRIC); + assertThatCode(() -> mailboxManager.getMailbox(MailboxPath.forUser(BOB, "test.child"), cedricSession)) + .doesNotThrowAnyException(); + } + } } \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
