You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2019/03/04 09:22:59 UTC
[james-project] 02/04: JAMES-2663 Vault restore all user message
routes
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 <dt...@linagora.com>
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 = "user@james.org",
+ 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@valid@user.com")
+ .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/userToRestore@domain.ext
+```
+
+**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": "userToRestore@domain.ext"
+}
+```
+
+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: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org