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 65cf760a17d210c468eb3949ba288f993c2223e8
Author: Rene Cordier <[email protected]>
AuthorDate: Wed Oct 15 21:56:40 2025 +0700

    JAMES-4148 Webadmin route to run filtering rule on all users (#2835)
---
 .../modules/servers/partials/operate/webadmin.adoc |   63 +
 .../james/modules/server/JmapTasksModule.java      |    7 +
 .../james/modules/server/MessagesRoutesModule.java |    2 +
 ...dminServerTaskSerializationIntegrationTest.java |   76 +
 .../james/webadmin/routes/ConditionalRoute.java}   |   21 +-
 ...Routes.java => RunRuleOnAllMailboxesRoute.java} |  116 +-
 .../data/jmap/RunRulesOnMailboxRoutes.java         |   24 +-
 .../james/webadmin/data/jmap/dto/UserTask.java}    |   28 +-
 .../data/jmap/RunRulesOnMailboxRoutesTest.java     | 2185 +++++++++++---------
 .../james/webadmin/routes/MessagesRoutes.java      |   31 +-
 .../webadmin/routes/MessageRoutesExpireTest.java   |    1 +
 .../james/webadmin/routes/MessageRoutesTest.java   |    1 +
 src/site/markdown/server/manage-webadmin.md        |   63 +
 13 files changed, 1573 insertions(+), 1045 deletions(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc 
b/docs/modules/servers/partials/operate/webadmin.adoc
index ccf1fdf780..0bd32faead 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -1537,6 +1537,69 @@ In this case you should also add the 
xref:{xref-base}/configure/mailets.adoc[mai
 
 include::{admin-messages-extend}[]
 
+=== Running a filtering rule on a specific mailbox for all users
+
+....
+curl -XPOST http://ip:port/messages?action=triage&mailboxName={mailboxName} \
+-d '{
+  "id": "1",
+  "name": "rule 1",
+  "action": {
+    "moveTo": {
+      "mailboxName": "Trash"
+    }
+  },
+  "conditionGroup": {
+    "conditionCombiner": "OR",
+    "conditions": [
+      {
+        "comparator": "contains",
+        "field": "subject",
+        "value": "plop"
+      },
+      {
+        "comparator": "exactly-equals",
+        "field": "from",
+        "value": "[email protected]"
+      }
+    ]
+  }
+}'
+....
+
+Will schedule a task for each user running a filtering rule passed as query 
parameter in ``mailboxName`` mailbox.
+
+Query parameter `mailboxName` should not be empty, nor contain `% *` 
characters, nor starting with `#`.
+If a user does not have a mailbox with that name, it will skip that user.
+
+The action of the rule should be `moveTo` with a mailbox name defined. If 
mailbox ids are defined in `appendIn` action,
+it will fail, as it makes no sense cluster scoped.
+
+Response codes:
+
+* 201: Success. Map[Username, TaskId] is returned.
+* 400: Invalid mailbox name
+* 400: Invalid JSON payload (including mailbox ids defined in the action)
+* 400: mailboxName query parameter is missing
+
+The response is a map of task id per user:
+
+....
+[
+  {
+    "username": "[email protected]", "taskId": 
"5641376-02ed-47bd-bcc7-76ff6262d92a"
+  },
+  {
+    "username": "[email protected]", "taskId": 
"5641376-02ed-47bd-bcc7-42cc1313f47b"
+  },
+
+  [...]
+
+]
+....
+
+link:#_running_a_filtering_rule_on_a_mailbox[More details about details 
returned by running a filtering rule on a mailbox].
+
 == Administrating user mailboxes
 
 === Creating a mailbox
diff --git 
a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
 
b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
index 08a91c9e09..65820b1713 100644
--- 
a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
+++ 
b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTasksModule.java
@@ -24,8 +24,11 @@ import 
org.apache.james.webadmin.data.jmap.PopulateEmailQueryViewRequestToTask;
 import 
org.apache.james.webadmin.data.jmap.PopulateFilteringProjectionRequestToTask;
 import 
org.apache.james.webadmin.data.jmap.RecomputeAllFastViewProjectionItemsRequestToTask;
 import 
org.apache.james.webadmin.data.jmap.RecomputeUserFastViewProjectionItemsRequestToTask;
+import org.apache.james.webadmin.data.jmap.RunRuleOnAllMailboxesRoute;
 import org.apache.james.webadmin.data.jmap.RunRulesOnMailboxRoutes;
+import org.apache.james.webadmin.routes.ConditionalRoute;
 import org.apache.james.webadmin.routes.MailboxesRoutes;
+import org.apache.james.webadmin.routes.MessagesRoutes;
 import org.apache.james.webadmin.routes.UserMailboxesRoutes;
 import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
 
@@ -52,5 +55,9 @@ public class JmapTasksModule extends AbstractModule {
 
         Multibinder<Routes> routesMultiBinder = 
Multibinder.newSetBinder(binder(), Routes.class);
         routesMultiBinder.addBinding().to(RunRulesOnMailboxRoutes.class);
+
+        Multibinder.newSetBinder(binder(), ConditionalRoute.class, 
Names.named(MessagesRoutes.ALL_MESSAGES_TASKS))
+            .addBinding()
+            .to(RunRuleOnAllMailboxesRoute.class);
     }
 }
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
 
b/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
index 5e0ec7a515..018511d311 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
+++ 
b/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
@@ -22,6 +22,7 @@ package org.apache.james.modules.server;
 import static 
org.apache.james.webadmin.tasks.TaskFromRequestRegistry.TaskRegistration;
 
 import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.routes.ConditionalRoute;
 import org.apache.james.webadmin.routes.MessagesRoutes;
 
 import com.google.inject.AbstractModule;
@@ -35,5 +36,6 @@ public class MessagesRoutesModule extends AbstractModule {
         routesMultibinder.addBinding().to(MessagesRoutes.class);
 
         Multibinder.newSetBinder(binder(), TaskRegistration.class, 
Names.named(MessagesRoutes.ALL_MESSAGES_TASKS));
+        Multibinder.newSetBinder(binder(), ConditionalRoute.class, 
Names.named(MessagesRoutes.ALL_MESSAGES_TASKS));
     }
 }
diff --git 
a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/RabbitMQWebAdminServerTaskSerializationIntegrationTest.java
 
b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/RabbitMQWebAdminServerTaskSerializationIntegrationTest.java
index cc5933b40d..ea01f9eb62 100644
--- 
a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/RabbitMQWebAdminServerTaskSerializationIntegrationTest.java
+++ 
b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/RabbitMQWebAdminServerTaskSerializationIntegrationTest.java
@@ -25,6 +25,8 @@ import static io.restassured.RestAssured.with;
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 import static 
org.apache.james.webadmin.vault.routes.DeletedMessagesVaultRoutes.MESSAGE_PATH_PARAM;
 import static 
org.apache.james.webadmin.vault.routes.DeletedMessagesVaultRoutes.USERS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.eclipse.jetty.http.HttpStatus.CREATED_201;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
@@ -32,6 +34,8 @@ import static 
org.hamcrest.collection.IsMapWithSize.anEmptyMap;
 
 import java.io.ByteArrayInputStream;
 import java.util.Date;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 import jakarta.mail.Flags;
