You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2025/09/26 14:47:45 UTC

(james-project) branch master updated (e57cbe523b -> 691c2beeb3)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


    from e57cbe523b Roll version to 3.10.0-SNAPSHOT (#2821)
     new f6bbb1ae2f JAMES-4148 Refactor HeaderExtractor to be able to extract headers on MessageResult and Mail
     new ae35ab4d6e JAMES-4148 Webadmin route to run a filtering rule on a mailbox of a user
     new 691c2beeb3 JAMES-4148 Webadmin route to run a filtering rule on a mailbox of a user

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../modules/servers/partials/operate/webadmin.adoc |  60 +++
 .../server/JmapTaskSerializationModule.java        |  19 +
 .../james/modules/server/JmapTasksModule.java      |   4 +
 .../james/jmap/mailet/filter/FilteringHeaders.java |  92 ++++
 .../james/jmap/mailet/filter/HeaderExtractor.java  |  23 +-
 .../james/jmap/mailet/filter/MailMatcher.java      |   7 +-
 .../james/jmap/mailet/filter/RuleMatcher.java      |  17 +-
 ...dminServerTaskSerializationIntegrationTest.java |  59 +++
 server/protocols/webadmin/webadmin-jmap/pom.xml    |   5 +-
 .../data/jmap/RunRulesOnMailboxRoutes.java         | 156 +++++++
 .../data/jmap/RunRulesOnMailboxService.java        | 126 ++++++
 .../webadmin/data/jmap/RunRulesOnMailboxTask.java} | 109 ++---
 ...ulesOnMailboxTaskAdditionalInformationDTO.java} |  63 ++-
 .../data/jmap/RunRulesOnMailboxTaskDTO.java}       |  41 +-
 .../data/jmap/RunRulesOnMailboxRoutesTest.java     | 480 +++++++++++++++++++++
 ...OnMailboxTaskAdditionalInformationDTOTest.java} |  10 +-
 .../RunRulesOnMailboxTaskSerializationTest.java}   |  33 +-
 .../runRulesOnMailbox.additionalInformation.json   |   8 +
 .../resources/json/runRulesOnMailbox.task.json     |  35 ++
 .../webadmin/service/UserMailboxesService.java     |   1 -
 .../UserMailboxesRoutesNoIndexationTest.java       |   3 +-
 .../routes/UserRoutesWithMailboxParamTest.java     |   3 +-
 src/site/markdown/server/manage-webadmin.md        |  59 +++
 23 files changed, 1279 insertions(+), 134 deletions(-)
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/FilteringHeaders.java
 create mode 100644 server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
 create mode 100644 server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
 copy server/protocols/webadmin/{webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/ClearMailboxContentTask.java => webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTask.java} (51%)
 copy server/protocols/webadmin/{webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/ClearMailboxContentTaskAdditionalInformationDTO.java => webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTO.java} (50%)
 copy server/protocols/webadmin/{webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/ClearMailboxContentTaskDTO.java => webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskDTO.java} (52%)
 create mode 100644 server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java
 copy server/protocols/webadmin/{webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/ClearMailboxContentTaskAdditionalInformationDTOTest.java => webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java} (81%)
 copy server/protocols/webadmin/{webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/ClearMailboxContentTaskSerializationTest.java => webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskSerializationTest.java} (54%)
 create mode 100644 server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.additionalInformation.json
 create mode 100644 server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.task.json


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


(james-project) 03/03: JAMES-4148 Webadmin route to run a filtering rule on a mailbox of a user

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 691c2beeb351777202629f5833f79836f17c14d4
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed Sep 24 16:31:00 2025 +0700

    JAMES-4148 Webadmin route to run a filtering rule on a mailbox of a user
---
 .../modules/servers/partials/operate/webadmin.adoc | 60 ++++++++++++++++++++++
 src/site/markdown/server/manage-webadmin.md        | 59 +++++++++++++++++++++
 2 files changed, 119 insertions(+)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc b/docs/modules/servers/partials/operate/webadmin.adoc
index c42505965a..fa0324519b 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -1800,6 +1800,66 @@ the following `additionalInformation`:
 }
 ....
 
