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