@@ -86,6 +90,7 @@ import 
org.apache.james.webadmin.vault.routes.DeletedMessagesVaultRoutes;
 import org.eclipse.jetty.http.HttpStatus;
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
@@ -118,6 +123,7 @@ class 
RabbitMQWebAdminServerTaskSerializationIntegrationTest {
 
     private static final String DOMAIN = "domain";
     private static final String USERNAME = "username@" + DOMAIN;
+    private static final String USERNAME_2 = "username2@" + DOMAIN;
 
     private DataProbe dataProbe;
     private MailboxProbe mailboxProbe;
@@ -774,6 +780,76 @@ class 
RabbitMQWebAdminServerTaskSerializationIntegrationTest {
             .body("additionalInformation.mailboxName", 
is(MailboxConstants.INBOX));
     }
 
+    @Disabled("JAMES-4148: Route not plugged yet")
+    @Test
+    void runRulesOnAllUsersMailboxShouldComplete(GuiceJamesServer server) 
throws Exception {
+        server.getProbe(DataProbeImpl.class).addUser(USERNAME, "secret");
+        server.getProbe(DataProbeImpl.class).addUser(USERNAME_2, "secret");
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, 
MailboxConstants.INBOX);
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, 
"otherMailbox");
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, 
USERNAME_2, MailboxConstants.INBOX);
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, 
USERNAME_2, "otherMailbox");
+
+        mailboxProbe.appendMessage(
+            USERNAME,
+            MailboxPath.inbox(Username.of(USERNAME)),
+            new ByteArrayInputStream("Subject: 
test\r\n\r\ntestmail".getBytes()),
+            new Date(),
+            false,
+            new Flags());
+
+        mailboxProbe.appendMessage(
+            USERNAME_2,
+            MailboxPath.inbox(Username.of(USERNAME_2)),
+            new ByteArrayInputStream("Subject: 
test\r\n\r\ntestmail".getBytes()),
+            new Date(),
+            false,
+            new Flags());
+
+        List<Map<String, String>> list = given()
+            .queryParams("action", "triage", "mailboxName", 
MailboxConstants.INBOX)
+            .body("""
+            {
+              "id": "1",
+              "name": "rule 1",
+              "action": {
+                "appendIn": {
+                  "mailboxIds": []
+                },
+                "moveTo": {
+                  "mailboxName": "otherMailbox"
+                },
+                "important": false,
+                "keyworkds": [],
+                "reject": false,
+                "seen": false
+              },
+              "conditionGroup": {
+                "conditionCombiner": "AND",
+                "conditions": [
+                  {
+                    "comparator": "contains",
+                    "field": "subject",
+                    "value": "test"
+                  }
+                ]
+              }
+            }""")
+            .post("/messages")
+        .then()
+            .statusCode(CREATED_201)
+            .extract()
+            .jsonPath()
+            .getList(".");
+
+        assertThat(list)
+            .hasSize(3)
+            .first()
+            .satisfies(map -> assertThat(map).hasSize(2)
+                .containsKeys("taskId")
+                .containsEntry("username", USERNAME));
+    }
+
     @Test
     void cleanUploadRepositoryShouldComplete() throws Exception {
         String taskId = given()
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
 
b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/ConditionalRoute.java
similarity index 61%
copy from 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
copy to 
server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/ConditionalRoute.java
index 5e0ec7a515..92eef046b3 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
+++ 
b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/ConditionalRoute.java
@@ -17,23 +17,12 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.modules.server;
+package org.apache.james.webadmin.routes;
 
-import static 
org.apache.james.webadmin.tasks.TaskFromRequestRegistry.TaskRegistration;
+import java.util.function.Predicate;
 
-import org.apache.james.webadmin.Routes;
-import org.apache.james.webadmin.routes.MessagesRoutes;
+import spark.Request;
+import spark.Route;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.multibindings.Multibinder;
-import com.google.inject.name.Names;
-
-public class MessagesRoutesModule extends AbstractModule {
-    @Override
-    protected void configure() {
-        Multibinder<Routes> routesMultibinder = 
Multibinder.newSetBinder(binder(), Routes.class);
-        routesMultibinder.addBinding().to(MessagesRoutes.class);
-
-        Multibinder.newSetBinder(binder(), TaskRegistration.class, 
Names.named(MessagesRoutes.ALL_MESSAGES_TASKS));
-    }
+public interface ConditionalRoute extends Route, Predicate<Request> {
 }
diff --git 
a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRuleOnAllMailboxesRoute.java
similarity index 55%
copy from 
server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
copy to 
server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRuleOnAllMailboxesRoute.java
index 8d183b4597..15c01c64e8 100644
--- 
a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRuleOnAllMailboxesRoute.java
@@ -19,27 +19,27 @@
 
 package org.apache.james.webadmin.data.jmap;
 
-import static org.apache.james.webadmin.Constants.SEPARATOR;
+import java.util.List;
+import java.util.Optional;
 
 import jakarta.inject.Inject;
 
 import org.apache.james.core.Username;
+import org.apache.james.jmap.api.filtering.Rule;
 import org.apache.james.jmap.api.filtering.RuleDTO;
 import org.apache.james.jmap.api.filtering.Rules;
 import org.apache.james.jmap.api.filtering.Version;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.task.Task;
+import org.apache.james.task.TaskId;
 import org.apache.james.task.TaskManager;
 import org.apache.james.user.api.UsersRepository;
-import org.apache.james.user.api.UsersRepositoryException;
-import org.apache.james.webadmin.Routes;
-import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import org.apache.james.webadmin.data.jmap.dto.UserTask;
+import org.apache.james.webadmin.routes.ConditionalRoute;
 import org.apache.james.webadmin.tasks.TaskRegistrationKey;
 import org.apache.james.webadmin.utils.ErrorResponder;
-import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.validation.MailboxName;
 import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
@@ -49,82 +49,66 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.guava.GuavaModule;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import spark.Request;
-import spark.Route;
-import spark.Service;
+import spark.Response;
 
-public class RunRulesOnMailboxRoutes implements Routes {
+public class RunRuleOnAllMailboxesRoute implements ConditionalRoute {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(RunRulesOnMailboxRoutes.class);
 
     private static final TaskRegistrationKey TRIAGE = 
TaskRegistrationKey.of("triage");
-    private static final String MAILBOX_NAME = ":mailboxName";
-    private static final String MAILBOXES = "mailboxes";
-    private static final String USER_NAME = ":userName";
-    private static final String USERS_BASE = "/users";
-    public static final String USER_MAILBOXES_BASE = USERS_BASE + SEPARATOR + 
USER_NAME + SEPARATOR + MAILBOXES;
-    public static final String SPECIFIC_MAILBOX = USER_MAILBOXES_BASE + 
SEPARATOR + MAILBOX_NAME;
-    public static final String MESSAGES_PATH = SPECIFIC_MAILBOX + "/messages";
+    private static final String ACTION_QUERY_PARAM = "action";
+    private static final String MAILBOX_NAME_QUERY_PARAM = "mailboxName";
 
     private final UsersRepository usersRepository;
     private final MailboxManager mailboxManager;
     private final RunRulesOnMailboxService runRulesOnMailboxService;
-    private final JsonTransformer jsonTransformer;
     private final TaskManager taskManager;
     private final ObjectMapper jsonDeserialize;
 
     @Inject
-    RunRulesOnMailboxRoutes(UsersRepository usersRepository,
-                            MailboxManager mailboxManager,
-                            TaskManager taskManager,
-                            JsonTransformer jsonTransformer,
-                            RunRulesOnMailboxService runRulesOnMailboxService) 
{
+    public RunRuleOnAllMailboxesRoute(UsersRepository usersRepository, 
MailboxManager mailboxManager, RunRulesOnMailboxService 
runRulesOnMailboxService, TaskManager taskManager) {
         this.usersRepository = usersRepository;
         this.mailboxManager = mailboxManager;
-        this.taskManager = taskManager;
-        this.jsonTransformer = jsonTransformer;
         this.runRulesOnMailboxService = runRulesOnMailboxService;
+        this.taskManager = taskManager;
+
         this.jsonDeserialize = new ObjectMapper()
             .registerModule(new Jdk8Module())
             .registerModule(new GuavaModule());
     }
 
     @Override
-    public String getBasePath() {
-        return USER_MAILBOXES_BASE;
+    public boolean test(Request request) {
+        return Optional.ofNullable(request.queryParams(ACTION_QUERY_PARAM))
+            .map(TRIAGE.asString()::equalsIgnoreCase)
+            .orElse(false);
     }
 
     @Override
-    public void define(Service service) {
-        service.post(MESSAGES_PATH, runRulesOnMailboxRoute(), jsonTransformer);
+    public Object handle(Request request, Response response) throws Exception {
+        return runRulesOnAllUsersMailbox(request, response);
     }
 
-    public Route runRulesOnMailboxRoute() {
-        return TaskFromRequestRegistry.builder()
-            .parameterName("action")
-            .register(TRIAGE, this::runRulesOnMailbox)
-            .buildAsRoute(taskManager);
-    }
-
-    public Task runRulesOnMailbox(Request request) throws 
UsersRepositoryException, MailboxException {
-        Username username = getUsernameParam(request);
-        MailboxName mailboxName = new 
MailboxName(request.params(MAILBOX_NAME));
+    public List<UserTask> runRulesOnAllUsersMailbox(Request request, Response 
response) {
         try {
-            usernamePreconditions(username);
-            mailboxExistPreconditions(username, mailboxName);
+            actionPrecondition(request);
+            MailboxName mailboxName = getMailboxNameQueryParam(request);
             RuleDTO ruleDTO = jsonDeserialize.readValue(request.body(), 
RuleDTO.class);
             Rules rules = new 
Rules(RuleDTO.toRules(ImmutableList.of(ruleDTO)), Version.INITIAL);
+            rulesPrecondition(rules);
 
-            return new RunRulesOnMailboxTask(username, mailboxName, rules, 
runRulesOnMailboxService);
+            response.status(HttpStatus.CREATED_201);
+            return runRulesOnAllUsersMailbox(mailboxName, rules);
         } catch (IllegalStateException e) {
-            LOGGER.info("Invalid argument on user mailboxes", e);
+            LOGGER.info("Invalid argument on /messages", e);
             throw ErrorResponder.builder()
                 .statusCode(HttpStatus.NOT_FOUND_404)
                 .type(ErrorResponder.ErrorType.NOT_FOUND)
-                .message("Invalid argument on user mailboxes")
+                .message("Invalid argument on /messages")
                 .cause(e)
                 .haltError();
         } catch (JsonProcessingException e) {
@@ -137,20 +121,44 @@ public class RunRulesOnMailboxRoutes implements Routes {
         }
     }
 
-    private Username getUsernameParam(Request request) {
-        return Username.of(request.params(USER_NAME));
+    private List<UserTask> runRulesOnAllUsersMailbox(MailboxName mailboxName, 
Rules rules) {
+        return Flux.from(usersRepository.listReactive())
+            .filterWhen(username -> mailboxForUserExists(username, 
mailboxName))
+            .map(username -> runRulesOnUserMailbox(username, mailboxName, 
rules))
+            .collectList()
+            .block();
     }
 
-    private void usernamePreconditions(Username username) throws 
UsersRepositoryException {
-        Preconditions.checkState(usersRepository.contains(username), "User 
does not exist");
+    private UserTask runRulesOnUserMailbox(Username username, MailboxName 
mailboxName, Rules rules) {
+        Task task = new RunRulesOnMailboxTask(username, mailboxName, rules, 
runRulesOnMailboxService);
+        TaskId taskId = taskManager.submit(task);
+        return new UserTask(username, taskId);
     }
 
-    private void mailboxExistPreconditions(Username username, MailboxName 
mailboxName) throws MailboxException {
+    private void actionPrecondition(Request request) {
+        if (!test(request)) {
+            throw new IllegalArgumentException("'action' query parameter is 
compulsory. Supported values are [triage]");
+        }
+    }
+
+    private MailboxName getMailboxNameQueryParam(Request request) {
+        return 
Optional.ofNullable(request.queryParams(MAILBOX_NAME_QUERY_PARAM))
+            .map(MailboxName::new)
+            .orElseThrow(() -> new IllegalArgumentException("mailboxName query 
param is missing"));
+    }
+
+    private Mono<Boolean> mailboxForUserExists(Username username, MailboxName 
mailboxName) {
         MailboxSession mailboxSession = 
mailboxManager.createSystemSession(username);
-        MailboxPath mailboxPath = MailboxPath.forUser(username, 
mailboxName.asString())
-            .assertAcceptable(mailboxSession.getPathDelimiter());
-        
Preconditions.checkState(Boolean.TRUE.equals(Mono.from(mailboxManager.mailboxExists(mailboxPath,
 mailboxSession)).block()),
-            "Mailbox does not exist. " + mailboxPath.asString());
-        mailboxManager.endProcessingRequest(mailboxSession);
+        MailboxPath mailboxPath = MailboxPath.forUser(username, 
mailboxName.asString());
+        return Mono.from(mailboxManager.mailboxExists(mailboxPath, 
mailboxSession));
+    }
+
+    private void rulesPrecondition(Rules rules) {
+        if (rules.getRules()
+            .stream()
+            .map(Rule::getAction)
+            .anyMatch(action -> 
!action.getAppendInMailboxes().getMailboxIds().isEmpty())) {
+            throw new IllegalArgumentException("Rule payload should not have 
[appendInMailboxes] action defined for runRulesOnAllUsersMailbox route");
+        }
     }
 }
diff --git 
a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
index 8d183b4597..084bafd78b 100644
--- 
a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
@@ -21,6 +21,8 @@ package org.apache.james.webadmin.data.jmap;
 
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 
+import java.util.Optional;
+
 import jakarta.inject.Inject;
 
 import org.apache.james.core.Username;
@@ -49,6 +51,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.guava.GuavaModule;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
@@ -61,13 +64,15 @@ public class RunRulesOnMailboxRoutes implements Routes {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(RunRulesOnMailboxRoutes.class);
 
     private static final TaskRegistrationKey TRIAGE = 
TaskRegistrationKey.of("triage");
+    private static final String ACTION_QUERY_PARAM = "action";
     private static final String MAILBOX_NAME = ":mailboxName";
     private static final String MAILBOXES = "mailboxes";
     private static final String USER_NAME = ":userName";
     private static final String USERS_BASE = "/users";
     public static final String USER_MAILBOXES_BASE = USERS_BASE + SEPARATOR + 
USER_NAME + SEPARATOR + MAILBOXES;
     public static final String SPECIFIC_MAILBOX = USER_MAILBOXES_BASE + 
SEPARATOR + MAILBOX_NAME;
-    public static final String MESSAGES_PATH = SPECIFIC_MAILBOX + "/messages";
+    public static final String MESSAGES_BASE = "/messages";
+    public static final String MESSAGES_PATH = SPECIFIC_MAILBOX + 
MESSAGES_BASE;
 
     private final UsersRepository usersRepository;
     private final MailboxManager mailboxManager;
@@ -75,6 +80,7 @@ public class RunRulesOnMailboxRoutes implements Routes {
     private final JsonTransformer jsonTransformer;
     private final TaskManager taskManager;
     private final ObjectMapper jsonDeserialize;
+    private final Optional<RunRuleOnAllMailboxesRoute> allMailboxesRoute;
 
     @Inject
     RunRulesOnMailboxRoutes(UsersRepository usersRepository,
@@ -82,6 +88,16 @@ public class RunRulesOnMailboxRoutes implements Routes {
                             TaskManager taskManager,
                             JsonTransformer jsonTransformer,
                             RunRulesOnMailboxService runRulesOnMailboxService) 
{
+        this(usersRepository, mailboxManager, taskManager, jsonTransformer, 
runRulesOnMailboxService, Optional.empty());
+    }
+
+    @VisibleForTesting
+    RunRulesOnMailboxRoutes(UsersRepository usersRepository,
+                            MailboxManager mailboxManager,
+                            TaskManager taskManager,
+                            JsonTransformer jsonTransformer,
+                            RunRulesOnMailboxService runRulesOnMailboxService,
+                            Optional<RunRuleOnAllMailboxesRoute> 
allMailboxesRoute) {
         this.usersRepository = usersRepository;
         this.mailboxManager = mailboxManager;
         this.taskManager = taskManager;
@@ -90,6 +106,7 @@ public class RunRulesOnMailboxRoutes implements Routes {
         this.jsonDeserialize = new ObjectMapper()
             .registerModule(new Jdk8Module())
             .registerModule(new GuavaModule());
+        this.allMailboxesRoute = allMailboxesRoute;
     }
 
     @Override
@@ -100,11 +117,14 @@ public class RunRulesOnMailboxRoutes implements Routes {
     @Override
     public void define(Service service) {
         service.post(MESSAGES_PATH, runRulesOnMailboxRoute(), jsonTransformer);
+
+        // TESTING only
+        allMailboxesRoute.ifPresent(route -> service.post(MESSAGES_BASE, 
route, jsonTransformer));
     }
 
     public Route runRulesOnMailboxRoute() {
         return TaskFromRequestRegistry.builder()
-            .parameterName("action")
+            .parameterName(ACTION_QUERY_PARAM)
             .register(TRIAGE, this::runRulesOnMailbox)
             .buildAsRoute(taskManager);
     }
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/dto/UserTask.java
similarity index 61%
copy from 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
copy to 
server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/dto/UserTask.java
index 5e0ec7a515..fce96f17a3 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MessagesRoutesModule.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/dto/UserTask.java
@@ -17,23 +17,25 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.modules.server;
+package org.apache.james.webadmin.data.jmap.dto;
 
-import static 
org.apache.james.webadmin.tasks.TaskFromRequestRegistry.TaskRegistration;
+import org.apache.james.core.Username;
+import org.apache.james.task.TaskId;
 
-import org.apache.james.webadmin.Routes;
-import org.apache.james.webadmin.routes.MessagesRoutes;
+public class UserTask {
+    private final Username username;
+    private final TaskId taskId;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.multibindings.Multibinder;
-import com.google.inject.name.Names;
+    public UserTask(Username username, TaskId taskId) {
+        this.username = username;
+        this.taskId = taskId;
+    }
 
-public class MessagesRoutesModule extends AbstractModule {
-    @Override
-    protected void configure() {
-        Multibinder<Routes> routesMultibinder = 
Multibinder.newSetBinder(binder(), Routes.class);
-        routesMultibinder.addBinding().to(MessagesRoutes.class);
+    public String getUsername() {
+        return username.asString();
+    }
 
-        Multibinder.newSetBinder(binder(), TaskRegistration.class, 
Names.named(MessagesRoutes.ALL_MESSAGES_TASKS));
+    public String getTaskId() {
+        return taskId.asString();
     }
 }
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 1111ea9d97..71ca7a9bf3 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
@@ -35,7 +35,9 @@ import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.Duration;
 import java.util.Date;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.stream.IntStream;
 
 import jakarta.mail.Flags;
@@ -67,14 +69,20 @@ import org.assertj.core.api.SoftAssertions;
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
 import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableList;
 
 import io.restassured.RestAssured;
+import reactor.core.publisher.Flux;
 
 public class RunRulesOnMailboxRoutesTest {
     private static final Username USERNAME = Username.of("username");
+    private static final Username BOB = Username.of("bob");
+    private static final Username ALICE = Username.of("alice");
     private static final String MAILBOX_NAME = "myMailboxName";
     private static final String OTHER_MAILBOX_NAME = "myOtherMailboxName";
     private static final String MOVE_TO_MAILBOX_NAME = "moveToMailbox";
@@ -110,14 +118,46 @@ public class RunRulesOnMailboxRoutesTest {
           }
         }""";
 
+    private static final String RULE_MOVE_TO_PAYLOAD = """
+                    {
+                      "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]"
+                          }
+                        ]
+                      }
+                    }""";
+
     private WebAdminServer webAdminServer;
     private UsersRepository usersRepository;
     private MemoryTaskManager taskManager;
     private InMemoryMailboxManager mailboxManager;
     MessageIdManager messageIdManager;
 
-    @BeforeEach
-    void setUp() throws Exception {
+    private void createServer(String path) throws Exception {
         InMemoryIntegrationResources resources = 
InMemoryIntegrationResources.builder()
             .preProvisionnedFakeAuthenticator()
             .fakeAuthorizator()
@@ -137,15 +177,16 @@ public class RunRulesOnMailboxRoutesTest {
 
         taskManager = new MemoryTaskManager(new Hostname("foo"));
 
+        RunRulesOnMailboxService service = new 
RunRulesOnMailboxService(mailboxManager, new InMemoryId.Factory(), 
messageIdManager);
         webAdminServer = WebAdminUtils.createWebAdminServer(
-                new RunRulesOnMailboxRoutes(usersRepository, mailboxManager, 
taskManager, new JsonTransformer(),
-                    new RunRulesOnMailboxService(mailboxManager, new 
InMemoryId.Factory(), messageIdManager)),
+                new RunRulesOnMailboxRoutes(usersRepository, mailboxManager, 
taskManager, new JsonTransformer(), service,
+                    Optional.of(new 
RunRuleOnAllMailboxesRoute(usersRepository, mailboxManager, service, 
taskManager))),
                 new TasksRoutes(taskManager, new JsonTransformer(),
                     
DTOConverter.of(RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE)))
             .start();
 
         RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer)
-            .setBasePath(USERS_BASE + SEPARATOR + USERNAME.asString() + 
SEPARATOR + UserMailboxesRoutes.MAILBOXES)
+            .setBasePath(path)
             .setUrlEncodingEnabled(false) // no further automatically encoding 
by Rest Assured client. rf: 
https://issues.apache.org/jira/projects/JAMES/issues/JAMES-3936
             .build();
     }
@@ -156,774 +197,730 @@ public class RunRulesOnMailboxRoutesTest {
         taskManager.stop();
     }
 
-    @Test
-    void runRulesOnMailboxShouldReturnErrorWhenUserIsNotFound() throws 
UsersRepositoryException {
-        when(usersRepository.contains(USERNAME)).thenReturn(false);
-
-        Map<String, Object> errors = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted("2"))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(NOT_FOUND_404)
-            .contentType(JSON)
-            .extract()
-            .body()
-            .jsonPath()
-            .getMap(".");
-
-        assertThat(errors)
-            .containsEntry("statusCode", NOT_FOUND_404)
-            .containsEntry("type", ERROR_TYPE_NOTFOUND)
-            .containsEntry("message", "Invalid argument on user mailboxes")
-            .containsEntry("details", "User does not exist");
-    }
-
-    @Test
-    void runRulesOnMailboxShouldReturnErrorWhenMailboxDoesNotExist() throws 
UsersRepositoryException {
-        Map<String, Object> errors = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted("2"))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(NOT_FOUND_404)
-            .contentType(JSON)
-            .extract()
-            .body()
-            .jsonPath()
-            .getMap(".");
-
-        assertThat(errors)
-            .containsEntry("statusCode", NOT_FOUND_404)
-            .containsEntry("type", ERROR_TYPE_NOTFOUND)
-            .containsEntry("message", "Invalid argument on user mailboxes")
-            .containsEntry("details", String.format("Mailbox does not exist. 
#private:%s:%s", USERNAME.asString(), MAILBOX_NAME));
-    }
-
-    @Test
-    void runRulesOnMailboxShouldReturnErrorWhenNoPayload() throws 
MailboxException {
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-
-        Map<String, Object> errors = given()
-            .queryParam("action", "triage")
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(BAD_REQUEST_400)
-            .contentType(JSON)
-            .extract()
-            .body()
-            .jsonPath()
-            .getMap(".");
-
-        assertThat(errors)
-            .containsEntry("statusCode", BAD_REQUEST_400)
-            .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
-            .containsEntry("message", "JSON payload of the request is not 
valid");
-    }
-
-    @Test
-    void runRulesOnMailboxShouldReturnErrorWhenBadPayload() throws 
MailboxException {
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-
-        Map<String, Object> errors = given()
-            .queryParam("action", "triage")
-            .body("""
+    @Nested
+    class RunRulesOnMailbox {
+        @BeforeEach
+        void setUp() throws Exception {
+            createServer(USERS_BASE + SEPARATOR + USERNAME.asString() + 
SEPARATOR + UserMailboxesRoutes.MAILBOXES);
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnErrorWhenUserIsNotFound() throws 
UsersRepositoryException {
+            when(usersRepository.contains(USERNAME)).thenReturn(false);
+
+            Map<String, Object> errors = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted("2"))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(NOT_FOUND_404)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", NOT_FOUND_404)
+                .containsEntry("type", ERROR_TYPE_NOTFOUND)
+                .containsEntry("message", "Invalid argument on user mailboxes")
+                .containsEntry("details", "User does not exist");
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnErrorWhenActionQueryParamIsMissing() 
throws UsersRepositoryException {
+            Map<String, Object> errors = given()
+                .body(RULE_PAYLOAD.formatted("2"))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "Invalid arguments supplied in the 
user request")
+                .containsEntry("details", "'action' query parameter is 
compulsory. Supported values are [triage]");
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnErrorWhenMailboxDoesNotExist() 
throws UsersRepositoryException {
+            Map<String, Object> errors = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted("2"))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(NOT_FOUND_404)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", NOT_FOUND_404)
+                .containsEntry("type", ERROR_TYPE_NOTFOUND)
+                .containsEntry("message", "Invalid argument on user mailboxes")
+                .containsEntry("details", String.format("Mailbox does not 
exist. #private:%s:%s", USERNAME.asString(), MAILBOX_NAME));
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnErrorWhenNoPayload() throws 
MailboxException {
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+
+            Map<String, Object> errors = given()
+                .queryParam("action", "triage")
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "JSON payload of the request is not 
valid");
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnErrorWhenBadPayload() throws 
MailboxException {
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+
+            Map<String, Object> errors = given()
+                .queryParam("action", "triage")
+                .body("""
                     {
                       "id": "1",
                       "name": "rule 1",
                       "condition": bad condition",
                       "action": "bad action"
                     }""")
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(BAD_REQUEST_400)
-            .contentType(JSON)
-            .extract()
-            .body()
-            .jsonPath()
-            .getMap(".");
-
-        assertThat(errors)
-            .containsEntry("statusCode", BAD_REQUEST_400)
-            .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
-            .containsEntry("message", "JSON payload of the request is not 
valid");
-    }
-
-    @Test
-    void runRulesOnMailboxShouldReturnTaskId() throws MailboxException {
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted("2"))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        assertThat(taskId)
-            .isNotEmpty();
-    }
-
-    @Test
-    void runRulesOnMailboxShouldMoveMatchingMessage() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
-                    .build(Message.Builder.of()
-                        .setSubject("plop")
-                        .setFrom("[email protected]")
-                        .setBody("body", StandardCharsets.UTF_8)),
-                systemSession);
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .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(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-            }
-        );
-    }
-
-    @Test
-    void runRulesOnMailboxShouldSupportMoveToMailboxNameWhenMatchingMessage() 
throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
-                    .build(Message.Builder.of()
-                        .setSubject("plop")
-                        .setFrom("[email protected]")
-                        .setBody("body", StandardCharsets.UTF_8)
-                        .addField(new RawField("X-Custom-Header", "value"))),
-                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": "any",
-                        "field": "header:X-Custom-Header",
-                        "value": "disregarded"
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "JSON payload of the request is not 
valid");
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnTaskId() throws MailboxException {
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted("2"))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .get("taskId");
+
+            assertThat(taskId)
+                .isNotEmpty();
+        }
+
+        @Test
+        void runRulesOnMailboxShouldMoveMatchingMessage() throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            mailboxManager.getMailbox(mailboxPath, systemSession)
+                .appendMessage(MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("body", StandardCharsets.UTF_8)),
+                    systemSession);
+
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .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(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                });
+        }
+
+        @Test
+        void 
runRulesOnMailboxShouldSupportMoveToMailboxNameWhenMatchingMessage() throws 
Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            mailboxManager.getMailbox(mailboxPath, systemSession)
+                .appendMessage(MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("body", StandardCharsets.UTF_8)
+                            .addField(new RawField("X-Custom-Header", 
"value"))),
+                    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": "any",
+                            "field": "header:X-Custom-Header",
+                            "value": "disregarded"
+                          }
+                        ]
                       }
-                    ]
-                  }
-                }"""
-                .formatted(OTHER_MAILBOX_NAME))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
+                    }""".formatted(OTHER_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(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-            }
-        );
-    }
-
-    @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"
+                .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(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                });
+        }
+
+        @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(RULE_MOVE_TO_PAYLOAD.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);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            mailboxManager.getMailbox(mailboxPath, systemSession)
+                .appendMessage(MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("not match rules")
+                            .setFrom("[email protected]")
+                            .setBody("body", StandardCharsets.UTF_8)),
+                    systemSession);
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_MOVE_TO_PAYLOAD.formatted(OTHER_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(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(0);
+                });
+        }
+
+        @Test
+        void bothMoveToAndAppendInMailboxesShouldWork() throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath appendIdMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxPath moveToMailboxPath = MailboxPath.forUser(USERNAME, 
MOVE_TO_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(appendIdMailboxPath, systemSession);
+            mailboxManager.createMailbox(moveToMailboxPath, systemSession);
+            MailboxId appendIdMailboxId = 
mailboxManager.getMailbox(appendIdMailboxPath, systemSession).getId();
+
+            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": ["%s"]
+                        },
+                        "moveTo": {
+                          "mailboxName": "%s"
+                        },
+                        "important": false,
+                        "keyworkds": [],
+                        "reject": false,
+                        "seen": false
                       },
-                      {
-                        "comparator": "exactly-equals",
-                        "field": "from",
-                        "value": "[email protected]"
+                      "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)
+                    }"""
+                    .formatted(appendIdMailboxId.serialize(), 
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);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
-                    .build(Message.Builder.of()
-                        .setSubject("not match rules")
-                        .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"
+                .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(appendIdMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(moveToMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                });
+        }
+
+        @Test
+        void 
bothMoveToAndAppendInMailboxesShouldNotDuplicateMessageWhenTheSameTargetMailbox()
 throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath targetMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(targetMailboxPath, systemSession);
+            MailboxId targetMailboxId = 
mailboxManager.getMailbox(targetMailboxPath, systemSession).getId();
+
+            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": ["%s"]
+                        },
+                        "moveTo": {
+                          "mailboxName": "%s"
+                        },
+                        "important": false,
+                        "keyworkds": [],
+                        "reject": false,
+                        "seen": false
                       },
-                      {
-                        "comparator": "exactly-equals",
-                        "field": "from",
-                        "value": "[email protected]"
+                      "conditionGroup": {
+                        "conditionCombiner": "OR",
+                        "conditions": [
+                          {
+                            "comparator": "contains",
+                            "field": "subject",
+                            "value": "plop"
+                          },
+                          {
+                            "comparator": "exactly-equals",
+                            "field": "from",
+                            "value": "[email protected]"
+                          }
+                        ]
                       }
-                    ]
-                  }
-                }"""
-                .formatted(OTHER_MAILBOX_NAME))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
+                    }"""
+                    .formatted(targetMailboxId.serialize(), 
OTHER_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(1);
-                softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(0);
-            }
-        );
-    }
-
-    @Test
-    void bothMoveToAndAppendInMailboxesShouldWork() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath appendIdMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxPath moveToMailboxPath = MailboxPath.forUser(USERNAME, 
MOVE_TO_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(appendIdMailboxPath, systemSession);
-        mailboxManager.createMailbox(moveToMailboxPath, systemSession);
-        MailboxId appendIdMailboxId = 
mailboxManager.getMailbox(appendIdMailboxPath, systemSession).getId();
-
-        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": ["%s"]
-                    },
-                    "moveTo": {
-                      "mailboxName": "%s"
-                    },
-                    "important": false,
-                    "keyworkds": [],
-                    "reject": false,
-                    "seen": false
-                  },
-                  "conditionGroup": {
-                    "conditionCombiner": "OR",
-                    "conditions": [
-                      {
-                        "comparator": "contains",
-                        "field": "subject",
-                        "value": "plop"
+                .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(targetMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                });
+        }
+
+        @Test
+        void runRulesShouldApplyDateCrieria() throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            mailboxManager.getMailbox(mailboxPath, systemSession)
+                .appendMessage(MessageManager.AppendCommand.builder()
+                        
.withInternalDate(Date.from(Clock.systemUTC().instant().minus(Duration.ofDays(2))))
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("body", StandardCharsets.UTF_8)),
+                    systemSession);
+
+            mailboxManager.getMailbox(mailboxPath, systemSession)
+                .appendMessage(MessageManager.AppendCommand.builder()
+                        
.withInternalDate(Date.from(Clock.systemUTC().instant().minus(Duration.ofDays(20))))
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("body", StandardCharsets.UTF_8)),
+                    systemSession);
+
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body("""
+                    {
+                      "id": "1",
+                      "name": "rule 1",
+                      "action": {
+                        "appendIn": {
+                          "mailboxIds": ["%s"]
+                        },
+                        "important": false,
+                        "keyworkds": [],
+                        "reject": false,
+                        "seen": false
                       },
-                      {
-                        "comparator": "exactly-equals",
-                        "field": "from",
-                        "value": "[email protected]"
+                      "conditionGroup": {
+                        "conditionCombiner": "AND",
+                        "conditions": [
+                          {
+                            "comparator": "contains",
+                            "field": "subject",
+                            "value": "plop"
+                          },
+                          {
+                            "comparator": "isOlderThan",
+                            "field": "internalDate",
+                            "value": "10d"
+                          }
+                        ]
                       }
-                    ]
-                  }
-                }"""
-                .formatted(appendIdMailboxId.serialize(), 
MOVE_TO_MAILBOX_NAME))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
+                    }""".formatted(otherMailboxId.serialize()))
+                .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(appendIdMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-                softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(moveToMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-            }
-        );
-    }
-
-    @Test
-    void 
bothMoveToAndAppendInMailboxesShouldNotDuplicateMessageWhenTheSameTargetMailbox()
 throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath targetMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(targetMailboxPath, systemSession);
-        MailboxId targetMailboxId = 
mailboxManager.getMailbox(targetMailboxPath, systemSession).getId();
-
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
+                .get(taskId + "/await");
+
+            SoftAssertions.assertSoftly(
+                softly -> {
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(mailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                }
+            );
+        }
+
+        @Test
+        void runRulesOnMailboxShouldNotMoveNonMatchingMessage() throws 
Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            mailboxManager.getMailbox(mailboxPath, systemSession)
+                .appendMessage(MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("hello")
+                            .setFrom("[email protected]")
+                            .setBody("body", StandardCharsets.UTF_8)),
+                    systemSession);
+
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .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(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(0);
+                }
+            );
+        }
+
+        @Test
+        void runRulesOnMailboxShouldManageMixedCase() throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            messageManager.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": ["%s"]
-                    },
-                    "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(targetMailboxId.serialize(), OTHER_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(targetMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-            }
-        );
-    }
-
-    @Test
-    void runRulesShouldApplyDateCrieria() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
-                    
.withInternalDate(Date.from(Clock.systemUTC().instant().minus(Duration.ofDays(2))))
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
                     .build(Message.Builder.of()
-                        .setSubject("plop")
+                        .setSubject("hello")
                         .setFrom("[email protected]")
                         .setBody("body", StandardCharsets.UTF_8)),
                 systemSession);
 
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
-                    
.withInternalDate(Date.from(Clock.systemUTC().instant().minus(Duration.ofDays(20))))
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
                     .build(Message.Builder.of()
-                        .setSubject("plop")
-                        .setFrom("[email protected]")
+                        .setSubject("hello")
+                        .setFrom("[email protected]")
                         .setBody("body", StandardCharsets.UTF_8)),
                 systemSession);
 
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body("""
-        {
-          "id": "1",
-          "name": "rule 1",
-          "action": {
-            "appendIn": {
-              "mailboxIds": ["%s"]
-            },
-            "important": false,
-            "keyworkds": [],
-            "reject": false,
-            "seen": false
-          },
-          "conditionGroup": {
-            "conditionCombiner": "AND",
-            "conditions": [
-              {
-                "comparator": "contains",
-                "field": "subject",
-                "value": "plop"
-              },
-              {
-                "comparator": "isOlderThan",
-                "field": "internalDate",
-                "value": "10d"
-              }
-            ]
-          }
-        }""".formatted(otherMailboxId.serialize()))
-            .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(1);
-                softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-            }
-        );
-    }
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
 
