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

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

commit 58013510fa8d6573b46deb2a8d740c94d8f9cbe1
Author: Trần Hồng Quân <[email protected]>
AuthorDate: Tue Oct 14 14:19:10 2025 +0700

    JAMES-4148 Apply maximum number of actions taken per mailbox (#2836)
    
    Avoid moving too many messages at once which could cause tombstone issue on 
Cassandra.
---
 .../modules/servers/partials/operate/webadmin.adoc |   5 +
 .../data/jmap/RunRulesOnMailboxService.java        |  38 ++++-
 .../webadmin/data/jmap/RunRulesOnMailboxTask.java  |  60 ++++++--
 ...RulesOnMailboxTaskAdditionalInformationDTO.java |  24 +++-
 .../data/jmap/RunRulesOnMailboxRoutesTest.java     | 158 ++++++++++++++++++++-
 ...sOnMailboxTaskAdditionalInformationDTOTest.java |   2 +-
 .../runRulesOnMailbox.additionalInformation.json   |   2 +
 7 files changed, 273 insertions(+), 16 deletions(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc 
b/docs/modules/servers/partials/operate/webadmin.adoc
index 36e5d7d985..ccf1fdf780 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -1864,14 +1864,19 @@ the following `additionalInformation`:
 ....
 {
     "mailboxName": "mbx1",
+    "processedMessagesCount": 15,
     "rulesOnMessagesApplySuccessfully": 9,
     "rulesOnMessagesApplyFailed": 3,
+    "maximumAppliedActionExceeded": false,
     "timestamp": "2024-12-03T10:15:30Z",
     "type": "RunRulesOnMailboxTask",
     "username": "[email protected]"
 }
 ....
 
+Note: you can configure the maximum number of actions applied per mailbox by 
setting the JVM property `james.rules.triage.max.actions.per.mailbox`. The 
default value is 25000.
+This limit may help avoid the tombstone threshold issue on Cassandra backends 
when too many messages are moved or deleted at once.
+
 === Subscribing a user to all of its mailboxes
 
 ....
diff --git 
a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
index d80664a1ef..881dc3fefb 100644
--- 
a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxService.java
@@ -22,6 +22,8 @@ package org.apache.james.webadmin.data.jmap;
 import static org.apache.james.util.ReactorUtils.DEFAULT_CONCURRENCY;
 
 import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
 
 import jakarta.inject.Inject;
 
@@ -49,23 +51,39 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
+import reactor.core.publisher.SynchronousSink;
 
 public class RunRulesOnMailboxService {
+    public static class TooManyAppliedActionsException extends 
RuntimeException {
+    }
+
     private static final Logger LOGGER = 
LoggerFactory.getLogger(RunRulesOnMailboxService.class);
+    private static final int MAX_ACTIONS_PER_MAILBOX = 
Optional.ofNullable(System.getProperty("james.rules.triage.max.actions.per.mailbox"))
+        .map(Integer::valueOf)
+        .orElse(25000);
 
     private final MailboxManager mailboxManager;
     private final MailboxId.Factory mailboxIdFactory;
     private final MessageIdManager messageIdManager;
+    private final int maxActionsPerMailbox;
 
     @Inject
     public RunRulesOnMailboxService(MailboxManager mailboxManager, 
MailboxId.Factory mailboxIdFactory, MessageIdManager messageIdManager) {
+        this(mailboxManager, mailboxIdFactory, messageIdManager, 
MAX_ACTIONS_PER_MAILBOX);
+    }
+
+    @VisibleForTesting
+    public RunRulesOnMailboxService(MailboxManager mailboxManager, 
MailboxId.Factory mailboxIdFactory, MessageIdManager messageIdManager,
+                                    int maxActionsPerMailbox) {
         this.mailboxManager = mailboxManager;
         this.mailboxIdFactory = mailboxIdFactory;
         this.messageIdManager = messageIdManager;
+        this.maxActionsPerMailbox = maxActionsPerMailbox;
     }
 
     public Mono<Task.Result> runRulesOnMailbox(Username username, MailboxName 
mailboxName, Rules rules, RunRulesOnMailboxTask.Context context) {
@@ -75,6 +93,11 @@ public class RunRulesOnMailboxService {
         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(TooManyAppliedActionsException.class, e -> {
+                LOGGER.info("Maximum number of actions exceeded for mailbox {} 
of user {}", mailboxName.asString(), username);
+                context.setMaximumAppliedActionExceeded();
+                return Mono.just(Task.Result.COMPLETED);
+            })
             .onErrorResume(e -> {
                 LOGGER.error("Error when applying rules to mailbox. Mailbox {} 
for user {}", mailboxName.asString(), username, e);
                 context.incrementFails();
@@ -86,11 +109,24 @@ public class RunRulesOnMailboxService {
     }
 
     private Flux<Task.Result> runRulesOnMessage(RuleMatcher ruleMatcher, 
MessageResult messageResult, MailboxSession mailboxSession, 
RunRulesOnMailboxTask.Context context) throws MailboxException {
+        context.increaseProcessedMessagesCount();
+
         return Flux.fromStream(ruleMatcher.findApplicableRules(messageResult))
             .map(Rule::getAction)
+            .handle(applyMaxActionsLimit(context))
             .concatMap(action -> applyActionOnMessage(messageResult, action, 
mailboxSession, context));
     }
 
+    private BiConsumer<Rule.Action, SynchronousSink<Rule.Action>> 
applyMaxActionsLimit(RunRulesOnMailboxTask.Context context) {
+        return (action, sink) -> {
+            if (context.snapshot().getRulesOnMessagesApplySuccessfully() >= 
maxActionsPerMailbox) {
+                sink.error(new TooManyAppliedActionsException());
+            } else {
+                sink.next(action);
+            }
+        };
+    }
+
     private Mono<Task.Result> applyActionOnMessage(MessageResult 
messageResult, Rule.Action action, MailboxSession mailboxSession, 
RunRulesOnMailboxTask.Context context) {
         actionOnMessagePreconditions(action);
         return appendInMailboxes(messageResult.getMessageId(), action, 
mailboxSession, context)
@@ -131,7 +167,7 @@ public class RunRulesOnMailboxService {
                 }
 
                 return 
Mono.from(messageIdManager.setInMailboxesReactive(messageId, mailboxIds, 
mailboxSession))
-                    .doOnSuccess(next -> context.incrementSuccesses())
+                    .then(Mono.fromRunnable(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
index 0b3f30be1d..e9c3f7f356 100644
--- 
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
@@ -23,6 +23,7 @@ import java.time.Clock;
 import java.time.Instant;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
 import org.apache.james.core.Username;
@@ -39,10 +40,14 @@ public class RunRulesOnMailboxTask implements Task {
         public static class Snapshot {
             private final long rulesOnMessagesApplySuccessfully;
             private final long rulesOnMessagesApplyFailed;
+            private final boolean maximumAppliedActionExceeded;
+            private final long processedMessagesCount;
 
-            private Snapshot(long rulesOnMessagesApplySuccessfully, long 
rulesOnMessagesApplyFailed) {
+            private Snapshot(long rulesOnMessagesApplySuccessfully, long 
rulesOnMessagesApplyFailed, boolean maximumAppliedActionExceeded, long 
processedMessagesCount) {
                 this.rulesOnMessagesApplySuccessfully = 
rulesOnMessagesApplySuccessfully;
                 this.rulesOnMessagesApplyFailed = rulesOnMessagesApplyFailed;
+                this.maximumAppliedActionExceeded = 
maximumAppliedActionExceeded;
+                this.processedMessagesCount = processedMessagesCount;
             }
 
             public long getRulesOnMessagesApplySuccessfully() {
@@ -59,14 +64,17 @@ public class RunRulesOnMailboxTask implements Task {
                     Context.Snapshot that = (Context.Snapshot) o;
 
                     return 
Objects.equals(this.rulesOnMessagesApplySuccessfully, 
that.rulesOnMessagesApplySuccessfully)
-                        && Objects.equals(this.rulesOnMessagesApplyFailed, 
that.rulesOnMessagesApplyFailed);
+                        && Objects.equals(this.rulesOnMessagesApplyFailed, 
that.rulesOnMessagesApplyFailed)
+                        && Objects.equals(this.maximumAppliedActionExceeded, 
that.maximumAppliedActionExceeded)
+                        && Objects.equals(this.processedMessagesCount, 
that.processedMessagesCount);
                 }
                 return false;
             }
 
             @Override
             public final int hashCode() {
-                return Objects.hash(rulesOnMessagesApplySuccessfully, 
rulesOnMessagesApplyFailed);
+                return Objects.hash(rulesOnMessagesApplySuccessfully, 
rulesOnMessagesApplyFailed, maximumAppliedActionExceeded,
+                    processedMessagesCount);
             }
 
             @Override
@@ -74,44 +82,62 @@ public class RunRulesOnMailboxTask implements Task {
                 return MoreObjects.toStringHelper(this)
                     .add("rulesOnMessagesApplySuccessfully", 
rulesOnMessagesApplySuccessfully)
                     .add("rulesOnMessagesApplyFailed", 
rulesOnMessagesApplyFailed)
+                    .add("maximumAppliedActionExceeded", 
maximumAppliedActionExceeded)
+                    .add("processedMessagesCount", processedMessagesCount)
                     .toString();
             }
         }
 
         private final AtomicLong rulesOnMessagesApplySuccessfully;
         private final AtomicLong rulesOnMessagesApplyFailed;
+        private final AtomicBoolean maximumAppliedActionExceeded;
+        private final AtomicLong processedMessagesCount;
 
         public Context() {
             this.rulesOnMessagesApplySuccessfully = new AtomicLong();
             this.rulesOnMessagesApplyFailed = new AtomicLong();
+            this.maximumAppliedActionExceeded = new AtomicBoolean();
+            this.processedMessagesCount = new AtomicLong();
         }
 
-        public Context(long rulesOnMessagesApplySuccessfully, long 
rulesOnMessagesApplyFailed) {
+        public Context(long rulesOnMessagesApplySuccessfully, long 
rulesOnMessagesApplyFailed, boolean maximumAppliedActionExceeded,
+                       long processedMessagesCount) {
             this.rulesOnMessagesApplySuccessfully = new 
AtomicLong(rulesOnMessagesApplySuccessfully);
             this.rulesOnMessagesApplyFailed = new 
AtomicLong(rulesOnMessagesApplyFailed);
+            this.maximumAppliedActionExceeded = new 
AtomicBoolean(maximumAppliedActionExceeded);
+            this.processedMessagesCount = new 
AtomicLong(processedMessagesCount);
         }
 
         public void incrementSuccesses() {
             rulesOnMessagesApplySuccessfully.incrementAndGet();
         }
 
-
         public void incrementFails() {
             rulesOnMessagesApplyFailed.incrementAndGet();
         }
 
+        public void setMaximumAppliedActionExceeded() {
+            maximumAppliedActionExceeded.set(true);
+        }
+
+        public void increaseProcessedMessagesCount() {
+            processedMessagesCount.incrementAndGet();
+        }
+
         public Context.Snapshot snapshot() {
-            return new 
Context.Snapshot(rulesOnMessagesApplySuccessfully.get(), 
rulesOnMessagesApplyFailed.get());
+            return new 
Context.Snapshot(rulesOnMessagesApplySuccessfully.get(), 
rulesOnMessagesApplyFailed.get(), maximumAppliedActionExceeded.get(),
+                processedMessagesCount.get());
         }
     }
 
     public static class AdditionalInformation implements 
TaskExecutionDetails.AdditionalInformation {
 
         private static AdditionalInformation from(Username username,
-                                                                        
MailboxName mailboxName,
-                                                                        
RunRulesOnMailboxTask.Context context) {
+                                                  MailboxName mailboxName,
+                                                  
RunRulesOnMailboxTask.Context context) {
             Context.Snapshot snapshot = context.snapshot();
-            return new AdditionalInformation(username, mailboxName, 
Clock.systemUTC().instant(), snapshot.rulesOnMessagesApplySuccessfully, 
snapshot.rulesOnMessagesApplyFailed);
+            return new AdditionalInformation(username, mailboxName, 
Clock.systemUTC().instant(), snapshot.rulesOnMessagesApplySuccessfully,
+                snapshot.rulesOnMessagesApplyFailed, 
snapshot.maximumAppliedActionExceeded, snapshot.processedMessagesCount);
         }
 
         private final Username username;
@@ -119,17 +145,23 @@ public class RunRulesOnMailboxTask implements Task {
         private final Instant timestamp;
         private final long rulesOnMessagesApplySuccessfully;
         private final long rulesOnMessagesApplyFailed;
+        private final boolean maximumAppliedActionExceeded;
+        private final long processedMessagesCount;
 
         public AdditionalInformation(Username username,
                                      MailboxName mailboxName,
                                      Instant timestamp,
                                      long rulesOnMessagesApplySuccessfully,
-                                     long rulesOnMessagesApplyFailed) {
+                                     long rulesOnMessagesApplyFailed,
+                                     boolean maximumAppliedActionExceeded,
+                                     long processedMessagesCount) {
             this.username = username;
             this.mailboxName = mailboxName;
             this.timestamp = timestamp;
             this.rulesOnMessagesApplySuccessfully = 
rulesOnMessagesApplySuccessfully;
             this.rulesOnMessagesApplyFailed = rulesOnMessagesApplyFailed;
+            this.maximumAppliedActionExceeded = maximumAppliedActionExceeded;
+            this.processedMessagesCount = processedMessagesCount;
         }
 
         public Username getUsername() {
@@ -152,6 +184,14 @@ public class RunRulesOnMailboxTask implements Task {
             return rulesOnMessagesApplyFailed;
         }
 
+        public boolean maximumAppliedActionExceeded() {
+            return maximumAppliedActionExceeded;
+        }
+
+        public long getProcessedMessagesCount() {
+            return processedMessagesCount;
+        }
+
         @Override
         public Instant timestamp() {
             return timestamp;
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
index a3b101f23e..4f3feff328 100644
--- 
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
@@ -44,7 +44,9 @@ public class RunRulesOnMailboxTaskAdditionalInformationDTO 
implements Additional
             new MailboxName(dto.getMailboxName()),
             dto.getTimestamp(),
             dto.getRulesOnMessagesApplySuccessfully(),
-            dto.getRulesOnMessagesApplyFailed());
+            dto.getRulesOnMessagesApplyFailed(),
+            dto.getMaximumAppliedActionExceeded(),
+            dto.getProcessedMessagesCount());
     }
 
     private static RunRulesOnMailboxTaskAdditionalInformationDTO 
toDto(RunRulesOnMailboxTask.AdditionalInformation domain, String type) {
@@ -54,7 +56,9 @@ public class RunRulesOnMailboxTaskAdditionalInformationDTO 
implements Additional
             domain.getMailboxName().asString(),
             domain.getTimestamp(),
             domain.getRulesOnMessagesApplySuccessfully(),
-            domain.getRulesOnMessagesApplyFailed());
+            domain.getRulesOnMessagesApplyFailed(),
+            domain.maximumAppliedActionExceeded(),
+            domain.getProcessedMessagesCount());
     }
 
     private final String type;
@@ -63,19 +67,25 @@ public class RunRulesOnMailboxTaskAdditionalInformationDTO 
implements Additional
     private final Instant timestamp;
     private final long rulesOnMessagesApplySuccessfully;
     private final long rulesOnMessagesApplyFailed;
+    private final boolean maximumAppliedActionExceeded;
+    private final long processedMessagesCount;
 
     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) {
+                                                         
@JsonProperty("rulesOnMessagesApplyFailed") long rulesOnMessagesApplyFailed,
+                                                         
@JsonProperty("maximumAppliedActionExceeded") boolean 
maximumAppliedActionExceeded,
+                                                         
@JsonProperty("processedMessagesCount") long processedMessagesCount) {
         this.type = type;
         this.username = username;
         this.mailboxName = mailboxName;
         this.timestamp = timestamp;
         this.rulesOnMessagesApplySuccessfully = 
rulesOnMessagesApplySuccessfully;
         this.rulesOnMessagesApplyFailed = rulesOnMessagesApplyFailed;
+        this.maximumAppliedActionExceeded = maximumAppliedActionExceeded;
+        this.processedMessagesCount = processedMessagesCount;
     }
 
     @Override
@@ -103,4 +113,12 @@ public class RunRulesOnMailboxTaskAdditionalInformationDTO 
implements Additional
     public long getRulesOnMessagesApplyFailed() {
         return rulesOnMessagesApplyFailed;
     }
+
+    public boolean getMaximumAppliedActionExceeded() {
+        return maximumAppliedActionExceeded;
+    }
+
+    public long getProcessedMessagesCount() {
+        return processedMessagesCount;
+    }
 }
diff --git 
a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java
 
b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java
index 3d36c605f8..1111ea9d97 100644
--- 
a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxRoutesTest.java
@@ -36,6 +36,7 @@ import java.time.Clock;
 import java.time.Duration;
 import java.util.Date;
 import java.util.Map;
+import java.util.stream.IntStream;
 
 import jakarta.mail.Flags;
 
@@ -1039,6 +1040,161 @@ public class RunRulesOnMailboxRoutesTest {
             .body("additionalInformation.username", 
Matchers.is(USERNAME.asString()))
             .body("additionalInformation.mailboxName", 
Matchers.is(MAILBOX_NAME))
             .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.is(2))
-            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0));
+            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false))
+            .body("additionalInformation.processedMessagesCount", 
Matchers.is(3));
+    }
+
+    @Test
+    void taskShouldStopAndCompleteWhenAppliedActionsExceedMaximumLimit() 
throws Exception {
+        overrideTriageRulesRouteWithActionsLimit(2);
+
+        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
+        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+        mailboxManager.createMailbox(mailboxPath, systemSession);
+        mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
+
+        // Add 20 matching messages, which exceeds the max actions of 2
+        IntStream.range(0, 20)
+            .forEach(Throwing.intConsumer(i -> messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .build(Message.Builder.of()
+                        .setSubject("plop")
+                        .setFrom("[email protected]")
+                        .setBody("matched mail", StandardCharsets.UTF_8)),
+                systemSession)));
+
+        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
+
+        String taskId = given()
+            .queryParam("action", "triage")
+            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+            .post(MAILBOX_NAME + "/messages")
+        .then()
+            .statusCode(CREATED_201)
+            .extract()
+            .jsonPath()
+            .get("taskId");
+
+        given()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("status", Matchers.is("completed"))
+            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.lessThan(20))
+            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(true));
+    }
+
+    @Test
+    void taskShouldCompleteWhenAppliedActionsReachMaximumLimit() throws 
Exception {
+        overrideTriageRulesRouteWithActionsLimit(2);
+
+        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
+        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+        mailboxManager.createMailbox(mailboxPath, systemSession);
+        mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
+
+        // Add 2 matching messages, reach the limit of 2
+        IntStream.range(0, 2)
+            .forEach(Throwing.intConsumer(i -> messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .build(Message.Builder.of()
+                        .setSubject("plop")
+                        .setFrom("[email protected]")
+                        .setBody("matched mail", StandardCharsets.UTF_8)),
+                systemSession)));
+
+        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
+
+        String taskId = given()
+            .queryParam("action", "triage")
+            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+            .post(MAILBOX_NAME + "/messages")
+        .then()
+            .statusCode(CREATED_201)
+            .extract()
+            .jsonPath()
+            .get("taskId");
+
+        given()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("status", Matchers.is("completed"))
+            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.is(2))
+            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false));
+    }
+
+    @Test
+    void taskShouldCompleteWhenAppliedActionsLessThanMaximumLimit() throws 
Exception {
+        overrideTriageRulesRouteWithActionsLimit(10);
+
+        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
+        MailboxPath otherMailboxPath = MailboxPath.forUser(USERNAME, 
OTHER_MAILBOX_NAME);
+        MailboxSession systemSession = 
mailboxManager.createSystemSession(USERNAME);
+
+        mailboxManager.createMailbox(mailboxPath, systemSession);
+        mailboxManager.createMailbox(otherMailboxPath, systemSession);
+
+        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, 
systemSession);
+
+        // Add 2 matching messages, < the limit of 10
+        IntStream.range(0, 2)
+            .forEach(Throwing.intConsumer(i -> messageManager.appendMessage(
+                MessageManager.AppendCommand.builder()
+                    .build(Message.Builder.of()
+                        .setSubject("plop")
+                        .setFrom("[email protected]")
+                        .setBody("matched mail", StandardCharsets.UTF_8)),
+                systemSession)));
+
+        MailboxId otherMailboxId = mailboxManager.getMailbox(otherMailboxPath, 
systemSession).getId();
+
+        String taskId = given()
+            .queryParam("action", "triage")
+            .body(RULE_PAYLOAD.formatted(otherMailboxId.serialize()))
+            .post(MAILBOX_NAME + "/messages")
+        .then()
+            .statusCode(CREATED_201)
+            .extract()
+            .jsonPath()
+            .get("taskId");
+
+        given()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("status", Matchers.is("completed"))
+            .body("additionalInformation.rulesOnMessagesApplySuccessfully", 
Matchers.is(2))
+            .body("additionalInformation.rulesOnMessagesApplyFailed", 
Matchers.is(0))
+            .body("additionalInformation.maximumAppliedActionExceeded", 
Matchers.is(false));
+    }
+
+    private void overrideTriageRulesRouteWithActionsLimit(int maxActionsLimit) 
{
+        webAdminServer.destroy();
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+                new RunRulesOnMailboxRoutes(usersRepository, mailboxManager, 
taskManager, new JsonTransformer(),
+                    new RunRulesOnMailboxService(mailboxManager, new 
InMemoryId.Factory(), messageIdManager, maxActionsLimit)),
+                new TasksRoutes(taskManager, new JsonTransformer(),
+                    
DTOConverter.of(RunRulesOnMailboxTaskAdditionalInformationDTO.SERIALIZATION_MODULE)))
+            .start();
+
+        RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath(USERS_BASE + SEPARATOR + USERNAME.asString() + 
SEPARATOR + UserMailboxesRoutes.MAILBOXES)
+            .setUrlEncodingEnabled(false)
+            .build();
     }
 }
