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