-    @Test
-    void runRulesOnMailboxShouldNotMoveNonMatchingMessage() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .get("taskId");
 
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await");
+
+            SoftAssertions.assertSoftly(
+                softly -> {
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(mailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(2);
+                }
+            );
+        }
+
+        @Test
+        void runRulesOnMailboxShouldApplyFlagCriteria() throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .withFlags(new FlagsBuilder().add(Flags.Flag.FLAGGED, 
Flags.Flag.SEEN)
+                        .build())
+                    .build(Message.Builder.of()
+                        .setSubject("plop")
+                        .setFrom("[email protected]")
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession).getId();
 
-        mailboxManager.getMailbox(mailboxPath, systemSession)
-            .appendMessage(MessageManager.AppendCommand.builder()
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .withFlags(new FlagsBuilder().add(Flags.Flag.ANSWERED)
+                        .add("custom")
+                        .build())
                     .build(Message.Builder.of()
                         .setSubject("hello")
                         .setFrom("[email protected]")
                         .setBody("body", StandardCharsets.UTF_8)),
-                systemSession);
+                systemSession).getId();
 
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .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(1);
-                softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(0);
-            }
-        );
-    }
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .withFlags(new FlagsBuilder().add(Flags.Flag.SEEN)
+                        .add("custom")
+                        .build())
+                    .build(Message.Builder.of()
+                        .setSubject("hello")
+                        .setFrom("[email protected]")
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession).getId();
 