+=== Running a filtering rule on a mailbox
+
+....
+curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes/{mailboxName}/messages?action=triage \
+-d '{
+  "id": "1",
+  "name": "rule 1",
+  "action": {
+    "appendIn": {
+      "mailboxIds": ["23"]
+    }
+  },
+  "conditionGroup": {
+    "conditionCombiner": "OR",
+    "conditions": [
+      {
+        "comparator": "contains",
+        "field": "subject",
+        "value": "plop"
+      },
+      {
+        "comparator": "exactly-equals",
+        "field": "from",
+        "value": "bob@example.com"
+      }
+    ]
+  }
+}'
+....
+
+Will schedule a task for running a filtering rule passed as payload in ``mailboxName`` mailbox of ``usernameToBeUsed``.
+
+link:#_endpoints_returning_a_task[More details about endpoints returning
+a task].
+
+Resource name `usernameToBeUsed` should be an existing user.
+
+Resource name `mailboxName` should not be empty, nor contain `% *` characters, nor starting with `#`.
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned.
+* 400: Invalid mailbox name
+* 400: Invalid JSON payload
+* 404: Invalid get on user mailboxes. The `username` or `mailboxName` does not exit
+
+The scheduled task will have the following type `RunRulesOnMailboxTask` and
+the following `additionalInformation`:
+
+....
+{
+    "mailboxName": "mbx1",
+    "rulesOnMessagesApplySuccessfully": 9,
+    "rulesOnMessagesApplyFailed": 3,
+    "timestamp": "2024-12-03T10:15:30Z",
+    "type": "RunRulesOnMailboxTask",
+    "username": "bob@domain.tld"
+}
+....
+
 === Subscribing a user to all of its mailboxes
 
 ....
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index b87c3bb03b..3b2cff8de5 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -1744,6 +1744,65 @@ the following `additionalInformation`:
 }
 ```
 
+### Running a filtering rule on a mailbox
+
+```
+curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes/{mailboxName}/messages?action=triage \
+-d '{
+    "id": "1",
+    "name": "rule 1",
+    "action": {
+        "appendIn": {
+            "mailboxIds": ["23"]
+        }
+    },
+    "conditionGroup": {
+        "conditionCombiner": "OR",
+        "conditions": [
+            {
+                "comparator": "contains",
+                "field": "subject",
+                "value": "plop"
+            },
+            {
+                "comparator": "exactly-equals",
+                "field": "from",
+                "value": "bob@example.com"
+            }
+        ]
+    }
+}'
+```
+
+Will schedule a task for running a filtering rule passed as payload in ``mailboxName`` mailbox of ``usernameToBeUsed``.
+
+[More details about endpoints returning a task](#Endpoints_returning_a_task).
+
+Resource name `usernameToBeUsed` should be an existing user.
+
+Resource name `mailboxName` should not be empty, nor contain `% *` characters, nor starting with `#`.
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned.
+* 400: Invalid mailbox name
+* 400: Invalid JSON payload
+* 404: Invalid get on user mailboxes. The `username` or `mailboxName` does not exit
+
+The scheduled task will have the following type `RunRulesOnMailboxTask` and
+the following `additionalInformation`:
+
+```
+{
+    "mailboxName": "mbx1",
+    "rulesOnMessagesApplySuccessfully": 9,
+    "rulesOnMessagesApplyFailed": 3,
+    "timestamp": "2024-12-03T10:15:30Z",
+    "type": "RunRulesOnMailboxTask",
+    "username": "bob@domain.tld"
+}
+```
+
 ### Subscribing a user to all of its mailboxes
  
 ```


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


(james-project) 01/03: JAMES-4148 Refactor HeaderExtractor to be able to extract headers on MessageResult and Mail

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit f6bbb1ae2f65e7675955ecedc64b9eab0fbfa164
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Fri Sep 19 16:54:10 2025 +0700

    JAMES-4148 Refactor HeaderExtractor to be able to extract headers on MessageResult and Mail
---
 .../james/jmap/mailet/filter/FilteringHeaders.java | 92 ++++++++++++++++++++++
 .../james/jmap/mailet/filter/HeaderExtractor.java  | 23 +++---
 .../james/jmap/mailet/filter/MailMatcher.java      |  7 +-
 .../james/jmap/mailet/filter/RuleMatcher.java      | 13 ++-
 4 files changed, 118 insertions(+), 17 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/FilteringHeaders.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/FilteringHeaders.java
new file mode 100644
index 0000000000..264847e74b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/FilteringHeaders.java
@@ -0,0 +1,92 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.mailet.filter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import jakarta.mail.MessagingException;
+
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.Header;
+import org.apache.james.mailbox.model.Headers;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.mailet.Mail;
+
+public interface FilteringHeaders {
+    class MailFilteringHeaders implements FilteringHeaders {
+        private final Mail mail;
+
+        public MailFilteringHeaders(Mail mail) {
+            this.mail = mail;
+        }
+
+        @Override
+        public String[] getHeader(String name) throws MessagingException {
+            return mail.getMessage().getHeader(name);
+        }
+
+        @Override
+        public String getSubject() throws MessagingException {
+            return mail.getMessage().getSubject();
+        }
+    }
+
+    class MessageResultFilteringHeaders implements FilteringHeaders {
+        private final Headers headers;
+
+        public MessageResultFilteringHeaders(MessageResult messageResult) throws MailboxException {
+            this.headers = messageResult.getHeaders();
+        }
+
+        @Override
+        public String[] getHeader(String name) throws MailboxException {
+            return getMatchingHeaders(name);
+        }
+
+        @Override
+        public String getSubject() throws MailboxException {
+            return Arrays.stream(getMatchingHeaders("Subject"))
+                .findFirst()
+                .orElse(null);
+        }
+
+        private String[] getMatchingHeaders(String name) throws MailboxException {
+            final List<String> results = new ArrayList<>();
+            if (name != null) {
+                Iterator<Header> iterator = headers.headers();
+                while (iterator.hasNext()) {
+                    Header header = iterator.next();
+                    final String headerName = header.getName();
+                    if (name.equalsIgnoreCase(headerName)) {
+                        results.add(header.getValue());
+                    }
+                }
+            }
+            return results.toArray(new String[0]);
+        }
+    }
+
+    String[] getHeader(String name) throws Exception;
+
+    String getSubject() throws Exception;
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/HeaderExtractor.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/HeaderExtractor.java
index 72430ad960..c5a57ffa11 100644
--- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/HeaderExtractor.java
+++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/HeaderExtractor.java
@@ -32,7 +32,6 @@ import org.apache.james.javax.AddressHelper;
 import org.apache.james.jmap.api.filtering.Rule;
 import org.apache.james.mime4j.util.MimeUtil;
 import org.apache.james.util.StreamUtils;
-import org.apache.mailet.Mail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,15 +40,15 @@ import com.github.fge.lambdas.functions.ThrowingFunction;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 
-public interface HeaderExtractor extends ThrowingFunction<Mail, Stream<String>> {
+public interface HeaderExtractor extends ThrowingFunction<FilteringHeaders, Stream<String>> {
     Logger LOGGER = LoggerFactory.getLogger(HeaderExtractor.class);
 
-    HeaderExtractor SUBJECT_EXTRACTOR = mail ->
-        StreamUtils.ofNullables(mail.getMessage().getSubject());
+    HeaderExtractor SUBJECT_EXTRACTOR = filteringHeaders ->
+        StreamUtils.ofNullables(filteringHeaders.getSubject());
     HeaderExtractor CC_EXTRACTOR = recipientExtractor(Message.RecipientType.CC);
     HeaderExtractor TO_EXTRACTOR = recipientExtractor(Message.RecipientType.TO);
     HeaderExtractor RECIPIENT_EXTRACTOR = and(TO_EXTRACTOR, CC_EXTRACTOR);
-    HeaderExtractor FROM_EXTRACTOR = addressExtractor(mail -> mail.getMessage().getHeader(FROM), FROM);
+    HeaderExtractor FROM_EXTRACTOR = addressExtractor(filteringHeaders -> filteringHeaders.getHeader(FROM), FROM);
 
     Map<Rule.Condition.Field, HeaderExtractor> HEADER_EXTRACTOR_REGISTRY = ImmutableMap.<Rule.Condition.Field, HeaderExtractor>builder()
         .put(Rule.Condition.FixedField.SUBJECT, SUBJECT_EXTRACTOR)
@@ -62,23 +61,23 @@ public interface HeaderExtractor extends ThrowingFunction<Mail, Stream<String>>
     boolean STRICT_PARSING = true;
 
     static HeaderExtractor and(HeaderExtractor headerExtractor1, HeaderExtractor headerExtractor2) {
-        return (Mail mail) -> StreamUtils.flatten(headerExtractor1.apply(mail), headerExtractor2.apply(mail));
+        return (FilteringHeaders filteringHeaders) -> StreamUtils.flatten(headerExtractor1.apply(filteringHeaders), headerExtractor2.apply(filteringHeaders));
     }
 
     static HeaderExtractor recipientExtractor(Message.RecipientType type) {
         String headerName = type.toString();
-        ThrowingFunction<Mail, String[]> addressGetter = mail -> mail.getMessage().getHeader(headerName);
+        ThrowingFunction<FilteringHeaders, String[]> addressGetter = filteringHeaders -> filteringHeaders.getHeader(headerName);
 
         return addressExtractor(addressGetter, headerName);
     }
 
-    static HeaderExtractor addressExtractor(ThrowingFunction<Mail, String[]> addressGetter, String fallbackHeaderName) {
-        return mail -> {
+    static HeaderExtractor addressExtractor(ThrowingFunction<FilteringHeaders, String[]> addressGetter, String fallbackHeaderName) {
+        return filteringHeaders -> {
             try {
-                return toAddressContents(addressGetter.apply(mail));
+                return toAddressContents(addressGetter.apply(filteringHeaders));
             } catch (Exception e) {
                 LOGGER.info("Failed parsing header. Falling back to unparsed header value matching", e);
-                return Stream.of(mail.getMessage().getHeader(fallbackHeaderName))
+                return Stream.of(filteringHeaders.getHeader(fallbackHeaderName))
                     .map(MimeUtil::unscrambleHeaderValue);
             }
         };
@@ -96,7 +95,7 @@ public interface HeaderExtractor extends ThrowingFunction<Mail, Stream<String>>
                 Preconditions.checkArgument(field instanceof Rule.Condition.CustomHeaderField);
                 Rule.Condition.CustomHeaderField customHeaderField = (Rule.Condition.CustomHeaderField) field;
 
-                return Optional.of(mail -> StreamUtils.ofNullables(mail.getMessage().getHeader(customHeaderField.headerName())));
+                return Optional.of(filteringHeaders -> StreamUtils.ofNullables(filteringHeaders.getHeader(customHeaderField.headerName())));
             });
     }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
index f583561cad..dc433411e8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
+++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/MailMatcher.java
@@ -23,7 +23,6 @@ import java.util.List;
 import java.util.stream.Stream;
 
 import org.apache.james.jmap.api.filtering.Rule;
-import org.apache.mailet.Mail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,10 +45,10 @@ public interface MailMatcher {
         }
 
         @Override
-        public boolean match(Mail mail) {
+        public boolean match(FilteringHeaders filteringHeaders) {
             try {
                 Predicate<MailMatchingCondition> predicate = (MailMatchingCondition mailMatchingCondition) -> {
-                    Stream<String> headerLines = mailMatchingCondition.getHeaderExtractor().apply(mail);
+                    Stream<String> headerLines = mailMatchingCondition.getHeaderExtractor().apply(filteringHeaders);
                     return mailMatchingCondition.getContentMatcher().match(headerLines, mailMatchingCondition.getRuleValue());
                 };
 
@@ -107,5 +106,5 @@ public interface MailMatcher {
             ).collect(ImmutableList.toImmutableList()), rule.getConditionGroup().getConditionCombiner());
     }
 
-    boolean match(Mail mail);
+    boolean match(FilteringHeaders filteringHeaders);
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
index 07f0371182..4e59fb4611 100644
--- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
+++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
@@ -23,6 +23,8 @@ import java.util.List;
 import java.util.stream.Stream;
 
 import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MessageResult;
 import org.apache.mailet.Mail;
 
 import com.google.common.base.Preconditions;
@@ -37,7 +39,16 @@ class RuleMatcher {
     }
 
     Stream<Rule> findApplicableRules(Mail mail) {
+        FilteringHeaders filteringHeaders = new FilteringHeaders.MailFilteringHeaders(mail);
+
+        return filteringRules.stream()
+            .filter(rule -> MailMatcher.from(rule).match(filteringHeaders));
+    }
+
+    Stream<Rule> findApplicableRules(MessageResult messageResult) throws MailboxException {
+        FilteringHeaders filteringHeaders = new FilteringHeaders.MessageResultFilteringHeaders(messageResult);
+
         return filteringRules.stream()
-            .filter(rule -> MailMatcher.from(rule).match(mail));
+            .filter(rule -> MailMatcher.from(rule).match(filteringHeaders));
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


(james-project) 02/03: JAMES-4148 Webadmin route to run a filtering rule on a mailbox of a user

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit ae35ab4d6e3ce4fce48f636b5e14248d0209d370
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Mon Sep 22 16:49:43 2025 +0700

    JAMES-4148 Webadmin route to run a filtering rule on a mailbox of a user
---
 .../server/JmapTaskSerializationModule.java        |  19 +
 .../james/modules/server/JmapTasksModule.java      |   4 +
 .../james/jmap/mailet/filter/RuleMatcher.java      |   6 +-
 ...dminServerTaskSerializationIntegrationTest.java |  59 +++
 server/protocols/webadmin/webadmin-jmap/pom.xml    |   5 +-
 .../data/jmap/RunRulesOnMailboxRoutes.java         | 156 +++++++
 .../data/jmap/RunRulesOnMailboxService.java        | 126 ++++++
 .../webadmin/data/jmap/RunRulesOnMailboxTask.java  | 207 +++++++++
 ...RulesOnMailboxTaskAdditionalInformationDTO.java | 106 +++++
 .../data/jmap/RunRulesOnMailboxTaskDTO.java        |  84 ++++
 .../data/jmap/RunRulesOnMailboxRoutesTest.java     | 480 +++++++++++++++++++++
 ...OnMailboxTaskAdditionalInformationDTOTest.java} |  45 +-
 .../RunRulesOnMailboxTaskSerializationTest.java    |  65 +++
 .../runRulesOnMailbox.additionalInformation.json   |   8 +
 .../resources/json/runRulesOnMailbox.task.json     |  35 ++
 .../webadmin/service/UserMailboxesService.java     |   1 -
 .../UserMailboxesRoutesNoIndexationTest.java       |   3 +-
 .../routes/UserRoutesWithMailboxParamTest.java     |   3 +-
 18 files changed, 1377 insertions(+), 35 deletions(-)

diff --git a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java
index c7233e1c28..c1c648cc71 100644
--- a/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java
+++ b/server/container/guice/protocols/webadmin-jmap/src/main/java/org/apache/james/modules/server/JmapTaskSerializationModule.java
@@ -38,6 +38,9 @@ import org.apache.james.webadmin.data.jmap.RecomputeAllFastViewProjectionItemsTa
 import org.apache.james.webadmin.data.jmap.RecomputeAllFastViewTaskAdditionalInformationDTO;
 import org.apache.james.webadmin.data.jmap.RecomputeUserFastViewProjectionItemsTask;
 import org.apache.james.webadmin.data.jmap.RecomputeUserFastViewTaskAdditionalInformationDTO;
+import org.apache.james.webadmin.data.jmap.RunRulesOnMailboxService;
+import org.apache.james.webadmin.data.jmap.RunRulesOnMailboxTaskAdditionalInformationDTO;
+import org.apache.james.webadmin.data.jmap.RunRulesOnMailboxTaskDTO;
 import org.apache.james.webadmin.dto.DTOModuleInjections;
 
 import com.google.inject.AbstractModule;
@@ -76,6 +79,11 @@ public class JmapTaskSerializationModule extends AbstractModule {
         return RecomputeUserFastViewProjectionItemsTask.module(corrector);
     }
 
+    @ProvidesIntoSet
+    public TaskDTOModule<? extends Task, ? extends TaskDTO> runRulesOnMailboxTask(RunRulesOnMailboxService runRulesOnMailboxService) {
+        return RunRulesOnMailboxTaskDTO.module(runRulesOnMailboxService);
+    }
+
     @ProvidesIntoSet
     public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> recomputeAllJmapPreviewsAdditionalInformation() {
         return RecomputeAllFastViewTaskAdditionalInformationDTO.module();
@@ -119,4 +127,15 @@ public class JmapTaskSerializationModule extends AbstractModule {
     public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> webAdminRecomputeUserJmapPreviewsAdditionalInformation() {
         return RecomputeUserFastViewTaskAdditionalInformationDTO.module();
     }
+
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> runRulesOnMailboxAdditionalInformation() {
+        return RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE;
+    }
+
+    @Named(DTOModuleInjections.WEBADMIN_DTO)
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends  AdditionalInformationDTO> webAdminRunRulesOnMailboxAdditionalInformation() {
+        return RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE;
+    }
 }
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 c035925e66..08a91c9e09 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
@@ -19,10 +19,12 @@
 
 package org.apache.james.modules.server;
 
+import org.apache.james.webadmin.Routes;
 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.RunRulesOnMailboxRoutes;
 import org.apache.james.webadmin.routes.MailboxesRoutes;
 import org.apache.james.webadmin.routes.UserMailboxesRoutes;
 import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
@@ -48,5 +50,7 @@ public class JmapTasksModule extends AbstractModule {
         Multibinder.newSetBinder(binder(), TaskFromRequestRegistry.TaskRegistration.class, Names.named(UserMailboxesRoutes.USER_MAILBOXES_OPERATIONS_INJECTION_KEY))
             .addBinding().to(RecomputeUserFastViewProjectionItemsRequestToTask.class);
 
+        Multibinder<Routes> routesMultiBinder = Multibinder.newSetBinder(binder(), Routes.class);
+        routesMultiBinder.addBinding().to(RunRulesOnMailboxRoutes.class);
     }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
index 4e59fb4611..c98ec1de8c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
+++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
@@ -29,10 +29,10 @@ import org.apache.mailet.Mail;
 
 import com.google.common.base.Preconditions;
 
-class RuleMatcher {
+public class RuleMatcher {
     private final List<Rule> filteringRules;
 
-    RuleMatcher(List<Rule> filteringRules) {
+    public RuleMatcher(List<Rule> filteringRules) {
         Preconditions.checkNotNull(filteringRules);
 
         this.filteringRules = filteringRules;
@@ -45,7 +45,7 @@ class RuleMatcher {
             .filter(rule -> MailMatcher.from(rule).match(filteringHeaders));
     }
 
-    Stream<Rule> findApplicableRules(MessageResult messageResult) throws MailboxException {
+    public Stream<Rule> findApplicableRules(MessageResult messageResult) throws MailboxException {
         FilteringHeaders filteringHeaders = new FilteringHeaders.MessageResultFilteringHeaders(messageResult);
 
         return filteringRules.stream()
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 6378713a4e..cc5933b40d 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
@@ -76,6 +76,7 @@ import org.apache.james.utils.MailRepositoryProbeImpl;
 import org.apache.james.utils.WebAdminGuiceProbe;
 import org.apache.james.vault.VaultConfiguration;
 import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.data.jmap.RunRulesOnMailboxTask;
 import org.apache.james.webadmin.routes.CassandraMailboxMergingRoutes;
 import org.apache.james.webadmin.routes.MailQueueRoutes;
 import org.apache.james.webadmin.routes.MailRepositoriesRoutes;
@@ -715,6 +716,64 @@ class RabbitMQWebAdminServerTaskSerializationIntegrationTest {
             .body("additionalInformation.mailboxName", is(MailboxConstants.INBOX));
     }
 
+    @Test
+    void runRulesOnMailboxShouldComplete(GuiceJamesServer server) throws Exception {
+        server.getProbe(DataProbeImpl.class).addUser(USERNAME, "secret");
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, MailboxConstants.INBOX);
+        MailboxId otherMailboxId = mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "otherMailbox");
+
+        mailboxProbe.appendMessage(
+            USERNAME,
+            MailboxPath.inbox(Username.of(USERNAME)),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()),
+            new Date(),
+            false,
+            new Flags());
+
+        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": "test"
+                  }
+                ]
+              }
+            }""".formatted(otherMailboxId.serialize()))
+            .post("users/" + USERNAME + "/mailboxes/" + MailboxConstants.INBOX + "/messages")
+            .jsonPath()
+            .getString("taskId");
+
+        with()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("status", is(TaskManager.Status.COMPLETED.getValue()))
+            .body("taskId", is(taskId))
+            .body("type", is(RunRulesOnMailboxTask.TASK_TYPE.asString()))
+            .body("additionalInformation.rulesOnMessagesApplySuccessfully", is(1))
+            .body("additionalInformation.rulesOnMessagesApplyFailed", is(0))
+            .body("additionalInformation.username", is(USERNAME))
+            .body("additionalInformation.mailboxName", is(MailboxConstants.INBOX));
+    }
+
     @Test
     void cleanUploadRepositoryShouldComplete() throws Exception {
         String taskId = given()
diff --git a/server/protocols/webadmin/webadmin-jmap/pom.xml b/server/protocols/webadmin/webadmin-jmap/pom.xml
index 3f7e364077..6237479a2d 100644
--- a/server/protocols/webadmin/webadmin-jmap/pom.xml
+++ b/server/protocols/webadmin/webadmin-jmap/pom.xml
@@ -69,7 +69,6 @@
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-jmap-rfc-8621</artifactId>
-            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
@@ -91,6 +90,10 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-webadmin-mailbox</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>metrics-tests</artifactId>
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
new file mode 100644
index 0000000000..8d183b4597
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutes.java
@@ -0,0 +1,156 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+
+import jakarta.inject.Inject;
+
+import org.apache.james.core.Username;
+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.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.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;
+import org.slf4j.LoggerFactory;
+
+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.Mono;
+import spark.Request;
+import spark.Route;
+import spark.Service;
+
+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 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 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) {
+        this.usersRepository = usersRepository;
+        this.mailboxManager = mailboxManager;
+        this.taskManager = taskManager;
+        this.jsonTransformer = jsonTransformer;
+        this.runRulesOnMailboxService = runRulesOnMailboxService;
+        this.jsonDeserialize = new ObjectMapper()
+            .registerModule(new Jdk8Module())
+            .registerModule(new GuavaModule());
+    }
+
+    @Override
+    public String getBasePath() {
+        return USER_MAILBOXES_BASE;
+    }
+
+    @Override
+    public void define(Service service) {
+        service.post(MESSAGES_PATH, runRulesOnMailboxRoute(), jsonTransformer);
+    }
+
+    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));
+        try {
+            usernamePreconditions(username);
+            mailboxExistPreconditions(username, mailboxName);
+            RuleDTO ruleDTO = jsonDeserialize.readValue(request.body(), RuleDTO.class);
+            Rules rules = new Rules(RuleDTO.toRules(ImmutableList.of(ruleDTO)), Version.INITIAL);
+
+            return new RunRulesOnMailboxTask(username, mailboxName, rules, runRulesOnMailboxService);
+        } catch (IllegalStateException e) {
+            LOGGER.info("Invalid argument on user mailboxes", e);
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.NOT_FOUND_404)
+                .type(ErrorResponder.ErrorType.NOT_FOUND)
+                .message("Invalid argument on user mailboxes")
+                .cause(e)
+                .haltError();
+        } catch (JsonProcessingException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("JSON payload of the request is not valid")
+                .cause(e)
+                .haltError();
+        }
+    }
+
+    private Username getUsernameParam(Request request) {
+        return Username.of(request.params(USER_NAME));
+    }
+
+    private void usernamePreconditions(Username username) throws UsersRepositoryException {
+        Preconditions.checkState(usersRepository.contains(username), "User does not exist");
+    }
+
+    private void mailboxExistPreconditions(Username username, MailboxName mailboxName) throws MailboxException {
+        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);
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
new file mode 100644
index 0000000000..0a5190dd03
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
@@ -0,0 +1,126 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static org.apache.james.util.ReactorUtils.DEFAULT_CONCURRENCY;
+
+import java.util.List;
+
+import jakarta.inject.Inject;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.jmap.api.filtering.Rules;
+import org.apache.james.jmap.mailet.filter.RuleMatcher;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.FetchGroup;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.task.Task;
+import org.apache.james.webadmin.validation.MailboxName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.Throwing;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class RunRulesOnMailboxService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(RunRulesOnMailboxService.class);
+
+    private final MailboxManager mailboxManager;
+    private final MailboxId.Factory mailboxIdFactory;
+    private final MessageIdManager messageIdManager;
+
+    @Inject
+    public RunRulesOnMailboxService(MailboxManager mailboxManager, MailboxId.Factory mailboxIdFactory, MessageIdManager messageIdManager) {
+        this.mailboxManager = mailboxManager;
+        this.mailboxIdFactory = mailboxIdFactory;
+        this.messageIdManager = messageIdManager;
+    }
+
+    public Mono<Task.Result> runRulesOnMailbox(Username username, MailboxName mailboxName, Rules rules, RunRulesOnMailboxTask.Context context) {
+        MailboxSession mailboxSession = mailboxManager.createSystemSession(username);
+        RuleMatcher ruleMatcher = new RuleMatcher(rules.getRules());
+
+        return Mono.from(mailboxManager.getMailboxReactive(MailboxPath.forUser(username, mailboxName.asString()), mailboxSession))
+            .flatMapMany(messageManager -> Flux.from(messageManager.getMessagesReactive(MessageRange.all(), FetchGroup.HEADERS, mailboxSession))
+                .flatMap(Throwing.function(messageResult -> runRulesOnMessage(ruleMatcher, messageResult, mailboxSession, context)), DEFAULT_CONCURRENCY))
+            .onErrorResume(e -> {
+                LOGGER.error("Error when applying rules to mailbox. Mailbox {} for user {}", mailboxName.asString(), username, e);
+                context.incrementFails();
+                return Mono.just(Task.Result.PARTIAL);
+            })
+            .reduce(Task::combine)
+            .switchIfEmpty(Mono.just(Task.Result.COMPLETED))
+            .doFinally(any -> mailboxManager.endProcessingRequest(mailboxSession));
+    }
+
+    private Flux<Task.Result> runRulesOnMessage(RuleMatcher ruleMatcher, MessageResult messageResult, MailboxSession mailboxSession, RunRulesOnMailboxTask.Context context) throws MailboxException {
+        return Flux.fromStream(ruleMatcher.findApplicableRules(messageResult))
+            .map(Rule::getAction)
+            .concatMap(action -> applyActionOnMessage(messageResult, action, mailboxSession, context));
+    }
+
+    private Mono<Task.Result> applyActionOnMessage(MessageResult messageResult, Rule.Action action, MailboxSession mailboxSession, RunRulesOnMailboxTask.Context context) {
+        actionOnMessagePreconditions(action);
+        return appendInMailboxes(messageResult.getMessageId(), action, mailboxSession, context)
+            .onErrorResume(e -> {
+                LOGGER.error("Error when moving message to mailboxes. Message {} for user {}", messageResult.getMessageId(), mailboxSession.getUser().asString(), e);
+                context.incrementFails();
+                return Mono.just(Task.Result.PARTIAL);
+            });
+    }
+
+    private void actionOnMessagePreconditions(Rule.Action action) {
+        if (action.isMarkAsSeen() || action.isMarkAsImportant() || action.isReject()
+            || action.getForward().isPresent() || !action.getWithKeywords().isEmpty()) {
+            throw new NotImplementedException("Only action on moving messages is supported for now");
+        }
+
+        if (action.getAppendInMailboxes().getMailboxIds().isEmpty()) {
+            throw new IllegalArgumentException("Move action should not be empty");
+        }
+    }
+
+    private Mono<Task.Result> appendInMailboxes(MessageId messageId, Rule.Action action, MailboxSession mailboxSession, RunRulesOnMailboxTask.Context context) {
+        List<MailboxId> mailboxIds = action.getAppendInMailboxes()
+            .getMailboxIds()
+            .stream()
+            .map(mailboxIdFactory::fromString)
+            .toList();
+
+        if (mailboxIds.isEmpty()) {
+            return Mono.just(Task.Result.COMPLETED);
+        }
+
+        return Mono.from(messageIdManager.setInMailboxesReactive(messageId, mailboxIds, mailboxSession))
+            .doOnSuccess(next -> context.incrementSuccesses())
+            .then(Mono.just(Task.Result.COMPLETED));
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTask.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTask.java
new file mode 100644
index 0000000000..0b3f30be1d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTask.java
@@ -0,0 +1,207 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.filtering.Rules;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.task.TaskType;
+import org.apache.james.webadmin.validation.MailboxName;
+
+import com.google.common.base.MoreObjects;
+
+public class RunRulesOnMailboxTask implements Task {
+    public static class Context {
+        public static class Snapshot {
+            private final long rulesOnMessagesApplySuccessfully;
+            private final long rulesOnMessagesApplyFailed;
+
+            private Snapshot(long rulesOnMessagesApplySuccessfully, long rulesOnMessagesApplyFailed) {
+                this.rulesOnMessagesApplySuccessfully = rulesOnMessagesApplySuccessfully;
+                this.rulesOnMessagesApplyFailed = rulesOnMessagesApplyFailed;
+            }
+
+            public long getRulesOnMessagesApplySuccessfully() {
+                return rulesOnMessagesApplySuccessfully;
+            }
+
+            public long getRulesOnMessagesApplyFailed() {
+                return rulesOnMessagesApplyFailed;
+            }
+
+            @Override
+            public final boolean equals(Object o) {
+                if (o instanceof Context.Snapshot) {
+                    Context.Snapshot that = (Context.Snapshot) o;
+
+                    return Objects.equals(this.rulesOnMessagesApplySuccessfully, that.rulesOnMessagesApplySuccessfully)
+                        && Objects.equals(this.rulesOnMessagesApplyFailed, that.rulesOnMessagesApplyFailed);
+                }
+                return false;
+            }
+
+            @Override
+            public final int hashCode() {
+                return Objects.hash(rulesOnMessagesApplySuccessfully, rulesOnMessagesApplyFailed);
+            }
+
+            @Override
+            public String toString() {
+                return MoreObjects.toStringHelper(this)
+                    .add("rulesOnMessagesApplySuccessfully", rulesOnMessagesApplySuccessfully)
+                    .add("rulesOnMessagesApplyFailed", rulesOnMessagesApplyFailed)
+                    .toString();
+            }
+        }
+
+        private final AtomicLong rulesOnMessagesApplySuccessfully;
+        private final AtomicLong rulesOnMessagesApplyFailed;
+
+        public Context() {
+            this.rulesOnMessagesApplySuccessfully = new AtomicLong();
+            this.rulesOnMessagesApplyFailed = new AtomicLong();
+        }
+
+        public Context(long rulesOnMessagesApplySuccessfully, long rulesOnMessagesApplyFailed) {
+            this.rulesOnMessagesApplySuccessfully = new AtomicLong(rulesOnMessagesApplySuccessfully);
+            this.rulesOnMessagesApplyFailed = new AtomicLong(rulesOnMessagesApplyFailed);
+        }
+
+        public void incrementSuccesses() {
+            rulesOnMessagesApplySuccessfully.incrementAndGet();
+        }
+
+
+        public void incrementFails() {
+            rulesOnMessagesApplyFailed.incrementAndGet();
+        }
+
+        public Context.Snapshot snapshot() {
+            return new Context.Snapshot(rulesOnMessagesApplySuccessfully.get(), rulesOnMessagesApplyFailed.get());
+        }
+    }
+
+    public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+
+        private static AdditionalInformation from(Username username,
+                                                                        MailboxName mailboxName,
+                                                                        RunRulesOnMailboxTask.Context context) {
+            Context.Snapshot snapshot = context.snapshot();
+            return new AdditionalInformation(username, mailboxName, Clock.systemUTC().instant(), snapshot.rulesOnMessagesApplySuccessfully, snapshot.rulesOnMessagesApplyFailed);
+        }
+
+        private final Username username;
+        private final MailboxName mailboxName;
+        private final Instant timestamp;
+        private final long rulesOnMessagesApplySuccessfully;
+        private final long rulesOnMessagesApplyFailed;
+
+        public AdditionalInformation(Username username,
+                                     MailboxName mailboxName,
+                                     Instant timestamp,
+                                     long rulesOnMessagesApplySuccessfully,
+                                     long rulesOnMessagesApplyFailed) {
+            this.username = username;
+            this.mailboxName = mailboxName;
+            this.timestamp = timestamp;
+            this.rulesOnMessagesApplySuccessfully = rulesOnMessagesApplySuccessfully;
+            this.rulesOnMessagesApplyFailed = rulesOnMessagesApplyFailed;
+        }
+
+        public Username getUsername() {
+            return username;
+        }
+
+        public MailboxName getMailboxName() {
+            return mailboxName;
+        }
+
+        public Instant getTimestamp() {
+            return timestamp;
+        }
+
+        public long getRulesOnMessagesApplySuccessfully() {
+            return rulesOnMessagesApplySuccessfully;
+        }
+
+        public long getRulesOnMessagesApplyFailed() {
+            return rulesOnMessagesApplyFailed;
+        }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
+    }
+
+    public static final TaskType TASK_TYPE = TaskType.of("RunRulesOnMailboxTask");
+
+    private final Context context;
+    private final Username username;
+    private final MailboxName mailboxName;
+    private final Rules rules;
+    private final RunRulesOnMailboxService runRulesOnMailboxService;
+
+    public RunRulesOnMailboxTask(Username username,
+                                 MailboxName mailboxName,
+                                 Rules rules,
+                                 RunRulesOnMailboxService runRulesOnMailboxService) {
+        this.username = username;
+        this.mailboxName = mailboxName;
+        this.rules = rules;
+        this.runRulesOnMailboxService = runRulesOnMailboxService;
+        this.context = new Context();
+    }
+
+    @Override
+    public Result run() {
+        return runRulesOnMailboxService.runRulesOnMailbox(username, mailboxName, rules, context)
+            .block();
+    }
+
+    @Override
+    public TaskType type() {
+        return TASK_TYPE;
+    }
+
+    @Override
+    public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+        return Optional.of(AdditionalInformation.from(username, mailboxName, context));
+    }
+
+    public Username getUsername() {
+        return username;
+    }
+
+    public MailboxName getMailboxName() {
+        return mailboxName;
+    }
+
+    public Rules getRules() {
+        return rules;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTO.java
new file mode 100644
index 0000000000..a3b101f23e
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTO.java
@@ -0,0 +1,106 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import java.time.Instant;
+
+import org.apache.james.core.Username;
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
+import org.apache.james.webadmin.validation.MailboxName;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class RunRulesOnMailboxTaskAdditionalInformationDTO implements AdditionalInformationDTO {
+    public static final AdditionalInformationDTOModule<RunRulesOnMailboxTask.AdditionalInformation, RunRulesOnMailboxTaskAdditionalInformationDTO> SERIALIZATION_MODULE =
+        DTOModule.forDomainObject(RunRulesOnMailboxTask.AdditionalInformation.class)
+            .convertToDTO(RunRulesOnMailboxTaskAdditionalInformationDTO.class)
+            .toDomainObjectConverter(RunRulesOnMailboxTaskAdditionalInformationDTO::toDomainObject)
+            .toDTOConverter(RunRulesOnMailboxTaskAdditionalInformationDTO::toDto)
+            .typeName(RunRulesOnMailboxTask.TASK_TYPE.asString())
+            .withFactory(AdditionalInformationDTOModule::new);
+
+    private static RunRulesOnMailboxTask.AdditionalInformation toDomainObject(RunRulesOnMailboxTaskAdditionalInformationDTO dto) {
+        return new RunRulesOnMailboxTask.AdditionalInformation(
+            Username.of(dto.getUsername()),
+            new MailboxName(dto.getMailboxName()),
+            dto.getTimestamp(),
+            dto.getRulesOnMessagesApplySuccessfully(),
+            dto.getRulesOnMessagesApplyFailed());
+    }
+
+    private static RunRulesOnMailboxTaskAdditionalInformationDTO toDto(RunRulesOnMailboxTask.AdditionalInformation domain, String type) {
+        return new RunRulesOnMailboxTaskAdditionalInformationDTO(
+            type,
+            domain.getUsername().asString(),
+            domain.getMailboxName().asString(),
+            domain.getTimestamp(),
+            domain.getRulesOnMessagesApplySuccessfully(),
+            domain.getRulesOnMessagesApplyFailed());
+    }
+
+    private final String type;
+    private final String username;
+    private final String mailboxName;
+    private final Instant timestamp;
+    private final long rulesOnMessagesApplySuccessfully;
+    private final long rulesOnMessagesApplyFailed;
+
+    public RunRulesOnMailboxTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+                                                         @JsonProperty("username") String username,
+                                                         @JsonProperty("mailboxName") String mailboxName,
+                                                         @JsonProperty("timestamp") Instant timestamp,
+                                                         @JsonProperty("rulesOnMessagesApplySuccessfully") long rulesOnMessagesApplySuccessfully,
+                                                         @JsonProperty("rulesOnMessagesApplyFailed") long rulesOnMessagesApplyFailed) {
+        this.type = type;
+        this.username = username;
+        this.mailboxName = mailboxName;
+        this.timestamp = timestamp;
+        this.rulesOnMessagesApplySuccessfully = rulesOnMessagesApplySuccessfully;
+        this.rulesOnMessagesApplyFailed = rulesOnMessagesApplyFailed;
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getMailboxName() {
+        return mailboxName;
+    }
+
+    public long getRulesOnMessagesApplySuccessfully() {
+        return rulesOnMessagesApplySuccessfully;
+    }
+
+    public long getRulesOnMessagesApplyFailed() {
+        return rulesOnMessagesApplyFailed;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskDTO.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskDTO.java
new file mode 100644
index 0000000000..266401be7a
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskDTO.java
@@ -0,0 +1,84 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import org.apache.james.core.Username;
+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.json.DTOModule;
+import org.apache.james.server.task.json.dto.TaskDTO;
+import org.apache.james.server.task.json.dto.TaskDTOModule;
+import org.apache.james.webadmin.validation.MailboxName;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+
+public class RunRulesOnMailboxTaskDTO implements TaskDTO {
+    private final String type;
+    private final String username;
+    private final String mailboxName;
+    private final ImmutableList<RuleDTO> rules;
+
+    public RunRulesOnMailboxTaskDTO(@JsonProperty("type") String type,
+                                    @JsonProperty("username") String username,
+                                    @JsonProperty("mailboxName") String mailboxName,
+                                    @JsonProperty("rules") ImmutableList<RuleDTO> rules) {
+        this.type = type;
+        this.username = username;
+        this.mailboxName = mailboxName;
+        this.rules = rules;
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getMailboxName() {
+        return mailboxName;
+    }
+
+    public ImmutableList<RuleDTO> getRules() {
+        return rules;
+    }
+
+    public static TaskDTOModule<RunRulesOnMailboxTask, RunRulesOnMailboxTaskDTO> module(RunRulesOnMailboxService runRulesOnMailboxService) {
+        return DTOModule
+            .forDomainObject(RunRulesOnMailboxTask.class)
+            .convertToDTO(RunRulesOnMailboxTaskDTO.class)
+            .toDomainObjectConverter(dto -> dto.fromDTO(runRulesOnMailboxService))
+            .toDTOConverter(RunRulesOnMailboxTaskDTO::toDTO)
+            .typeName(RunRulesOnMailboxTask.TASK_TYPE.asString())
+            .withFactory(TaskDTOModule::new);
+    }
+
+    public RunRulesOnMailboxTask fromDTO(RunRulesOnMailboxService runRulesOnMailboxService) {
+        return new RunRulesOnMailboxTask(Username.of(username), new MailboxName(mailboxName), new Rules(RuleDTO.toRules(rules), Version.INITIAL), runRulesOnMailboxService);
+    }
+
+    public static RunRulesOnMailboxTaskDTO toDTO(RunRulesOnMailboxTask domainObject, String typeName) {
+        return new RunRulesOnMailboxTaskDTO(typeName, domainObject.getUsername().asString(), domainObject.getMailboxName().asString(), RuleDTO.from(domainObject.getRules().getRules()));
+    }
+}
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
new file mode 100644
index 0000000000..43cd0cc357
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java
@@ -0,0 +1,480 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.http.ContentType.JSON;
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static org.apache.james.webadmin.routes.UserMailboxesRoutes.USERS_BASE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400;
+import static org.eclipse.jetty.http.HttpStatus.CREATED_201;
+import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.apache.james.core.Username;
+import org.apache.james.json.DTOConverter;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.inmemory.InMemoryId;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.routes.TasksRoutes;
+import org.apache.james.webadmin.routes.UserMailboxesRoutes;
+import org.apache.james.webadmin.utils.JsonTransformer;
+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.Test;
+
+import com.github.fge.lambdas.Throwing;
+
+import io.restassured.RestAssured;
+
+public class RunRulesOnMailboxRoutesTest {
+    private static final Username USERNAME = Username.of("username");
+    private static final String MAILBOX_NAME = "myMailboxName";
+    private static final String OTHER_MAILBOX_NAME = "myOtherMailboxName";
+    private static final String ERROR_TYPE_NOTFOUND = "notFound";
+    private static final String ERROR_TYPE_INVALIDARGUMENT = "InvalidArgument";
+    private static final String RULE_PAYLOAD = """
+        {
+          "id": "1",
+          "name": "rule 1",
+          "action": {
+            "appendIn": {
+              "mailboxIds": ["%s"]
+            },
+            "important": false,
+            "keyworkds": [],
+            "reject": false,
+            "seen": false
+          },
+          "conditionGroup": {
+            "conditionCombiner": "OR",
+            "conditions": [
+              {
+                "comparator": "contains",
+                "field": "subject",
+                "value": "plop"
+              },
+              {
+                "comparator": "exactly-equals",
+                "field": "from",
+                "value": "bob@example.com"
+              }
+            ]
+          }
+        }""";
+
+    private WebAdminServer webAdminServer;
+    private UsersRepository usersRepository;
+    private MemoryTaskManager taskManager;
+    private InMemoryMailboxManager mailboxManager;
+    MessageIdManager messageIdManager;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        InMemoryIntegrationResources resources = InMemoryIntegrationResources.builder()
+            .preProvisionnedFakeAuthenticator()
+            .fakeAuthorizator()
+            .inVmEventBus()
+            .defaultAnnotationLimits()
+            .defaultMessageParser()
+            .scanningSearchIndex()
+            .noPreDeletionHooks()
+            .storeQuotaManager()
+            .build();
+
+        mailboxManager = resources.getMailboxManager();
+        messageIdManager = resources.getMessageIdManager();
+
+        usersRepository = mock(UsersRepository.class);
+        when(usersRepository.contains(USERNAME)).thenReturn(true);
+
+        taskManager = new MemoryTaskManager(new Hostname("foo"));
+
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+                new RunRulesOnMailboxRoutes(usersRepository, mailboxManager, taskManager, new JsonTransformer(),
+                    new RunRulesOnMailboxService(mailboxManager, new InMemoryId.Factory(), messageIdManager)),
+                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) // no further automatically encoding by Rest Assured client. rf: https://issues.apache.org/jira/projects/JAMES/issues/JAMES-3936
+            .build();
+    }
+
+    @AfterEach
+    void tearDown() {
+        webAdminServer.destroy();
+        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("""
+                    {
+                      "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("alice@example.com")
+                        .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 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("alice@example.com")
+                        .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("alice@example.com")
+                    .setBody("body", StandardCharsets.UTF_8)),
+            systemSession);
+
+        messageManager.appendMessage(
+            MessageManager.AppendCommand.builder()
+                .build(Message.Builder.of()
+                    .setSubject("hello")
+                    .setFrom("alice@example.com")
+                    .setBody("body", StandardCharsets.UTF_8)),
+            systemSession);
+
+        messageManager.appendMessage(
+            MessageManager.AppendCommand.builder()
+                .build(Message.Builder.of()
+                    .setSubject("hello")
+                    .setFrom("bob@example.com")
+                    .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);
+            }
+        );
+    }
+
+    @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("alice@example.com")
+                    .setBody("body", StandardCharsets.UTF_8)),
+            systemSession);
+
+        messageManager.appendMessage(
+            MessageManager.AppendCommand.builder()
+                .build(Message.Builder.of()
+                    .setSubject("hello")
+                    .setFrom("alice@example.com")
+                    .setBody("body", StandardCharsets.UTF_8)),
+            systemSession);
+
+        messageManager.appendMessage(
+            MessageManager.AppendCommand.builder()
+                .build(Message.Builder.of()
+                    .setSubject("hello")
+                    .setFrom("bob@example.com")
+                    .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));
+    }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
similarity index 51%
copy from server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
copy to server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
index 4e59fb4611..0ce8572d5a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/mailet/filter/RuleMatcher.java
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
@@ -17,38 +17,27 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.mailet.filter;
+package org.apache.james.webadmin.data.jmap;
 
-import java.util.List;
-import java.util.stream.Stream;
+import java.time.Instant;
 
-import org.apache.james.jmap.api.filtering.Rule;
-import org.apache.james.mailbox.exception.MailboxException;
-import org.apache.james.mailbox.model.MessageResult;
-import org.apache.mailet.Mail;
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.core.Username;
+import org.apache.james.util.ClassLoaderUtils;
+import org.apache.james.webadmin.validation.MailboxName;
+import org.junit.jupiter.api.Test;
 
-import com.google.common.base.Preconditions;
+public class RunRulesOnMailboxTaskAdditionalInformationDTOTest {
+    private static final Instant INSTANT = Instant.parse("2007-12-03T10:15:30.00Z");
 
-class RuleMatcher {
-    private final List<Rule> filteringRules;
+    private static final RunRulesOnMailboxTask.AdditionalInformation DOMAIN_OBJECT = new RunRulesOnMailboxTask.AdditionalInformation(
+        Username.of("bob@domain.tld"), new MailboxName("mbx1"), INSTANT, 10, 9);
 
-    RuleMatcher(List<Rule> filteringRules) {
-        Preconditions.checkNotNull(filteringRules);
-
-        this.filteringRules = filteringRules;
-    }
-
-    Stream<Rule> findApplicableRules(Mail mail) {
-        FilteringHeaders filteringHeaders = new FilteringHeaders.MailFilteringHeaders(mail);
-
-        return filteringRules.stream()
-            .filter(rule -> MailMatcher.from(rule).match(filteringHeaders));
-    }
-
-    Stream<Rule> findApplicableRules(MessageResult messageResult) throws MailboxException {
-        FilteringHeaders filteringHeaders = new FilteringHeaders.MessageResultFilteringHeaders(messageResult);
-
-        return filteringRules.stream()
-            .filter(rule -> MailMatcher.from(rule).match(filteringHeaders));
+    @Test
+    void shouldMatchJsonSerializationContract() throws Exception {
+        JsonSerializationVerifier.dtoModule(RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE)
+            .bean(DOMAIN_OBJECT)
+            .json(ClassLoaderUtils.getSystemResourceAsString("json/runRulesOnMailbox.additionalInformation.json"))
+            .verify();
     }
 }
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskSerializationTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskSerializationTest.java
new file mode 100644
index 0000000000..b95fc5f026
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskSerializationTest.java
@@ -0,0 +1,65 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.webadmin.data.jmap;
+
+import static org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.CONTAINS;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.Comparator.NOT_EXACTLY_EQUALS;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.FixedField.FROM;
+import static org.apache.james.jmap.api.filtering.Rule.Condition.FixedField.SUBJECT;
+import static org.mockito.Mockito.mock;
+
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.filtering.Rule;
+import org.apache.james.jmap.api.filtering.Rules;
+import org.apache.james.jmap.api.filtering.Version;
+import org.apache.james.util.ClassLoaderUtils;
+import org.apache.james.webadmin.validation.MailboxName;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class RunRulesOnMailboxTaskSerializationTest {
+    private RunRulesOnMailboxService runRulesOnMailboxService;
+    private static final Username USERNAME = Username.of("bob@domain.tld");
+    private static final MailboxName MAILBOX_NAME = new MailboxName("mbx1");
+    private static final Rule RULE = Rule.builder()
+        .id(Rule.Id.of("1"))
+        .name("rule 1")
+            .conditionGroup(Rule.ConditionGroup.of(Rule.ConditionCombiner.AND, Rule.Condition.of(SUBJECT, CONTAINS, "plop"),
+                Rule.Condition.of(FROM, NOT_EXACTLY_EQUALS, "bob@example.com")))
+        .action(Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds(ImmutableList.of("mbx2"))))
+        .build();
+    private static final Rules RULES = new Rules(ImmutableList.of(RULE), Version.INITIAL);
+
+    @BeforeEach
+    void setUp() {
+        runRulesOnMailboxService = mock(RunRulesOnMailboxService.class);
+    }
+
+    @Test
+    void shouldMatchJsonSerializationContract() throws Exception {
+        JsonSerializationVerifier.dtoModule(RunRulesOnMailboxTaskDTO.module(runRulesOnMailboxService))
+            .bean(new RunRulesOnMailboxTask(USERNAME, MAILBOX_NAME, RULES, runRulesOnMailboxService))
+            .json(ClassLoaderUtils.getSystemResourceAsString("json/runRulesOnMailbox.task.json"))
+            .verify();
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.additionalInformation.json b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.additionalInformation.json
new file mode 100644
index 0000000000..a3f8bd738b
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.additionalInformation.json
@@ -0,0 +1,8 @@
+{
+  "mailboxName": "mbx1",
+  "rulesOnMessagesApplyFailed": 9,
+  "rulesOnMessagesApplySuccessfully": 10,
+  "timestamp": "2007-12-03T10:15:30Z",
+  "type": "RunRulesOnMailboxTask",
+  "username": "bob@domain.tld"
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.task.json b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.task.json
new file mode 100644
index 0000000000..63445e07e9
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/resources/json/runRulesOnMailbox.task.json
@@ -0,0 +1,35 @@
+{
+  "mailboxName": "mbx1",
+  "type": "RunRulesOnMailboxTask",
+  "username": "bob@domain.tld",
+  "rules": [
+    {
+      "action": {
+        "appendIn": {
+          "mailboxIds": ["mbx2"]
+        },
+        "important": false,
+        "keyworkds": [],
+        "reject": false,
+        "seen": false
+      },
+      "conditionGroup": {
+        "conditionCombiner": "AND",
+        "conditions": [
+          {
+            "comparator": "contains",
+            "field": "subject",
+            "value": "plop"
+          },
+          {
+            "comparator": "not-exactly-equals",
+            "field": "from",
+            "value": "bob@example.com"
+          }
+        ]
+      },
+      "id": "1",
+      "name": "rule 1"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserMailboxesService.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserMailboxesService.java
index 02e75ac050..e84e22b8cb 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserMailboxesService.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserMailboxesService.java
@@ -212,5 +212,4 @@ public class UserMailboxesService {
             Minimal, mailboxSession)
             .toStream();
     }
-
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesNoIndexationTest.java b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesNoIndexationTest.java
index 71cf5647a2..4480262376 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesNoIndexationTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesNoIndexationTest.java
@@ -56,7 +56,8 @@ class UserMailboxesRoutesNoIndexationTest {
 
     @BeforeEach
     void setUp() throws Exception {
-        InMemoryMailboxManager mailboxManager = InMemoryIntegrationResources.defaultResources().getMailboxManager();
+        InMemoryIntegrationResources memoryResources = InMemoryIntegrationResources.defaultResources();
+        InMemoryMailboxManager mailboxManager = memoryResources.getMailboxManager();
         UsersRepository usersRepository = mock(UsersRepository.class);
         when(usersRepository.contains(USERNAME)).thenReturn(true);
 
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserRoutesWithMailboxParamTest.java b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserRoutesWithMailboxParamTest.java
index 5c5ba294b0..bf6c1d3e4d 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserRoutesWithMailboxParamTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserRoutesWithMailboxParamTest.java
@@ -78,7 +78,8 @@ public class UserRoutesWithMailboxParamTest {
         SimpleDomainList domainList = new SimpleDomainList();
         domainList.addDomain(DOMAIN);
         usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
-        mailboxManager = InMemoryIntegrationResources.defaultResources().getMailboxManager();
+        InMemoryIntegrationResources memoryResources = InMemoryIntegrationResources.defaultResources();
+        mailboxManager = memoryResources.getMailboxManager();
         UserMailboxesService userMailboxService = new UserMailboxesService(mailboxManager, usersRepository);
         MemoryRecipientRewriteTable recipientRewriteTable = new MemoryRecipientRewriteTable();
         recipientRewriteTable.setDomainList(domainList);


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org