diff --git 
a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
 
b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
index 0ce8572d5a..b6cfcc7a35 100644
--- 
a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
+++ 
b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/RunRulesOnMailboxTaskAdditionalInformationDTOTest.java
@@ -31,7 +31,7 @@ public class 
RunRulesOnMailboxTaskAdditionalInformationDTOTest {
     private static final Instant INSTANT = 
Instant.parse("2007-12-03T10:15:30.00Z");
 
     private static final RunRulesOnMailboxTask.AdditionalInformation 
DOMAIN_OBJECT = new RunRulesOnMailboxTask.AdditionalInformation(
-        Username.of("[email protected]"), new MailboxName("mbx1"), INSTANT, 10, 
9);
+        Username.of("[email protected]"), new MailboxName("mbx1"), INSTANT, 10, 
9, false, 30);
 
     @Test
     void shouldMatchJsonSerializationContract() throws Exception {
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
index a3f8bd738b..6be4f207e4 100644
--- 
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
@@ -2,6 +2,8 @@
   "mailboxName": "mbx1",
   "rulesOnMessagesApplyFailed": 9,
   "rulesOnMessagesApplySuccessfully": 10,
+  "processedMessagesCount": 30,
+  "maximumAppliedActionExceeded": false,
   "timestamp": "2007-12-03T10:15:30Z",
   "type": "RunRulesOnMailboxTask",
   "username": "[email protected]"


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

Reply via email to