Skip to content

Commit d7e73bd

Browse files
JAMES-2182 StoreMailboxManager should avoid throwing exception in `.filterWhen
We observed a few `MailboxExistsException` error logs since the change 91f50b0#diff-51e67dcd44af864286be49fbe7d5474bff07b8fe6cd0536121b4f2898dbbda9e. Actually, the errors are still caught by the main reactor stream therefore no impact to users. However, it seems errors emitted by asynchronous operators like `.filterWhen` can be propagated through their own internal asynchronous paths and result in unhandled exceptions being logged (e.g., "onErrorDropped").
1 parent 4db70ee commit d7e73bd

File tree

3 files changed

+122
-20
lines changed

3 files changed

+122
-20
lines changed

mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,23 @@ void renameMailboxShouldRenamedChildMailboxesWithRenameOption() throws MailboxEx
18731873
MailboxPath.forUser(USER_1, "mbx9.mbx2.mbx3"));
18741874
}
18751875

1876+
@Test
1877+
void renameMailboxToAnExistingMailboxShouldThrowMailboxExistsException() throws MailboxException {
1878+
MailboxSession session = mailboxManager.createSystemSession(USER_1);
1879+
1880+
MailboxPath existingPath = MailboxPath.forUser(USER_1, "mbx1");
1881+
MailboxPath toBeRenamed = MailboxPath.forUser(USER_1, "mbx2");
1882+
1883+
mailboxManager.createMailbox(existingPath, session);
1884+
subscriptionManager.subscribe(session, existingPath);
1885+
1886+
mailboxManager.createMailbox(toBeRenamed, session);
1887+
subscriptionManager.subscribe(session, toBeRenamed);
1888+
1889+
assertThatThrownBy(() -> mailboxManager.renameMailbox(toBeRenamed, existingPath, RENAME_SUBSCRIPTIONS, session))
1890+
.isInstanceOf(MailboxExistsException.class);
1891+
}
1892+
18761893
@Test
18771894
void renameMailboxShouldRenameSubscriptionWhenCalledWithRenameSubscriptionOption() throws MailboxException {
18781895
MailboxSession session = mailboxManager.createSystemSession(USER_1);

mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -434,15 +434,20 @@ private Mono<MailboxId> performConcurrentMailboxCreation(MailboxSession mailboxS
434434
.flatMap(any -> inheritRightsReactive(mailboxSession, mailboxPath).thenReturn(any));
435435
}
436436

437-
438-
private Mono<Void> assertCanCreateReactive(MailboxSession session, MailboxPath path) {
437+
private Mono<Boolean> canCreateReactive(MailboxSession session, MailboxPath path) {
439438
if (path.belongsTo(session)) {
440-
return Mono.empty();
439+
return Mono.just(true);
441440
}
442441

443442
return nearestExistingParent(session, path)
444443
.filterWhen(parent -> hasRightReactive(parent, Right.CreateMailbox, session))
445-
.switchIfEmpty(Mono.error(new InsufficientRightsException("user '" + session.getUser().asString() + "' is not allowed to create the mailbox '" + path.asString() + "'")))
444+
.hasElement();
445+
}
446+
447+
private Mono<Void> assertCanCreateReactive(MailboxSession session, MailboxPath path) {
448+
return canCreateReactive(session, path)
449+
.filter(canCreate -> canCreate)
450+
.switchIfEmpty(Mono.error(() -> new InsufficientRightsException("user '" + session.getUser().asString() + "' is not allowed to create the mailbox '" + path.asString() + "'")))
446451
.then();
447452
}
448453

@@ -469,7 +474,7 @@ public void deleteMailbox(final MailboxPath mailboxPath, final MailboxSession se
469474
MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session);
470475

471476
mailboxMapper.execute(() -> block(mailboxMapper.findMailboxByPath(mailboxPath)
472-
.filterWhen(mailbox -> assertCanDeleteReactive(session, mailbox.generateAssociatedPath()).thenReturn(true))
477+
.flatMap(mailbox -> assertCanDeleteReactive(session, mailbox))
473478
.flatMap(mailbox -> doDeleteMailbox(mailboxMapper, mailbox, session))
474479
.switchIfEmpty(Mono.error(() -> new MailboxNotFoundException(mailboxPath)))));
475480
}
@@ -480,7 +485,7 @@ public Mailbox deleteMailbox(MailboxId mailboxId, MailboxSession session) throws
480485
MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session);
481486

482487
return mailboxMapper.execute(() -> block(mailboxMapper.findMailboxById(mailboxId)
483-
.filterWhen(mailbox -> assertCanDeleteReactive(session, mailbox.generateAssociatedPath()).thenReturn(true))
488+
.flatMap(mailbox -> assertCanDeleteReactive(session, mailbox))
484489
.flatMap(mailbox -> doDeleteMailbox(mailboxMapper, mailbox, session))));
485490
}
486491

@@ -490,7 +495,7 @@ public Mono<Mailbox> deleteMailboxReactive(MailboxId mailboxId, MailboxSession s
490495
MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session);
491496

492497
return mailboxMapper.executeReactive(mailboxMapper.findMailboxById(mailboxId)
493-
.filterWhen(mailbox -> assertCanDeleteReactive(session, mailbox.generateAssociatedPath()).thenReturn(true))
498+
.flatMap(mailbox -> assertCanDeleteReactive(session, mailbox))
494499
.flatMap(mailbox -> doDeleteMailbox(mailboxMapper, mailbox, session)));
495500
}
496501

@@ -500,33 +505,34 @@ public Mono<Void> deleteMailboxReactive(MailboxPath mailboxPath, MailboxSession
500505
MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session);
501506

502507
return mailboxMapper.executeReactive(mailboxMapper.findMailboxByPath(mailboxPath)
503-
.filterWhen(mailbox -> assertCanDeleteReactive(session, mailbox.generateAssociatedPath()).thenReturn(true))
508+
.flatMap(mailbox -> assertCanDeleteReactive(session, mailbox))
504509
.flatMap(mailbox -> doDeleteMailbox(mailboxMapper, mailbox, session))
505510
.switchIfEmpty(Mono.error(() -> new MailboxNotFoundException(mailboxPath))))
506511
.then();
507512
}
508513

509-
private Mono<Void> assertCanDeleteReactive(MailboxSession session, MailboxPath path) {
514+
private Mono<Mailbox> assertCanDeleteReactive(MailboxSession session, Mailbox mailbox) {
515+
MailboxPath path = mailbox.generateAssociatedPath();
510516
if (path.belongsTo(session)) {
511-
return Mono.empty();
517+
return Mono.just(mailbox);
512518
}
513519
return Mono.from(hasRightReactive(path, Right.DeleteMailbox, session))
514520
.flatMap(hasRight -> {
515521
if (hasRight) {
516-
return Mono.empty();
522+
return Mono.just(mailbox);
517523
}
518524
return Mono.error(new InsufficientRightsException("user '" + session.getUser().asString() + "' is not allowed to delete the mailbox '" + path.asString() + "'"));
519525
});
520526
}
521527

522-
private Mono<Void> assertCanDeleteWhenRename(MailboxSession session, MailboxPath path) {
528+
private Mono<MailboxPath> assertCanDeleteWhenRename(MailboxSession session, MailboxPath path) {
523529
if (path.belongsTo(session)) {
524-
return Mono.empty();
530+
return Mono.just(path);
525531
}
526532
return Mono.from(myRightsReactive(path, session))
527533
.flatMap(rights -> {
528534
if (rights.contains(Right.DeleteMailbox)) {
529-
return Mono.empty();
535+
return Mono.just(path);
530536
} else if (!rights.contains(Right.Lookup)) {
531537
return Mono.error(new MailboxNotFoundException(path));
532538
} else {
@@ -633,7 +639,8 @@ public Mono<List<MailboxRenamedResult>> renameMailboxReactive(MailboxId mailboxI
633639
MailboxMapper mapper = mailboxSessionMapperFactory.getMailboxMapper(session);
634640

635641
Mono<Mailbox> fromMailboxPublisher = mapper.findMailboxById(mailboxId)
636-
.filterWhen(mailbox -> assertCanDeleteWhenRename(session, mailbox.generateAssociatedPath()).thenReturn(true))
642+
.flatMap(mailbox -> assertCanDeleteWhenRename(session, mailbox.generateAssociatedPath())
643+
.thenReturn(mailbox))
637644
.switchIfEmpty(Mono.error(() -> new MailboxNotFoundException(mailboxId)));
638645

639646
return sanitizedMailboxPath(newMailboxPath, session)
@@ -642,16 +649,15 @@ public Mono<List<MailboxRenamedResult>> renameMailboxReactive(MailboxId mailboxI
642649

643650
private Mono<MailboxPath> sanitizedMailboxPath(MailboxPath mailboxPath, MailboxSession session) {
644651
Function<MailboxPath, Mono<Boolean>> assertNewMailboxPathDoesNotExist = newMailboxPath -> mailboxExists(mailboxPath, session)
645-
.filter(FunctionalUtils.identityPredicate().negate())
646-
.switchIfEmpty(Mono.error(new MailboxExistsException(mailboxPath.toString())))
647-
.thenReturn(true);
652+
.map(FunctionalUtils.negate());
648653

649-
Function<MailboxPath, Mono<Boolean>> assertRightToCreate = newMailboxPath -> assertCanCreateReactive(session, mailboxPath)
650-
.thenReturn(true);
654+
Function<MailboxPath, Mono<Boolean>> assertRightToCreate = newMailboxPath -> canCreateReactive(session, mailboxPath);
651655

652656
return Mono.fromCallable(() -> mailboxPath.sanitize(session.getPathDelimiter()))
653657
.filterWhen(assertNewMailboxPathDoesNotExist)
658+
.switchIfEmpty(Mono.error(() -> new MailboxExistsException(mailboxPath.toString())))
654659
.filterWhen(assertRightToCreate)
660+
.switchIfEmpty(Mono.error(() -> new InsufficientRightsException("user '" + session.getUser().asString() + "' is not allowed to create the mailbox '" + mailboxPath.asString() + "'")))
655661
.flatMap(newMailboxPath -> Mono.fromCallable(() -> newMailboxPath.assertAcceptable(session.getPathDelimiter())));
656662
}
657663

server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import org.apache.james.mailbox.ModSeq;
9090
import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
9191
import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
92+
import org.apache.james.mailbox.model.MailboxACL;
9293
import org.apache.james.mailbox.model.MailboxPath;
9394
import org.apache.james.mailbox.model.MessageRange;
9495
import org.apache.james.mailbox.model.UidValidity;
@@ -3498,4 +3499,82 @@ void concurrentIdCommandsInTheSameSessionShouldSucceed() throws Exception {
34983499
}
34993500
}
35003501

3502+
@Nested
3503+
class RenameMailboxTest {
3504+
IMAPServer imapServer;
3505+
private int port;
3506+
3507+
@BeforeEach
3508+
void beforeEach() throws Exception {
3509+
imapServer = createImapServer("imapServer.xml");
3510+
port = imapServer.getListenAddresses().get(0).getPort();
3511+
MailboxSession mailboxSession = memoryIntegrationResources.getMailboxManager().createSystemSession(USER);
3512+
memoryIntegrationResources.getMailboxManager()
3513+
.createMailbox(MailboxPath.forUser(USER, "mailbox1"), mailboxSession);
3514+
memoryIntegrationResources.getMailboxManager()
3515+
.createMailbox(MailboxPath.forUser(USER, "mailbox2"), mailboxSession);
3516+
}
3517+
3518+
@AfterEach
3519+
void tearDown() {
3520+
imapServer.destroy();
3521+
}
3522+
3523+
@Test
3524+
void renameShouldFailWhenTargetMailboxAlreadyExists() throws Exception {
3525+
testIMAPClient.connect("127.0.0.1", port)
3526+
.login(USER.asString(), USER_PASS);
3527+
3528+
String response = testIMAPClient.sendCommand("RENAME mailbox1 mailbox2");
3529+
3530+
assertThat(response).contains("NO RENAME failed. Mailbox already exists.");
3531+
}
3532+
3533+
@Test
3534+
void renameShouldFailWhenRequestedMailboxDoesNotExist() throws Exception {
3535+
testIMAPClient.connect("127.0.0.1", port)
3536+
.login(USER.asString(), USER_PASS);
3537+
3538+
String response = testIMAPClient.sendCommand("RENAME nonExistingMailbox newMailboxName");
3539+
3540+
assertThat(response).contains("NO RENAME failed. Mailbox not found.");
3541+
}
3542+
3543+
@Test
3544+
void renameShouldFailWhenInsufficientRightsOnSharedMailbox() throws Exception {
3545+
// Create a mailbox for another user
3546+
memoryIntegrationResources.getMailboxManager()
3547+
.createMailbox(MailboxPath.forUser(USER2, "sharedMailbox.child1"),
3548+
memoryIntegrationResources.getMailboxManager().createSystemSession(USER2));
3549+
3550+
// Ensure the current user does not have the "delete mailbox" right on the shared mailbox
3551+
memoryIntegrationResources.getMailboxManager()
3552+
.applyRightsCommand(MailboxPath.forUser(USER2, "sharedMailbox"),
3553+
MailboxACL.command()
3554+
.forUser(USER)
3555+
.rights(MailboxACL.Right.Lookup, MailboxACL.Right.Read, MailboxACL.Right.Insert,
3556+
MailboxACL.Right.CreateMailbox,
3557+
MailboxACL.Right.Administer, MailboxACL.Right.Write)
3558+
.asAddition(),
3559+
memoryIntegrationResources.getMailboxManager().createSystemSession(USER2));
3560+
memoryIntegrationResources.getMailboxManager()
3561+
.applyRightsCommand(MailboxPath.forUser(USER2, "sharedMailbox.child1"),
3562+
MailboxACL.command()
3563+
.forUser(USER)
3564+
.rights(MailboxACL.Right.Lookup, MailboxACL.Right.Read, MailboxACL.Right.Insert,
3565+
MailboxACL.Right.CreateMailbox,
3566+
MailboxACL.Right.Administer, MailboxACL.Right.Write)
3567+
.asAddition(),
3568+
memoryIntegrationResources.getMailboxManager().createSystemSession(USER2));
3569+
3570+
// Connect and attempt to rename the shared mailbox
3571+
testIMAPClient.connect("127.0.0.1", port)
3572+
.login(USER.asString(), USER_PASS);
3573+
String response = testIMAPClient.sendCommand("RENAME #user.bobo.sharedMailbox.child1 #user.bobo.sharedMailbox.newChild");
3574+
3575+
// Assert that the operation fails due to insufficient rights
3576+
assertThat(response).contains("NO RENAME processing failed.");
3577+
}
3578+
}
3579+
35013580
}

0 commit comments

Comments
 (0)