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