This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 9a434fe2e12e929d86176038136c4a7d3e490034 Author: Tran Tien Duc <[email protected]> AuthorDate: Tue Feb 26 14:32:33 2019 +0700 JAMES-2663 Vault restore all user message routes --- .../vault/memory/MemoryDeletedMessagesVault.java | 2 +- server/protocols/webadmin/pom.xml | 1 + .../webadmin-mailbox-deleted-message-vault/pom.xml | 177 ++++++++++ .../routes/DeletedMessagesVaultRestoreTask.java | 123 +++++++ .../vault/routes/DeletedMessagesVaultRoutes.java | 116 ++++++ .../webadmin/vault/routes/RestoreService.java | 107 ++++++ .../routes/DeletedMessagesVaultRoutesTest.java | 387 +++++++++++++++++++++ src/site/markdown/server/manage-webadmin.md | 45 +++ 8 files changed, 957 insertions(+), 1 deletion(-) diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/MemoryDeletedMessagesVault.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/MemoryDeletedMessagesVault.java index e23d87a..7e9faa3 100644 --- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/MemoryDeletedMessagesVault.java +++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/MemoryDeletedMessagesVault.java @@ -43,7 +43,7 @@ import reactor.core.publisher.Mono; public class MemoryDeletedMessagesVault implements DeletedMessageVault { private final Table<User, MessageId, Pair<DeletedMessage, byte[]>> table; - MemoryDeletedMessagesVault() { + public MemoryDeletedMessagesVault() { table = HashBasedTable.create(); } diff --git a/server/protocols/webadmin/pom.xml b/server/protocols/webadmin/pom.xml index 38d0fe3..65c60f9 100644 --- a/server/protocols/webadmin/pom.xml +++ b/server/protocols/webadmin/pom.xml @@ -38,6 +38,7 @@ <module>webadmin-core</module> <module>webadmin-data</module> <module>webadmin-mailbox</module> + <module>webadmin-mailbox-deleted-message-vault</module> <module>webadmin-mailqueue</module> <module>webadmin-mailrepository</module> <module>webadmin-swagger</module> diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml new file mode 100644 index 0000000..aa6847d --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.james</groupId> + <artifactId>james-server</artifactId> + <version>3.4.0-SNAPSHOT</version> + <relativePath>../../../pom.xml</relativePath> + </parent> + + <artifactId>james-server-webadmin-mailbox-deleted-message-vault</artifactId> + <packaging>jar</packaging> + + <name>Apache James :: Server :: Web Admin :: Mailbox :: Deleted Message Vault</name> + + <dependencies> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-api</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-api</artifactId> + <scope>test</scope> + <type>test-jar</type> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-deleted-messages-vault</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-deleted-messages-vault</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-memory</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-memory</artifactId> + <scope>test</scope> + <type>test-jar</type> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-mailet-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-core</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-webadmin-core</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-webadmin-core</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>metrics-logger</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>java-hamcrest</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.github.kongchen</groupId> + <artifactId>swagger-maven-plugin</artifactId> + <configuration> + <apiSources> + <apiSource> + <springmvc>false</springmvc> + <locations>org.apache.james.webadmin</locations> + <info> + <title>Swagger Maven Plugin</title> + <version>v1</version> + </info> + <swaggerDirectory>${project.build.directory}</swaggerDirectory> + <swaggerFileName>webadmin-mailbox-deleted-message-vault</swaggerFileName> + </apiSource> + </apiSources> + </configuration> + <executions> + <execution> + <phase>compile</phase> + <goals> + <goal>generate</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <reuseForks>true</reuseForks> + <forkCount>1C</forkCount> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java new file mode 100644 index 0000000..b6aa3e5 --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java @@ -0,0 +1,123 @@ +/**************************************************************** + * 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.vault.routes; + +import static org.apache.james.webadmin.vault.routes.RestoreService.RestoreResult.RESTORE_SUCCEED; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.james.core.User; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.task.Task; +import org.apache.james.task.TaskExecutionDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class DeletedMessagesVaultRestoreTask implements Task { + + public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation { + private final User user; + private final AtomicLong successfulRestoreCount; + private final AtomicLong errorRestoreCount; + + AdditionalInformation(User user) { + this.user = user; + this.successfulRestoreCount = new AtomicLong(); + this.errorRestoreCount = new AtomicLong(); + } + + public long getSuccessfulRestoreCount() { + return successfulRestoreCount.get(); + } + + public long getErrorRestoreCount() { + return errorRestoreCount.get(); + } + + public String getUser() { + return user.asString(); + } + + void incrementSuccessfulRestoreCount() { + successfulRestoreCount.incrementAndGet(); + } + + void incrementErrorRestoreCount() { + errorRestoreCount.incrementAndGet(); + } + } + + private static final Logger LOGGER = LoggerFactory.getLogger(DeletedMessagesVaultRestoreTask.class); + + static final String TYPE = "deletedMessages/restore"; + + private final User userToRestore; + private final RestoreService vaultRestore; + private final AdditionalInformation additionalInformation; + + DeletedMessagesVaultRestoreTask(User userToRestore, RestoreService vaultRestore) { + this.userToRestore = userToRestore; + this.vaultRestore = vaultRestore; + this.additionalInformation = new AdditionalInformation(userToRestore); + } + + @Override + public Result run() { + try { + return vaultRestore.restore(userToRestore).toStream() + .peek(this::updateInformation) + .map(this::restoreResultToTaskResult) + .reduce(Task::combine) + .orElse(Result.COMPLETED); + } catch (MailboxException e) { + LOGGER.error("Error happens while restoring user {}", userToRestore.asString(), e); + return Result.PARTIAL; + } + } + + private Task.Result restoreResultToTaskResult(RestoreService.RestoreResult restoreResult) { + if (restoreResult.equals(RESTORE_SUCCEED)) { + return Result.COMPLETED; + } + return Result.PARTIAL; + } + + private void updateInformation(RestoreService.RestoreResult restoreResult) { + switch (restoreResult) { + case RESTORE_FAILED: + additionalInformation.incrementErrorRestoreCount(); + break; + case RESTORE_SUCCEED: + additionalInformation.incrementSuccessfulRestoreCount(); + break; + } + } + + @Override + public String type() { + return TYPE; + } + + @Override + public Optional<TaskExecutionDetails.AdditionalInformation> details() { + return Optional.of(additionalInformation); + } +} diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java new file mode 100644 index 0000000..f47a331 --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java @@ -0,0 +1,116 @@ +/**************************************************************** + * 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.vault.routes; + +import static org.apache.james.webadmin.Constants.SEPARATOR; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.apache.james.core.User; +import org.apache.james.task.TaskId; +import org.apache.james.task.TaskManager; +import org.apache.james.webadmin.Constants; +import org.apache.james.webadmin.Routes; +import org.apache.james.webadmin.dto.TaskIdDto; +import org.apache.james.webadmin.utils.ErrorResponder; +import org.apache.james.webadmin.utils.JsonTransformer; +import org.eclipse.jetty.http.HttpStatus; + +import com.google.common.annotations.VisibleForTesting; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import spark.Request; +import spark.Response; +import spark.Service; + +@Api(tags = "Deleted Messages Vault") +@Path(DeletedMessagesVaultRoutes.ROOT_PATH) +@Produces(Constants.JSON_CONTENT_TYPE) +public class DeletedMessagesVaultRoutes implements Routes { + + static final String ROOT_PATH = "deletedMessages/user"; + private static final String USER_PATH_PARAM = "user"; + private static final String RESTORE_PATH = ROOT_PATH + SEPARATOR + ":" + USER_PATH_PARAM; + + private final RestoreService vaultRestore; + private final JsonTransformer jsonTransformer; + private final TaskManager taskManager; + + @VisibleForTesting + DeletedMessagesVaultRoutes(RestoreService vaultRestore, JsonTransformer jsonTransformer, + TaskManager taskManager) { + this.vaultRestore = vaultRestore; + this.jsonTransformer = jsonTransformer; + this.taskManager = taskManager; + } + + @Override + public String getBasePath() { + return ROOT_PATH; + } + + @Override + public void define(Service service) { + service.post(RESTORE_PATH, this::restore, jsonTransformer); + } + + @POST + @Path(ROOT_PATH) + @ApiOperation(value = "Restore deleted emails from a specified user to his new restore mailbox") + @ApiImplicitParams({ + @ApiImplicitParam( + required = true, + name = "user", + paramType = "path parameter", + dataType = "String", + defaultValue = "none", + example = "[email protected]", + value = "Compulsory. Needs to be a valid username represent for an user had requested to restore deleted emails") + }) + @ApiResponses(value = { + @ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class), + @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - user param is invalid"), + @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.") + }) + private TaskIdDto restore(Request request, Response response) { + User userToRestore = extractUser(request); + TaskId taskId = taskManager.submit(new DeletedMessagesVaultRestoreTask(userToRestore, vaultRestore)); + return TaskIdDto.respond(response, taskId); + } + + private User extractUser(Request request) { + try { + return User.fromUsername(request.params(USER_PATH_PARAM)); + } catch (IllegalArgumentException e) { + throw ErrorResponder.builder() + .statusCode(HttpStatus.BAD_REQUEST_400) + .type(ErrorResponder.ErrorType.INVALID_ARGUMENT) + .message(e.getMessage()) + .haltError(); + } + } +} diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java new file mode 100644 index 0000000..392b1fa --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java @@ -0,0 +1,107 @@ +/**************************************************************** + * 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.vault.routes; + +import static org.apache.james.mailbox.MessageManager.AppendCommand; +import static org.apache.james.webadmin.vault.routes.RestoreService.RestoreResult.RESTORE_FAILED; +import static org.apache.james.webadmin.vault.routes.RestoreService.RestoreResult.RESTORE_SUCCEED; + +import org.apache.james.core.User; +import org.apache.james.mailbox.MailboxManager; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MessageManager; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.exception.MailboxNotFoundException; +import org.apache.james.mailbox.model.ComposedMessageId; +import org.apache.james.mailbox.model.MailboxId; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.vault.DeletedMessage; +import org.apache.james.vault.DeletedMessageVault; +import org.apache.james.vault.search.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.fge.lambdas.Throwing; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +class RestoreService { + + enum RestoreResult { + RESTORE_SUCCEED, + RESTORE_FAILED + } + + private static final Logger LOGGER = LoggerFactory.getLogger(RestoreService.class); + + static final String RESTORE_MAILBOX_NAME = "Restored-Messages"; + + private final DeletedMessageVault deletedMessageVault; + private final MailboxManager mailboxManager; + + RestoreService(DeletedMessageVault deletedMessageVault, MailboxManager mailboxManager) { + this.deletedMessageVault = deletedMessageVault; + this.mailboxManager = mailboxManager; + } + + Flux<RestoreResult> restore(User userToRestore) throws MailboxException { + MailboxSession session = mailboxManager.createSystemSession(userToRestore.asString()); + MessageManager restoreMessageManager = restoreMailboxManager(session); + + return Flux.from(deletedMessageVault.search(userToRestore, Query.ALL)) + .flatMap(deletedMessage -> appendToMailbox(restoreMessageManager, deletedMessage, session)); + } + + private Mono<RestoreResult> appendToMailbox(MessageManager restoreMailboxManager, DeletedMessage deletedMessage, MailboxSession session) { + return appendCommand(deletedMessage) + .map(Throwing.<AppendCommand, ComposedMessageId>function( + appendCommand -> restoreMailboxManager.appendMessage(appendCommand, session)).sneakyThrow()) + .map(any -> RESTORE_SUCCEED) + .onErrorResume(throwable -> { + LOGGER.error("append message {} to restore mailbox of user {} didn't success", + deletedMessage.getMessageId().serialize(), deletedMessage.getOwner().asString(), throwable); + return Mono.just(RESTORE_FAILED); + }); + } + + private Mono<AppendCommand> appendCommand(DeletedMessage deletedMessage) { + return Mono.from(deletedMessageVault.loadMimeMessage(deletedMessage.getOwner(), deletedMessage.getMessageId())) + .map(messageContentStream -> AppendCommand.builder() + .build(messageContentStream)); + } + + private MessageManager restoreMailboxManager(MailboxSession session) throws MailboxException { + MailboxPath restoreMailbox = MailboxPath.forUser(session.getUser().asString(), RESTORE_MAILBOX_NAME); + try { + return mailboxManager.getMailbox(restoreMailbox, session); + } catch (MailboxNotFoundException e) { + LOGGER.debug("mailbox {} doesn't exist, create a new one", restoreMailbox); + return createRestoreMailbox(session, restoreMailbox); + } + } + + private MessageManager createRestoreMailbox(MailboxSession session, MailboxPath restoreMailbox) throws MailboxException { + return mailboxManager.createMailbox(restoreMailbox, session) + .map(Throwing.<MailboxId, MessageManager>function(mailboxId -> mailboxManager.getMailbox(mailboxId, session)).sneakyThrow()) + .orElseThrow(() -> new RuntimeException("createMailbox " + restoreMailbox.asString() + " returns an empty mailboxId")); + } + +} diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java new file mode 100644 index 0000000..7349d5a --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java @@ -0,0 +1,387 @@ +/**************************************************************** + * 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.vault.routes; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static io.restassured.RestAssured.with; +import static org.apache.james.vault.DeletedMessageFixture.CONTENT; +import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE; +import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE_2; +import static org.apache.james.vault.DeletedMessageFixture.USER; +import static org.apache.james.vault.DeletedMessageFixture.USER_2; +import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION; +import static org.apache.james.webadmin.vault.routes.RestoreService.RESTORE_MAILBOX_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + +import org.apache.james.core.User; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MessageManager; +import org.apache.james.mailbox.acl.SimpleGroupMembershipResolver; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.inmemory.InMemoryMailboxManager; +import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources; +import org.apache.james.mailbox.model.FetchGroupImpl; +import org.apache.james.mailbox.model.MailboxId; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.model.MessageRange; +import org.apache.james.mailbox.model.MessageResult; +import org.apache.james.mailbox.model.MultimailboxesSearchQuery; +import org.apache.james.mailbox.model.SearchQuery; +import org.apache.james.metrics.logger.DefaultMetricFactory; +import org.apache.james.task.MemoryTaskManager; +import org.apache.james.vault.memory.MemoryDeletedMessagesVault; +import org.apache.james.vault.search.Query; +import org.apache.james.webadmin.WebAdminServer; +import org.apache.james.webadmin.WebAdminUtils; +import org.apache.james.webadmin.routes.TasksRoutes; +import org.apache.james.webadmin.utils.ErrorResponder; +import org.apache.james.webadmin.utils.JsonTransformer; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.google.common.collect.ImmutableList; + +import io.restassured.RestAssured; +import io.restassured.filter.log.LogDetail; +import reactor.core.publisher.Flux; + +class DeletedMessagesVaultRoutesTest { + + private WebAdminServer webAdminServer; + private MemoryDeletedMessagesVault vault; + private InMemoryMailboxManager mailboxManager; + + @BeforeEach + void beforeEach() throws Exception { + vault = spy(new MemoryDeletedMessagesVault()); + InMemoryIntegrationResources inMemoryIntegrationResources = new InMemoryIntegrationResources(); + InMemoryIntegrationResources.Resources inMemoryResource = inMemoryIntegrationResources.createResources(new SimpleGroupMembershipResolver()); + mailboxManager = spy(inMemoryResource.getMailboxManager()); + + MemoryTaskManager taskManager = new MemoryTaskManager(); + JsonTransformer jsonTransformer = new JsonTransformer(); + + RestoreService vaultRestore = new RestoreService(vault, mailboxManager); + webAdminServer = WebAdminUtils.createWebAdminServer( + new DefaultMetricFactory(), + new TasksRoutes(taskManager, jsonTransformer), + new DeletedMessagesVaultRoutes(vaultRestore, jsonTransformer, taskManager)); + + webAdminServer.configure(NO_CONFIGURATION); + webAdminServer.await(); + RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer) + .setBasePath(DeletedMessagesVaultRoutes.ROOT_PATH) + .log(LogDetail.METHOD) + .build(); + } + + @AfterEach + void afterEach() { + webAdminServer.destroy(); + } + + @Nested + class ValidationTest { + + @Test + void restoreShouldReturnInvalidWhenUserIsInvalid() { + when() + .post("not@[email protected]") + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is("The username should not contain multiple domain delimiter.")); + } + + @Test + void postShouldReturnNotFoundWhenNoUserPathParameter() { + when() + .post() + .then() + .statusCode(HttpStatus.NOT_FOUND_404) + .body("statusCode", is(404)) + .body("type", is(ErrorResponder.ErrorType.NOT_FOUND.getType())) + .body("message", is("POST /deletedMessages/user can not be found")); + } + } + + @Nested + class FailingRestoreTest { + + @Test + void restoreShouldProduceFailedTaskWhenTheVaultGetsError() { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + doThrow(new RuntimeException("mock exception")) + .when(vault) + .search(any(), any()); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("failed")) + .body("taskId", is(taskId)) + .body("type", is(DeletedMessagesVaultRestoreTask.TYPE)) + .body("additionalInformation.successfulRestoreCount", is(0)) + .body("additionalInformation.errorRestoreCount", is(0)) + .body("additionalInformation.user", is(USER.asString())) + .body("startedDate", is(notNullValue())) + .body("submitDate", is(notNullValue())); + } + + @Test + void restoreShouldProduceFailedTaskWithErrorRestoreCountWhenMessageAppendGetsError() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + MessageManager mockMessageManager = Mockito.mock(MessageManager.class); + doReturn(mockMessageManager) + .when(mailboxManager) + .getMailbox(any(MailboxId.class), any(MailboxSession.class)); + + doThrow(new MailboxException("mock exception")) + .when(mockMessageManager) + .appendMessage(any(), any()); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("failed")) + .body("taskId", is(taskId)) + .body("type", is(DeletedMessagesVaultRestoreTask.TYPE)) + .body("additionalInformation.successfulRestoreCount", is(0)) + .body("additionalInformation.errorRestoreCount", is(2)) + .body("additionalInformation.user", is(USER.asString())) + .body("startedDate", is(notNullValue())) + .body("submitDate", is(notNullValue())); + } + + @Test + void restoreShouldProduceFailedTaskWhenMailboxMangerGetsError() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + doThrow(new RuntimeException("mock exception")) + .when(mailboxManager) + .createMailbox(any(MailboxPath.class), any(MailboxSession.class)); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("failed")) + .body("taskId", is(taskId)) + .body("type", is(DeletedMessagesVaultRestoreTask.TYPE)) + .body("additionalInformation.successfulRestoreCount", is(0)) + .body("additionalInformation.errorRestoreCount", is(0)) + .body("additionalInformation.user", is(USER.asString())) + .body("startedDate", is(notNullValue())) + .body("submitDate", is(notNullValue())); + } + } + + @Test + void restoreShouldReturnATaskCreated() { + when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.CREATED_201) + .body("taskId", notNullValue()); + } + + @Test + void restoreShouldProduceASuccessfulTaskWithAdditionalInformation() { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")) + .body("taskId", is(taskId)) + .body("type", is(DeletedMessagesVaultRestoreTask.TYPE)) + .body("additionalInformation.successfulRestoreCount", is(2)) + .body("additionalInformation.errorRestoreCount", is(0)) + .body("additionalInformation.user", is(USER.asString())) + .body("startedDate", is(notNullValue())) + .body("submitDate", is(notNullValue())) + .body("completedDate", is(notNullValue())); + } + + @Test + void restoreShouldKeepAllMessagesInTheVaultOfCorrespondingUser() { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(Flux.from(vault.search(USER, Query.ALL)).toStream()) + .containsOnly(DELETED_MESSAGE, DELETED_MESSAGE_2); + } + + @Test + void restoreShouldNotDeleteExistingMessagesInTheUserMailbox() throws Exception { + MailboxSession session = mailboxManager.createSystemSession(USER.asString()); + MailboxPath restoreMailboxPath = MailboxPath.forUser(USER.asString(), RESTORE_MAILBOX_NAME); + mailboxManager.createMailbox(restoreMailboxPath, session); + MessageManager messageManager = mailboxManager.getMailbox(restoreMailboxPath, session); + messageManager.appendMessage( + MessageManager.AppendCommand.builder().build(new ByteArrayInputStream(CONTENT)), + session); + + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMailboxMessages(USER)) + .hasSize(3); + } + + @Test + void restoreShouldAppendAllMessageFromVaultToRestoreMailboxOfCorrespondingUser() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMailboxMessages(USER)) + .hasSize(2) + .anySatisfy(messageResult -> assertThat(fullContent(messageResult)).hasSameContentAs(new ByteArrayInputStream(CONTENT))) + .anySatisfy(messageResult -> assertThat(fullContent(messageResult)).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessagesToAnOtherUserMailbox() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String taskId = with() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER_2)) + .isFalse(); + } + + private boolean hasAnyMail(User user) throws MailboxException { + MailboxSession session = mailboxManager.createSystemSession(user.asString()); + int limitToOneMessage = 1; + + return !mailboxManager.search(MultimailboxesSearchQuery.from(new SearchQuery()).build(), session, limitToOneMessage) + .isEmpty(); + } + + private InputStream fullContent(MessageResult messageResult) { + try { + return messageResult.getFullContent().getInputStream(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private List<MessageResult> restoreMailboxMessages(User user) throws Exception { + MailboxSession session = mailboxManager.createSystemSession(user.asString()); + MessageManager messageManager = mailboxManager.getMailbox(MailboxPath.forUser(user.asString(), RESTORE_MAILBOX_NAME), session); + return ImmutableList.copyOf(messageManager.getMessages(MessageRange.all(), FetchGroupImpl.MINIMAL, session)); + } +} \ No newline at end of file diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md index 8709d9f..678a69a 100644 --- a/src/site/markdown/server/manage-webadmin.md +++ b/src/site/markdown/server/manage-webadmin.md @@ -44,6 +44,7 @@ as exposed above). To avoid information duplication, this is ommited on endpoint - [Administrating Sieve quotas](#Administrating_Sieve_quotas) - [ReIndexing](#ReIndexing) - [Event Dead Letter](#Event_Dead_Letter) + - [Deleted Messages Vault](#Deleted_Messages_Vault) - [Task management](#Task_management) - [Cassandra extra operations](#Cassandra_extra_operations) @@ -2496,6 +2497,50 @@ Response codes: Not implemented yet. +## Deleted Messages Vault + +The 'Deleted Message Vault plugin' allows you to keep users deleted messages during a given retention time. This set of routes allow you to *restore* users deleted messages or export them in an archive (not implemented yet). + +To move deleted messages in the vault, you need to specifically configure the DeletedMessageVault PreDeletionHook. + +Here are the following actions available on the 'Deleted Messages Vault' + + - [Restore Deleted Messages](#Restore_deleted_messages) + + Note that the 'Deleted Messages Vault' feature is only supported on top of Cassandra-Guice. + +### Restore Deleted Messages + +Deleted messages of a specific user can be restored by calling the following endpoint: + +``` +curl -XPOST http://ip:port/deletedMessages/user/[email protected] +``` + +**All** messages in the Deleted Messages Vault of an specified user will be appended to his 'Restored-Messages' mailbox, which will be created if needed. + +**Note**: Restoring matched messages by queries is not supported yet + +Response code: + + - 201: Task for restoring deleted has been created + - 400: Bad request, user parameter is invalid + +The scheduled task will have the following type `deletedMessages/restore` and the following `additionalInformation`: + +``` +{ + "successfulRestoreCount": 47, + "errorRestoreCount": 0 + "user": "[email protected]" +} +``` + +while: + - successfulRestoreCount: number of restored messages + - errorRestoreCount: number of messages that failed to restore + - user: owner of deleted messages need to restore + ## Task management Some webadmin features schedules tasks. The task management API allow to monitor and manage the execution of the following tasks. --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
