This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch 3.9.x in repository https://gitbox.apache.org/repos/asf/james-project.git
commit f89c3ca5b1d659f87e6a29433bf26a0152979437 Author: Quan Tran <[email protected]> AuthorDate: Wed Oct 1 12:32:11 2025 +0700 JAMES-4148 MoveTo action should provision mailbox if needed --- .../james/mailets/FilterIntegrationTest.java | 15 +++++ .../james/jmap/mailet/filter/ActionApplier.java | 29 ++++++--- .../data/jmap/RunRulesOnMailboxService.java | 10 ++- .../data/jmap/RunRulesOnMailboxRoutesTest.java | 73 ++++++++++++++++++++++ 4 files changed, 119 insertions(+), 8 deletions(-) diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java index 6901446449..8b81c77146 100644 --- a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java +++ b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/FilterIntegrationTest.java @@ -266,6 +266,21 @@ public class FilterIntegrationTest { .awaitMessageCount(Constants.awaitAtMostOneMinute, 1); } + @Test + void moveToShouldCreateMailboxWhenMailboxDoesNotExist() throws Exception { + filteringManagementProbe.defineRulesForUser(BOB, asRule(Action.builder() + .setMoveTo(Optional.of(new Action.MoveTo("customMailbox"))))); + + messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort()) + .authenticate(ALICE.asString(), PASSWORD) + .sendMessage(ALICE.asString(), BOB.asString()); + + imapClient.connect(LOCALHOST_IP, jamesServer.getProbe(ImapGuiceProbe.class).getImapPort()) + .login(BOB, PASSWORD) + .select("customMailbox") + .awaitMessageCount(Constants.awaitAtMostOneMinute, 1); + } + @Test void moveToWithAppendInShouldWork() throws Exception { MailboxProbeImpl mailboxProbe = jamesServer.getProbe(MailboxProbeImpl.class); diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java index cca4fa0b00..adc6f86cb1 100644 --- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java +++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/ActionApplier.java @@ -37,6 +37,8 @@ import org.apache.james.lifecycle.api.LifecycleUtil; import org.apache.james.mailbox.MailboxManager; import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.MessageManager; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.exception.MailboxExistsException; import org.apache.james.mailbox.exception.MailboxNotFoundException; import org.apache.james.mailbox.model.MailboxId; import org.apache.james.mailbox.model.MailboxPath; @@ -229,19 +231,32 @@ public class ActionApplier { } private Stream<String> validateMailboxName(String mailboxName) { + MailboxPath mailboxPath = MailboxPath.forUser(username, mailboxName); + MailboxSession mailboxSession = mailboxManager.createSystemSession(username); try { - MailboxSession mailboxSession = mailboxManager.createSystemSession(username); - MessageManager messageManager = mailboxManager.getMailbox(MailboxPath.forUser(username, mailboxName), mailboxSession); - mailboxManager.endProcessingRequest(mailboxSession); - + MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, mailboxSession); return Stream.of(messageManager.getMailboxPath().getName()); } catch (MailboxNotFoundException e) { - LOGGER.info("Mailbox {} does not exist for user {}, but it was mentioned in a JMAP filtering rule", mailboxName, username.asString(), e); - return Stream.empty(); - } catch (Exception e) { + return createMailbox(mailboxName, mailboxPath, mailboxSession); + } catch (MailboxException e) { LOGGER.error("Unexpected failure while validating mailbox name {} for user {}", mailboxName, username.asString(), e); return Stream.empty(); + } finally { + mailboxManager.endProcessingRequest(mailboxSession); + } + } + + private Stream<String> createMailbox(String mailboxName, MailboxPath mailboxPath, MailboxSession mailboxSession) { + try { + return mailboxManager.createMailbox(mailboxPath, MailboxManager.CreateOption.CREATE_SUBSCRIPTION, mailboxSession) + .stream() + .map(createdMailbox -> mailboxName); + } catch (MailboxExistsException mailboxExistsException) { + LOGGER.info("Mailbox {} created by concurrent call", mailboxPath.asString()); + } catch (MailboxException mailboxException) { + LOGGER.error("Failed to provision mailbox {}", mailboxPath.asString(), mailboxException); } + return Stream.empty(); } private void sendACopy(LoopPrevention.RecordedRecipients recordedRecipients, diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java index 1a64ccfeb3..d80664a1ef 100644 --- a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java +++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java @@ -35,6 +35,8 @@ import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.MessageIdManager; import org.apache.james.mailbox.MessageManager; import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.exception.MailboxExistsException; +import org.apache.james.mailbox.exception.MailboxNotFoundException; import org.apache.james.mailbox.model.FetchGroup; import org.apache.james.mailbox.model.MailboxId; import org.apache.james.mailbox.model.MailboxPath; @@ -136,6 +138,12 @@ public class RunRulesOnMailboxService { private Mono<MailboxId> getMailboxId(MailboxSession mailboxSession, MailboxPath mailboxPath) { return Mono.from(mailboxManager.getMailboxReactive(mailboxPath, mailboxSession)) - .map(MessageManager::getId); + .map(MessageManager::getId) + .onErrorResume(MailboxNotFoundException.class, e -> Mono.from(mailboxManager.createMailboxReactive(mailboxPath, MailboxManager.CreateOption.CREATE_SUBSCRIPTION, mailboxSession)) + .onErrorResume(MailboxExistsException.class, e2 -> { + LOGGER.info("Mailbox {} created concurrently", mailboxPath.asString()); + return Mono.from(mailboxManager.getMailboxReactive(mailboxPath, mailboxSession)) + .map(MessageManager::getId); + })); } } diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java index 2950d2b5f8..e59e6825a9 100644 --- a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java +++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java @@ -386,6 +386,79 @@ public class RunRulesOnMailboxRoutesTest { ); } + @Test + void runRulesOnMailboxShouldSupportMoveToMailboxWhenMatchingMessageAndTargetMailboxDoesNotExist() throws Exception { + MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME); + MailboxPath moveToMailboxPath = MailboxPath.forUser(USERNAME, MOVE_TO_MAILBOX_NAME); + MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME); + + mailboxManager.createMailbox(mailboxPath, systemSession); + + mailboxManager.getMailbox(mailboxPath, systemSession) + .appendMessage(MessageManager.AppendCommand.builder() + .build(Message.Builder.of() + .setSubject("plop") + .setFrom("[email protected]") + .setBody("body", StandardCharsets.UTF_8)), + systemSession); + + String taskId = given() + .queryParam("action", "triage") + .body(""" + { + "id": "1", + "name": "rule 1", + "action": { + "appendIn": { + "mailboxIds": [] + }, + "moveTo": { + "mailboxName": "%s" + }, + "important": false, + "keyworkds": [], + "reject": false, + "seen": false + }, + "conditionGroup": { + "conditionCombiner": "OR", + "conditions": [ + { + "comparator": "contains", + "field": "subject", + "value": "plop" + }, + { + "comparator": "exactly-equals", + "field": "from", + "value": "[email protected]" + } + ] + } + }""" + .formatted(MOVE_TO_MAILBOX_NAME)) + .post(MAILBOX_NAME + "/messages") + .then() + .statusCode(CREATED_201) + .extract() + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await"); + + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(Throwing.supplier(() -> mailboxManager.getMailbox(mailboxPath, systemSession).getMailboxCounters(systemSession).getCount()).get()) + .isEqualTo(0); + softly.assertThat(Throwing.supplier(() -> mailboxManager.getMailbox(moveToMailboxPath, systemSession).getMailboxCounters(systemSession).getCount()).get()) + .isEqualTo(1); + } + ); + } + @Test void runRulesOnMailboxShouldNotMoveToMailboxNameWhenNonMatchingMessage() throws Exception { MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
