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/14 02:04:55 UTC

[james-project] 03/07: JAMES-2663 Vault route: restore by query

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 8174c65554645bc6487e9996b3cd8e711dda3672
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Thu Mar 7 11:51:35 2019 +0700

    JAMES-2663 Vault route: restore by query
---
 .../java/org/apache/james/vault/search/Query.java  |    2 +-
 .../integration/DeletedMessagesVaultTest.java      |    6 +
 .../routes/DeletedMessagesVaultRestoreTask.java    |    7 +-
 .../vault/routes/DeletedMessagesVaultRoutes.java   |   47 +-
 .../webadmin/vault/routes/RestoreService.java      |    4 +-
 .../webadmin/vault/routes/query/QueryElement.java  |    2 +-
 .../vault/routes/query/QueryTranslator.java        |   21 +-
 .../routes/DeletedMessagesVaultRoutesTest.java     | 1270 +++++++++++++++++++-
 .../vault/routes/query/QueryTranslatorTest.java    |    7 +
 src/site/markdown/server/manage-webadmin.md        |  101 +-
 10 files changed, 1402 insertions(+), 65 deletions(-)

diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java
index 8595a43..3092cb9 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java
@@ -30,7 +30,7 @@ public class Query {
     public static final Query ALL = new Query(ImmutableList.of());
     private static final Predicate<DeletedMessage> MATCH_ALL = any -> true;
 
-    public static Query of(List<Criterion> criteria) {
+    public static Query and(List<Criterion> criteria) {
         return new Query(criteria);
     }
 
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java
index 55476f0..4336a08 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java
@@ -76,6 +76,11 @@ public abstract class DeletedMessagesVaultTest {
     private static final ConditionFactory WAIT_TWO_MINUTES = calmlyAwait.atMost(Duration.TWO_MINUTES);
     private static final String SUBJECT = "This mail will be restored from the vault!!";
     private static final String MAILBOX_NAME = "toBeDeleted";
+    private static final String MATCH_ALL_QUERY = "{" +
+        "\"combinator\": \"and\"," +
+        "\"criteria\": []" +
+        "}";
+
     private MailboxId otherMailboxId;
 
     protected abstract GuiceJamesServer createJmapServer() throws IOException;
@@ -416,6 +421,7 @@ public abstract class DeletedMessagesVaultTest {
 
     private void restoreMessagesFor(String user) {
         String taskId = webAdminApi.with()
+            .body(MATCH_ALL_QUERY)
             .post("/deletedMessages/user/" + user + "?action=restore")
             .jsonPath()
             .get("taskId");
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
index b6aa3e5..9d855b8 100644
--- 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
@@ -28,6 +28,7 @@ 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.apache.james.vault.search.Query;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -72,8 +73,10 @@ class DeletedMessagesVaultRestoreTask implements Task {
     private final User userToRestore;
     private final RestoreService vaultRestore;
     private final AdditionalInformation additionalInformation;
+    private final Query query;
 
-    DeletedMessagesVaultRestoreTask(User userToRestore, RestoreService vaultRestore) {
+    DeletedMessagesVaultRestoreTask(RestoreService vaultRestore, User userToRestore, Query query) {
+        this.query = query;
         this.userToRestore = userToRestore;
         this.vaultRestore = vaultRestore;
         this.additionalInformation = new AdditionalInformation(userToRestore);
@@ -82,7 +85,7 @@ class DeletedMessagesVaultRestoreTask implements Task {
     @Override
     public Result run() {
         try {
-            return vaultRestore.restore(userToRestore).toStream()
+            return vaultRestore.restore(userToRestore, query).toStream()
                 .peek(this::updateInformation)
                 .map(this::restoreResultToTaskResult)
                 .reduce(Task::combine)
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
index edca31a..527b8d9 100644
--- 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
@@ -36,10 +36,16 @@ import org.apache.james.core.User;
 import org.apache.james.task.Task;
 import org.apache.james.task.TaskId;
 import org.apache.james.task.TaskManager;
+import org.apache.james.vault.search.Query;
 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.JsonExtractException;
+import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.vault.routes.query.QueryElement;
+import org.apache.james.webadmin.vault.routes.query.QueryTranslator;
 import org.eclipse.jetty.http.HttpStatus;
 
 import com.github.steveash.guavate.Guavate;
@@ -53,6 +59,7 @@ import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
+import spark.HaltException;
 import spark.Request;
 import spark.Response;
 import spark.Service;
@@ -99,14 +106,18 @@ public class DeletedMessagesVaultRoutes implements Routes {
     private final RestoreService vaultRestore;
     private final JsonTransformer jsonTransformer;
     private final TaskManager taskManager;
+    private final JsonExtractor<QueryElement> jsonExtractor;
+    private final QueryTranslator queryTranslator;
 
     @Inject
     @VisibleForTesting
     DeletedMessagesVaultRoutes(RestoreService vaultRestore, JsonTransformer jsonTransformer,
-                               TaskManager taskManager) {
+                               TaskManager taskManager, QueryTranslator queryTranslator) {
         this.vaultRestore = vaultRestore;
         this.jsonTransformer = jsonTransformer;
         this.taskManager = taskManager;
+        this.queryTranslator = queryTranslator;
+        this.jsonExtractor = new JsonExtractor<>(QueryElement.class);
     }
 
     @Override
@@ -145,7 +156,7 @@ public class DeletedMessagesVaultRoutes implements Routes {
         @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 userActions(Request request, Response response) {
+    private TaskIdDto userActions(Request request, Response response) throws JsonExtractException {
         UserVaultAction requestedAction = extractUserVaultAction(request);
 
         Task requestedTask = generateTask(requestedAction, request);
@@ -153,17 +164,43 @@ public class DeletedMessagesVaultRoutes implements Routes {
         return TaskIdDto.respond(response, taskId);
     }
 
-    private Task generateTask(UserVaultAction requestedAction, Request request) {
-        User userToRestore = User.fromUsername(request.params(USER_PATH_PARAM));
+    private Task generateTask(UserVaultAction requestedAction, Request request) throws JsonExtractException {
+        User userToRestore = extractUser(request);
+        Query query = translate(jsonExtractor.parse(request.body()));
 
         switch (requestedAction) {
             case RESTORE:
-                return new DeletedMessagesVaultRestoreTask(userToRestore, vaultRestore);
+                return new DeletedMessagesVaultRestoreTask(vaultRestore, userToRestore, query);
             default:
                 throw new NotImplementedException(requestedAction + " is not yet handled.");
         }
     }
 
+    private Query translate(QueryElement queryElement) {
+        try {
+            return queryTranslator.translate(queryElement);
+        } catch (QueryTranslator.QueryTranslatorException e) {
+            throw badRequest("Invalid payload passing to the route", e);
+        }
+    }
+
+    private User extractUser(Request request) {
+        try {
+            return User.fromUsername(request.params(USER_PATH_PARAM));
+        } catch (IllegalArgumentException e) {
+            throw badRequest("Invalid 'user' parameter", e);
+        }
+    }
+
+    private HaltException badRequest(String message, Exception e) {
+        return ErrorResponder.builder()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+            .message(message)
+            .cause(e)
+            .haltError();
+    }
+
     private UserVaultAction extractUserVaultAction(Request request) {
         String actionParam = request.queryParams(ACTION_QUERY_PARAM);
         return Optional.ofNullable(actionParam)
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
index f175c51..aa6efef 100644
--- 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
@@ -65,11 +65,11 @@ class RestoreService {
         this.mailboxManager = mailboxManager;
     }
 
-    Flux<RestoreResult> restore(User userToRestore) throws MailboxException {
+    Flux<RestoreResult> restore(User userToRestore, Query searchQuery) throws MailboxException {
         MailboxSession session = mailboxManager.createSystemSession(userToRestore.asString());
         MessageManager restoreMessageManager = restoreMailboxManager(session);
 
-        return Flux.from(deletedMessageVault.search(userToRestore, Query.ALL))
+        return Flux.from(deletedMessageVault.search(userToRestore, searchQuery))
             .flatMap(deletedMessage -> appendToMailbox(restoreMessageManager, deletedMessage, session));
     }
 
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java
index 2c0e9e1..1f258ba 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java
@@ -22,5 +22,5 @@ package org.apache.james.webadmin.vault.routes.query;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 
 @JsonDeserialize(using = QueryElementDeserializer.class)
-interface QueryElement {
+public interface QueryElement {
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java
index 33067d6..611c2a4 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java
@@ -38,6 +38,7 @@ import static org.apache.james.webadmin.vault.routes.query.QueryTranslator.Opera
 import static org.apache.james.webadmin.vault.routes.query.QueryTranslator.Operator.EQUALS_IGNORE_CASE;
 
 import java.time.ZonedDateTime;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Stream;
@@ -205,18 +206,28 @@ public class QueryTranslator {
             Operator.getOperator(dto.getOperator()));
     }
 
-    public Query translate(QueryDTO queryDTO) throws QueryTranslatorException {
-        Preconditions.checkArgument(isAndCombinator(queryDTO.getCombinator()), "combinator '" + queryDTO.getCombinator() + "' is not yet handled");
+    public Query translate(QueryElement queryElement) throws QueryTranslatorException {
+        if (queryElement instanceof QueryDTO) {
+            return translate((QueryDTO) queryElement);
+        } else if (queryElement instanceof CriterionDTO) {
+            return Query.of(translate((CriterionDTO) queryElement));
+        }
+        throw new IllegalArgumentException("cannot resolve query type: " + queryElement.getClass().getName());
+    }
+
+    Query translate(QueryDTO queryDTO) throws QueryTranslatorException {
+        Preconditions.checkArgument(combinatorIsValid(queryDTO.getCombinator()), "combinator '" + queryDTO.getCombinator() + "' is not yet handled");
         Preconditions.checkArgument(queryDTO.getCriteria().stream().allMatch(this::isCriterion), "nested query structure is not yet handled");
 
-        return Query.of(queryDTO.getCriteria().stream()
+        return Query.and(queryDTO.getCriteria().stream()
             .map(queryElement -> (CriterionDTO) queryElement)
             .map(Throwing.function(this::translate))
             .collect(Guavate.toImmutableList()));
     }
 
-    private boolean isAndCombinator(String combinator) {
-        return Combinator.AND.getValue().equals(combinator);
+    private boolean combinatorIsValid(String combinator) {
+        return Combinator.AND.getValue().equals(combinator)
+            || Objects.isNull(combinator);
     }
 
     private boolean isCriterion(QueryElement queryElement) {
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
index 460e3c2..1afa30d 100644
--- 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
@@ -21,14 +21,25 @@ 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.DELETION_DATE;
+import static org.apache.james.vault.DeletedMessageFixture.DELIVERY_DATE;
+import static org.apache.james.vault.DeletedMessageFixture.FINAL_STAGE;
+import static org.apache.james.vault.DeletedMessageFixture.MAILBOX_ID_1;
+import static org.apache.james.vault.DeletedMessageFixture.MAILBOX_ID_3;
+import static org.apache.james.vault.DeletedMessageFixture.SUBJECT;
 import static org.apache.james.vault.DeletedMessageFixture.USER;
 import static org.apache.james.vault.DeletedMessageFixture.USER_2;
+import static org.apache.james.vault.DeletedMessageVaultSearchContract.MESSAGE_ID_GENERATOR;
 import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION;
 import static org.apache.james.webadmin.vault.routes.RestoreService.RESTORE_MAILBOX_NAME;
+import static org.apache.mailet.base.MailAddressFixture.RECIPIENT1;
+import static org.apache.mailet.base.MailAddressFixture.RECIPIENT2;
+import static org.apache.mailet.base.MailAddressFixture.RECIPIENT3;
+import static org.apache.mailet.base.MailAddressFixture.SENDER;
+import static org.apache.mailet.base.MailAddressFixture.SENDER2;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
@@ -40,13 +51,17 @@ import static org.mockito.Mockito.spy;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.util.List;
+import java.util.stream.Stream;
 
+import org.apache.james.core.MaybeSender;
 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.InMemoryId;
 import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.inmemory.InMemoryMessageId;
 import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
 import org.apache.james.mailbox.model.FetchGroupImpl;
 import org.apache.james.mailbox.model.MailboxId;
@@ -57,6 +72,7 @@ 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.DeletedMessage;
 import org.apache.james.vault.memory.MemoryDeletedMessagesVault;
 import org.apache.james.vault.search.Query;
 import org.apache.james.webadmin.WebAdminServer;
@@ -64,6 +80,7 @@ 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.apache.james.webadmin.vault.routes.query.QueryTranslator;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -76,12 +93,19 @@ import com.google.common.collect.ImmutableList;
 import io.restassured.RestAssured;
 import io.restassured.filter.log.LogDetail;
 import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
 class DeletedMessagesVaultRoutesTest {
 
+    private static final String MATCH_ALL_QUERY = "{" +
+        "\"combinator\": \"and\"," +
+        "\"criteria\": []" +
+        "}";
+
     private WebAdminServer webAdminServer;
     private MemoryDeletedMessagesVault vault;
     private InMemoryMailboxManager mailboxManager;
+    private MemoryTaskManager taskManager;
 
     @BeforeEach
     void beforeEach() throws Exception {
@@ -90,14 +114,15 @@ class DeletedMessagesVaultRoutesTest {
         InMemoryIntegrationResources.Resources inMemoryResource = inMemoryIntegrationResources.createResources(new SimpleGroupMembershipResolver());
         mailboxManager = spy(inMemoryResource.getMailboxManager());
 
-        MemoryTaskManager taskManager = new MemoryTaskManager();
+        taskManager = new MemoryTaskManager();
         JsonTransformer jsonTransformer = new JsonTransformer();
 
         RestoreService vaultRestore = new RestoreService(vault, mailboxManager);
+        QueryTranslator queryTranslator = new QueryTranslator(new InMemoryId.Factory());
         webAdminServer = WebAdminUtils.createWebAdminServer(
             new DefaultMetricFactory(),
             new TasksRoutes(taskManager, jsonTransformer),
-            new DeletedMessagesVaultRoutes(vaultRestore, jsonTransformer, taskManager));
+            new DeletedMessagesVaultRoutes(vaultRestore, jsonTransformer, taskManager, queryTranslator));
 
         webAdminServer.configure(NO_CONFIGURATION);
         webAdminServer.await();
@@ -110,6 +135,947 @@ class DeletedMessagesVaultRoutesTest {
     @AfterEach
     void afterEach() {
         webAdminServer.destroy();
+        taskManager.stop();
+    }
+
+    @Nested
+    class QueryTest {
+
+        @Nested
+        class SubjectTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingSubjectContains() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("subject contains should match")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"contains\"," +
+                    "  \"value\": \"subject contains\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntContains() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("subject")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"contains\"," +
+                    "  \"value\": \"james\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingSubjectContainsIgnoreCase() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("SUBJECT contains should match")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"containsIgnoreCase\"," +
+                    "  \"value\": \"subject contains\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntContainsIgnoreCase() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("subject")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"containsIgnoreCase\"," +
+                    "  \"value\": \"JAMES\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingSubjectEquals() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("subject should match")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"subject should match\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntEquals() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("subject")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"SUBJECT\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingSubjectEqualsIgnoreCase() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("SUBJECT should MatCH")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"equalsIgnoreCase\"," +
+                    "  \"value\": \"subject should match\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntEqualsIgnoreCase() throws Exception {
+                vault.append(USER, FINAL_STAGE.get()
+                    .subject("subject")
+                    .build(), new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"subject\"," +
+                    "  \"operator\": \"equalsIgnoreCase\"," +
+                    "  \"value\": \"SUBJECT Of the mail\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class DeletionDateTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingDeletionDateBeforeOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deletionDate\"," +
+                    "  \"operator\": \"beforeOrEquals\"," +
+                    "  \"value\": \"" + DELETION_DATE.plusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeletionDateBeforeOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deletionDate\"," +
+                    "  \"operator\": \"beforeOrEquals\"," +
+                    "  \"value\": \"" + DELETION_DATE.minusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingDeletionDateAfterOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deletionDate\"," +
+                    "  \"operator\": \"afterOrEquals\"," +
+                    "  \"value\": \"" + DELETION_DATE.minusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeletionDateAfterOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deletionDate\"," +
+                    "  \"operator\": \"afterOrEquals\"," +
+                    "  \"value\": \"" + DELETION_DATE.plusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class DeliveryDateTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingDeliveryDateBeforeOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deliveryDate\"," +
+                    "  \"operator\": \"beforeOrEquals\"," +
+                    "  \"value\": \"" + DELIVERY_DATE.plusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeliveryDateBeforeOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deliveryDate\"," +
+                    "  \"operator\": \"beforeOrEquals\"," +
+                    "  \"value\": \"" + DELIVERY_DATE.minusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingDeliveryDateAfterOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deliveryDate\"," +
+                    "  \"operator\": \"afterOrEquals\"," +
+                    "  \"value\": \"" + DELIVERY_DATE.minusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeliveryDateAfterOrEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"deliveryDate\"," +
+                    "  \"operator\": \"afterOrEquals\"," +
+                    "  \"value\": \"" + DELIVERY_DATE.plusHours(1).toString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class RecipientsTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingRecipientContains() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"recipients\"," +
+                    "  \"operator\": \"contains\"," +
+                    "  \"value\": \"" + RECIPIENT1.asString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenMatchingRecipientsDoNotContain() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"recipients\"," +
+                    "  \"operator\": \"contains\"," +
+                    "  \"value\": \"" + RECIPIENT3.asString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class SenderTest {
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingSenderEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"sender\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"" + SENDER.asString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingSenderDoesntEquals() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"sender\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"" + SENDER2.asString() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class HasAttachmentTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingNoAttachment() throws Exception {
+                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
+                    .hasAttachment(false)
+                    .build();
+                storeDeletedMessage(deletedMessage);
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"hasAttachment\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"false\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenMatchingHasAttachment() throws Exception {
+                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
+                    .hasAttachment()
+                    .build();
+                storeDeletedMessage(deletedMessage);
+
+                String query =
+                    " {" +
+                    "  \"fieldName\": \"hasAttachment\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"true\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenMatchingHasNoAttachment() throws Exception {
+                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
+                    .hasAttachment(false)
+                    .build();
+                storeDeletedMessage(deletedMessage);
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"hasAttachment\"," +
+                    "  \"operator\": \"equals\"," +
+                    "  \"value\": \"true\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class OriginMailboxIdsTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenContainsMailboxId() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"originMailboxes\"," +
+                    "  \"operator\": \"contains\"," +
+                    "  \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(1)
+                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenDoNotContainsMailboxId() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+                String query =
+                    "{" +
+                    "  \"fieldName\": \"originMailboxes\"," +
+                    "  \"operator\": \"contains\"," +
+                    "  \"value\": \"" + MAILBOX_ID_3.serialize() + "\"" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
+
+        @Nested
+        class MultipleCriteriaTest {
+
+            @Test
+            void restoreShouldAppendMessageToMailboxWhenAllcriteriaAreMatched() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+                vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
+
+                String query = "" +
+                    "{" +
+                    "  \"combinator\": \"and\"," +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"deliveryDate\"," +
+                    "      \"operator\": \"beforeOrEquals\"," +
+                    "      \"value\": \"" + DELIVERY_DATE.toString() + "\"" +
+                    "    }," +
+                    "    {" +
+                    "      \"fieldName\": \"recipients\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"" + RECIPIENT1.asString() + "\"" +
+                    "    }," +
+                    "    {" +
+                    "      \"fieldName\": \"hasAttachment\"," +
+                    "      \"operator\": \"equals\"," +
+                    "      \"value\": \"false\"" +
+                    "    }," +
+                    "    {" +
+                    "      \"fieldName\": \"originMailboxes\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" +
+                    "    }" +
+                    "  ]" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(restoreMessageContents(USER))
+                    .hasSize(2)
+                    .allSatisfy(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+            }
+
+            @Test
+            void restoreShouldNotAppendMessageToMailboxWhenASingleCriterionDoesntMatch() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+                vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
+
+                String query = "" +
+                    "{" +
+                    "  \"combinator\": \"and\"," +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"deliveryDate\"," +
+                    "      \"operator\": \"beforeOrEquals\"," +
+                    "      \"value\": \"" + DELIVERY_DATE.toString() + "\"" +
+                    "    }," +
+                    "    {" +
+                    "      \"fieldName\": \"recipients\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"allMessageDoNotHaveThisRecipient@domain.tld\"" +
+                    "    }," +
+                    "    {" +
+                    "      \"fieldName\": \"hasAttachment\"," +
+                    "      \"operator\": \"equals\"," +
+                    "      \"value\": \"false\"" +
+                    "    }," +
+                    "    {" +
+                    "      \"fieldName\": \"originMailboxes\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" +
+                    "    }" +
+                    "  ]" +
+                    "}";
+
+                String taskId =
+                    given()
+                        .queryParam("action", "restore")
+                        .body(query)
+                    .when()
+                        .post(USER.asString())
+                        .jsonPath()
+                        .get("taskId");
+
+                given()
+                    .basePath(TasksRoutes.BASE)
+                .when()
+                    .get(taskId + "/await")
+                .then()
+                    .body("status", is("completed"));
+
+                assertThat(hasAnyMail(USER)).isFalse();
+            }
+        }
     }
 
     @Nested
@@ -192,8 +1158,175 @@ class DeletedMessagesVaultRoutesTest {
             .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"));
+                .body("type", is(notNullValue()))
+                .body("message", is(notNullValue()));
+        }
+
+        @Test
+        void restoreShouldReturnBadRequestWhenPassingUnsupportedField() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"unsupported\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .body(query)
+            .when()
+                .post(USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is(notNullValue()))
+                .body("details", is(notNullValue()));
+        }
+
+        @Test
+        void restoreShouldReturnBadRequestWhenPassingUnsupportedOperator() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"subject\"," +
+                "      \"operator\": \"isLongerThan\"," +
+                "      \"value\": \"" + SUBJECT + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .body(query)
+            .when()
+                .post(USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is(notNullValue()))
+                .body("details", is(notNullValue()));
+        }
+
+        @Test
+        void restoreShouldReturnBadRequestWhenPassingUnsupportedPairOfFieldNameAndOperator() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"sender\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"" + SENDER.asString() + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .body(query)
+            .when()
+                .post(USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is(notNullValue()))
+                .body("details", is(notNullValue()));
+        }
+
+        @Test
+        void restoreShouldReturnBadRequestWhenPassingInvalidMailAddress() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"sender\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"invalid@mail@domain.tld\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .body(query)
+            .when()
+                .post(USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is(notNullValue()))
+                .body("details", is(notNullValue()));
+        }
+
+        @Test
+        void restoreShouldReturnBadRequestWhenPassingOrCombinator() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"combinator\": \"or\"," +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"sender\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"" + SENDER.asString() + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .body(query)
+            .when()
+                .post(USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is(notNullValue()))
+                .body("details", is(notNullValue()));
+        }
+
+        @Test
+        void restoreShouldReturnBadRequestWhenPassingNestedStructuredQuery() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"combinator\": \"and\"," +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"combinator\": \"or\"," +
+                "      \"criteria\": [" +
+                "        {\"fieldName\": \"subject\", \"operator\": \"containsIgnoreCase\", \"value\": \"Apache James\"}," +
+                "        {\"fieldName\": \"subject\", \"operator\": \"containsIgnoreCase\", \"value\": \"Apache James\"}" +
+                "      ]" +
+                "    }," +
+                "    {\"fieldName\": \"subject\", \"operator\": \"containsIgnoreCase\", \"value\": \"Apache James\"}" +
+                "  ]" +
+                "}";
+
+            given()
+                .body(query)
+            .when()
+                .post(USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is(notNullValue()))
+                .body("details", is(notNullValue()));
         }
     }
 
@@ -209,11 +1342,14 @@ class DeletedMessagesVaultRoutesTest {
                 .when(vault)
                 .search(any(), any());
 
-            String taskId = with()
-                .queryParam("action", "restore")
-                .post(USER.asString())
-                .jsonPath()
-                .get("taskId");
+            String taskId =
+                given()
+                    .queryParam("action", "restore")
+                    .body(MATCH_ALL_QUERY)
+                .when()
+                    .post(USER.asString())
+                    .jsonPath()
+                    .get("taskId");
 
             given()
                 .queryParam("action", "restore")
@@ -245,11 +1381,14 @@ class DeletedMessagesVaultRoutesTest {
                 .when(mockMessageManager)
                 .appendMessage(any(), any());
 
-            String taskId = with()
-                .queryParam("action", "restore")
-                .post(USER.asString())
-                .jsonPath()
-                .get("taskId");
+            String taskId =
+                given()
+                    .queryParam("action", "restore")
+                    .body(MATCH_ALL_QUERY)
+                .when()
+                    .post(USER.asString())
+                    .jsonPath()
+                    .get("taskId");
 
             given()
                 .basePath(TasksRoutes.BASE)
@@ -275,11 +1414,14 @@ class DeletedMessagesVaultRoutesTest {
                 .when(mailboxManager)
                 .createMailbox(any(MailboxPath.class), any(MailboxSession.class));
 
-            String taskId = with()
-                .queryParam("action", "restore")
-                .post(USER.asString())
-                .jsonPath()
-                .get("taskId");
+            String taskId =
+                given()
+                    .queryParam("action", "restore")
+                    .body(MATCH_ALL_QUERY)
+                .when()
+                    .post(USER.asString())
+                    .jsonPath()
+                    .get("taskId");
 
             given()
                 .basePath(TasksRoutes.BASE)
@@ -301,6 +1443,7 @@ class DeletedMessagesVaultRoutesTest {
     void restoreShouldReturnATaskCreated() {
         given()
             .queryParam("action", "restore")
+            .body(MATCH_ALL_QUERY)
         .when()
             .post(USER.asString())
         .then()
@@ -313,11 +1456,14 @@ class DeletedMessagesVaultRoutesTest {
         vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
         vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-        String taskId = with()
-            .queryParam("action", "restore")
-            .post(USER.asString())
-            .jsonPath()
-            .get("taskId");
+        String taskId =
+            given()
+                .queryParam("action", "restore")
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(USER.asString())
+                .jsonPath()
+                .get("taskId");
 
         given()
             .basePath(TasksRoutes.BASE)
@@ -340,11 +1486,14 @@ class DeletedMessagesVaultRoutesTest {
         vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
         vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-        String taskId = with()
-            .queryParam("action", "restore")
-            .post(USER.asString())
-            .jsonPath()
-            .get("taskId");
+        String taskId =
+            given()
+                .queryParam("action", "restore")
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(USER.asString())
+                .jsonPath()
+                .get("taskId");
 
         given()
             .basePath(TasksRoutes.BASE)
@@ -370,11 +1519,14 @@ class DeletedMessagesVaultRoutesTest {
         vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
         vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-        String taskId = with()
-            .queryParam("action", "restore")
-            .post(USER.asString())
-            .jsonPath()
-            .get("taskId");
+        String taskId =
+            given()
+                .queryParam("action", "restore")
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(USER.asString())
+                .jsonPath()
+                .get("taskId");
 
         given()
             .basePath(TasksRoutes.BASE)
@@ -392,11 +1544,14 @@ class DeletedMessagesVaultRoutesTest {
         vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
         vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-        String taskId = with()
-            .queryParam("action", "restore")
-            .post(USER.asString())
-            .jsonPath()
-            .get("taskId");
+        String taskId =
+            given()
+                .queryParam("action", "restore")
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(USER.asString())
+                .jsonPath()
+                .get("taskId");
 
         given()
             .basePath(TasksRoutes.BASE)
@@ -416,11 +1571,14 @@ class DeletedMessagesVaultRoutesTest {
         vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
         vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-        String taskId = with()
-            .queryParam("action", "restore")
-            .post(USER.asString())
-            .jsonPath()
-            .get("taskId");
+        String taskId =
+            given()
+                .queryParam("action", "restore")
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(USER.asString())
+                .jsonPath()
+                .get("taskId");
 
         given()
             .basePath(TasksRoutes.BASE)
@@ -449,9 +1607,31 @@ class DeletedMessagesVaultRoutesTest {
         }
     }
 
+    private Stream<InputStream> restoreMessageContents(User user) throws Exception {
+        return restoreMailboxMessages(user).stream()
+            .map(this::fullContent);
+    }
+
     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));
     }
+
+    private DeletedMessage.Builder.RequireHasAttachment<DeletedMessage.Builder.FinalStage> messageWithAttachmentBuilder() {
+        return DeletedMessage.builder()
+            .messageId(InMemoryMessageId.of(MESSAGE_ID_GENERATOR.incrementAndGet()))
+            .originMailboxes(MAILBOX_ID_1)
+            .user(USER)
+            .deliveryDate(DELIVERY_DATE)
+            .deletionDate(DELETION_DATE)
+            .sender(MaybeSender.of(SENDER))
+            .recipients(RECIPIENT1, RECIPIENT2);
+    }
+
+    private DeletedMessage storeDeletedMessage(DeletedMessage deletedMessage) {
+        Mono.from(vault.append(USER, deletedMessage, new ByteArrayInputStream(CONTENT)))
+            .block();
+        return deletedMessage;
+    }
 }
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java
index 6689781..f159503 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java
@@ -47,6 +47,13 @@ class QueryTranslatorTest {
     }
 
     @Test
+    void translateShouldNotThrowWhenPassingNullOperator() {
+        String nullOperator = null;
+        assertThatCode(() -> queryTranslator.translate(new QueryDTO(nullOperator, ImmutableList.of())))
+            .doesNotThrowAnyException();
+    }
+
+    @Test
     void translateShouldThrowWhenPassingNestedQuery() {
         assertThatThrownBy(() -> queryTranslator.translate(QueryDTO.and(
                 QueryDTO.and(new CriterionDTO(FieldName.SUBJECT.getValue(), Operator.CONTAINS.getValue(), "james"))
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index a660766..f039c15 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -2561,14 +2561,104 @@ Deleted messages of a specific user can be restored by calling the following end
 
 ```
 curl -XPOST http://ip:port/deletedMessages/user/userToRestore@domain.ext?action=restore
+
+{"
+  "combinator": "and",
+  "criteria": [
+    {
+      "fieldName": "subject",
+      "operator": "containsIgnoreCase",
+      "value": "Apache James"
+    },
+    {
+      "fieldName": "deliveryDate",
+      "operator": "beforeOrEquals",
+      "value": "2014-10-30T14:12:00Z"
+    },
+    {
+      "fieldName": "deletionDate",
+      "operator": "afterOrEquals",
+      "value": "2015-10-20T09:08:00Z"
+    },
+    {
+      "fieldName": "recipients","
+      "operator": "contains","
+      "value": "recipient@james.org"
+    },
+    {
+      "fieldName": "hasAttachment",
+      "operator": "equals",
+      "value": "false"
+    },
+    {
+      "fieldName": "sender",
+      "operator": "equals",
+      "value": "sender@apache.org"
+    },
+    {
+      "fieldName": "originMailboxes",
+      "operator": "contains",
+      "value":  "02874f7c-d10e-102f-acda-0015176f7922"
+    }
+  ]
+};
 ```
 
-**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.
+The requested Json body is made from list of criterion objects which have following structure:
+```
+{
+  "fieldName": "supportedFieldName",
+  "operator": "supportedOperator",
+  "testedValue": "plain string represents for the matching value of corresponding field"
+}
+```
+Deleted Messages which are matched with **all** criterions in the query body will be restored. Here are list of supported fieldName for the restoring:
+ - subject: represents for deleted message `subject` field matching. Supports below string operators:
+   - contains
+   - containsIgnoreCase
+   - equals
+   - equalsIgnoreCase
+ - deliveryDate: represents for deleted message `deliveryDate` field matching. Tested value should follow the right date time with zone offset format (ISO-8601) like
+   `2008-09-15T15:53:00+05:00` or `2008-09-15T15:53:00Z` 
+   Supports below date time operators:
+   - beforeOrEquals: is the deleted message's `deliveryDate` before or equals the time of tested value.
+   - afterOrEquals: is the deleted message's `deliveryDate` after or equals the time of tested value
+ - deletionDate: represents for deleted message `deletionDate` field matching. Tested value & Supports operators: similar to `deliveryDate`
+ - sender: represents for deleted message `sender` field matching. Tested value should be a valid mail address. Supports mail address operator:
+   - equals: does the tested sender equal to the sender of the tested deleted message ?   
+ - recipients: represents for deleted message `recipients` field matching. Tested value should be a valid mail address. Supports list mail address operator:
+   - contains: does the tested deleted message's recipients contain tested recipient ?
+ - hasAttachment: represents for deleted message `hasAttachment` field matching. Tested value could be `false` or `true`. Supports boolean operator:
+   - equals: does the tested deleted message's hasAttachment property equal to the tested hasAttachment value?
+ - originMailboxes: represents for deleted message `originMailboxes` field matching. Tested value is a string serialized of mailbox id. Supports list mailbox id operators:
+   - contains: does the tested deleted message's originMailbox ids contain tested mailbox id ?
+   
+Messages in the Deleted Messages Vault of an specified user that are matched with Query Json Object in the body will be appended to his 'Restored-Messages' mailbox, which will be created if needed.
 
-**Note**: 
- - Restoring matched messages by queries is not supported yet 
+**Note**:
  - Query parameter `action` is required and should have value `restore` to represent for restoring feature. Otherwise, a bad request response will be returned
  - Query parameter `action` is case sensitive
+ - fieldName & operator for passing to the routes are case sensitive
+ - Currently, we only support query combinator `and` value, otherwise, requests will be rejected 
+ - If you only want to restore by only one criterion, the json body could be simplified to a single criterion:
+```
+{
+  "fieldName": "subject", 
+  "operator": "containsIgnoreCase", 
+  "value": "Apache James"
+}
+```
+ - For restoring all deleted messages, passing a query json with empty criterion list to represent `matching all deleted messages`: 
+```
+{
+  "combinator": "and",
+  "criteria": []
+}
+```
+
+**Warning**: Current web-admin uses `US` locale as the default. Therefore, there might be some conflicts when using String `containsIgnoreCase` comparators to apply 
+on the String data of other special locales stored in the Vault. More details at [JIRA](https://issues.apache.org/jira/browse/MAILBOX-384) 
+
 Response code:
 
  - 201: Task for restoring deleted has been created
@@ -2576,13 +2666,16 @@ Response code:
    - action query param is not present
    - action query param is not a valid action
    - user parameter is invalid
+   - can not parse the JSON body
+   - Json query object contains unsupported operator, fieldName
+   - Json query object values violate parsing rules 
 
 The scheduled task will have the following type `deletedMessages/restore` and the following `additionalInformation`:
 
 ```
 {
   "successfulRestoreCount": 47,
-  "errorRestoreCount": 0
+  "errorRestoreCount": 0,
   "user": "userToRestore@domain.ext"
 }
 ```


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org