-    @Test
-    void runRulesOnMailboxShouldManageMixedCase() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .build(Message.Builder.of()
-                    .setSubject("plop")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .build(Message.Builder.of()
-                    .setSubject("hello")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .build(Message.Builder.of()
-                    .setSubject("hello")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession);
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .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(1);
-                softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(2);
-            }
-        );
-    }
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
 
-    @Test
-    void runRulesOnMailboxShouldApplyFlagCriteria() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .withFlags(new FlagsBuilder().add(Flags.Flag.FLAGGED, 
Flags.Flag.SEEN)
-                    .build())
-                .build(Message.Builder.of()
-                    .setSubject("plop")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession).getId();
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .withFlags(new FlagsBuilder().add(Flags.Flag.ANSWERED)
-                    .add("custom")
-                    .build())
-                .build(Message.Builder.of()
-                    .setSubject("hello")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession).getId();
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .withFlags(new FlagsBuilder().add(Flags.Flag.SEEN)
-                    .add("custom")
-                    .build())
-                .build(Message.Builder.of()
-                    .setSubject("hello")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession).getId();
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body("""
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body("""
             {
               "id": "1",
               "name": "rule 1",
@@ -957,244 +954,528 @@ public class RunRulesOnMailboxRoutesTest {
                 ]
               }
             }""".formatted(otherMailboxId.serialize()))
-            .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(2);
-                softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
-                    .isEqualTo(1);
-            }
-        );
-    }
-
-    @Test
-    void runRulesOnMailboxShouldReturnTaskDetails() throws Exception {
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .build(Message.Builder.of()
-                    .setSubject("plop")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .build(Message.Builder.of()
-                    .setSubject("hello")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession);
-
-        messageManager.appendMessage(
-            MessageManager.AppendCommand.builder()
-                .build(Message.Builder.of()
-                    .setSubject("hello")
-                    .setFrom("[email protected]")
-                    .setBody("body", StandardCharsets.UTF_8)),
-            systemSession);
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
-        .when()
-            .get(taskId + "/await")
-        .then()
-            .body("status", Matchers.is("completed"))
-            .body("taskId", Matchers.is(notNullValue()))
-            .body("type", 
Matchers.is(RunRulesOnMailboxTask.TASK_TYPE.asString()))
-            .body("startedDate", Matchers.is(notNullValue()))
-            .body("submitDate", Matchers.is(notNullValue()))
-            .body("completedDate", Matchers.is(notNullValue()))
-            .body("additionalInformation.username", 
Matchers.is(USERNAME.asString()))
-            .body("additionalInformation.mailboxName", 
Matchers.is(MAILBOX_NAME))
-            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.is(2))
-            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
-            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false))
-            .body("additionalInformation.processedMessagesCount", 
Matchers.is(3));
-    }
-
-    @Test
-    void taskShouldStopAndCompleteWhenAppliedActionsExceedMaximumLimit() 
throws Exception {
-        overrideTriageRulesRouteWithActionsLimit(2);
-
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
-
-        // Add 20 matching messages, which exceeds the max actions of 2
-        IntStream.range(0, 20)
-            .forEach(Throwing.intConsumer(i -> messageManager.appendMessage(
+                .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(2);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getMailboxCounters(systemSession).getCount()).get())
+                        .isEqualTo(1);
+                }
+            );
+        }
+
+        @Test
+        void runRulesOnMailboxShouldReturnTaskDetails() throws Exception {
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            messageManager.appendMessage(
                 MessageManager.AppendCommand.builder()
                     .build(Message.Builder.of()
                         .setSubject("plop")
                         .setFrom("[email protected]")
-                        .setBody("matched mail", StandardCharsets.UTF_8)),
-                systemSession)));
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
-        .when()
-            .get(taskId + "/await")
-        .then()
-            .body("status", Matchers.is("completed"))
-            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.lessThan(20))
-            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
-            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(true));
-    }
-
-    @Test
-    void taskShouldCompleteWhenAppliedActionsReachMaximumLimit() throws 
Exception {
-        overrideTriageRulesRouteWithActionsLimit(2);
-
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
-
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
-
-        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession);
 
-        // Add 2 matching messages, reach the limit of 2
-        IntStream.range(0, 2)
-            .forEach(Throwing.intConsumer(i -> messageManager.appendMessage(
+            messageManager.appendMessage(
                 MessageManager.AppendCommand.builder()
                     .build(Message.Builder.of()
-                        .setSubject("plop")
+                        .setSubject("hello")
                         .setFrom("[email protected]")
-                        .setBody("matched mail", StandardCharsets.UTF_8)),
-                systemSession)));
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
-        .when()
-            .get(taskId + "/await")
-        .then()
-            .body("status", Matchers.is("completed"))
-            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.is(2))
-            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
-            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false));
-    }
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession);
 
-    @Test
-    void taskShouldCompleteWhenAppliedActionsLessThanMaximumLimit() throws 
Exception {
-        overrideTriageRulesRouteWithActionsLimit(10);
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .build(Message.Builder.of()
+                        .setSubject("hello")
+                        .setFrom("[email protected]")
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession);
 
-        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
-        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
-        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
 
-        mailboxManager.createMailbox(mailboxPath, systemSession);
-        mailboxManager.createMailbox(otherMailboxPath, systemSession);
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .get("taskId");
 
-        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await")
+            .then()
+                .body("status", Matchers.is("completed"))
+                .body("taskId", Matchers.is(notNullValue()))
+                .body("type", 
Matchers.is(RunRulesOnMailboxTask.TASK_TYPE.asString()))
+                .body("startedDate", Matchers.is(notNullValue()))
+                .body("submitDate", Matchers.is(notNullValue()))
+                .body("completedDate", Matchers.is(notNullValue()))
+                .body("additionalInformation.username", 
Matchers.is(USERNAME.asString()))
+                .body("additionalInformation.mailboxName", 
Matchers.is(MAILBOX_NAME))
+                
.body("additionalInformation.rulesOnMessagesApplySuccessfully", Matchers.is(2))
+                .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0));
+        }
+    }
 
-        // Add 2 matching messages, < the limit of 10
-        IntStream.range(0, 2)
-            .forEach(Throwing.intConsumer(i -> messageManager.appendMessage(
-                MessageManager.AppendCommand.builder()
+    @Disabled("JAMES-4148: Route not plugged yet")
+    @Nested
+    class RunRulesOnAllUsersMailbox {
+        @BeforeEach
+        void setUp() throws Exception {
+            createServer("/messages");
+
+            when(usersRepository.listReactive())
+                .thenReturn(Flux.fromIterable(ImmutableList.of(USERNAME, 
ALICE, BOB)));
+        }
+
+        @Test
+        void 
runRulesOnAllUsersMailboxShouldReturnErrorWhenMailboxNameQueryParamIsMissing() {
+            Map<String, Object> errors = given()
+                .queryParam("action", "triage")
+                .body(RULE_MOVE_TO_PAYLOAD.formatted("2"))
+                .post()
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "Invalid arguments supplied in the 
user request")
+                .containsEntry("details", "mailboxName query param is 
missing");
+        }
+
+        @Test
+        void 
runRulesOnAllUsersMailboxShouldReturnErrorWhenActionQueryParamIsMissing() {
+            Map<String, Object> errors = given()
+                .queryParam("mailboxName", MAILBOX_NAME)
+                .body(RULE_MOVE_TO_PAYLOAD.formatted("2"))
+                .post()
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "Invalid arguments supplied in the 
user request")
+                .containsEntry("details", "'action' query parameter is 
compulsory. Supported values are [triage]");
+        }
+
+        @Test
+        void runRulesOnAllUsersMailboxShouldReturnErrorWhenNoPayload() {
+            Map<String, Object> errors = given()
+                .queryParams("action", "triage", "mailboxName", MAILBOX_NAME)
+                .post()
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "JSON payload of the request is not 
valid");
+        }
+
+        @Test
+        void runRulesOnAllUsersMailboxShouldReturnErrorWhenBadPayload() {
+            Map<String, Object> errors = given()
+                .queryParams("action", "triage", "mailboxName", MAILBOX_NAME)
+                .body("""
+                    {
+                      "id": "1",
+                      "name": "rule 1",
+                      "condition": bad condition",
+                      "action": "bad action"
+                    }""")
+                .post()
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "JSON payload of the request is not 
valid");
+        }
+
+        @Test
+        void 
runRulesOnAllUsersMailboxShouldReturnErrorWhenRuleActionAppendInMailboxesIsDefined()
 {
+            Map<String, Object> errors = given()
+                .queryParams("action", "triage", "mailboxName", MAILBOX_NAME)
+                .body("""
+                    {
+                      "id": "1",
+                      "name": "rule 1",
+                      "action": {
+                        "appendIn": {
+                          "mailboxIds": ["123"]
+                        },
+                        "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]"
+                          }
+                        ]
+                      }
+                    }""")
+                .post()
+            .then()
+                .statusCode(BAD_REQUEST_400)
+                .contentType(JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", BAD_REQUEST_400)
+                .containsEntry("type", ERROR_TYPE_INVALIDARGUMENT)
+                .containsEntry("message", "Invalid arguments supplied in the 
user request")
+                .containsEntry("details", "Rule payload should not have 
[appendInMailboxes] action defined for runRulesOnAllUsersMailbox route");
+        }
+
+        @Test
+        void runRulesOnAllUsersMailboxShouldReturnListOfTaskIdPerUser() throws 
MailboxException {
+            createUserMailboxes();
+
+            List<Map<String, String>> list = given()
+                .queryParams("action", "triage", "mailboxName", MAILBOX_NAME)
+                .body(RULE_MOVE_TO_PAYLOAD.formatted(MOVE_TO_MAILBOX_NAME))
+                .post()
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .getList(".");
+
+            assertThat(list)
+                .hasSize(3)
+                .first()
+                .satisfies(map -> assertThat(map).hasSize(2)
+                    .containsKeys("taskId")
+                    .containsEntry("username", USERNAME.asString()));
+        }
+
+        @Test
+        void runRulesOnAllUsersMailboxShouldManageMixedCase() throws Exception 
{
+            createUserMailboxes();
+            createUserMessages(MailboxPath.forUser(USERNAME, MAILBOX_NAME));
+            createUserMessages(MailboxPath.forUser(ALICE, MAILBOX_NAME));
+            createUserMessages(MailboxPath.forUser(BOB, MAILBOX_NAME));
+
+            List<Map<String, String>> results = given()
+                .queryParams("action", "triage", "mailboxName", MAILBOX_NAME)
+                .body(RULE_MOVE_TO_PAYLOAD.formatted(MOVE_TO_MAILBOX_NAME))
+                .post()
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .getList(".");
+
+            results.stream()
+                .map(result -> result.get("taskId"))
+                .forEach(taskId ->
+                    given()
+                        .basePath(TasksRoutes.BASE)
+                        .when()
+                        .get(taskId + "/await"));
+
+            SoftAssertions.assertSoftly(
+                softly -> {
+                    MailboxSession usernameSession = 
mailboxManager.createSystemSession(USERNAME);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(MailboxPath.forUser(USERNAME, MAILBOX_NAME), 
usernameSession).getMailboxCounters(usernameSession).getCount()).get())
+                        .isEqualTo(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(MailboxPath.forUser(USERNAME, MOVE_TO_MAILBOX_NAME), 
usernameSession).getMailboxCounters(usernameSession).getCount()).get())
+                        .isEqualTo(2);
+
+                    MailboxSession aliceSession = 
mailboxManager.createSystemSession(ALICE);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(MailboxPath.forUser(ALICE, MAILBOX_NAME), 
aliceSession).getMailboxCounters(aliceSession).getCount()).get())
+                        .isEqualTo(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(MailboxPath.forUser(ALICE, MOVE_TO_MAILBOX_NAME), 
aliceSession).getMailboxCounters(aliceSession).getCount()).get())
+                        .isEqualTo(2);
+
+                    MailboxSession bobSession = 
mailboxManager.createSystemSession(BOB);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(MailboxPath.forUser(BOB, MAILBOX_NAME), 
bobSession).getMailboxCounters(bobSession).getCount()).get())
+                        .isEqualTo(1);
+                    softly.assertThat(Throwing.supplier(() -> 
mailboxManager.getMailbox(MailboxPath.forUser(BOB, MOVE_TO_MAILBOX_NAME), 
bobSession).getMailboxCounters(bobSession).getCount()).get())
+                        .isEqualTo(2);
+                }
+            );
+        }
+
+        @Test
+        void 
runRulesOnAllUsersMailboxShouldReturnNoopOnUsersWhenMailboxNameDoesNotExist() 
throws Exception {
+            createUserMailboxes();
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            mailboxManager.createMailbox(MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME), systemSession);
+
+            List<Map<String, String>> results = given()
+                .queryParams("action", "triage", "mailboxName", 
OTHER_MAILBOX_NAME)
+                .body(RULE_MOVE_TO_PAYLOAD.formatted(MOVE_TO_MAILBOX_NAME))
+                .post()
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .getList(".");
+
+            assertThat(results)
+                .hasSize(1)
+                .first()
+                .satisfies(map -> assertThat(map).hasSize(2)
+                    .containsKeys("taskId")
+                    .containsEntry("username", USERNAME.asString()));
+        }
+
+        @Test
+        void taskShouldStopAndCompleteWhenAppliedActionsExceedMaximumLimit() 
throws Exception {
+            overrideTriageRulesRouteWithActionsLimit(2);
+
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            // Add 20 matching messages, which exceeds the max actions of 2
+            IntStream.range(0, 20)
+                .forEach(Throwing.intConsumer(i -> 
messageManager.appendMessage(
+                    MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("matched mail", StandardCharsets.UTF_8)),
+                    systemSession)));
+
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+                .when()
+                .get(taskId + "/await")
+                .then()
+                .body("status", Matchers.is("completed"))
+                
.body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.lessThan(20))
+                .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+                .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(true));
+        }
+
+        private void createUserMessages(MailboxPath mailboxPath) throws 
Exception {
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(mailboxPath.getUser());
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            messageManager.appendMessage(MessageManager.AppendCommand.builder()
                     .build(Message.Builder.of()
                         .setSubject("plop")
                         .setFrom("[email protected]")
-                        .setBody("matched mail", StandardCharsets.UTF_8)),
-                systemSession)));
-
-        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
-
-        String taskId = given()
-            .queryParam("action", "triage")
-            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
-            .post(MAILBOX_NAME + "/messages")
-        .then()
-            .statusCode(CREATED_201)
-            .extract()
-            .jsonPath()
-            .get("taskId");
-
-        given()
-            .basePath(TasksRoutes.BASE)
-        .when()
-            .get(taskId + "/await")
-        .then()
-            .body("status", Matchers.is("completed"))
-            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.is(2))
-            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
-            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false));
-    }
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession);
 
-    private void overrideTriageRulesRouteWithActionsLimit(int maxActionsLimit) 
{
-        webAdminServer.destroy();
-        webAdminServer = WebAdminUtils.createWebAdminServer(
-                new RunRulesOnMailboxRoutes(usersRepository, mailboxManager, 
taskManager, new JsonTransformer(),
-                    new RunRulesOnMailboxService(mailboxManager, new 
InMemoryId.Factory(), messageIdManager, maxActionsLimit)),
-                new TasksRoutes(taskManager, new JsonTransformer(),
-                    
DTOConverter.of(RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE)))
-            .start();
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .build(Message.Builder.of()
+                        .setSubject("hello")
+                        .setFrom("[email protected]")
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession);
 
-        RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer)
-            .setBasePath(USERS_BASE + SEPARATOR + USERNAME.asString() + 
SEPARATOR + UserMailboxesRoutes.MAILBOXES)
-            .setUrlEncodingEnabled(false)
-            .build();
+            messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .build(Message.Builder.of()
+                        .setSubject("hello")
+                        .setFrom("[email protected]")
+                        .setBody("body", StandardCharsets.UTF_8)),
+                systemSession);
+        }
+
+        private void createUserMailboxes() throws MailboxException {
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+            mailboxManager.createMailbox(MailboxPath.forUser(USERNAME, 
MAILBOX_NAME), systemSession);
+            mailboxManager.createMailbox(MailboxPath.forUser(USERNAME, 
MOVE_TO_MAILBOX_NAME), systemSession);
+
+            systemSession = mailboxManager.createSystemSession(ALICE);
+            mailboxManager.createMailbox(MailboxPath.forUser(ALICE, 
MAILBOX_NAME), systemSession);
+            mailboxManager.createMailbox(MailboxPath.forUser(ALICE, 
MOVE_TO_MAILBOX_NAME), systemSession);
+
+            systemSession = mailboxManager.createSystemSession(BOB);
+            mailboxManager.createMailbox(MailboxPath.forUser(BOB, 
MAILBOX_NAME), systemSession);
+            mailboxManager.createMailbox(MailboxPath.forUser(BOB, 
MOVE_TO_MAILBOX_NAME), systemSession);
+        }
+
+        @Test
+        void taskShouldCompleteWhenAppliedActionsReachMaximumLimit() throws 
Exception {
+            overrideTriageRulesRouteWithActionsLimit(2);
+
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            // Add 2 matching messages, reach the limit of 2
+            IntStream.range(0, 2)
+                .forEach(Throwing.intConsumer(i -> 
messageManager.appendMessage(
+                    MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("matched mail", StandardCharsets.UTF_8)),
+                    systemSession)));
+
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .post(MAILBOX_NAME + "/messages")
+            .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+                .when()
+                .get(taskId + "/await")
+                .then()
+                .body("status", Matchers.is("completed"))
+                
.body("additionalInformation.rulesOnMessagesApplySuccessfully", Matchers.is(2))
+                .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+                .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false));
+        }
+
+        @Test
+        void taskShouldCompleteWhenAppliedActionsLessThanMaximumLimit() throws 
Exception {
+            overrideTriageRulesRouteWithActionsLimit(10);
+
+            MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, 
MAILBOX_NAME);
+            MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+            MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+            mailboxManager.createMailbox(mailboxPath, systemSession);
+            mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+            MessageManager messageManager = 
mailboxManager.getMailbox(mailboxPath, systemSession);
+
+            // Add 2 matching messages, < the limit of 10
+            IntStream.range(0, 2)
+                .forEach(Throwing.intConsumer(i -> 
messageManager.appendMessage(
+                    MessageManager.AppendCommand.builder()
+                        .build(Message.Builder.of()
+                            .setSubject("plop")
+                            .setFrom("[email protected]")
+                            .setBody("matched mail", StandardCharsets.UTF_8)),
+                    systemSession)));
+
+            MailboxId otherMailboxId = 
mailboxManager.getMailbox(otherMailboxPath, systemSession).getId();
+
+            String taskId = given()
+                .queryParam("action", "triage")
+                .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+                .post(MAILBOX_NAME + "/messages")
+                .then()
+                .statusCode(CREATED_201)
+                .extract()
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+                .when()
+                .get(taskId + "/await")
+                .then()
+                .body("status", Matchers.is("completed"))
+                
.body("additionalInformation.rulesOnMessagesApplySuccessfully", Matchers.is(2))
+                .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+                .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false));
+        }
+
+        private void overrideTriageRulesRouteWithActionsLimit(int 
maxActionsLimit) {
+            webAdminServer.destroy();
+            RunRulesOnMailboxService service = new 
RunRulesOnMailboxService(mailboxManager, new InMemoryId.Factory(), 
messageIdManager, maxActionsLimit);
+            webAdminServer = WebAdminUtils.createWebAdminServer(
+                    new RunRulesOnMailboxRoutes(usersRepository, 
mailboxManager, taskManager, new JsonTransformer(), service,
+                        Optional.of(new 
RunRuleOnAllMailboxesRoute(usersRepository, mailboxManager, service, 
taskManager))),
+                    new TasksRoutes(taskManager, new JsonTransformer(),
+                        
DTOConverter.of(RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE)))
+                .start();
+
+            RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer)
+                .setBasePath(USERS_BASE + SEPARATOR + USERNAME.asString() + 
SEPARATOR + UserMailboxesRoutes.MAILBOXES)
+                .setUrlEncodingEnabled(false)
+                .build();
+        }
     }
 }
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/MessagesRoutes.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/MessagesRoutes.java
index 44db76b497..5142f9c09d 100644
--- 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/MessagesRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/MessagesRoutes.java
@@ -60,6 +60,7 @@ public class MessagesRoutes implements Routes {
     private final ExpireMailboxService expireMailboxService;
     private final JsonTransformer jsonTransformer;
     private final Set<TaskFromRequestRegistry.TaskRegistration> 
allMessagesTaskRegistration;
+    private final Set<ConditionalRoute> postOverrides;
     private final UsersRepository usersRepository;
 
     public static final String ALL_MESSAGES_TASKS = "allMessagesTasks";
@@ -67,13 +68,16 @@ public class MessagesRoutes implements Routes {
     @Inject
     MessagesRoutes(TaskManager taskManager, MessageId.Factory 
messageIdFactory, MessageIdReIndexer reIndexer,
                    ExpireMailboxService expireMailboxService, JsonTransformer 
jsonTransformer,
-                   @Named(ALL_MESSAGES_TASKS) 
Set<TaskFromRequestRegistry.TaskRegistration> allMessagesTaskRegistration, 
UsersRepository usersRepository) {
+                   @Named(ALL_MESSAGES_TASKS) 
Set<TaskFromRequestRegistry.TaskRegistration> allMessagesTaskRegistration,
+                   @Named(ALL_MESSAGES_TASKS) Set<ConditionalRoute> 
postOverrides,
+                   UsersRepository usersRepository) {
         this.taskManager = taskManager;
         this.messageIdFactory = messageIdFactory;
         this.reIndexer = reIndexer;
         this.expireMailboxService = expireMailboxService;
         this.jsonTransformer = jsonTransformer;
         this.allMessagesTaskRegistration = allMessagesTaskRegistration;
+        this.postOverrides = postOverrides;
         this.usersRepository = usersRepository;
     }
 
@@ -87,8 +91,10 @@ public class MessagesRoutes implements Routes {
         TaskFromRequest expireMailboxTaskRequest = this::expireMailbox;
         service.delete(BASE_PATH, 
expireMailboxTaskRequest.asRoute(taskManager), jsonTransformer);
         service.post(MESSAGE_PATH, reIndexMessage(), jsonTransformer);
-        allMessagesOperations()
-            .ifPresent(route -> service.post(BASE_PATH, route, 
jsonTransformer));
+
+        if (!postOverrides.isEmpty() && 
!allMessagesTaskRegistration.isEmpty()) {
+            service.post(BASE_PATH, allMessagesOperations(), jsonTransformer);
+        }
     }
 
     private Task expireMailbox(Request request) {
@@ -135,10 +141,19 @@ public class MessagesRoutes implements Routes {
         }
     }
 
-    private Optional<Route> allMessagesOperations() {
-        return TaskFromRequestRegistry.builder()
-            .parameterName(TASK_PARAMETER)
-            .registrations(allMessagesTaskRegistration)
-            .buildAsRouteOptional(taskManager);
+    private Route allMessagesOperations() {
+        return (request, response) -> {
+            Optional<Route> override = postOverrides.stream()
+                .filter(postOverride -> postOverride.test(request))
+                .map(r -> (Route) r)
+                .findAny();
+
+            return override.or(() -> TaskFromRequestRegistry.builder()
+                    .parameterName(TASK_PARAMETER)
+                    .registrations(allMessagesTaskRegistration)
+                    .buildAsRouteOptional(taskManager))
+                .get()
+                .handle(request, response);
+        };
     }
 }
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesExpireTest.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesExpireTest.java
index 2926f3a272..57e5b3010a 100644
--- 
a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesExpireTest.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesExpireTest.java
@@ -88,6 +88,7 @@ class MessageRoutesExpireTest {
                     new ExpireMailboxService(usersRepository, mailboxManager, 
Clock.systemUTC()),
                     jsonTransformer,
                     ImmutableSet.of(),
+                    ImmutableSet.of(),
                     usersRepository))
             .start();
 
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesTest.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesTest.java
index 5171283246..ef86640659 100644
--- 
a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/MessageRoutesTest.java
@@ -96,6 +96,7 @@ class MessageRoutesTest {
                     null,
                     jsonTransformer,
                     ImmutableSet.of(),
+                    ImmutableSet.of(),
                     null))
             .start();
 
diff --git a/src/site/markdown/server/manage-webadmin.md 
b/src/site/markdown/server/manage-webadmin.md
index de0f231a14..14f57fc1b2 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -1485,6 +1485,69 @@ However the source of truth will not be impacted, hence 
rerunning the task will
 This task could be run safely online and can be scheduled on a recurring basis 
outside of peak traffic 
 by an admin to ensure Cassandra message consistency.
 
+=== Running a filtering rule on a specific mailbox for all users
+
+```
+curl -XPOST http://ip:port/messages?action=triage&mailboxName={mailboxName} \
+-d '{
+  "id": "1",
+  "name": "rule 1",
+  "action": {
+    "moveTo": {
+      "mailboxName": "Trash"
+    }
+  },
+  "conditionGroup": {
+    "conditionCombiner": "OR",
+    "conditions": [
+      {
+        "comparator": "contains",
+        "field": "subject",
+        "value": "plop"
+      },
+      {
+        "comparator": "exactly-equals",
+        "field": "from",
+        "value": "[email protected]"
+      }
+    ]
+  }
+}'
+```
+
+Will schedule a task for each user running a filtering rule passed as query 
parameter in `mailboxName` mailbox.
+
+Query parameter `mailboxName` should not be empty, nor contain `% *` 
characters, nor starting with `#`.
+If a user does not have a mailbox with that name, it will skip that user.
+
+The action of the rule should be `moveTo` with a mailbox name defined. If 
mailbox ids are defined in `appendIn` action,
+it will fail, as it makes no sense cluster scoped.
+
+Response codes:
+
+* 201: Success. Map[Username, TaskId] is returned.
+* 400: Invalid mailbox name
+* 400: Invalid JSON payload (including mailbox ids defined in the action)
+* 400: mailboxName query parameter is missing
+
+The response is a map of task id per user:
+
+```
+[
+  {
+    "username": "[email protected]", "taskId": 
"5641376-02ed-47bd-bcc7-76ff6262d92a"
+  },
+  {
+    "username": "[email protected]", "taskId": 
"5641376-02ed-47bd-bcc7-42cc1313f47b"
+  },
+
+  [...]
+
+]
+```
+
+[More details about details returned by running a filtering rule on a 
mailbox](#Running_a_filtering_rule_on_a_mailbox).
+
 ## Administrating user mailboxes
 
  - [Creating a mailbox](#Creating_a_mailbox)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to