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/28 03:21:44 UTC

[james-project] branch master updated (a6081b5 -> 08ef402)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from a6081b5  Merge remote-tracking branch 'btellier/3.3.0-website-release'
     new c67fb17  JAMES-2685 make usages component become public
     new 205626f  JAMES-2685 Moving `Restore` tests to a nested class
     new 9d8b470  JAMES-2685 DMV Route export API
     new c0c1ca3  JAMES-2685 DMV Route webadmin markdown
     new 85cfd17  JAMES-2685 revise user vault api path
     new 994c58f  JAMES-2685 BlobMemoryModule for MemoryBlobStore
     new f52c3b6  JAMES-2685 LocalFileBlobExportModule for blob sharing
     new 2aa3780  JAMES-2685 Zipper.ContentLoader should return Optional<InputStream>
     new 02a63a3  JAMES-2702 TimeConverter is more a DurationParser
     new 1497fb2  JAMES-2702 Improve DurationParser exception handling
     new d983389  JAMES-2702 Improve DurationParser tests
     new a142a23  JAMES-2702 DurationParser should handle weeks, months and years
     new 1fe0d84  MAILBOX-388 Add runnable -> function util
     new 85d3b92  MAILBOX-388 FunctionUtils: turn toPredicate into identityPredicate
     new dde54c8  MAILBOX-388 MailRepositoryPath::hasPrefix
     new c394009  MAILBOX-388 MailRepositoryPath::parts
     new cb97101  MAILBOX-388 MailRepositoryUrl::hasPrefix
     new cfa2105  MAILBOX-388 DeletedMessagesVault::usersWithVault
     new 948f85b  MAILBOX-388 Add a VaultConfiguration object
     new 488faff  MAILBOX-388 Implement DeletedMessagesVault retention
     new e2e9f9c  MAILBOX-388 Error handling testing for DeleteByQueryExecutor
     new c89529e  MAILBOX-388 Move vault helper class out of Memory implementation package
     new 08ef402  MAILBOX-373 deadLetter::remove operation was not bound

The 23 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../custom/mailets/IsDelayedForMoreThan.java       |    4 +-
 .../mailbox/cassandra/mail/CassandraACLMapper.java |    5 +-
 .../cassandra/mail/CassandraMessageIdMapper.java   |    3 +-
 .../cassandra/mail/CassandraModSeqProvider.java    |    4 +-
 mailbox/plugin/deleted-messages-vault/pom.xml      |    1 +
 .../apache/james/vault/DeletedMessageVault.java    |    5 +
 .../apache/james/vault/DeletedMessageZipper.java   |   24 +-
 .../apache/james/vault/RetentionConfiguration.java |   61 +-
 .../vault/memory/MemoryDeletedMessagesVault.java   |   36 +-
 .../james/vault/utils/DeleteByQueryExecutor.java   |   98 +
 .../vault/utils/VaultGarbageCollectionTask.java    |  115 +
 .../apache/james/vault/DeletedMessageFixture.java  |   15 +
 .../james/vault/DeletedMessageVaultContract.java   |   84 +
 .../james/vault/DeletedMessageVaultHookTest.java   |    2 +-
 .../james/vault/DeletedMessageZipperTest.java      |   26 +-
 .../james/vault/RetentionConfigurationTest.java    |   61 +-
 .../memory/MemoryDeletedMessagesVaultTest.java     |    3 +-
 .../vault/utils/DeleteByQueryExecutorTest.java     |  180 ++
 .../mailing/QuotaMailingListenerConfiguration.java |    4 +-
 .../transport/matchers/SMTPIsAuthNetwork.java      |    3 +-
 pom.xml                                            |   15 +
 .../export/file/LocalFileBlobExportMechanism.java  |    7 +
 .../file/LocalFileBlobExportMechanismTest.java     |    5 +-
 .../apache/james/blob/memory/MemoryBlobStore.java  |    4 +
 .../guice/blob-export-guice}/pom.xml               |   15 +-
 .../LocalFileBlobExportMechanismModule.java}       |   12 +-
 .../guice/blob-memory-guice}/pom.xml               |   16 +-
 .../apache/james/modules/BlobMemoryModule.java}    |   10 +-
 server/container/guice/cassandra-guice/pom.xml     |    4 +
 .../org/apache/james/CassandraJamesServerMain.java |    4 +-
 .../modules/mailbox/TikaConfigurationReader.java   |    4 +-
 .../modules/vault/DeletedMessageVaultModule.java   |   14 +
 server/container/guice/memory-guice/pom.xml        |    8 +
 .../org/apache/james/MemoryJamesServerMain.java    |    8 +-
 server/container/guice/pom.xml                     |    2 +
 server/container/util/pom.xml                      |    5 +
 .../{TimeConverter.java => DurationParser.java}    |   25 +-
 .../org/apache/james/util/FunctionalUtils.java     |   12 +-
 .../org/apache/james/util/DurationParserTest.java  |  159 ++
 .../org/apache/james/util/FunctionalUtilsTest.java |   42 +-
 .../org/apache/james/util/TimeConverterTest.java   |  180 --
 .../sieve/cassandra/CassandraSieveRepository.java  |    3 +-
 .../transport/mailets/remote/delivery/Delay.java   |    8 +-
 .../vault/MailRepositoryDeletedMessageVault.java   |   46 +-
 .../MailRepositoryDeletedMessageVaultTest.java     |    3 +-
 .../mailrepository/api/MailRepositoryPath.java     |   18 +
 .../mailrepository/api/MailRepositoryUrl.java      |    5 +
 .../mailrepository/api/MailRepositoryPathTest.java |   96 +
 .../mailrepository/api/MailRepositoryUrlTest.java  |   72 +
 .../integration/DeletedMessagesVaultTest.java      |    2 +-
 .../memory-jmap-integration-testing/pom.xml        |    5 +
 .../james/smtpserver/POP3BeforeSMTPHandler.java    |    4 +-
 .../smtpserver/fastfail/JDBCGreylistHandler.java   |    8 +-
 .../webadmin-mailbox-deleted-message-vault/pom.xml |   27 +
 .../routes/DeletedMessagesVaultExportTask.java     |  100 +
 .../vault/routes/DeletedMessagesVaultRoutes.java   |  101 +-
 .../james/webadmin/vault/routes/ExportService.java |   86 +
 .../routes/DeletedMessagesVaultRoutesTest.java     | 2575 +++++++++++---------
 .../service/EventDeadLettersRedeliverService.java  |    6 +-
 .../CassandraMailQueueViewConfiguration.java       |    4 +-
 src/site/markdown/server/manage-webadmin.md        |   44 +-
 61 files changed, 2966 insertions(+), 1532 deletions(-)
 copy server/container/util/src/main/java/org/apache/james/util/streams/Limit.java => mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/RetentionConfiguration.java (55%)
 create mode 100644 mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/DeleteByQueryExecutor.java
 create mode 100644 mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/VaultGarbageCollectionTask.java
 copy server/container/guice/jmx/src/test/java/org/apache/james/modules/server/JmxConfigurationTest.java => mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/RetentionConfigurationTest.java (53%)
 create mode 100644 mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/utils/DeleteByQueryExecutorTest.java
 copy server/{blob/blob-export-api => container/guice/blob-export-guice}/pom.xml (76%)
 copy server/container/guice/{guice-common/src/main/java/org/apache/james/modules/MimeMessageModule.java => blob-export-guice/src/main/java/org/apache/james/modules/LocalFileBlobExportMechanismModule.java} (71%)
 copy server/{blob/blob-export-api => container/guice/blob-memory-guice}/pom.xml (77%)
 copy server/container/guice/{blob-api-guice/src/main/java/org/apache/james/modules/mailbox/BlobStoreAPIModule.java => blob-memory-guice/src/main/java/org/apache/james/modules/BlobMemoryModule.java} (85%)
 rename server/container/util/src/main/java/org/apache/james/util/{TimeConverter.java => DurationParser.java} (82%)
 create mode 100644 server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
 delete mode 100644 server/container/util/src/test/java/org/apache/james/util/TimeConverterTest.java
 create mode 100644 server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
 create mode 100644 server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java


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


[james-project] 18/23: MAILBOX-388 DeletedMessagesVault::usersWithVault

Posted by bt...@apache.org.
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 cfa210532f774947657a97abb422774c129b98c4
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 15:51:02 2019 +0700

    MAILBOX-388 DeletedMessagesVault::usersWithVault
---
 .../java/org/apache/james/vault/DeletedMessageVault.java  |  2 ++
 .../james/vault/memory/MemoryDeletedMessagesVault.java    |  8 ++++++++
 .../apache/james/vault/DeletedMessageVaultContract.java   | 15 +++++++++++++++
 .../james/vault/MailRepositoryDeletedMessageVault.java    | 14 ++++++++++++++
 4 files changed, 39 insertions(+)

diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java
index ff10a63..54d3579 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java
@@ -34,4 +34,6 @@ public interface DeletedMessageVault {
     Publisher<Void> delete(User user, MessageId messageId);
 
     Publisher<DeletedMessage> search(User user, Query query);
+
+    Publisher<User> usersWithVault();
 }
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 7e9faa3..3389dcb 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
@@ -31,6 +31,7 @@ import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.vault.DeletedMessage;
 import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.search.Query;
+import org.reactivestreams.Publisher;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.HashBasedTable;
@@ -96,6 +97,13 @@ public class MemoryDeletedMessagesVault implements DeletedMessageVault {
             .filter(query.toPredicate());
     }
 
+    @Override
+    public Publisher<User> usersWithVault() {
+        synchronized (table) {
+            return Flux.fromIterable(ImmutableList.copyOf(table.rowKeySet()));
+        }
+    }
+
     private Flux<DeletedMessage> listAll(User user) {
         synchronized (table) {
             return Flux.fromIterable(ImmutableList.copyOf(table.row(user).values()))
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java
index 7405c3e..0f4b3c6 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java
@@ -164,6 +164,21 @@ public interface DeletedMessageVaultContract {
     }
 
     @Test
+    default void usersWithVaultShouldReturnEmptyWhenNoItem() {
+        assertThat(Flux.from(getVault().usersWithVault()).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    default void usersWithVaultShouldReturnAllUsers() {
+        Mono.from(getVault().append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(getVault().append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        assertThat(Flux.from(getVault().usersWithVault()).collectList().block())
+            .containsOnly(USER, USER_2);
+    }
+
+    @Test
     default void appendShouldRunSuccessfullyInAConcurrentContext() throws Exception {
         int operationCount = 10;
         int threadCount = 10;
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
index bd1943c..a99014b 100644
--- a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
+++ b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
@@ -28,6 +28,7 @@ import org.apache.james.core.User;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailrepository.api.MailKey;
 import org.apache.james.mailrepository.api.MailRepository;
+import org.apache.james.mailrepository.api.MailRepositoryPath;
 import org.apache.james.mailrepository.api.MailRepositoryStore;
 import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.server.core.MimeMessageInputStream;
@@ -37,7 +38,9 @@ import org.reactivestreams.Publisher;
 
 import com.github.fge.lambdas.Throwing;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Schedulers;
 
@@ -124,6 +127,17 @@ public class MailRepositoryDeletedMessageVault implements DeletedMessageVault {
         }
     }
 
+    @Override
+    public Publisher<User> usersWithVault() {
+        return Flux.fromStream(mailRepositoryStore.getUrls()
+            .filter(url -> url.hasPrefix(configuration.urlPrefix))
+            .map(MailRepositoryUrl::getPath)
+            .map(MailRepositoryPath::parts)
+            .peek(parts -> Preconditions.checkState(!parts.isEmpty()))
+            .map(Iterables::getLast)
+            .map(User::fromUsername));
+    }
+
     private MailRepository repositoryForUser(User user) {
         MailRepositoryUrl mailRepositoryUrl = configuration.urlPrefix.subUrl(user.asString());
 


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


[james-project] 03/23: JAMES-2685 DMV Route export API

Posted by bt...@apache.org.
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 9d8b47050eafc39f489d10ccc6dca805a5a5b3ef
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Thu Mar 14 17:10:36 2019 +0700

    JAMES-2685 DMV Route export API
---
 .../webadmin-mailbox-deleted-message-vault/pom.xml |  27 +
 .../routes/DeletedMessagesVaultExportTask.java     | 100 +++
 .../vault/routes/DeletedMessagesVaultRoutes.java   |  99 ++-
 .../james/webadmin/vault/routes/ExportService.java | 128 ++++
 .../routes/DeletedMessagesVaultRoutesTest.java     | 759 ++++++++++++++-------
 5 files changed, 841 insertions(+), 272 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml
index 2c06be8..50adb89 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/pom.xml
@@ -71,12 +71,34 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>blob-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>blob-memory</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>blob-export-api</artifactId>
+        </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-data-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-memory</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-webadmin-core</artifactId>
         </dependency>
         <dependency>
@@ -130,6 +152,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.hamcrest</groupId>
             <artifactId>java-hamcrest</artifactId>
             <scope>test</scope>
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
new file mode 100644
index 0000000..de4204b
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
@@ -0,0 +1,100 @@
+/****************************************************************
+ * 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 java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.User;
+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;
+
+class DeletedMessagesVaultExportTask implements Task {
+
+    public class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+
+        private final User userExportFrom;
+        private final MailAddress exportTo;
+        private final long totalExportedMessages;
+
+        public AdditionalInformation(User userExportFrom, MailAddress exportTo, long totalExportedMessages) {
+            this.userExportFrom = userExportFrom;
+            this.exportTo = exportTo;
+            this.totalExportedMessages = totalExportedMessages;
+        }
+
+        public String getUserExportFrom() {
+            return userExportFrom.asString();
+        }
+
+        public String getExportTo() {
+            return exportTo.asString();
+        }
+
+        public long getTotalExportedMessages() {
+            return totalExportedMessages;
+        }
+    }
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(DeletedMessagesVaultExportTask.class);
+
+    static final String TYPE = "deletedMessages/export";
+
+    private final ExportService exportService;
+    private final User userExportFrom;
+    private final Query exportQuery;
+    private final MailAddress exportTo;
+    private final AtomicLong totalExportedMessages;
+
+    DeletedMessagesVaultExportTask(ExportService exportService, User userExportFrom, Query exportQuery, MailAddress exportTo) {
+        this.exportService = exportService;
+        this.userExportFrom = userExportFrom;
+        this.exportQuery = exportQuery;
+        this.exportTo = exportTo;
+        this.totalExportedMessages = new AtomicLong();
+    }
+
+    @Override
+    public Result run() {
+        try {
+            Runnable messageToShareCallback = totalExportedMessages::incrementAndGet;
+            exportService.export(userExportFrom, exportQuery, exportTo, messageToShareCallback)
+                .block();
+            return Result.COMPLETED;
+        } catch (Exception e) {
+            LOGGER.error("Error happens when exporting deleted messages from {} to {}", userExportFrom.asString(), exportTo.asString());
+            return Result.PARTIAL;
+        }
+    }
+
+    @Override
+    public String type() {
+        return TYPE;
+    }
+
+    @Override
+    public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+        return Optional.of(new AdditionalInformation(userExportFrom, exportTo, totalExportedMessages.get()));
+    }
+}
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 527b8d9..ba8864c 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
@@ -26,16 +26,20 @@ import java.util.Optional;
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
+import javax.mail.internet.AddressException;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 
 import org.apache.commons.lang3.NotImplementedException;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.james.core.MailAddress;
 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.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.vault.search.Query;
 import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
@@ -70,7 +74,8 @@ import spark.Service;
 public class DeletedMessagesVaultRoutes implements Routes {
 
     enum UserVaultAction {
-        RESTORE("restore");
+        RESTORE("restore"),
+        EXPORT("export");
 
         static Optional<UserVaultAction> getAction(String value) {
             Preconditions.checkNotNull(value, "action cannot be null");
@@ -102,21 +107,26 @@ public class DeletedMessagesVaultRoutes implements Routes {
     private static final String USER_PATH_PARAM = "user";
     private static final String RESTORE_PATH = ROOT_PATH + SEPARATOR + ":" + USER_PATH_PARAM;
     private static final String ACTION_QUERY_PARAM = "action";
+    private static final String EXPORT_TO_QUERY_PARAM = "exportTo";
 
     private final RestoreService vaultRestore;
+    private final ExportService vaultExport;
     private final JsonTransformer jsonTransformer;
     private final TaskManager taskManager;
     private final JsonExtractor<QueryElement> jsonExtractor;
     private final QueryTranslator queryTranslator;
+    private final UsersRepository usersRepository;
 
     @Inject
     @VisibleForTesting
-    DeletedMessagesVaultRoutes(RestoreService vaultRestore, JsonTransformer jsonTransformer,
-                               TaskManager taskManager, QueryTranslator queryTranslator) {
+    DeletedMessagesVaultRoutes(RestoreService vaultRestore, ExportService vaultExport, JsonTransformer jsonTransformer,
+                               TaskManager taskManager, QueryTranslator queryTranslator, UsersRepository usersRepository) {
         this.vaultRestore = vaultRestore;
+        this.vaultExport = vaultExport;
         this.jsonTransformer = jsonTransformer;
         this.taskManager = taskManager;
         this.queryTranslator = queryTranslator;
+        this.usersRepository = usersRepository;
         this.jsonExtractor = new JsonExtractor<>(QueryElement.class);
     }
 
@@ -149,11 +159,19 @@ public class DeletedMessagesVaultRoutes implements Routes {
             paramType = "query",
             example = "?action=restore",
             value = "Compulsory. Needs to be a valid action represent for an operation to perform on the Deleted Message Vault, " +
-                "valid action should be in the list (restore)")
+                "valid action should be in the list (restore, export)"),
+        @ApiImplicitParam(
+            dataType = "String",
+            name = "exportTo",
+            paramType = "query",
+            example = "?exportTo=user@james.org",
+            value = "Compulsory if action is export. Needs to be a valid mail address to represent for the destination " +
+                "where deleted messages content is export to")
     })
     @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.NOT_FOUND_404, message = "Not found - requested user is not existed in the system"),
         @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) throws JsonExtractException {
@@ -165,22 +183,73 @@ public class DeletedMessagesVaultRoutes implements Routes {
     }
 
     private Task generateTask(UserVaultAction requestedAction, Request request) throws JsonExtractException {
-        User userToRestore = extractUser(request);
+        User user = extractUser(request);
+        validateUserExist(user);
         Query query = translate(jsonExtractor.parse(request.body()));
 
         switch (requestedAction) {
             case RESTORE:
-                return new DeletedMessagesVaultRestoreTask(vaultRestore, userToRestore, query);
+                return new DeletedMessagesVaultRestoreTask(vaultRestore, user, query);
+            case EXPORT:
+                return new DeletedMessagesVaultExportTask(vaultExport, user, query, extractMailAddress(request));
             default:
                 throw new NotImplementedException(requestedAction + " is not yet handled.");
         }
     }
 
+    private void validateUserExist(User user) {
+        try {
+            if (!usersRepository.contains(user.asString())) {
+                throw ErrorResponder.builder()
+                    .statusCode(HttpStatus.NOT_FOUND_404)
+                    .type(ErrorResponder.ErrorType.NOT_FOUND)
+                    .message("User '" + user.asString() + "' does not exist in the system")
+                    .haltError();
+            }
+        } catch (UsersRepositoryException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .type(ErrorResponder.ErrorType.SERVER_ERROR)
+                .message("Unable to validate 'user' parameter")
+                .cause(e)
+                .haltError();
+        }
+    }
+
+    private MailAddress extractMailAddress(Request request) {
+        return Optional.ofNullable(request.queryParams(EXPORT_TO_QUERY_PARAM))
+            .filter(StringUtils::isNotBlank)
+            .map(this::parseToMailAddress)
+            .orElseThrow(() -> ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid 'exportTo' parameter, null or blank value is not accepted")
+                .haltError());
+    }
+
+    private MailAddress parseToMailAddress(String addressString) {
+        try {
+            return new MailAddress(addressString);
+        } catch (AddressException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid 'exportTo' parameter")
+                .cause(e)
+                .haltError();
+        }
+    }
+
     private Query translate(QueryElement queryElement) {
         try {
             return queryTranslator.translate(queryElement);
         } catch (QueryTranslator.QueryTranslatorException e) {
-            throw badRequest("Invalid payload passing to the route", e);
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid payload passing to the route")
+                .cause(e)
+                .haltError();
         }
     }
 
@@ -188,19 +257,15 @@ public class DeletedMessagesVaultRoutes implements Routes {
         try {
             return User.fromUsername(request.params(USER_PATH_PARAM));
         } catch (IllegalArgumentException e) {
-            throw badRequest("Invalid 'user' parameter", e);
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid 'user' parameter")
+                .cause(e)
+                .haltError();
         }
     }
 
-    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/ExportService.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
new file mode 100644
index 0000000..bab02b8
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
@@ -0,0 +1,128 @@
+/****************************************************************
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.export.api.BlobExportMechanism;
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.User;
+import org.apache.james.vault.DeletedMessage;
+import org.apache.james.vault.DeletedMessageVault;
+import org.apache.james.vault.DeletedMessageZipper;
+import org.apache.james.vault.search.Query;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.Throwing;
+import com.github.fge.lambdas.functions.ThrowingFunction;
+import com.github.steveash.guavate.Guavate;
+import com.google.common.annotations.VisibleForTesting;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+class ExportService {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ExportService.class);
+
+    private final BlobExportMechanism blobExport;
+    private final BlobStore blobStore;
+    private final DeletedMessageZipper zipper;
+    private final DeletedMessageVault vault;
+
+    @Inject
+    @VisibleForTesting
+    ExportService(BlobExportMechanism blobExport, BlobStore blobStore, DeletedMessageZipper zipper, DeletedMessageVault vault) {
+        this.blobExport = blobExport;
+        this.blobStore = blobStore;
+        this.zipper = zipper;
+        this.vault = vault;
+    }
+
+    Mono<Void> export(User user, Query exportQuery, MailAddress exportToAddress,
+                      Runnable messageToExportCallback) {
+
+        return matchingMessages(user, exportQuery)
+            .doOnNext(any -> messageToExportCallback.run())
+            .collect(Guavate.toImmutableList())
+            .map(Collection::stream)
+            .map(sneakyThrow(messages -> zipData(user, messages)))
+            .flatMap(sneakyThrow(zippedStream -> blobStore.save(zippedStream, zippedStream.available())))
+            .flatMap(blobId -> exportTo(user, exportToAddress, blobId))
+            .then();
+    }
+
+    private Flux<DeletedMessage> matchingMessages(User user, Query exportQuery) {
+        return Flux.from(vault.search(user, exportQuery))
+            .publishOn(Schedulers.elastic());
+    }
+
+    private PipedInputStream zipData(User user, Stream<DeletedMessage> messages) throws IOException {
+        PipedOutputStream outputStream = new PipedOutputStream();
+        PipedInputStream inputStream = new PipedInputStream();
+        inputStream.connect(outputStream);
+
+        asyncZipData(user, messages, outputStream).subscribe();
+
+        return inputStream;
+    }
+
+    private Mono<Void> asyncZipData(User user, Stream<DeletedMessage> messages, PipedOutputStream outputStream) {
+        return Mono.fromRunnable(Throwing.runnable(() -> zipper.zip(message -> loadMessageContent(user, message), messages, outputStream)).sneakyThrow())
+            .doOnSuccessOrError(Throwing.biConsumer((result, throwable) -> {
+                if (throwable != null) {
+                    LOGGER.error("Error happens when zipping deleted messages", throwable);
+                }
+                outputStream.flush();
+                outputStream.close();
+            }))
+            .subscribeOn(Schedulers.elastic())
+            .then();
+    }
+
+    private InputStream loadMessageContent(User user, DeletedMessage message) {
+        return Mono.from(vault.loadMimeMessage(user, message.getMessageId()))
+            .block();
+    }
+
+    private Mono<Void> exportTo(User user, MailAddress exportToAddress, BlobId blobId) {
+        return Mono.fromRunnable(() -> blobExport
+            .blobId(blobId)
+            .with(exportToAddress)
+            .explanation(String.format("Some deleted messages from user %s has been shared to you", user.asString()))
+            .export());
+    }
+
+    private <T, R> Function<T, R> sneakyThrow(ThrowingFunction<T, R> function) {
+        return Throwing.function(function).sneakyThrow();
+    }
+}
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 3ed17dd..fcc563f 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,6 +21,7 @@ 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;
@@ -44,17 +45,33 @@ 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.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
 import java.util.stream.Stream;
 
+import org.apache.commons.configuration.DefaultConfigurationBuilder;
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.blob.export.api.BlobExportMechanism;
+import org.apache.james.blob.memory.MemoryBlobStore;
+import org.apache.james.core.Domain;
+import org.apache.james.core.MailAddress;
 import org.apache.james.core.MaybeSender;
 import org.apache.james.core.User;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.domainlist.lib.DomainListConfiguration;
+import org.apache.james.domainlist.memory.MemoryDomainList;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.exception.MailboxException;
@@ -71,7 +88,9 @@ 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.user.memory.MemoryUsersRepository;
 import org.apache.james.vault.DeletedMessage;
+import org.apache.james.vault.DeletedMessageZipper;
 import org.apache.james.vault.memory.MemoryDeletedMessagesVault;
 import org.apache.james.vault.search.Query;
 import org.apache.james.webadmin.WebAdminServer;
@@ -85,7 +104,8 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import com.google.common.collect.ImmutableList;
 
@@ -96,15 +116,33 @@ import reactor.core.publisher.Mono;
 
 class DeletedMessagesVaultRoutesTest {
 
+    private class NoopBlobExporting implements BlobExportMechanism {
+        @Override
+        public ShareeStage blobId(BlobId blobId) {
+            return exportTo -> explanation -> () -> export(exportTo, explanation);
+        }
+
+        void export(MailAddress exportTo, String explanation) {
+            // do nothing
+        }
+    }
+
     private static final String MATCH_ALL_QUERY = "{" +
         "\"combinator\": \"and\"," +
         "\"criteria\": []" +
         "}";
+    private static final Domain DOMAIN = Domain.of("apache.org");
 
     private WebAdminServer webAdminServer;
     private MemoryDeletedMessagesVault vault;
     private InMemoryMailboxManager mailboxManager;
     private MemoryTaskManager taskManager;
+    private NoopBlobExporting blobExporting;
+    private MemoryBlobStore blobStore;
+    private DeletedMessageZipper zipper;
+    private MemoryUsersRepository usersRepository;
+    private ExportService exportService;
+    private HashBlobId.Factory blobIdFactory;
 
     @BeforeEach
     void beforeEach() throws Exception {
@@ -116,11 +154,17 @@ class DeletedMessagesVaultRoutesTest {
         JsonTransformer jsonTransformer = new JsonTransformer();
 
         RestoreService vaultRestore = new RestoreService(vault, mailboxManager);
+        blobExporting = spy(new NoopBlobExporting());
+        blobIdFactory = new HashBlobId.Factory();
+        blobStore = new MemoryBlobStore(blobIdFactory);
+        zipper = new DeletedMessageZipper();
+        exportService = new ExportService(blobExporting, blobStore, zipper, vault);
         QueryTranslator queryTranslator = new QueryTranslator(new InMemoryId.Factory());
+        usersRepository = createUsersRepository();
         webAdminServer = WebAdminUtils.createWebAdminServer(
             new DefaultMetricFactory(),
             new TasksRoutes(taskManager, jsonTransformer),
-            new DeletedMessagesVaultRoutes(vaultRestore, jsonTransformer, taskManager, queryTranslator));
+            new DeletedMessagesVaultRoutes(vaultRestore, exportService, jsonTransformer, taskManager, queryTranslator, usersRepository));
 
         webAdminServer.configure(NO_CONFIGURATION);
         webAdminServer.await();
@@ -130,6 +174,23 @@ class DeletedMessagesVaultRoutesTest {
             .build();
     }
 
+    private MemoryUsersRepository createUsersRepository() throws Exception {
+        DNSService dnsService = mock(DNSService.class);
+        MemoryDomainList domainList = new MemoryDomainList(dnsService);
+        domainList.configure(DomainListConfiguration.builder()
+            .autoDetect(false)
+            .autoDetectIp(false));
+        domainList.addDomain(DOMAIN);
+
+        MemoryUsersRepository usersRepository = MemoryUsersRepository.withVirtualHosting();
+        usersRepository.setDomainList(domainList);
+        usersRepository.configure(new DefaultConfigurationBuilder());
+
+        usersRepository.addUser(USER.asString(), "userPassword");
+
+        return usersRepository;
+    }
+
     @AfterEach
     void afterEach() {
         webAdminServer.destroy();
@@ -137,6 +198,286 @@ class DeletedMessagesVaultRoutesTest {
     }
 
     @Nested
+    class UserVaultActionsValidationTest {
+
+        @Test
+        void userVaultAPIShouldReturnInvalidWhenActionIsMissing() {
+            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 userVaultAPIShouldReturnInvalidWhenPassingEmptyAction() {
+            given()
+                .queryParam("action", "")
+            .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 userVaultAPIShouldReturnInvalidWhenActionIsInValid() {
+            given()
+                .queryParam("action", "invalid action")
+            .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 userVaultAPIShouldReturnInvalidWhenPassingCaseInsensitiveAction() {
+            given()
+                .queryParam("action", "RESTORE")
+            .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()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnInvalidWhenUserIsInvalid(String action) {
+            given()
+                .queryParam("action", action)
+            .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(notNullValue()))
+                .body("details", is(notNullValue()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnNotFoundWhenUserIsNotFoundInSystem(String action) {
+            given()
+                .queryParam("action", action)
+            .when()
+                .post(USER_2.asString())
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404)
+                .body("statusCode", is(404))
+                .body("type", is(ErrorResponder.ErrorType.NOT_FOUND.getType()))
+                .body("message", is(notNullValue()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnNotFoundWhenNoUserPathParameter(String action) {
+            given()
+                .queryParam("action", action)
+            .when()
+                .post()
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404)
+                .body("statusCode", is(404))
+                .body("type", is(notNullValue()))
+                .body("message", is(notNullValue()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnBadRequestWhenPassingUnsupportedField(String action) throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"unsupported\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .queryParam("action", action)
+                .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()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnBadRequestWhenPassingUnsupportedOperator(String action) throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"subject\"," +
+                "      \"operator\": \"isLongerThan\"," +
+                "      \"value\": \"" + SUBJECT + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .queryParam("action", action)
+                .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()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnBadRequestWhenPassingUnsupportedPairOfFieldNameAndOperator(String action) throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"sender\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"" + SENDER.asString() + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .queryParam("action", action)
+                .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()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnBadRequestWhenPassingInvalidMailAddress(String action) throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"sender\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"invalid@mail@domain.tld\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .queryParam("action", action)
+                .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()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnBadRequestWhenPassingOrCombinator(String action) throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+
+            String query =
+                "{" +
+                "  \"combinator\": \"or\"," +
+                "  \"criteria\": [" +
+                "    {" +
+                "      \"fieldName\": \"sender\"," +
+                "      \"operator\": \"contains\"," +
+                "      \"value\": \"" + SENDER.asString() + "\"" +
+                "    }" +
+                "  ]" +
+                "}";
+
+            given()
+                .queryParam("action", action)
+                .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()));
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"restore", "export"})
+        void userVaultAPIShouldReturnBadRequestWhenPassingNestedStructuredQuery(String action) 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()
+                .queryParam("action", action)
+                .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()));
+        }
+    }
+
+    @Nested
     class RestoreTest {
 
         @Nested
@@ -1083,258 +1424,6 @@ class DeletedMessagesVaultRoutesTest {
         }
 
         @Nested
-        class ValidationTest {
-
-            @Test
-            void restoreShouldReturnInvalidWhenActionIsMissing() {
-                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 restoreShouldReturnInvalidWhenPassingEmptyAction() {
-                given()
-                    .queryParam("action", "")
-                .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 restoreShouldReturnInvalidWhenActionIsInValid() {
-                given()
-                    .queryParam("action", "invalid action")
-                .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 restoreShouldReturnInvalidWhenPassingCaseInsensitiveAction() {
-                given()
-                    .queryParam("action", "RESTORE")
-                .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 restoreShouldReturnInvalidWhenUserIsInvalid() {
-                given()
-                    .queryParam("action", "restore")
-                .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(notNullValue()))
-                    .body("details", is(notNullValue()));
-            }
-
-            @Test
-            void postShouldReturnNotFoundWhenNoUserPathParameter() {
-                given()
-                    .queryParam("action", "restore")
-                .when()
-                    .post()
-                .then()
-                    .statusCode(HttpStatus.NOT_FOUND_404)
-                    .body("statusCode", is(404))
-                    .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()));
-            }
-        }
-
-        @Nested
         class FailingRestoreTest {
 
             @Test
@@ -1376,7 +1465,7 @@ class DeletedMessagesVaultRoutesTest {
                 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);
+                MessageManager mockMessageManager = mock(MessageManager.class);
                 doReturn(mockMessageManager)
                     .when(mailboxManager)
                     .getMailbox(any(MailboxId.class), any(MailboxSession.class));
@@ -1597,6 +1686,166 @@ class DeletedMessagesVaultRoutesTest {
 
     }
 
+    @Nested
+    class ExportTest {
+
+        @Nested
+        class ValidationTest {
+
+            @Test
+            void exportShouldReturnBadRequestWhenExportToIsMissing() {
+                given()
+                    .queryParam("action", "export")
+                    .body(MATCH_ALL_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()));
+            }
+
+            @Test
+            void exportShouldReturnBadRequestWhenExportToIsInvalid() {
+                given()
+                    .queryParam("action", "export")
+                    .queryParam("exportTo", "export@to#me@")
+                    .body(MATCH_ALL_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()));
+            }
+        }
+
+        @Nested
+        class TaskGeneratingTest {
+
+            @Test
+            void exportShouldReturnATaskCreated() {
+                given()
+                    .queryParam("action", "export")
+                    .queryParam("exportTo", "exportTo@james.org")
+                    .body(MATCH_ALL_QUERY)
+                .when()
+                    .post(USER.asString())
+                .then()
+                    .statusCode(HttpStatus.CREATED_201)
+                    .body("taskId", notNullValue());
+            }
+
+            @Test
+            void exportShouldProduceASuccessfulTaskWithInformation() {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+                vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
+
+                String taskId =
+                    with()
+                        .queryParam("action", "export")
+                        .queryParam("exportTo", USER_2.asString())
+                        .body(MATCH_ALL_QUERY)
+                        .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(DeletedMessagesVaultExportTask.TYPE))
+                    .body("additionalInformation.userExportFrom", is(USER.asString()))
+                    .body("additionalInformation.exportTo", is(USER_2.asString()))
+                    .body("additionalInformation.totalExportedMessages", is(2))
+                    .body("startedDate", is(notNullValue()))
+                    .body("submitDate", is(notNullValue()))
+                    .body("completedDate", is(notNullValue()));
+            }
+        }
+
+        @Test
+        void exportShouldCallBlobExportingTargetToExportAddress() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+            vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
+
+            String taskId =
+                with()
+                    .queryParam("action", "export")
+                    .queryParam("exportTo", USER_2.asString())
+                    .body(MATCH_ALL_QUERY)
+                    .post(USER.asString())
+                .jsonPath()
+                    .get("taskId");
+
+            with()
+                .basePath(TasksRoutes.BASE)
+                .get(taskId + "/await");
+
+            verify(blobExporting, times(1))
+                .export(eq(USER_2.asMailAddress()), any());
+        }
+
+        @Test
+        void exportShouldNotDeleteMessagesInTheVault() {
+            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")
+                    .body(MATCH_ALL_QUERY)
+                    .post(USER.asString())
+                .jsonPath()
+                    .get("taskId");
+
+            with()
+                .basePath(TasksRoutes.BASE)
+                .get(taskId + "/await");
+
+            assertThat(Flux.from(vault.search(USER, Query.ALL)).toStream())
+                .containsOnly(DELETED_MESSAGE, DELETED_MESSAGE_2);
+        }
+
+        @Test
+        void exportShouldSaveDeletedMessagesDataToBlobStore() throws Exception {
+            vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+            vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
+
+            String taskId =
+                with()
+                    .queryParam("action", "export")
+                    .queryParam("exportTo", USER_2.asString())
+                    .body(MATCH_ALL_QUERY)
+                    .post(USER.asString())
+                .jsonPath()
+                    .get("taskId");
+
+            with()
+                .basePath(TasksRoutes.BASE)
+                .get(taskId + "/await");
+
+            byte[] expectedZippedData = zippedMessagesData();
+
+            assertThat(blobStore.read(blobIdFactory.forPayload(expectedZippedData)))
+                .hasSameContentAs(new ByteArrayInputStream(expectedZippedData));
+        }
+
+        private byte[] zippedMessagesData() throws IOException {
+            ByteArrayOutputStream expectedZippedData = new ByteArrayOutputStream();
+            zipper.zip(message -> new ByteArrayInputStream(CONTENT),
+                Stream.of(DELETED_MESSAGE, DELETED_MESSAGE_2),
+                expectedZippedData);
+            return expectedZippedData.toByteArray();
+        }
+    }
+
     private boolean hasAnyMail(User user) throws MailboxException {
         MailboxSession session = mailboxManager.createSystemSession(user.asString());
         int limitToOneMessage = 1;


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


[james-project] 21/23: MAILBOX-388 Error handling testing for DeleteByQueryExecutor

Posted by bt...@apache.org.
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 e2e9f9c7386eaafc59a69bf00332caf39709d5fc
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 17:08:22 2019 +0700

    MAILBOX-388 Error handling testing for DeleteByQueryExecutor
---
 mailbox/plugin/deleted-messages-vault/pom.xml      |   1 +
 .../james/vault/memory/DeleteByQueryExecutor.java  |   4 +
 .../vault/memory/DeleteByQueryExecutorTest.java    | 179 +++++++++++++++++++++
 3 files changed, 184 insertions(+)

diff --git a/mailbox/plugin/deleted-messages-vault/pom.xml b/mailbox/plugin/deleted-messages-vault/pom.xml
index 97e2aea..66ba8d1 100644
--- a/mailbox/plugin/deleted-messages-vault/pom.xml
+++ b/mailbox/plugin/deleted-messages-vault/pom.xml
@@ -110,6 +110,7 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
index 02e6936..81234f5 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
@@ -63,6 +63,10 @@ public class DeleteByQueryExecutor {
         return Flux.from(deletedMessageVault.usersWithVault())
             .flatMap(user -> deleteByQueryForUser(query, user, notifiers))
             .reduce(Task::combine)
+            .onErrorResume(e -> {
+                LOGGER.error("Unexpected error encountered while deleting by query", e);
+                return Mono.just(Task.Result.PARTIAL);
+            })
             .blockOptional()
             .orElse(Task.Result.COMPLETED);
     }
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/DeleteByQueryExecutorTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/DeleteByQueryExecutorTest.java
new file mode 100644
index 0000000..285192b
--- /dev/null
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/DeleteByQueryExecutorTest.java
@@ -0,0 +1,179 @@
+/****************************************************************
+ * 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.vault.memory;
+
+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.vault.DeletedMessageVaultContract.CLOCK;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+
+import org.apache.james.task.Task;
+import org.apache.james.vault.DeletedMessageVault;
+import org.apache.james.vault.RetentionConfiguration;
+import org.apache.james.vault.search.Query;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+class DeleteByQueryExecutorTest {
+    private DeletedMessageVault vault;
+    private DeleteByQueryExecutor testee;
+    private DeleteByQueryExecutor.Notifiers notifiers;
+    private DeleteByQueryExecutor.Notifier userHandledNotifier;
+    private DeleteByQueryExecutor.Notifier searchErrorNotifier;
+    private DeleteByQueryExecutor.Notifier deletionErrorNotifier;
+    private DeleteByQueryExecutor.Notifier permanentlyDeletedMessageNotifyer;
+
+    @BeforeEach
+    void setUp() {
+        vault = spy(new MemoryDeletedMessagesVault(RetentionConfiguration.DEFAULT, CLOCK));
+        testee = new DeleteByQueryExecutor(vault);
+
+        userHandledNotifier = mock(DeleteByQueryExecutor.Notifier.class);
+        searchErrorNotifier = mock(DeleteByQueryExecutor.Notifier.class);
+        deletionErrorNotifier = mock(DeleteByQueryExecutor.Notifier.class);
+        permanentlyDeletedMessageNotifyer = mock(DeleteByQueryExecutor.Notifier.class);
+        notifiers = new DeleteByQueryExecutor.Notifiers(
+            userHandledNotifier,
+            searchErrorNotifier,
+            deletionErrorNotifier,
+            permanentlyDeletedMessageNotifyer);
+    }
+
+    @Test
+    void deleteByQueryShouldReturnPartialWhenListingUserFailed() {
+        when(vault.usersWithVault()).thenReturn(Mono.error(new RuntimeException()));
+
+        assertThat(testee.deleteByQuery(Query.ALL, notifiers)).isEqualTo(Task.Result.PARTIAL);
+    }
+
+    @Test
+    void deleteByQueryShouldNotUpdateNotifiersWhenListingUserFailed() {
+        when(vault.usersWithVault()).thenReturn(Mono.error(new RuntimeException()));
+
+        testee.deleteByQuery(Query.ALL, notifiers);
+
+        verifyZeroInteractions(userHandledNotifier);
+        verifyZeroInteractions(searchErrorNotifier);
+        verifyZeroInteractions(deletionErrorNotifier);
+        verifyZeroInteractions(permanentlyDeletedMessageNotifyer);
+    }
+
+    @Test
+    void deleteByQueryShouldReturnCompletedUponNormalExecution() {
+        Mono.from(vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        assertThat(testee.deleteByQuery(Query.ALL, notifiers)).isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    void deleteByQueryShouldUpdateNotifiesdUponNormalExecution() {
+        Mono.from(vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        testee.deleteByQuery(Query.ALL, notifiers);
+
+        verify(userHandledNotifier, times(2)).doNotify();
+        verify(permanentlyDeletedMessageNotifyer, times(3)).doNotify();
+        verifyZeroInteractions(searchErrorNotifier);
+        verifyZeroInteractions(deletionErrorNotifier);
+
+        verifyNoMoreInteractions(userHandledNotifier);
+        verifyNoMoreInteractions(searchErrorNotifier);
+        verifyNoMoreInteractions(deletionErrorNotifier);
+        verifyNoMoreInteractions(permanentlyDeletedMessageNotifyer);
+    }
+
+    @Test
+    void deleteByQueryShouldReturnPartialWhenSearchingFails() {
+        Mono.from(vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        when(vault.search(USER, Query.ALL)).thenReturn(Flux.error(new RuntimeException()));
+
+        assertThat(testee.deleteByQuery(Query.ALL, notifiers)).isEqualTo(Task.Result.PARTIAL);
+    }
+
+    @Test
+    void deleteByQueryShouldUpdateNotifiesWhenSearchingFails() {
+        Mono.from(vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        when(vault.search(USER, Query.ALL)).thenReturn(Flux.error(new RuntimeException()));
+
+        testee.deleteByQuery(Query.ALL, notifiers);
+
+        verify(userHandledNotifier, times(2)).doNotify();
+        verify(searchErrorNotifier, times(1)).doNotify();
+        verify(permanentlyDeletedMessageNotifyer, times(1)).doNotify();
+        verifyZeroInteractions(deletionErrorNotifier);
+
+        verifyNoMoreInteractions(userHandledNotifier);
+        verifyNoMoreInteractions(searchErrorNotifier);
+        verifyNoMoreInteractions(deletionErrorNotifier);
+        verifyNoMoreInteractions(permanentlyDeletedMessageNotifyer);
+    }
+
+    @Test
+    void deleteByQueryShouldReturnPartialWhenDeletionFails() {
+        when(vault.delete(USER, DELETED_MESSAGE.getMessageId())).thenReturn(Mono.error(new RuntimeException()));
+        Mono.from(vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        assertThat(testee.deleteByQuery(Query.ALL, notifiers)).isEqualTo(Task.Result.PARTIAL);
+    }
+
+    @Test
+    void deleteByQueryShouldUpdateNotifiesWhenDeletionFails() {
+        when(vault.delete(USER, DELETED_MESSAGE.getMessageId())).thenReturn(Mono.error(new RuntimeException()));
+        Mono.from(vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(vault.append(USER_2, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        testee.deleteByQuery(Query.ALL, notifiers);
+
+        verify(userHandledNotifier, times(2)).doNotify();
+        verify(permanentlyDeletedMessageNotifyer, times(2)).doNotify();
+        verify(deletionErrorNotifier, times(1)).doNotify();
+        verifyZeroInteractions(searchErrorNotifier);
+
+        verifyNoMoreInteractions(userHandledNotifier);
+        verifyNoMoreInteractions(searchErrorNotifier);
+        verifyNoMoreInteractions(deletionErrorNotifier);
+        verifyNoMoreInteractions(permanentlyDeletedMessageNotifyer);
+    }
+}
\ No newline at end of file


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


[james-project] 11/23: JAMES-2702 Improve DurationParser tests

Posted by bt...@apache.org.
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 d983389d9d9eb2cb8d5f218eba6abbe27b04a72c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Mar 26 11:16:37 2019 +0700

    JAMES-2702 Improve DurationParser tests
    
     - Rely on parametrized tests
     - Rename out of date methods
     - Add some edge cases tests
---
 server/container/util/pom.xml                      |   5 +
 .../java/org/apache/james/util/DurationParser.java |   2 +-
 .../org/apache/james/util/DurationParserTest.java  | 164 ++++++++-------------
 3 files changed, 67 insertions(+), 104 deletions(-)

diff --git a/server/container/util/pom.xml b/server/container/util/pom.xml
index 9177ff6..e14c5f1 100644
--- a/server/container/util/pom.xml
+++ b/server/container/util/pom.xml
@@ -94,6 +94,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.junit.platform</groupId>
             <artifactId>junit-platform-launcher</artifactId>
             <scope>test</scope>
diff --git a/server/container/util/src/main/java/org/apache/james/util/DurationParser.java b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
index f30fb6d..6a562f9 100644
--- a/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
+++ b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
@@ -40,7 +40,7 @@ public class DurationParser {
 
     private enum Unit {
         MILLI_SECONDS(ImmutableList.of("ms", "msec", "msecs"), ChronoUnit.MILLIS),
-        SECONDS(ImmutableList.of("s", "sec", "secs"), ChronoUnit.SECONDS),
+        SECONDS(ImmutableList.of("s", "sec", "secs", "second", "seconds"), ChronoUnit.SECONDS),
         MINUTES(ImmutableList.of("m", "min", "mins", "minute", "minutes"), ChronoUnit.MINUTES),
         HOURS(ImmutableList.of("h", "hour", "hours"), ChronoUnit.HOURS),
         DAYS(ImmutableList.of("d", "day", "days"), ChronoUnit.DAYS);
diff --git a/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java b/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
index 7ef870c..e1c962d 100644
--- a/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
+++ b/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
@@ -25,155 +25,113 @@ import java.time.Duration;
 import java.time.temporal.ChronoUnit;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 class DurationParserTest {
-
-    @Test
-    void getMilliSecondsShouldConvertValueWhenNoUnitAmountAsString() {
-        Duration actual = DurationParser.parse("2");
-        assertThat(actual).isEqualTo(Duration.ofMillis(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldUseProvidedUnitWhenNoUnitAmountAsString() {
-        Duration actual = DurationParser.parse("2", ChronoUnit.SECONDS);
-        assertThat(actual).isEqualTo(Duration.ofSeconds(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldNotUseProvidedUnitWhenNoUnitAmountAsString() {
-        Duration actual = DurationParser.parse("2 minutes", ChronoUnit.SECONDS);
-        assertThat(actual).isEqualTo(Duration.ofMinutes(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMsecAmountAsString() {
-        Duration actual = DurationParser.parse("2 msec");
-        assertThat(actual).isEqualTo(Duration.ofMillis(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMsAmountAsString() {
-        Duration actual = DurationParser.parse("2 ms");
-        assertThat(actual).isEqualTo(Duration.ofMillis(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMsCapitalAmountAsString() {
-        Duration actual = DurationParser.parse("2 Ms");
-        assertThat(actual).isEqualTo(Duration.ofMillis(2));
-    }
-
     @Test
-    void getMilliSecondsShouldConvertValueWhenMsecsAmountAsString() {
-        Duration actual = DurationParser.parse("2 msecs");
-        assertThat(actual).isEqualTo(Duration.ofMillis(2));
+    void parseShouldUseMsAsDefaultUnit() {
+        assertThat(DurationParser.parse("2"))
+            .isEqualTo(Duration.ofMillis(2));
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenSAmountAsString() {
-        Duration actual = DurationParser.parse("2 s");
-        assertThat(actual).isEqualTo(Duration.ofSeconds(2));
+    void parseShouldUseSpecifiedDefaultUnit() {
+        assertThat(DurationParser.parse("2", ChronoUnit.SECONDS))
+            .isEqualTo(Duration.ofSeconds(2));
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenSecAmountAsString() {
-        Duration actual = DurationParser.parse("2 sec");
-        assertThat(actual).isEqualTo(Duration.ofSeconds(2));
+    void parseShouldUseSpecifiedUnit() {
+        assertThat(DurationParser.parse("2 minutes", ChronoUnit.SECONDS))
+            .isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenSecCapitalAmountAsString() {
-        Duration actual = DurationParser.parse("2 Sec");
-        assertThat(actual).isEqualTo(Duration.ofSeconds(2));
+    void parseShouldSupportStartingSpaces() {
+        assertThat(DurationParser.parse("  2 minutes"))
+            .isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenSecsAmountAsString() {
-        Duration actual = DurationParser.parse("2 secs");
-        assertThat(actual).isEqualTo(Duration.ofSeconds(2));
+    void parseShouldSupportEndingSpaces() {
+        assertThat(DurationParser.parse("2 minutes  "))
+            .isEqualTo(Duration.ofMinutes(2));
     }
 
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMAmountAsString() {
-        Duration actual = DurationParser.parse("2 m");
-        assertThat(actual).isEqualTo(Duration.ofMinutes(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMinuteAmountAsString() {
-        Duration actual = DurationParser.parse("2 minute");
-        assertThat(actual).isEqualTo(Duration.ofMinutes(2));
+    @ParameterizedTest
+    @ValueSource(strings = {"2", "2 ms", "2 msec", "2 msecs", "2 Ms"})
+    void parseShouldHandleMilliseconds(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(Duration.ofMillis(2));
     }
 
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMinuteCapitalAmountAsString() {
-        Duration actual = DurationParser.parse("2 Minute");
-        assertThat(actual).isEqualTo(Duration.ofMinutes(2));
-    }
-
-    @Test
-    void getMilliSecondsShouldConvertValueWhenMinutesAmountAsString() {
-        Duration actual = DurationParser.parse("2 minutes");
-        assertThat(actual).isEqualTo(Duration.ofMinutes(2));
+    @ParameterizedTest
+    @ValueSource(strings = {"2 s", "2 sec", "2 Sec", "2 second", "2 seconds"})
+    void parseShouldHandleSeconds(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(Duration.ofSeconds(2));
     }
 
-    @Test
-    void getMilliSecondsShouldConvertValueWhenHAmountAsString() {
-        Duration actual = DurationParser.parse("2 h");
-        assertThat(actual).isEqualTo(Duration.ofHours(2));
+    @ParameterizedTest
+    @ValueSource(strings = {"2 m", "2 min", "2 mins", "2 minute", "2 Minute", "2 minutes"})
+    void parseShouldHandleMinutes(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(Duration.ofMinutes(2));
     }
 
-    @Test
-    void getMilliSecondsShouldConvertValueWhenHourAmountAsString() {
-        Duration actual = DurationParser.parse("2 hour");
-        assertThat(actual).isEqualTo(Duration.ofHours(2));
+    @ParameterizedTest
+    @ValueSource(strings = {"2 h", "2 hour", "2 Hour", "2 hours"})
+    void parseShouldHandleHours(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(Duration.ofHours(2));
     }
 
-    @Test
-    void getMilliSecondsShouldConvertValueWhenHourCapitalAmountAsString() {
-        Duration actual = DurationParser.parse("2 Hour");
-        assertThat(actual).isEqualTo(Duration.ofHours(2));
+    @ParameterizedTest
+    @ValueSource(strings = {"2 d", "2 day", "2 Day", "2 days"})
+    void parseShouldHandleDays(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(Duration.ofDays(2));
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenHoursAmountAsString() {
-        Duration actual = DurationParser.parse("2 hours");
-        assertThat(actual).isEqualTo(Duration.ofHours(2));
+    void parseShouldThrowWhenIllegalUnitInRawString() {
+        assertThatThrownBy(() -> DurationParser.parse("2 unknown"))
+            .isInstanceOf(NumberFormatException.class);
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenDAmountAsString() {
-        Duration actual = DurationParser.parse("2 d");
-        assertThat(actual).isEqualTo(Duration.ofDays(2));
+    void parseShouldThrowWhenMissingAmount() {
+        assertThatThrownBy(() -> DurationParser.parse("seconds"))
+            .isInstanceOf(NumberFormatException.class);
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenDayAmountAsString() {
-        Duration actual = DurationParser.parse("2 day");
-        assertThat(actual).isEqualTo(Duration.ofDays(2));
+    void parseShouldThrowWhenMissingAmountWithExtraWhiteSpace() {
+        assertThatThrownBy(() -> DurationParser.parse(" seconds"))
+            .isInstanceOf(NumberFormatException.class);
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenDayCapitalAmountAsString() {
-        Duration actual = DurationParser.parse("2 Day");
-        assertThat(actual).isEqualTo(Duration.ofDays(2));
+    void parseShouldThrowWhenEmpty() {
+        assertThatThrownBy(() -> DurationParser.parse(""))
+            .isInstanceOf(NumberFormatException.class);
     }
 
     @Test
-    void getMilliSecondsShouldConvertValueWhenDaysAmountAsString() {
-        Duration actual = DurationParser.parse("2 days");
-        assertThat(actual).isEqualTo(Duration.ofDays(2));
+    void parseShouldThrowWhenNegativeAmount() {
+        assertThatThrownBy(() -> DurationParser.parse("-1 s"))
+            .isInstanceOf(NumberFormatException.class);
     }
 
     @Test
-    void getMilliSecondsShouldThrowWhenIllegalUnitInRawString() {
-        assertThatThrownBy(() -> DurationParser.parse("2 week"))
-            .isInstanceOf(NumberFormatException.class);
+    void parseShouldThrowWhenZero() {
+        assertThat(DurationParser.parse("0 s"))
+            .isEqualTo(Duration.ofSeconds(0));
     }
 
     @Test
-    void getMilliSecondsShouldThrowWhenIllegalPattern() {
+    void parseShouldThrowWhenIllegalPattern() {
         assertThatThrownBy(() -> DurationParser.parse("illegal pattern"))
             .isInstanceOf(NumberFormatException.class);
     }


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


[james-project] 22/23: MAILBOX-388 Move vault helper class out of Memory implementation package

Posted by bt...@apache.org.
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 c89529e47b13a3ba210bc9049c5513bf923cc608
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Mar 27 15:00:14 2019 +0700

    MAILBOX-388 Move vault helper class out of Memory implementation package
---
 .../org/apache/james/vault/memory/MemoryDeletedMessagesVault.java  | 2 ++
 .../james/vault/{memory => utils}/DeleteByQueryExecutor.java       | 2 +-
 .../james/vault/{memory => utils}/VaultGarbageCollectionTask.java  | 2 +-
 .../james/vault/{memory => utils}/DeleteByQueryExecutorTest.java   | 7 ++++---
 .../org/apache/james/vault/MailRepositoryDeletedMessageVault.java  | 4 ++--
 5 files changed, 10 insertions(+), 7 deletions(-)

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 d48bfda..773faec 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
@@ -36,6 +36,8 @@ import org.apache.james.vault.DeletedMessage;
 import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.RetentionConfiguration;
 import org.apache.james.vault.search.Query;
+import org.apache.james.vault.utils.DeleteByQueryExecutor;
+import org.apache.james.vault.utils.VaultGarbageCollectionTask;
 import org.reactivestreams.Publisher;
 
 import com.google.common.base.Preconditions;
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/DeleteByQueryExecutor.java
similarity index 99%
rename from mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
rename to mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/DeleteByQueryExecutor.java
index 81234f5..97fe2b1 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/DeleteByQueryExecutor.java
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.vault.memory;
+package org.apache.james.vault.utils;
 
 import org.apache.james.core.User;
 import org.apache.james.mailbox.model.MessageId;
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/VaultGarbageCollectionTask.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/VaultGarbageCollectionTask.java
similarity index 99%
rename from mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/VaultGarbageCollectionTask.java
rename to mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/VaultGarbageCollectionTask.java
index 93c8ce3..36170f3 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/VaultGarbageCollectionTask.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/utils/VaultGarbageCollectionTask.java
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.vault.memory;
+package org.apache.james.vault.utils;
 
 import java.time.ZonedDateTime;
 import java.util.Optional;
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/DeleteByQueryExecutorTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/utils/DeleteByQueryExecutorTest.java
similarity index 97%
rename from mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/DeleteByQueryExecutorTest.java
rename to mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/utils/DeleteByQueryExecutorTest.java
index 285192b..452772a 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/DeleteByQueryExecutorTest.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/utils/DeleteByQueryExecutorTest.java
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.vault.memory;
+package org.apache.james.vault.utils;
 
 import static org.apache.james.vault.DeletedMessageFixture.CONTENT;
 import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE;
@@ -27,7 +27,6 @@ import static org.apache.james.vault.DeletedMessageFixture.USER_2;
 import static org.apache.james.vault.DeletedMessageVaultContract.CLOCK;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -39,9 +38,11 @@ import java.io.ByteArrayInputStream;
 import org.apache.james.task.Task;
 import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.RetentionConfiguration;
+import org.apache.james.vault.memory.MemoryDeletedMessagesVault;
 import org.apache.james.vault.search.Query;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -57,7 +58,7 @@ class DeleteByQueryExecutorTest {
 
     @BeforeEach
     void setUp() {
-        vault = spy(new MemoryDeletedMessagesVault(RetentionConfiguration.DEFAULT, CLOCK));
+        vault = Mockito.spy(new MemoryDeletedMessagesVault(RetentionConfiguration.DEFAULT, CLOCK));
         testee = new DeleteByQueryExecutor(vault);
 
         userHandledNotifier = mock(DeleteByQueryExecutor.Notifier.class);
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
index 4d8ab8c..912fddb 100644
--- a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
+++ b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
@@ -37,9 +37,9 @@ import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.server.core.MimeMessageInputStream;
 import org.apache.james.task.Task;
 import org.apache.james.util.streams.Iterators;
-import org.apache.james.vault.memory.DeleteByQueryExecutor;
-import org.apache.james.vault.memory.VaultGarbageCollectionTask;
 import org.apache.james.vault.search.Query;
+import org.apache.james.vault.utils.DeleteByQueryExecutor;
+import org.apache.james.vault.utils.VaultGarbageCollectionTask;
 import org.reactivestreams.Publisher;
 
 import com.github.fge.lambdas.Throwing;


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


[james-project] 10/23: JAMES-2702 Improve DurationParser exception handling

Posted by bt...@apache.org.
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 1497fb290866f61e170a3633c39d94b70780b9c7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Mar 27 10:17:04 2019 +0700

    JAMES-2702 Improve DurationParser exception handling
---
 .../util/src/main/java/org/apache/james/util/DurationParser.java  | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/server/container/util/src/main/java/org/apache/james/util/DurationParser.java b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
index 9e1095e..f30fb6d 100644
--- a/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
+++ b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
@@ -84,20 +84,14 @@ public class DurationParser {
     public static Duration parse(String rawString, ChronoUnit defaultUnit) throws NumberFormatException {
         Matcher res = PATTERN.matcher(rawString);
         if (res.matches()) {
-
             if (res.group(AMOUNT) != null && res.group(UNIT) != null) {
                 long time = Integer.parseInt(res.group(AMOUNT).trim());
                 return parseUnitAsDuration(res.group(UNIT))
                     .orElse(defaultUnit.getDuration())
                     .multipliedBy(time);
-            } else {
-                // This should never Happen anyway throw an exception
-                throw new NumberFormatException("The supplied String is not a supported format " + rawString);
             }
-        } else {
-            // The rawString not match our pattern. So its not supported
-            throw new NumberFormatException("The supplied String is not a supported format " + rawString);
         }
+        throw new NumberFormatException("The supplied String is not a supported format " + rawString);
     }
 
     private static Optional<Duration> parseUnitAsDuration(String unit) {


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


[james-project] 12/23: JAMES-2702 DurationParser should handle weeks, months and years

Posted by bt...@apache.org.
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 a142a230c09025071b762fa315243a1b3676b8c0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Mar 26 11:26:03 2019 +0700

    JAMES-2702 DurationParser should handle weeks, months and years
---
 .../java/org/apache/james/util/DurationParser.java  |  5 ++++-
 .../org/apache/james/util/DurationParserTest.java   | 21 +++++++++++++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/server/container/util/src/main/java/org/apache/james/util/DurationParser.java b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
index 6a562f9..b78fd6e 100644
--- a/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
+++ b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
@@ -43,7 +43,10 @@ public class DurationParser {
         SECONDS(ImmutableList.of("s", "sec", "secs", "second", "seconds"), ChronoUnit.SECONDS),
         MINUTES(ImmutableList.of("m", "min", "mins", "minute", "minutes"), ChronoUnit.MINUTES),
         HOURS(ImmutableList.of("h", "hour", "hours"), ChronoUnit.HOURS),
-        DAYS(ImmutableList.of("d", "day", "days"), ChronoUnit.DAYS);
+        DAYS(ImmutableList.of("d", "day", "days"), ChronoUnit.DAYS),
+        WEEKS(ImmutableList.of("w", "week", "weeks"), ChronoUnit.WEEKS),
+        MONTH(ImmutableList.of("month", "months"), ChronoUnit.MONTHS),
+        YEARS(ImmutableList.of("y", "year", "years"), ChronoUnit.YEARS);
 
         public static ChronoUnit parse(String string) {
             return Arrays.stream(values())
diff --git a/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java b/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
index e1c962d..fa997ae 100644
--- a/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
+++ b/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
@@ -94,6 +94,27 @@ class DurationParserTest {
             .isEqualTo(Duration.ofDays(2));
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"2 w", "2 week", "2 Week", "2 weeks"})
+    void parseShouldHandleWeeks(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(ChronoUnit.WEEKS.getDuration().multipliedBy(2));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"2 months", "2 month", "2 Month"})
+    void parseShouldHandleMonths(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(ChronoUnit.MONTHS.getDuration().multipliedBy(2));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"2 y", "2 year", "2 Year", "2 years"})
+    void parseShouldHandleYears(String input) {
+        assertThat(DurationParser.parse(input))
+            .isEqualTo(ChronoUnit.YEARS.getDuration().multipliedBy(2));
+    }
+
     @Test
     void parseShouldThrowWhenIllegalUnitInRawString() {
         assertThatThrownBy(() -> DurationParser.parse("2 unknown"))


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


[james-project] 09/23: JAMES-2702 TimeConverter is more a DurationParser

Posted by bt...@apache.org.
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 02a63a3915a592e9e18902bb7eb0919788edd22c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Mar 27 10:11:04 2019 +0700

    JAMES-2702 TimeConverter is more a DurationParser
---
 .../custom/mailets/IsDelayedForMoreThan.java       |  4 +-
 .../mailing/QuotaMailingListenerConfiguration.java |  4 +-
 .../modules/mailbox/TikaConfigurationReader.java   |  4 +-
 .../{TimeConverter.java => DurationParser.java}    | 10 ++---
 ...eConverterTest.java => DurationParserTest.java} | 52 +++++++++++-----------
 .../transport/mailets/remote/delivery/Delay.java   |  8 ++--
 .../james/smtpserver/POP3BeforeSMTPHandler.java    |  4 +-
 .../smtpserver/fastfail/JDBCGreylistHandler.java   |  8 ++--
 .../CassandraMailQueueViewConfiguration.java       |  4 +-
 9 files changed, 49 insertions(+), 49 deletions(-)

diff --git a/examples/custom-mailets/src/main/java/org/apache/james/examples/custom/mailets/IsDelayedForMoreThan.java b/examples/custom-mailets/src/main/java/org/apache/james/examples/custom/mailets/IsDelayedForMoreThan.java
index 1c5c72b..aad8b37 100644
--- a/examples/custom-mailets/src/main/java/org/apache/james/examples/custom/mailets/IsDelayedForMoreThan.java
+++ b/examples/custom-mailets/src/main/java/org/apache/james/examples/custom/mailets/IsDelayedForMoreThan.java
@@ -28,7 +28,7 @@ import java.util.Date;
 import javax.mail.MessagingException;
 
 import org.apache.james.core.MailAddress;
-import org.apache.james.util.TimeConverter;
+import org.apache.james.util.DurationParser;
 import org.apache.mailet.Mail;
 import org.apache.mailet.base.GenericMatcher;
 
@@ -61,7 +61,7 @@ public class IsDelayedForMoreThan extends GenericMatcher {
     @Override
     public void init() {
         String condition = getCondition();
-        maxDelay = TimeConverter.parseDuration(condition, DEFAULT_UNIT);
+        maxDelay = DurationParser.parse(condition, DEFAULT_UNIT);
     }
 
     @Override
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
index ab29dd3..46ff99b 100644
--- a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
@@ -31,8 +31,8 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.james.filesystem.api.FileSystem;
 import org.apache.james.mailbox.quota.model.QuotaThreshold;
 import org.apache.james.mailbox.quota.model.QuotaThresholds;
+import org.apache.james.util.DurationParser;
 import org.apache.james.util.OptionalUtils;
-import org.apache.james.util.TimeConverter;
 
 import com.github.steveash.guavate.Guavate;
 import com.google.common.base.MoreObjects;
@@ -76,7 +76,7 @@ public class QuotaMailingListenerConfiguration {
 
     private static Optional<Duration> readGracePeriod(HierarchicalConfiguration config) {
         return Optional.ofNullable(config.getString(XmlKeys.GRACE_PERIOD, null))
-            .map(string -> TimeConverter.parseDuration(string, ChronoUnit.DAYS));
+            .map(string -> DurationParser.parse(string, ChronoUnit.DAYS));
     }
 
     private static ImmutableMap<QuotaThreshold, RenderingInformation> readThresholds(HierarchicalConfiguration config) {
diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/TikaConfigurationReader.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/TikaConfigurationReader.java
index 07d1936..68763ec 100644
--- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/TikaConfigurationReader.java
+++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/TikaConfigurationReader.java
@@ -27,9 +27,9 @@ import java.util.Set;
 import org.apache.commons.configuration.AbstractConfiguration;
 import org.apache.commons.configuration.Configuration;
 import org.apache.james.mailbox.tika.TikaConfiguration;
+import org.apache.james.util.DurationParser;
 import org.apache.james.util.Size;
 import org.apache.james.util.StreamUtils;
-import org.apache.james.util.TimeConverter;
 
 import com.github.fge.lambdas.Throwing;
 import com.google.common.collect.ImmutableSet;
@@ -64,7 +64,7 @@ public class TikaConfigurationReader {
         Optional<Duration> cacheEvictionPeriod = Optional.ofNullable(
             configuration.getString(TIKA_CACHE_EVICTION_PERIOD,
                 null))
-            .map(rawString -> TimeConverter.parseDuration(rawString, ChronoUnit.SECONDS));
+            .map(rawString -> DurationParser.parse(rawString, ChronoUnit.SECONDS));
 
         Optional<Long> cacheWeight = Optional.ofNullable(
             configuration.getString(TIKA_CACHE_WEIGHT_MAX, null))
diff --git a/server/container/util/src/main/java/org/apache/james/util/TimeConverter.java b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
similarity index 93%
rename from server/container/util/src/main/java/org/apache/james/util/TimeConverter.java
rename to server/container/util/src/main/java/org/apache/james/util/DurationParser.java
index bc7c3cb..9e1095e 100644
--- a/server/container/util/src/main/java/org/apache/james/util/TimeConverter.java
+++ b/server/container/util/src/main/java/org/apache/james/util/DurationParser.java
@@ -30,7 +30,7 @@ import java.util.regex.Pattern;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 
-public class TimeConverter {
+public class DurationParser {
 
     private static final String PATTERN_STRING = "\\s*([0-9]+)\\s*([a-z,A-Z]*)\\s*";
     private static final int AMOUNT = 1;
@@ -64,7 +64,7 @@ public class TimeConverter {
     }
 
     // Get sure it can not be instantiated
-    private TimeConverter() {
+    private DurationParser() {
     }
 
     /**
@@ -77,11 +77,11 @@ public class TimeConverter {
      * @throws NumberFormatException
      *             Get thrown if an illegal rawString was used
      */
-    public static Duration parseDuration(String rawString) throws NumberFormatException {
-        return parseDuration(rawString, ChronoUnit.MILLIS);
+    public static Duration parse(String rawString) throws NumberFormatException {
+        return parse(rawString, ChronoUnit.MILLIS);
     }
 
-    public static Duration parseDuration(String rawString, ChronoUnit defaultUnit) throws NumberFormatException {
+    public static Duration parse(String rawString, ChronoUnit defaultUnit) throws NumberFormatException {
         Matcher res = PATTERN.matcher(rawString);
         if (res.matches()) {
 
diff --git a/server/container/util/src/test/java/org/apache/james/util/TimeConverterTest.java b/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
similarity index 75%
rename from server/container/util/src/test/java/org/apache/james/util/TimeConverterTest.java
rename to server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
index ca9fe83..7ef870c 100644
--- a/server/container/util/src/test/java/org/apache/james/util/TimeConverterTest.java
+++ b/server/container/util/src/test/java/org/apache/james/util/DurationParserTest.java
@@ -26,155 +26,155 @@ import java.time.temporal.ChronoUnit;
 
 import org.junit.jupiter.api.Test;
 
-class TimeConverterTest {
+class DurationParserTest {
 
     @Test
     void getMilliSecondsShouldConvertValueWhenNoUnitAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2");
+        Duration actual = DurationParser.parse("2");
         assertThat(actual).isEqualTo(Duration.ofMillis(2));
     }
 
     @Test
     void getMilliSecondsShouldUseProvidedUnitWhenNoUnitAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2", ChronoUnit.SECONDS);
+        Duration actual = DurationParser.parse("2", ChronoUnit.SECONDS);
         assertThat(actual).isEqualTo(Duration.ofSeconds(2));
     }
 
     @Test
     void getMilliSecondsShouldNotUseProvidedUnitWhenNoUnitAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 minutes", ChronoUnit.SECONDS);
+        Duration actual = DurationParser.parse("2 minutes", ChronoUnit.SECONDS);
         assertThat(actual).isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMsecAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 msec");
+        Duration actual = DurationParser.parse("2 msec");
         assertThat(actual).isEqualTo(Duration.ofMillis(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMsAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 ms");
+        Duration actual = DurationParser.parse("2 ms");
         assertThat(actual).isEqualTo(Duration.ofMillis(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMsCapitalAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 Ms");
+        Duration actual = DurationParser.parse("2 Ms");
         assertThat(actual).isEqualTo(Duration.ofMillis(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMsecsAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 msecs");
+        Duration actual = DurationParser.parse("2 msecs");
         assertThat(actual).isEqualTo(Duration.ofMillis(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenSAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 s");
+        Duration actual = DurationParser.parse("2 s");
         assertThat(actual).isEqualTo(Duration.ofSeconds(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenSecAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 sec");
+        Duration actual = DurationParser.parse("2 sec");
         assertThat(actual).isEqualTo(Duration.ofSeconds(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenSecCapitalAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 Sec");
+        Duration actual = DurationParser.parse("2 Sec");
         assertThat(actual).isEqualTo(Duration.ofSeconds(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenSecsAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 secs");
+        Duration actual = DurationParser.parse("2 secs");
         assertThat(actual).isEqualTo(Duration.ofSeconds(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 m");
+        Duration actual = DurationParser.parse("2 m");
         assertThat(actual).isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMinuteAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 minute");
+        Duration actual = DurationParser.parse("2 minute");
         assertThat(actual).isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMinuteCapitalAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 Minute");
+        Duration actual = DurationParser.parse("2 Minute");
         assertThat(actual).isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenMinutesAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 minutes");
+        Duration actual = DurationParser.parse("2 minutes");
         assertThat(actual).isEqualTo(Duration.ofMinutes(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenHAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 h");
+        Duration actual = DurationParser.parse("2 h");
         assertThat(actual).isEqualTo(Duration.ofHours(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenHourAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 hour");
+        Duration actual = DurationParser.parse("2 hour");
         assertThat(actual).isEqualTo(Duration.ofHours(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenHourCapitalAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 Hour");
+        Duration actual = DurationParser.parse("2 Hour");
         assertThat(actual).isEqualTo(Duration.ofHours(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenHoursAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 hours");
+        Duration actual = DurationParser.parse("2 hours");
         assertThat(actual).isEqualTo(Duration.ofHours(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenDAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 d");
+        Duration actual = DurationParser.parse("2 d");
         assertThat(actual).isEqualTo(Duration.ofDays(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenDayAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 day");
+        Duration actual = DurationParser.parse("2 day");
         assertThat(actual).isEqualTo(Duration.ofDays(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenDayCapitalAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 Day");
+        Duration actual = DurationParser.parse("2 Day");
         assertThat(actual).isEqualTo(Duration.ofDays(2));
     }
 
     @Test
     void getMilliSecondsShouldConvertValueWhenDaysAmountAsString() {
-        Duration actual = TimeConverter.parseDuration("2 days");
+        Duration actual = DurationParser.parse("2 days");
         assertThat(actual).isEqualTo(Duration.ofDays(2));
     }
 
     @Test
     void getMilliSecondsShouldThrowWhenIllegalUnitInRawString() {
-        assertThatThrownBy(() -> TimeConverter.parseDuration("2 week"))
+        assertThatThrownBy(() -> DurationParser.parse("2 week"))
             .isInstanceOf(NumberFormatException.class);
     }
 
     @Test
     void getMilliSecondsShouldThrowWhenIllegalPattern() {
-        assertThatThrownBy(() -> TimeConverter.parseDuration("illegal pattern"))
+        assertThatThrownBy(() -> DurationParser.parse("illegal pattern"))
             .isInstanceOf(NumberFormatException.class);
     }
 }
diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Delay.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Delay.java
index fd04df9..8dcc7c4 100644
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Delay.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Delay.java
@@ -24,7 +24,7 @@ import java.util.List;
 
 import javax.mail.MessagingException;
 
-import org.apache.james.util.TimeConverter;
+import org.apache.james.util.DurationParser;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
@@ -35,7 +35,7 @@ import com.google.common.base.Strings;
 public class Delay {
     /**
      * <p> The optional attempt is the number of tries this delay should be used (default = 1).
-     * The delayTime is parsed by {@link TimeConverter}</p>
+     * The delayTime is parsed by {@link DurationParser}</p>
      *
      * @param initString the string to initialize this Delay object from. It has the form "[attempt\*]delaytime[unit]"
      */
@@ -46,14 +46,14 @@ public class Delay {
         List<String> parts = Splitter.on('*').trimResults().splitToList(initString);
 
         if (parts.size() == 1) {
-            return new Delay(DEFAULT_ATTEMPTS, TimeConverter.parseDuration(parts.get(0)));
+            return new Delay(DEFAULT_ATTEMPTS, DurationParser.parse(parts.get(0)));
         }
         if (parts.size() == 2) {
             int attempts = Integer.parseInt(parts.get(0));
             if (attempts < 0) {
                 throw new MessagingException("Number of attempts negative in " + initString);
             }
-            return new Delay(attempts, TimeConverter.parseDuration(parts.get(1)));
+            return new Delay(attempts, DurationParser.parse(parts.get(1)));
         }
         throw new MessagingException(initString + " contains too much parts");
     }
diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/POP3BeforeSMTPHandler.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/POP3BeforeSMTPHandler.java
index 5558614..4b8f6f6 100644
--- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/POP3BeforeSMTPHandler.java
+++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/POP3BeforeSMTPHandler.java
@@ -29,7 +29,7 @@ import org.apache.james.protocols.api.Response;
 import org.apache.james.protocols.api.handler.ConnectHandler;
 import org.apache.james.protocols.lib.POP3BeforeSMTPHelper;
 import org.apache.james.protocols.smtp.SMTPSession;
-import org.apache.james.util.TimeConverter;
+import org.apache.james.util.DurationParser;
 
 /**
  * This ConnectHandler can be used to activate pop-before-smtp
@@ -56,7 +56,7 @@ public class POP3BeforeSMTPHandler implements ConnectHandler<SMTPSession>, Confi
      */
     public void setExpireTime(String rawExpireTime) {
         if (rawExpireTime != null) {
-            this.expireTime = TimeConverter.parseDuration(rawExpireTime);
+            this.expireTime = DurationParser.parse(rawExpireTime);
         }
     }
 
diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/JDBCGreylistHandler.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/JDBCGreylistHandler.java
index 071958f..4f3647a 100644
--- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/JDBCGreylistHandler.java
+++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/JDBCGreylistHandler.java
@@ -47,7 +47,7 @@ import org.apache.james.protocols.api.handler.ProtocolHandler;
 import org.apache.james.protocols.smtp.SMTPSession;
 import org.apache.james.protocols.smtp.core.fastfail.AbstractGreylistHandler;
 import org.apache.james.protocols.smtp.hook.HookResult;
-import org.apache.james.util.TimeConverter;
+import org.apache.james.util.DurationParser;
 import org.apache.james.util.sql.JDBCUtil;
 import org.apache.james.util.sql.SqlResources;
 import org.slf4j.Logger;
@@ -136,7 +136,7 @@ public class JDBCGreylistHandler extends AbstractGreylistHandler implements Prot
      *            The temporary blocking time
      */
     public void setTempBlockTime(String tempBlockTime) {
-        setTempBlockTime(TimeConverter.parseDuration(tempBlockTime));
+        setTempBlockTime(DurationParser.parse(tempBlockTime));
     }
 
     /**
@@ -147,7 +147,7 @@ public class JDBCGreylistHandler extends AbstractGreylistHandler implements Prot
      *            The lifeTime
      */
     public void setAutoWhiteListLifeTime(String autoWhiteListLifeTime) {
-        setAutoWhiteListLifeTime(TimeConverter.parseDuration(autoWhiteListLifeTime));
+        setAutoWhiteListLifeTime(DurationParser.parse(autoWhiteListLifeTime));
     }
 
     /**
@@ -158,7 +158,7 @@ public class JDBCGreylistHandler extends AbstractGreylistHandler implements Prot
      *            The lifetime
      */
     public void setUnseenLifeTime(String unseenLifeTime) {
-        setUnseenLifeTime(TimeConverter.parseDuration(unseenLifeTime));
+        setUnseenLifeTime(DurationParser.parse(unseenLifeTime));
     }
 
     @Inject
diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/configuration/CassandraMailQueueViewConfiguration.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/configuration/CassandraMailQueueViewConfiguration.java
index dc13b23..dee8f89 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/configuration/CassandraMailQueueViewConfiguration.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/configuration/CassandraMailQueueViewConfiguration.java
@@ -24,7 +24,7 @@ import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.commons.configuration.Configuration;
-import org.apache.james.util.TimeConverter;
+import org.apache.james.util.DurationParser;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
@@ -95,7 +95,7 @@ public class CassandraMailQueueViewConfiguration {
             .bucketCount(bucketCount)
             .updateBrowseStartPace(updateBrowseStartPace)
             .sliceWindow(sliceWindowAsString
-                .map(TimeConverter::parseDuration)
+                .map(DurationParser::parse)
                 .orElse(DEFAULT_SLICE_WINDOW))
             .build();
     }


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


[james-project] 05/23: JAMES-2685 revise user vault api path

Posted by bt...@apache.org.
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 85cfd171fc94fd85483d86f4a710c219ad863a6a
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Mon Mar 18 14:28:32 2019 +0700

    JAMES-2685 revise user vault api path
    
    From `.../user/...` to `.../users/...`
---
 .../james/jmap/methods/integration/DeletedMessagesVaultTest.java      | 2 +-
 .../james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java       | 3 +--
 src/site/markdown/server/manage-webadmin.md                           | 4 ++--
 3 files changed, 4 insertions(+), 5 deletions(-)

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 67f91a9..109fa2b 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
@@ -489,7 +489,7 @@ public abstract class DeletedMessagesVaultTest {
     private void restoreMessagesFor(String user, String criteria) {
         String taskId = webAdminApi.with()
             .body(criteria)
-            .post("/deletedMessages/user/" + user + "?action=restore")
+            .post("/deletedMessages/users/" + 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/DeletedMessagesVaultRoutes.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java
index ba8864c..940e3a8 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
@@ -63,7 +63,6 @@ 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;
@@ -103,7 +102,7 @@ public class DeletedMessagesVaultRoutes implements Routes {
         }
     }
 
-    public static final String ROOT_PATH = "deletedMessages/user";
+    public static final String ROOT_PATH = "deletedMessages/users";
     private static final String USER_PATH_PARAM = "user";
     private static final String RESTORE_PATH = ROOT_PATH + SEPARATOR + ":" + USER_PATH_PARAM;
     private static final String ACTION_QUERY_PARAM = "action";
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 55e6e41..56d8bc0 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -2561,7 +2561,7 @@ Here are the following actions available on the 'Deleted Messages Vault'
 Deleted messages of a specific user can be restored by calling the following endpoint:
 
 ```
-curl -XPOST http://ip:port/deletedMessages/user/userToRestore@domain.ext?action=restore
+curl -XPOST http://ip:port/deletedMessages/users/userToRestore@domain.ext?action=restore
 
 {"
   "combinator": "and",
@@ -2692,7 +2692,7 @@ while:
 Retrieve deleted messages matched with requested query from an user then share the content to a targeted mail address (exportTo)
 
 ```
-curl -XPOST http://ip:port/deletedMessages/user/userExportFrom@domain.ext?action=export&exportTo=userReceiving@domain.ext
+curl -XPOST http://ip:port/deletedMessages/users/userExportFrom@domain.ext?action=export&exportTo=userReceiving@domain.ext
 
 BODY: is the json query has the same structure with Restore Deleted Messages section
 ```


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


[james-project] 01/23: JAMES-2685 make usages component become public

Posted by bt...@apache.org.
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 c67fb1760127c774bac941e393279ca91bfe0598
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Fri Mar 15 14:30:40 2019 +0700

    JAMES-2685 make usages component become public
---
 .../src/main/java/org/apache/james/vault/DeletedMessageZipper.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java
index cad293f..22e3fd2 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java
@@ -38,11 +38,11 @@ import com.github.fge.lambdas.consumers.ThrowingConsumer;
 import com.google.common.annotations.VisibleForTesting;
 
 public class DeletedMessageZipper {
-    interface DeletedMessageContentLoader {
+    public interface DeletedMessageContentLoader {
         InputStream load(DeletedMessage deletedMessage);
     }
 
-    DeletedMessageZipper() {
+    public DeletedMessageZipper() {
         ExtraFieldUtils.register(MessageIdExtraField.class);
         ExtraFieldUtils.register(SizeExtraField.class);
     }


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


[james-project] 02/23: JAMES-2685 Moving `Restore` tests to a nested class

Posted by bt...@apache.org.
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 205626f05dce32818dff031452b816a669ec4b3e
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Thu Mar 14 17:02:44 2019 +0700

    JAMES-2685 Moving `Restore` tests to a nested class
---
 .../routes/DeletedMessagesVaultRoutesTest.java     | 2235 ++++++++++----------
 1 file changed, 1120 insertions(+), 1115 deletions(-)

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 793edb9..3ed17dd 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
@@ -137,878 +137,1258 @@ class DeletedMessagesVaultRoutesTest {
     }
 
     @Nested
-    class QueryTest {
+    class RestoreTest {
 
         @Nested
-        class SubjectTest {
+        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");
 
-            @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\"" +
-                    "}";
+                    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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    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");
 
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
-            }
+                    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");
 
-            @Test
-            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntContains() throws Exception {
-                vault.append(USER, FINAL_STAGE.get()
-                    .subject("subject")
-                    .build(), new ByteArrayInputStream(CONTENT)).block();
+                    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");
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"subject\"," +
-                    "  \"operator\": \"contains\"," +
-                    "  \"value\": \"james\"" +
-                    "}";
+                    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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    given()
+                        .basePath(TasksRoutes.BASE)
+                    .when()
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                assertThat(hasAnyMail(USER)).isFalse();
+                    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();
+            @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");
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"subject\"," +
-                    "  \"operator\": \"containsIgnoreCase\"," +
-                    "  \"value\": \"subject contains\"" +
-                    "}";
-
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .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)));
-            }
+                        .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");
 
-            @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\"" +
-                    "}";
+                    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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    given()
+                        .basePath(TasksRoutes.BASE)
+                    .when()
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                assertThat(hasAnyMail(USER)).isFalse();
+                    assertThat(hasAnyMail(USER)).isFalse();
+                }
             }
 
-            @Test
-            void restoreShouldAppendMessageToMailboxWhenMatchingSubjectEquals() throws Exception {
-                vault.append(USER, FINAL_STAGE.get()
-                    .subject("subject should match")
-                    .build(), new ByteArrayInputStream(CONTENT)).block();
+            @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");
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"subject\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"subject should match\"" +
-                    "}";
+                    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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    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");
 
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
-            }
+                    given()
+                        .basePath(TasksRoutes.BASE)
+                    .when()
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-            @Test
-            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntEquals() throws Exception {
-                vault.append(USER, FINAL_STAGE.get()
-                    .subject("subject")
-                    .build(), new ByteArrayInputStream(CONTENT)).block();
+                    assertThat(hasAnyMail(USER)).isFalse();
+                }
+            }
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"subject\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"SUBJECT\"" +
-                    "}";
+            @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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    given()
+                        .basePath(TasksRoutes.BASE)
+                    .when()
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                assertThat(hasAnyMail(USER)).isFalse();
+                    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\"" +
-                    "}";
+            @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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    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)));
+                    assertThat(hasAnyMail(USER)).isFalse();
+                }
             }
 
+            @Nested
+            class HasAttachmentTest {
+
             @Test
-            void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntEqualsIgnoreCase() throws Exception {
-                vault.append(USER, FINAL_STAGE.get()
-                    .subject("subject")
-                    .build(), new ByteArrayInputStream(CONTENT)).block();
+            void restoreShouldAppendMessageToMailboxWhenMatchingNoAttachment() throws Exception {
+                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
+                    .hasAttachment(false)
+                    .size(CONTENT.length)
+                    .build();
+                storeDeletedMessage(deletedMessage);
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"subject\"," +
-                    "  \"operator\": \"equalsIgnoreCase\"," +
-                    "  \"value\": \"SUBJECT Of the mail\"" +
-                    "}";
+                    String query =
+                        "{" +
+                        "  \"fieldName\": \"hasAttachment\"," +
+                        "  \"operator\": \"equals\"," +
+                        "  \"value\": \"false\"" +
+                        "}";
+
+                    String taskId =
+                        given()
+                            .queryParam("action", "restore")
+                            .body(query)
+                        .when()
+                            .post(USER.asString())
+                            .jsonPath()
+                            .get("taskId");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
-                given()
-                    .basePath(TasksRoutes.BASE)
-                .when()
-                    .get(taskId + "/await")
-                .then()
-                    .body("status", is("completed"));
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                assertThat(hasAnyMail(USER)).isFalse();
-            }
-        }
-
-        @Nested
-        class DeletionDateTest {
+                    assertThat(restoreMessageContents(USER))
+                        .hasSize(1)
+                        .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                }
 
             @Test
-            void restoreShouldAppendMessageToMailboxWhenMatchingDeletionDateBeforeOrEquals() throws Exception {
-                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+            void restoreShouldAppendMessageToMailboxWhenMatchingHasAttachment() throws Exception {
+                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
+                    .hasAttachment()
+                    .size(CONTENT.length)
+                    .build();
+                storeDeletedMessage(deletedMessage);
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"deletionDate\"," +
-                    "  \"operator\": \"beforeOrEquals\"," +
-                    "  \"value\": \"" + DELETION_DATE.plusHours(1).toString() + "\"" +
-                    "}";
+                    String query =
+                        " {" +
+                        "  \"fieldName\": \"hasAttachment\"," +
+                        "  \"operator\": \"equals\"," +
+                        "  \"value\": \"true\"" +
+                        "}";
+
+                    String taskId =
+                        given()
+                            .queryParam("action", "restore")
+                            .body(query)
+                        .when()
+                            .post(USER.asString())
+                            .jsonPath()
+                            .get("taskId");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
-                given()
-                    .basePath(TasksRoutes.BASE)
-                .when()
-                    .get(taskId + "/await")
-                .then()
-                    .body("status", is("completed"));
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
-            }
+                    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();
+            void restoreShouldNotAppendMessageToMailboxWhenMatchingHasNoAttachment() throws Exception {
+                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
+                    .hasAttachment(false)
+                    .size(CONTENT.length)
+                    .build();
+                storeDeletedMessage(deletedMessage);
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"deletionDate\"," +
-                    "  \"operator\": \"beforeOrEquals\"," +
-                    "  \"value\": \"" + DELETION_DATE.minusHours(1).toString() + "\"" +
-                    "}";
+                    String query =
+                        "{" +
+                        "  \"fieldName\": \"hasAttachment\"," +
+                        "  \"operator\": \"equals\"," +
+                        "  \"value\": \"true\"" +
+                        "}";
+
+                    String taskId =
+                        given()
+                            .queryParam("action", "restore")
+                            .body(query)
+                        .when()
+                            .post(USER.asString())
+                            .jsonPath()
+                            .get("taskId");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                given()
-                    .basePath(TasksRoutes.BASE)
-                .when()
-                    .get(taskId + "/await")
-                .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    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() + "\"" +
-                    "}";
+            @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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    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)));
+                    assertThat(hasAnyMail(USER)).isFalse();
+                }
             }
 
-            @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() + "\"" +
-                    "}";
+            @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");
 
-                String taskId =
                     given()
-                        .queryParam("action", "restore")
-                        .body(query)
+                        .basePath(TasksRoutes.BASE)
                     .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
+                        .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"));
+                    given()
+                        .basePath(TasksRoutes.BASE)
+                    .when()
+                        .get(taskId + "/await")
+                    .then()
+                        .body("status", is("completed"));
 
-                assertThat(hasAnyMail(USER)).isFalse();
+                    assertThat(hasAnyMail(USER)).isFalse();
+                }
             }
         }
 
         @Nested
-        class DeliveryDateTest {
+        class ValidationTest {
 
             @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")
+            void restoreShouldReturnInvalidWhenActionIsMissing() {
+                when()
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 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");
-
+            void restoreShouldReturnInvalidWhenPassingEmptyAction() {
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .queryParam("action", "")
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .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 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");
-
+            void restoreShouldReturnInvalidWhenActionIsInValid() {
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .queryParam("action", "invalid action")
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 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");
-
+            void restoreShouldReturnInvalidWhenPassingCaseInsensitiveAction() {
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .queryParam("action", "RESTORE")
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .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()));
             }
-        }
-
-        @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");
-
+            void restoreShouldReturnInvalidWhenUserIsInvalid() {
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .queryParam("action", "restore")
                 .when()
-                    .get(taskId + "/await")
+                    .post("not@valid@user.com")
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 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");
-
+            void postShouldReturnNotFoundWhenNoUserPathParameter() {
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .queryParam("action", "restore")
                 .when()
-                    .get(taskId + "/await")
+                    .post()
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .statusCode(HttpStatus.NOT_FOUND_404)
+                    .body("statusCode", is(404))
+                    .body("type", is(notNullValue()))
+                    .body("message", is(notNullValue()));
             }
-        }
 
-        @Nested
-        class SenderTest {
             @Test
-            void restoreShouldAppendMessageToMailboxWhenMatchingSenderEquals() throws Exception {
+            void restoreShouldReturnBadRequestWhenPassingUnsupportedField() throws Exception {
                 vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
 
                 String query =
                     "{" +
-                    "  \"fieldName\": \"sender\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"" + SENDER.asString() + "\"" +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"unsupported\"," +
+                    "      \"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)
+                    .body(query)
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 restoreShouldAppendMessageToMailboxWhenMatchingSenderDoesntEquals() throws Exception {
+            void restoreShouldReturnBadRequestWhenPassingUnsupportedOperator() throws Exception {
                 vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
 
                 String query =
                     "{" +
-                    "  \"fieldName\": \"sender\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"" + SENDER2.asString() + "\"" +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"subject\"," +
+                    "      \"operator\": \"isLongerThan\"," +
+                    "      \"value\": \"" + SUBJECT + "\"" +
+                    "    }" +
+                    "  ]" +
                     "}";
 
-                String taskId =
-                    given()
-                        .queryParam("action", "restore")
-                        .body(query)
-                    .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .body(query)
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .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()));
             }
-        }
-
-        @Nested
-        class HasAttachmentTest {
 
             @Test
-            void restoreShouldAppendMessageToMailboxWhenMatchingNoAttachment() throws Exception {
-                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
-                    .hasAttachment(false)
-                    .size(CONTENT.length)
-                    .build();
-                storeDeletedMessage(deletedMessage);
+            void restoreShouldReturnBadRequestWhenPassingUnsupportedPairOfFieldNameAndOperator() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
 
                 String query =
                     "{" +
-                    "  \"fieldName\": \"hasAttachment\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"false\"" +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"sender\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"" + SENDER.asString() + "\"" +
+                    "    }" +
+                    "  ]" +
                     "}";
 
-                String taskId =
-                    given()
-                        .queryParam("action", "restore")
-                        .body(query)
-                    .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .body(query)
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 restoreShouldAppendMessageToMailboxWhenMatchingHasAttachment() throws Exception {
-                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
-                    .hasAttachment()
-                    .size(CONTENT.length)
-                    .build();
-                storeDeletedMessage(deletedMessage);
+            void restoreShouldReturnBadRequestWhenPassingInvalidMailAddress() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
 
                 String query =
-                    " {" +
-                    "  \"fieldName\": \"hasAttachment\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"true\"" +
+                    "{" +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"sender\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"invalid@mail@domain.tld\"" +
+                    "    }" +
+                    "  ]" +
                     "}";
 
-                String taskId =
-                    given()
-                        .queryParam("action", "restore")
-                        .body(query)
-                    .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .body(query)
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 restoreShouldNotAppendMessageToMailboxWhenMatchingHasNoAttachment() throws Exception {
-                DeletedMessage deletedMessage = messageWithAttachmentBuilder()
-                    .hasAttachment(false)
-                    .size(CONTENT.length)
-                    .build();
-                storeDeletedMessage(deletedMessage);
+            @Test
+            void restoreShouldReturnBadRequestWhenPassingOrCombinator() throws Exception {
+                vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
 
                 String query =
                     "{" +
-                    "  \"fieldName\": \"hasAttachment\"," +
-                    "  \"operator\": \"equals\"," +
-                    "  \"value\": \"true\"" +
+                    "  \"combinator\": \"or\"," +
+                    "  \"criteria\": [" +
+                    "    {" +
+                    "      \"fieldName\": \"sender\"," +
+                    "      \"operator\": \"contains\"," +
+                    "      \"value\": \"" + SENDER.asString() + "\"" +
+                    "    }" +
+                    "  ]" +
                     "}";
 
-                String taskId =
-                    given()
-                        .queryParam("action", "restore")
-                        .body(query)
-                    .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .body(query)
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .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()));
             }
-        }
-
-        @Nested
-        class OriginMailboxIdsTest {
 
             @Test
-            void restoreShouldAppendMessageToMailboxWhenContainsMailboxId() throws Exception {
+            void restoreShouldReturnBadRequestWhenPassingNestedStructuredQuery() throws Exception {
                 vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
 
                 String query =
                     "{" +
-                    "  \"fieldName\": \"originMailboxes\"," +
-                    "  \"operator\": \"contains\"," +
-                    "  \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" +
+                    "  \"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\"}" +
+                    "  ]" +
                     "}";
 
-                String taskId =
-                    given()
-                        .queryParam("action", "restore")
-                        .body(query)
-                    .when()
-                        .post(USER.asString())
-                        .jsonPath()
-                        .get("taskId");
-
                 given()
-                    .basePath(TasksRoutes.BASE)
+                    .body(query)
                 .when()
-                    .get(taskId + "/await")
+                    .post(USER.asString())
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(1)
-                    .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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()));
             }
+        }
+
+        @Nested
+        class FailingRestoreTest {
 
             @Test
-            void restoreShouldNotAppendMessageToMailboxWhenDoNotContainsMailboxId() throws Exception {
+            void restoreShouldProduceFailedTaskWhenTheVaultGetsError() {
                 vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+                vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-                String query =
-                    "{" +
-                    "  \"fieldName\": \"originMailboxes\"," +
-                    "  \"operator\": \"contains\"," +
-                    "  \"value\": \"" + MAILBOX_ID_3.serialize() + "\"" +
-                    "}";
+                doThrow(new RuntimeException("mock exception"))
+                    .when(vault)
+                    .search(any(), any());
 
                 String taskId =
                     given()
                         .queryParam("action", "restore")
-                        .body(query)
+                        .body(MATCH_ALL_QUERY)
                     .when()
                         .post(USER.asString())
                         .jsonPath()
                         .get("taskId");
 
                 given()
+                    .queryParam("action", "restore")
                     .basePath(TasksRoutes.BASE)
                 .when()
                     .get(taskId + "/await")
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .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()));
             }
-        }
-
-        @Nested
-        class MultipleCriteriaTest {
 
             @Test
-            void restoreShouldAppendMessageToMailboxWhenAllcriteriaAreMatched() throws Exception {
+            void restoreShouldProduceFailedTaskWithErrorRestoreCountWhenMessageAppendGetsError() 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() + "\"" +
-                    "    }" +
-                    "  ]" +
-                    "}";
+                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 =
                     given()
                         .queryParam("action", "restore")
-                        .body(query)
+                        .body(MATCH_ALL_QUERY)
                     .when()
                         .post(USER.asString())
                         .jsonPath()
@@ -1019,49 +1399,29 @@ class DeletedMessagesVaultRoutesTest {
                 .when()
                     .get(taskId + "/await")
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(restoreMessageContents(USER))
-                    .hasSize(2)
-                    .allSatisfy(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT)));
+                    .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 restoreShouldNotAppendMessageToMailboxWhenASingleCriterionDoesntMatch() throws Exception {
+            void restoreShouldProduceFailedTaskWhenMailboxMangerGetsError() 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() + "\"" +
-                    "    }" +
-                    "  ]" +
-                    "}";
+                doThrow(new RuntimeException("mock exception"))
+                    .when(mailboxManager)
+                    .createMailbox(any(MailboxPath.class), any(MailboxSession.class));
 
                 String taskId =
                     given()
                         .queryParam("action", "restore")
-                        .body(query)
+                        .body(MATCH_ALL_QUERY)
                     .when()
                         .post(USER.asString())
                         .jsonPath()
@@ -1072,277 +1432,97 @@ class DeletedMessagesVaultRoutesTest {
                 .when()
                     .get(taskId + "/await")
                 .then()
-                    .body("status", is("completed"));
-
-                assertThat(hasAnyMail(USER)).isFalse();
+                    .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()));
             }
         }
-    }
-
-    @Nested
-    class ValidationTest {
-
-        @Test
-        void restoreShouldReturnInvalidWhenActionIsMissing() {
-            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 restoreShouldReturnInvalidWhenPassingEmptyAction() {
-            given()
-                .queryParam("action", "")
-            .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 restoreShouldReturnInvalidWhenActionIsInValid() {
-            given()
-                .queryParam("action", "invalid action")
-            .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 restoreShouldReturnInvalidWhenPassingCaseInsensitiveAction() {
-            given()
-                .queryParam("action", "RESTORE")
-            .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 restoreShouldReturnInvalidWhenUserIsInvalid() {
-            given()
-                .queryParam("action", "restore")
-            .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(notNullValue()))
-                .body("details", is(notNullValue()));
-        }
 
         @Test
-        void postShouldReturnNotFoundWhenNoUserPathParameter() {
+        void restoreShouldReturnATaskCreated() {
             given()
                 .queryParam("action", "restore")
-            .when()
-                .post()
-            .then()
-                .statusCode(HttpStatus.NOT_FOUND_404)
-                .body("statusCode", is(404))
-                .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)
+                .body(MATCH_ALL_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()));
+                .statusCode(HttpStatus.CREATED_201)
+                .body("taskId", notNullValue());
         }
 
         @Test
-        void restoreShouldReturnBadRequestWhenPassingUnsupportedPairOfFieldNameAndOperator() throws Exception {
+        void restoreShouldProduceASuccessfulTaskWithAdditionalInformation() {
             vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+            vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-            String query =
-                "{" +
-                "  \"criteria\": [" +
-                "    {" +
-                "      \"fieldName\": \"sender\"," +
-                "      \"operator\": \"contains\"," +
-                "      \"value\": \"" + SENDER.asString() + "\"" +
-                "    }" +
-                "  ]" +
-                "}";
+            String taskId =
+                given()
+                    .queryParam("action", "restore")
+                    .body(MATCH_ALL_QUERY)
+                .when()
+                    .post(USER.asString())
+                    .jsonPath()
+                    .get("taskId");
 
             given()
-                .body(query)
+                .basePath(TasksRoutes.BASE)
             .when()
-                .post(USER.asString())
+                .get(taskId + "/await")
             .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()));
+                .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 restoreShouldReturnBadRequestWhenPassingInvalidMailAddress() throws Exception {
+        void restoreShouldKeepAllMessagesInTheVaultOfCorrespondingUser() {
             vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
+            vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
 
-            String query =
-                "{" +
-                "  \"criteria\": [" +
-                "    {" +
-                "      \"fieldName\": \"sender\"," +
-                "      \"operator\": \"contains\"," +
-                "      \"value\": \"invalid@mail@domain.tld\"" +
-                "    }" +
-                "  ]" +
-                "}";
+            String taskId =
+                given()
+                    .queryParam("action", "restore")
+                    .body(MATCH_ALL_QUERY)
+                .when()
+                    .post(USER.asString())
+                    .jsonPath()
+                    .get("taskId");
 
             given()
-                .body(query)
+                .basePath(TasksRoutes.BASE)
             .when()
-                .post(USER.asString())
+                .get(taskId + "/await")
             .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() + "\"" +
-                "    }" +
-                "  ]" +
-                "}";
+                .body("status", is("completed"));
 
-            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()));
+            assertThat(Flux.from(vault.search(USER, Query.ALL)).toStream())
+                .containsOnly(DELETED_MESSAGE, DELETED_MESSAGE_2);
         }
 
         @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()));
-        }
-    }
-
-    @Nested
-    class FailingRestoreTest {
+        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);
 
-        @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 =
                 given()
                     .queryParam("action", "restore")
@@ -1353,35 +1533,21 @@ class DeletedMessagesVaultRoutesTest {
                     .get("taskId");
 
             given()
-                .queryParam("action", "restore")
                 .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()));
+                .body("status", is("completed"));
+
+            assertThat(restoreMailboxMessages(USER))
+                .hasSize(3);
         }
 
         @Test
-        void restoreShouldProduceFailedTaskWithErrorRestoreCountWhenMessageAppendGetsError() throws Exception {
+        void restoreShouldAppendAllMessageFromVaultToRestoreMailboxOfCorrespondingUser() 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 =
                 given()
                     .queryParam("action", "restore")
@@ -1396,25 +1562,19 @@ class DeletedMessagesVaultRoutesTest {
             .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()));
+                .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 restoreShouldProduceFailedTaskWhenMailboxMangerGetsError() throws Exception {
+        void restoreShouldNotAppendMessagesToAnOtherUserMailbox() 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 =
                 given()
                     .queryParam("action", "restore")
@@ -1429,167 +1589,12 @@ class DeletedMessagesVaultRoutesTest {
             .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() {
-        given()
-            .queryParam("action", "restore")
-            .body(MATCH_ALL_QUERY)
-        .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 =
-            given()
-                .queryParam("action", "restore")
-                .body(MATCH_ALL_QUERY)
-            .when()
-                .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 =
-            given()
-                .queryParam("action", "restore")
-                .body(MATCH_ALL_QUERY)
-            .when()
-                .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 =
-            given()
-                .queryParam("action", "restore")
-                .body(MATCH_ALL_QUERY)
-            .when()
-                .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 =
-            given()
-                .queryParam("action", "restore")
-                .body(MATCH_ALL_QUERY)
-            .when()
-                .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)));
-    }
+                .body("status", is("completed"));
 
-    @Test
-    void restoreShouldNotAppendMessagesToAnOtherUserMailbox() throws Exception {
-        vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block();
-        vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block();
+            assertThat(hasAnyMail(USER_2))
+                .isFalse();
+        }
 
-        String taskId =
-            given()
-                .queryParam("action", "restore")
-                .body(MATCH_ALL_QUERY)
-            .when()
-                .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 {


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


[james-project] 20/23: MAILBOX-388 Implement DeletedMessagesVault retention

Posted by bt...@apache.org.
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 488faff1d8f9c0499a27f017ea50d0f5ee5fdc58
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 16:15:07 2019 +0700

    MAILBOX-388 Implement DeletedMessagesVault retention
---
 .../apache/james/vault/DeletedMessageVault.java    |   3 +
 .../james/vault/memory/DeleteByQueryExecutor.java  |  94 +++++++++++++++++
 .../vault/memory/MemoryDeletedMessagesVault.java   |  26 ++++-
 .../vault/memory/VaultGarbageCollectionTask.java   | 115 +++++++++++++++++++++
 .../apache/james/vault/DeletedMessageFixture.java  |  15 +++
 .../james/vault/DeletedMessageVaultContract.java   |  69 +++++++++++++
 .../james/vault/DeletedMessageVaultHookTest.java   |   2 +-
 .../memory/MemoryDeletedMessagesVaultTest.java     |   3 +-
 .../modules/vault/DeletedMessageVaultModule.java   |  14 +++
 .../vault/MailRepositoryDeletedMessageVault.java   |  46 +++++++--
 .../MailRepositoryDeletedMessageVaultTest.java     |   3 +-
 .../routes/DeletedMessagesVaultRoutesTest.java     |   4 +-
 12 files changed, 379 insertions(+), 15 deletions(-)

diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java
index 54d3579..420c7cd 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageVault.java
@@ -23,6 +23,7 @@ import java.io.InputStream;
 
 import org.apache.james.core.User;
 import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.task.Task;
 import org.apache.james.vault.search.Query;
 import org.reactivestreams.Publisher;
 
@@ -36,4 +37,6 @@ public interface DeletedMessageVault {
     Publisher<DeletedMessage> search(User user, Query query);
 
     Publisher<User> usersWithVault();
+
+    Task deleteExpiredMessagesTask();
 }
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
new file mode 100644
index 0000000..02e6936
--- /dev/null
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/DeleteByQueryExecutor.java
@@ -0,0 +1,94 @@
+/****************************************************************
+ * 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.vault.memory;
+
+import org.apache.james.core.User;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.task.Task;
+import org.apache.james.util.FunctionalUtils;
+import org.apache.james.vault.DeletedMessageVault;
+import org.apache.james.vault.search.Query;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class DeleteByQueryExecutor {
+    @FunctionalInterface
+    interface Notifier {
+        void doNotify();
+    }
+
+    static class Notifiers {
+        private final Notifier userHandledNotifier;
+        private final Notifier searchErrorNotifier;
+        private final Notifier deletionErrorNotifier;
+        private final Notifier permanentlyDeletedMessageNotifyer;
+
+        Notifiers(Notifier userHandledNotifier, Notifier searchErrorNotifier, Notifier deletionErrorNotifier, Notifier permanentlyDeletedMessageNotifyer) {
+            this.userHandledNotifier = userHandledNotifier;
+            this.searchErrorNotifier = searchErrorNotifier;
+            this.deletionErrorNotifier = deletionErrorNotifier;
+            this.permanentlyDeletedMessageNotifyer = permanentlyDeletedMessageNotifyer;
+        }
+    }
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(DeleteByQueryExecutor.class);
+
+    private final DeletedMessageVault deletedMessageVault;
+
+    public DeleteByQueryExecutor(DeletedMessageVault deletedMessageVault) {
+        this.deletedMessageVault = deletedMessageVault;
+    }
+
+    public Task.Result deleteByQuery(Query query, Notifiers notifiers) {
+        return Flux.from(deletedMessageVault.usersWithVault())
+            .flatMap(user -> deleteByQueryForUser(query, user, notifiers))
+            .reduce(Task::combine)
+            .blockOptional()
+            .orElse(Task.Result.COMPLETED);
+    }
+
+    private Mono<Task.Result> deleteByQueryForUser(Query query, User user, Notifiers notifiers) {
+        return Flux.from(deletedMessageVault.search(user, query))
+            .flatMap(message -> deleteMessage(user, message.getMessageId(), notifiers))
+            .onErrorResume(e -> {
+                LOGGER.error("Error encountered while searching old mails in {} vault", user.asString(), e);
+                notifiers.searchErrorNotifier.doNotify();
+                return Mono.just(Task.Result.PARTIAL);
+            })
+            .reduce(Task::combine)
+
+            .map(FunctionalUtils.identityWithSideEffect(() -> LOGGER.info("Retention applied for {} vault", user.asString())))
+            .map(FunctionalUtils.identityWithSideEffect(notifiers.userHandledNotifier::doNotify));
+    }
+
+    private Mono<Task.Result> deleteMessage(User user, MessageId messageId, Notifiers notifiers) {
+        return Mono.from(deletedMessageVault.delete(user, messageId))
+            .then(Mono.fromRunnable(notifiers.permanentlyDeletedMessageNotifyer::doNotify))
+            .thenReturn(Task.Result.COMPLETED)
+            .onErrorResume(e -> {
+                LOGGER.error("Error encountered while deleting a mail in {} vault: {}", user.asString(), messageId.serialize(), e);
+                notifiers.deletionErrorNotifier.doNotify();
+                return Mono.just(Task.Result.PARTIAL);
+            });
+    }
+}
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 3389dcb..d48bfda 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
@@ -22,14 +22,19 @@ package org.apache.james.vault.memory;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.Optional;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.james.core.User;
 import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.task.Task;
 import org.apache.james.vault.DeletedMessage;
 import org.apache.james.vault.DeletedMessageVault;
+import org.apache.james.vault.RetentionConfiguration;
 import org.apache.james.vault.search.Query;
 import org.reactivestreams.Publisher;
 
@@ -42,10 +47,16 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public class MemoryDeletedMessagesVault implements DeletedMessageVault {
+    private final RetentionConfiguration retentionConfiguration;
     private final Table<User, MessageId, Pair<DeletedMessage, byte[]>> table;
-
-    public MemoryDeletedMessagesVault() {
-        table = HashBasedTable.create();
+    private final Clock clock;
+    private DeleteByQueryExecutor deleteByQueryExecutor;
+
+    public MemoryDeletedMessagesVault(RetentionConfiguration retentionConfiguration, Clock clock) {
+        this.deleteByQueryExecutor = new DeleteByQueryExecutor(this);
+        this.retentionConfiguration = retentionConfiguration;
+        this.clock = clock;
+        this.table = HashBasedTable.create();
     }
 
     @Override
@@ -104,6 +115,15 @@ public class MemoryDeletedMessagesVault implements DeletedMessageVault {
         }
     }
 
+    public Task deleteExpiredMessagesTask() {
+        ZonedDateTime now = ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
+        ZonedDateTime beginningOfRetentionPeriod = now.minus(retentionConfiguration.getRetentionPeriod());
+
+        return new VaultGarbageCollectionTask(
+            deleteByQueryExecutor,
+            beginningOfRetentionPeriod);
+    }
+
     private Flux<DeletedMessage> listAll(User user) {
         synchronized (table) {
             return Flux.fromIterable(ImmutableList.copyOf(table.row(user).values()))
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/VaultGarbageCollectionTask.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/VaultGarbageCollectionTask.java
new file mode 100644
index 0000000..93c8ce3
--- /dev/null
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/memory/VaultGarbageCollectionTask.java
@@ -0,0 +1,115 @@
+/****************************************************************
+ * 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.vault.memory;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.vault.search.CriterionFactory;
+import org.apache.james.vault.search.Query;
+
+public class VaultGarbageCollectionTask implements Task {
+    public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+        private final ZonedDateTime beginningOfRetentionPeriod;
+        private final long handledUserCount;
+        private final long permanantlyDeletedMessages;
+        private final long vaultSearchErrorCount;
+        private final long deletionErrorCount;
+
+        AdditionalInformation(ZonedDateTime beginningOfRetentionPeriod, long handledUserCount, long permanantlyDeletedMessages, long vaultSearchErrorCount, long deletionErrorCount) {
+            this.beginningOfRetentionPeriod = beginningOfRetentionPeriod;
+            this.handledUserCount = handledUserCount;
+            this.permanantlyDeletedMessages = permanantlyDeletedMessages;
+            this.vaultSearchErrorCount = vaultSearchErrorCount;
+            this.deletionErrorCount = deletionErrorCount;
+        }
+
+        public  ZonedDateTime getBeginningOfRetentionPeriod() {
+            return beginningOfRetentionPeriod;
+        }
+
+        public long getHandledUserCount() {
+            return handledUserCount;
+        }
+
+        public long getPermanantlyDeletedMessages() {
+            return permanantlyDeletedMessages;
+        }
+
+        public long getVaultSearchErrorCount() {
+            return vaultSearchErrorCount;
+        }
+
+        public long getDeletionErrorCount() {
+            return deletionErrorCount;
+        }
+    }
+
+    private static final String TYPE = "deletedMessages/garbageCollection";
+
+    private final DeleteByQueryExecutor deleteByQueryExecutor;
+    private final DeleteByQueryExecutor.Notifiers notifiers;
+    private final AtomicLong handledUserCount;
+    private final AtomicLong permanantlyDeletedMessages;
+    private final AtomicLong vaultSearchErrorCount;
+    private final AtomicLong deletionErrorCount;
+    private final ZonedDateTime beginningOfRetentionPeriod;
+
+    public VaultGarbageCollectionTask(DeleteByQueryExecutor deleteByQueryExecutor, ZonedDateTime beginningOfRetentionPeriod) {
+        this.deleteByQueryExecutor = deleteByQueryExecutor;
+        this.beginningOfRetentionPeriod = beginningOfRetentionPeriod;
+
+        this.handledUserCount = new AtomicLong(0);
+        this.permanantlyDeletedMessages = new AtomicLong(0);
+        this.vaultSearchErrorCount = new AtomicLong(0);
+        this.deletionErrorCount = new AtomicLong(0);
+
+        this.notifiers = new DeleteByQueryExecutor.Notifiers(
+            handledUserCount::incrementAndGet,
+            vaultSearchErrorCount::incrementAndGet,
+            deletionErrorCount::incrementAndGet,
+            permanantlyDeletedMessages::incrementAndGet);
+    }
+
+    @Override
+    public Result run() {
+        Query query = Query.of(CriterionFactory.deletionDate().beforeOrEquals(beginningOfRetentionPeriod));
+
+        return deleteByQueryExecutor.deleteByQuery(query, notifiers);
+    }
+
+    @Override
+    public String type() {
+        return TYPE;
+    }
+
+    @Override
+    public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+        return Optional.of(new AdditionalInformation(
+            beginningOfRetentionPeriod,
+            handledUserCount.get(),
+            permanantlyDeletedMessages.get(),
+            vaultSearchErrorCount.get(),
+            deletionErrorCount.get()));
+    }
+}
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageFixture.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageFixture.java
index 73c72fb..0144cac 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageFixture.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageFixture.java
@@ -36,6 +36,7 @@ import org.apache.james.mailbox.inmemory.InMemoryMessageId;
 
 public interface DeletedMessageFixture {
     InMemoryMessageId MESSAGE_ID = InMemoryMessageId.of(42);
+    InMemoryMessageId OLD_MESSAGE_ID = InMemoryMessageId.of(58);
     InMemoryMessageId MESSAGE_ID_2 = InMemoryMessageId.of(45);
     InMemoryId MAILBOX_ID_1 = InMemoryId.of(43);
     InMemoryId MAILBOX_ID_2 = InMemoryId.of(44);
@@ -44,6 +45,9 @@ public interface DeletedMessageFixture {
     User USER_2 = User.fromUsername("dimitri@apache.org");
     ZonedDateTime DELIVERY_DATE = ZonedDateTime.parse("2014-10-30T14:12:00Z");
     ZonedDateTime DELETION_DATE = ZonedDateTime.parse("2015-10-30T14:12:00Z");
+    ZonedDateTime NOW = ZonedDateTime.parse("2015-10-30T16:12:00Z");
+    ZonedDateTime OLD_DELIVERY_DATE = ZonedDateTime.parse("2010-10-30T14:12:00Z");
+    ZonedDateTime OLD_DELETION_DATE = ZonedDateTime.parse("2010-10-30T15:12:00Z");
     Date INTERNAL_DATE = Date.from(DELIVERY_DATE.toInstant());
     byte[] CONTENT = "header: value\r\n\r\ncontent".getBytes(StandardCharsets.UTF_8);
     String SUBJECT = "subject";
@@ -74,5 +78,16 @@ public interface DeletedMessageFixture {
         .subject(SUBJECT)
         .build();
     DeletedMessage DELETED_MESSAGE = FINAL_STAGE.get().build();
+    DeletedMessage OLD_DELETED_MESSAGE = DeletedMessage.builder()
+        .messageId(OLD_MESSAGE_ID)
+        .originMailboxes(MAILBOX_ID_1, MAILBOX_ID_2)
+        .user(USER)
+        .deliveryDate(OLD_DELIVERY_DATE)
+        .deletionDate(OLD_DELETION_DATE)
+        .sender(MaybeSender.of(SENDER))
+        .recipients(RECIPIENT1, RECIPIENT2)
+        .hasAttachment(false)
+        .size(CONTENT.length)
+        .build();
     DeletedMessage DELETED_MESSAGE_2 = DELETED_MESSAGE_GENERATOR.apply(MESSAGE_ID_2.getRawId());
 }
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java
index 0f4b3c6..c28f79f 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultContract.java
@@ -24,6 +24,8 @@ 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.DELETED_MESSAGE_GENERATOR;
 import static org.apache.james.vault.DeletedMessageFixture.MESSAGE_ID;
+import static org.apache.james.vault.DeletedMessageFixture.NOW;
+import static org.apache.james.vault.DeletedMessageFixture.OLD_DELETED_MESSAGE;
 import static org.apache.james.vault.DeletedMessageFixture.USER;
 import static org.apache.james.vault.DeletedMessageFixture.USER_2;
 import static org.apache.james.vault.search.Query.ALL;
@@ -31,9 +33,11 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.ByteArrayInputStream;
+import java.time.Clock;
 import java.time.Duration;
 
 import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.task.Task;
 import org.apache.james.util.concurrency.ConcurrentTestRunner;
 import org.junit.jupiter.api.Test;
 
@@ -41,6 +45,8 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public interface DeletedMessageVaultContract {
+    Clock CLOCK = Clock.fixed(NOW.toInstant(), NOW.getZone());
+
     DeletedMessageVault getVault();
 
     @Test
@@ -213,4 +219,67 @@ public interface DeletedMessageVaultContract {
         assertThat(Flux.from(getVault().search(USER, ALL)).collectList().block())
             .isEmpty();
     }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldCompleteWhenNoMail() {
+        Task.Result result = getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(result).isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldCompleteWhenAllMailsDeleted() {
+        Mono.from(getVault().append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+        Mono.from(getVault().delete(USER, DELETED_MESSAGE.getMessageId())).block();
+
+        Task.Result result = getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(result).isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldCompleteWhenOnlyRecentMails() {
+        Mono.from(getVault().append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        Task.Result result = getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(result).isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldCompleteWhenOnlyOldMails() {
+        Mono.from(getVault().append(USER, OLD_DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        Task.Result result = getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(result).isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldDoNothingWhenEmpty() {
+        getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(Flux.from(getVault().search(USER, ALL)).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldNotDeleteRecentMails() {
+        Mono.from(getVault().append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(Flux.from(getVault().search(USER, ALL)).collectList().block())
+            .containsOnly(DELETED_MESSAGE);
+    }
+
+    @Test
+    default void deleteExpiredMessagesTaskShouldDeleteOldMails() {
+        Mono.from(getVault().append(USER, OLD_DELETED_MESSAGE, new ByteArrayInputStream(CONTENT))).block();
+
+        getVault().deleteExpiredMessagesTask().run();
+
+        assertThat(Flux.from(getVault().search(USER, ALL)).collectList().block())
+            .isEmpty();
+    }
 }
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultHookTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultHookTest.java
index 26c3ca4..324f20c 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultHookTest.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageVaultHookTest.java
@@ -99,7 +99,7 @@ class DeletedMessageVaultHookTest {
     @BeforeEach
     void setUp() throws Exception {
         clock = Clock.fixed(DELETION_DATE.toInstant(), ZoneOffset.UTC);
-        messageVault = new MemoryDeletedMessagesVault();
+        messageVault = new MemoryDeletedMessagesVault(RetentionConfiguration.DEFAULT, clock);
 
         DeletedMessageConverter deletedMessageConverter = new DeletedMessageConverter();
 
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/MemoryDeletedMessagesVaultTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/MemoryDeletedMessagesVaultTest.java
index 9b58f29..0fec41e 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/MemoryDeletedMessagesVaultTest.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/memory/MemoryDeletedMessagesVaultTest.java
@@ -22,6 +22,7 @@ package org.apache.james.vault.memory;
 import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.DeletedMessageVaultContract;
 import org.apache.james.vault.DeletedMessageVaultSearchContract;
+import org.apache.james.vault.RetentionConfiguration;
 import org.junit.jupiter.api.BeforeEach;
 
 public class MemoryDeletedMessagesVaultTest implements DeletedMessageVaultContract, DeletedMessageVaultSearchContract.AllContracts {
@@ -30,7 +31,7 @@ public class MemoryDeletedMessagesVaultTest implements DeletedMessageVaultContra
 
     @BeforeEach
     void setUp() {
-        memoryDeletedMessagesVault = new MemoryDeletedMessagesVault();
+        memoryDeletedMessagesVault = new MemoryDeletedMessagesVault(RetentionConfiguration.DEFAULT, CLOCK);
     }
 
     @Override
diff --git a/server/container/guice/mailbox-plugin-deleted-messages-vault-guice/src/main/java/org/apache/james/modules/vault/DeletedMessageVaultModule.java b/server/container/guice/mailbox-plugin-deleted-messages-vault-guice/src/main/java/org/apache/james/modules/vault/DeletedMessageVaultModule.java
index 9c99fcd..32741a6 100644
--- a/server/container/guice/mailbox-plugin-deleted-messages-vault-guice/src/main/java/org/apache/james/modules/vault/DeletedMessageVaultModule.java
+++ b/server/container/guice/mailbox-plugin-deleted-messages-vault-guice/src/main/java/org/apache/james/modules/vault/DeletedMessageVaultModule.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.modules.vault;
 
+import java.io.FileNotFoundException;
+
 import org.apache.commons.configuration.Configuration;
 import org.apache.james.mailrepository.api.MailRepositoryPath;
 import org.apache.james.mailrepository.api.MailRepositoryUrl;
@@ -27,6 +29,7 @@ import org.apache.james.mailrepository.memory.MailRepositoryStoreConfiguration;
 import org.apache.james.utils.PropertiesProvider;
 import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.MailRepositoryDeletedMessageVault;
+import org.apache.james.vault.RetentionConfiguration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -58,4 +61,15 @@ public class DeletedMessageVaultModule extends AbstractModule {
                 MailRepositoryUrl.fromPathAndProtocol(defaultProtocol, DEFAULT_PATH));
         }
     }
+
+    @Provides
+    RetentionConfiguration providesRetentionConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException, org.apache.commons.configuration.ConfigurationException {
+        try {
+            Configuration configuration = propertiesProvider.getConfiguration("deletedMessageVault");
+            return RetentionConfiguration.from(configuration);
+        } catch (FileNotFoundException e) {
+            LOGGER.warn("Error encountered while retrieving Deleted message vault configuration. Using default MailRepository RetentionTime (1 year) instead.");
+            return RetentionConfiguration.DEFAULT;
+        }
+    }
 }
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
index a99014b..4d8ab8c 100644
--- a/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
+++ b/server/mailrepository/deleted-messages-vault-repository/src/main/java/org/apache/james/vault/MailRepositoryDeletedMessageVault.java
@@ -20,6 +20,10 @@
 package org.apache.james.vault;
 
 import java.io.InputStream;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.List;
 
 import javax.inject.Inject;
 import javax.mail.MessagingException;
@@ -28,11 +32,13 @@ import org.apache.james.core.User;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailrepository.api.MailKey;
 import org.apache.james.mailrepository.api.MailRepository;
-import org.apache.james.mailrepository.api.MailRepositoryPath;
 import org.apache.james.mailrepository.api.MailRepositoryStore;
 import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.server.core.MimeMessageInputStream;
+import org.apache.james.task.Task;
 import org.apache.james.util.streams.Iterators;
+import org.apache.james.vault.memory.DeleteByQueryExecutor;
+import org.apache.james.vault.memory.VaultGarbageCollectionTask;
 import org.apache.james.vault.search.Query;
 import org.reactivestreams.Publisher;
 
@@ -59,11 +65,17 @@ public class MailRepositoryDeletedMessageVault implements DeletedMessageVault {
     }
 
     private final MailRepositoryStore mailRepositoryStore;
+    private final RetentionConfiguration retentionConfiguration;
     private final Configuration configuration;
     private final MailConverter mailConverter;
+    private final DeleteByQueryExecutor deleteByQueryExecutor;
+    private final Clock clock;
 
     @Inject
-    MailRepositoryDeletedMessageVault(MailRepositoryStore mailRepositoryStore, Configuration configuration, MailConverter mailConverter) {
+    MailRepositoryDeletedMessageVault(MailRepositoryStore mailRepositoryStore, RetentionConfiguration retentionConfiguration, Configuration configuration, MailConverter mailConverter, Clock clock) {
+        this.retentionConfiguration = retentionConfiguration;
+        this.clock = clock;
+        this.deleteByQueryExecutor = new DeleteByQueryExecutor(this);
         this.mailRepositoryStore = mailRepositoryStore;
         this.configuration = configuration;
         this.mailConverter = mailConverter;
@@ -130,12 +142,30 @@ public class MailRepositoryDeletedMessageVault implements DeletedMessageVault {
     @Override
     public Publisher<User> usersWithVault() {
         return Flux.fromStream(mailRepositoryStore.getUrls()
-            .filter(url -> url.hasPrefix(configuration.urlPrefix))
-            .map(MailRepositoryUrl::getPath)
-            .map(MailRepositoryPath::parts)
-            .peek(parts -> Preconditions.checkState(!parts.isEmpty()))
-            .map(Iterables::getLast)
-            .map(User::fromUsername));
+            .filter(this::isVault)
+            .map(this::userForRepository));
+    }
+
+    private boolean isVault(MailRepositoryUrl url) {
+        return url.hasPrefix(configuration.urlPrefix);
+    }
+
+    private User userForRepository(MailRepositoryUrl url) {
+        Preconditions.checkArgument(isVault(url));
+
+        List<String> parts = url.getPath().parts();
+
+        return User.fromUsername(Iterables.getLast(parts));
+    }
+
+    @Override
+    public Task deleteExpiredMessagesTask() {
+        ZonedDateTime now = ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
+        ZonedDateTime beginningOfRetentionPeriod = now.minus(retentionConfiguration.getRetentionPeriod());
+
+        return new VaultGarbageCollectionTask(
+            deleteByQueryExecutor,
+            beginningOfRetentionPeriod);
     }
 
     private MailRepository repositoryForUser(User user) {
diff --git a/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailRepositoryDeletedMessageVaultTest.java b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailRepositoryDeletedMessageVaultTest.java
index 87c6205..2cc58eb 100644
--- a/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailRepositoryDeletedMessageVaultTest.java
+++ b/server/mailrepository/deleted-messages-vault-repository/src/test/java/org/apache/james/vault/MailRepositoryDeletedMessageVaultTest.java
@@ -55,8 +55,9 @@ public class MailRepositoryDeletedMessageVaultTest implements DeletedMessageVaul
 
         testee = new MailRepositoryDeletedMessageVault(
             mailRepositoryStore,
+            RetentionConfiguration.DEFAULT,
             new MailRepositoryDeletedMessageVault.Configuration(MailRepositoryUrl.from("memory://deletedMessages/vault/")),
-            new MailConverter(new InMemoryId.Factory(), new InMemoryMessageId.Factory()));
+            new MailConverter(new InMemoryId.Factory(), new InMemoryMessageId.Factory()), CLOCK);
     }
 
     @Override
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 82f84be..b08c456 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
@@ -57,6 +57,7 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Clock;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Stream;
@@ -92,6 +93,7 @@ import org.apache.james.task.MemoryTaskManager;
 import org.apache.james.user.memory.MemoryUsersRepository;
 import org.apache.james.vault.DeletedMessage;
 import org.apache.james.vault.DeletedMessageZipper;
+import org.apache.james.vault.RetentionConfiguration;
 import org.apache.james.vault.memory.MemoryDeletedMessagesVault;
 import org.apache.james.vault.search.Query;
 import org.apache.james.webadmin.WebAdminServer;
@@ -147,7 +149,7 @@ class DeletedMessagesVaultRoutesTest {
 
     @BeforeEach
     void beforeEach() throws Exception {
-        vault = spy(new MemoryDeletedMessagesVault());
+        vault = spy(new MemoryDeletedMessagesVault(RetentionConfiguration.DEFAULT, Clock.systemUTC()));
         InMemoryIntegrationResources inMemoryResource = InMemoryIntegrationResources.defaultResources();
         mailboxManager = spy(inMemoryResource.getMailboxManager());
 


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


[james-project] 17/23: MAILBOX-388 MailRepositoryUrl::hasPrefix

Posted by bt...@apache.org.
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 cb97101180ce85ffe4a6d5f2f84bcfa077ac4dc2
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 15:35:06 2019 +0700

    MAILBOX-388 MailRepositoryUrl::hasPrefix
---
 .../mailrepository/api/MailRepositoryUrl.java      |  5 ++
 .../mailrepository/api/MailRepositoryUrlTest.java  | 72 ++++++++++++++++++++++
 2 files changed, 77 insertions(+)

diff --git a/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryUrl.java b/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryUrl.java
index 7bd99b3..ae00938 100644
--- a/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryUrl.java
+++ b/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryUrl.java
@@ -91,6 +91,11 @@ public class MailRepositoryUrl {
         return URLEncoder.encode(value, StandardCharsets.UTF_8.displayName());
     }
 
+    public boolean hasPrefix(MailRepositoryUrl other) {
+        return Objects.equals(this.protocol, other.protocol)
+            && this.path.hasPrefix(other.path);
+    }
+
     public Protocol getProtocol() {
        return protocol;
     }
diff --git a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryUrlTest.java b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryUrlTest.java
index f0842c3..3420906 100644
--- a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryUrlTest.java
+++ b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryUrlTest.java
@@ -117,4 +117,76 @@ public class MailRepositoryUrlTest {
         assertThatThrownBy(() -> MailRepositoryUrl.from("proto://abc://def").subUrl("/ghi"))
             .isInstanceOf(IllegalArgumentException.class);
     }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenEquals() {
+        assertThat(MailRepositoryUrl.from("proto://abc/def").hasPrefix(MailRepositoryUrl.from("proto://abc/def")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenSiblings() {
+        assertThat(MailRepositoryUrl.from("proto://abc/def").hasPrefix(MailRepositoryUrl.from("proto://abc/ghi")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenAncestor() {
+        assertThat(MailRepositoryUrl.from("proto://abc").hasPrefix(MailRepositoryUrl.from("proto://abc/ghi")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenDescendant() {
+        assertThat(MailRepositoryUrl.from("proto://abc/ghi").hasPrefix(MailRepositoryUrl.from("proto://abc")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenFarDescendant() {
+        assertThat(MailRepositoryUrl.from("proto://abc/ghi/klm").hasPrefix(MailRepositoryUrl.from("proto://abc")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenEmpty() {
+        assertThat(MailRepositoryUrl.from("proto://abc").hasPrefix(MailRepositoryUrl.from("proto://")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenBothEmpty() {
+        assertThat(MailRepositoryUrl.from("proto://").hasPrefix(MailRepositoryUrl.from("proto://")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenMissingSlah() {
+        assertThat(MailRepositoryUrl.from("proto://abcghi").hasPrefix(MailRepositoryUrl.from("proto://abc")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenDifferentProtocol() {
+        assertThat(MailRepositoryUrl.from("proto://abc/ghi").hasPrefix(MailRepositoryUrl.from("proto2://abc")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenDescendantStartingWithSlash() {
+        assertThat(MailRepositoryUrl.from("proto:///abc/ghi").hasPrefix(MailRepositoryUrl.from("proto:///abc")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenDescendantAdditionalFirstSlash() {
+        assertThat(MailRepositoryUrl.from("proto://abc/ghi").hasPrefix(MailRepositoryUrl.from("proto:///abc")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenDescendantMissingFirstSlash() {
+        assertThat(MailRepositoryUrl.from("proto:///abc/ghi").hasPrefix(MailRepositoryUrl.from("proto://abc")))
+            .isFalse();
+    }
 }


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


[james-project] 23/23: MAILBOX-373 deadLetter::remove operation was not bound

Posted by bt...@apache.org.
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 08ef4028de8a049fb6e209517569d676a4cc58c7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 13:51:49 2019 +0700

    MAILBOX-373 deadLetter::remove operation was not bound
    
     - Depending of deadLetter implementation, the computation might not have been triggered.
     - Asynchronous isseus might have arised: the task could very well complete before the event to be removed, resulting in broken assertions
---
 .../james/webadmin/service/EventDeadLettersRedeliverService.java    | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverService.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverService.java
index 5aeb52b..7053c9d 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverService.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverService.java
@@ -55,10 +55,8 @@ public class EventDeadLettersRedeliverService {
 
     private Mono<Task.Result> redeliverGroupEvents(Group group, Event event, EventDeadLetters.InsertionId insertionId) {
         return eventBus.reDeliver(group, event)
-            .then(Mono.fromCallable(() -> {
-                deadLetters.remove(group, insertionId);
-                return Task.Result.COMPLETED;
-            }))
+            .then(deadLetters.remove(group, insertionId))
+            .thenReturn(Task.Result.COMPLETED)
             .onErrorResume(e -> {
                 LOGGER.error("Error while performing redelivery of event: {} for group: {}",
                     event.getEventId().toString(), group.asString(), e);


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


[james-project] 08/23: JAMES-2685 Zipper.ContentLoader should return Optional

Posted by bt...@apache.org.
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 2aa3780338026e45290d2fe2627279fafef8d6ba
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Tue Mar 26 19:24:34 2019 +0700

    JAMES-2685 Zipper.ContentLoader should return Optional<InputStream>
    
    To let the zipper aware the empty-ness of the content of deleted messages
---
 .../apache/james/vault/DeletedMessageZipper.java   | 20 ++---
 .../james/vault/DeletedMessageZipperTest.java      | 26 +++++--
 pom.xml                                            |  4 +-
 .../routes/DeletedMessagesVaultExportTask.java     |  6 +-
 .../james/webadmin/vault/routes/ExportService.java | 87 ++++++----------------
 .../routes/DeletedMessagesVaultRoutesTest.java     |  3 +-
 6 files changed, 58 insertions(+), 88 deletions(-)

diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java
index 22e3fd2..1d4a89b 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/DeletedMessageZipper.java
@@ -23,6 +23,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.apache.commons.compress.archivers.zip.ExtraFieldUtils;
@@ -39,7 +40,7 @@ import com.google.common.annotations.VisibleForTesting;
 
 public class DeletedMessageZipper {
     public interface DeletedMessageContentLoader {
-        InputStream load(DeletedMessage deletedMessage);
+        Optional<InputStream> load(DeletedMessage deletedMessage);
     }
 
     public DeletedMessageZipper() {
@@ -49,7 +50,8 @@ public class DeletedMessageZipper {
 
     public void zip(DeletedMessageContentLoader contentLoader, Stream<DeletedMessage> deletedMessages, OutputStream outputStream) throws IOException {
         try (ZipArchiveOutputStream zipOutputStream = newZipArchiveOutputStream(outputStream)) {
-            ThrowingConsumer<DeletedMessage> putInZip = message -> putMessageToEntry(zipOutputStream, message, contentLoader);
+            ThrowingConsumer<DeletedMessage> putInZip =
+                message -> putMessageToEntry(zipOutputStream, message, contentLoader.load(message));
 
             deletedMessages.forEach(Throwing.consumer(putInZip).sneakyThrow());
 
@@ -63,13 +65,13 @@ public class DeletedMessageZipper {
     }
 
     @VisibleForTesting
-    void putMessageToEntry(ZipArchiveOutputStream zipOutputStream, DeletedMessage message, DeletedMessageContentLoader contentLoader) throws IOException {
-        try (InputStream content = contentLoader.load(message)) {
-            zipOutputStream.putArchiveEntry(createEntry(zipOutputStream, message));
-
-            IOUtils.copy(content, zipOutputStream);
-
-            zipOutputStream.closeArchiveEntry();
+    void putMessageToEntry(ZipArchiveOutputStream zipOutputStream, DeletedMessage message, Optional<InputStream> maybeContent) throws IOException {
+        if (maybeContent.isPresent()) {
+            try (InputStream closableMessageContent = maybeContent.get()) {
+                zipOutputStream.putArchiveEntry(createEntry(zipOutputStream, message));
+                IOUtils.copy(closableMessageContent, zipOutputStream);
+                zipOutputStream.closeArchiveEntry();
+            }
         }
     }
 
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageZipperTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageZipperTest.java
index 2c8bfdf..efd9355 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageZipperTest.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/DeletedMessageZipperTest.java
@@ -44,6 +44,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Collection;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Stream;
@@ -60,7 +61,7 @@ import org.mockito.stubbing.Answer;
 import com.github.fge.lambdas.Throwing;
 
 class DeletedMessageZipperTest {
-    private static final DeletedMessageContentLoader CONTENT_LOADER = message -> new ByteArrayInputStream(CONTENT);
+    private static final DeletedMessageContentLoader CONTENT_LOADER = message -> Optional.of(new ByteArrayInputStream(CONTENT));
     private static final String MESSAGE_CONTENT = new String(CONTENT, StandardCharsets.UTF_8);
     private DeletedMessageZipper zipper;
 
@@ -183,8 +184,8 @@ class DeletedMessageZipperTest {
             DeletedMessageContentLoader contentLoader = spy(new DeletedMessageContentLoader() {
                 // lambdas are final and thus can't be spied
                 @Override
-                public InputStream load(DeletedMessage deletedMessage) {
-                    return new ByteArrayInputStream(CONTENT);
+                public Optional<InputStream> load(DeletedMessage deletedMessage) {
+                    return Optional.of(new ByteArrayInputStream(CONTENT));
                 }
             });
 
@@ -196,13 +197,24 @@ class DeletedMessageZipperTest {
 
             verify(contentLoader, times(1)).load(any());
         }
+
+        @Test
+        void zipShouldNotPutEntryIfContentLoaderReturnsEmptyResult() throws Exception {
+            DeletedMessageContentLoader contentLoader = message -> Optional.empty();
+            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            zipper.zip(contentLoader, Stream.of(DELETED_MESSAGE, DELETED_MESSAGE_2), outputStream);
+
+            try (ZipAssert zipAssert = assertThatZip(outputStream)) {
+                zipAssert.hasNoEntry();
+            }
+        }
     }
 
     private DeletedMessageZipper.DeletedMessageContentLoader spyLoadedContents(Collection<InputStream> loadedContents) {
-        Answer<InputStream> spyedContent = invocationOnMock -> {
-            InputStream result = spy(new ByteArrayInputStream(CONTENT));
-            loadedContents.add(result);
-            return result;
+        Answer<Optional<InputStream>> spyedContent = invocationOnMock -> {
+            InputStream spied = spy(new ByteArrayInputStream(CONTENT));
+            loadedContents.add(spied);
+            return Optional.of(spied);
         };
         DeletedMessageContentLoader contentLoader = mock(DeletedMessageContentLoader.class);
         when(contentLoader.load(any())).thenAnswer(spyedContent);
diff --git a/pom.xml b/pom.xml
index 0a66da5..335423e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1103,12 +1103,12 @@
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
-                <artifactId>blob-export-guice</artifactId>
+                <artifactId>blob-export-file</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
-                <artifactId>blob-export-file</artifactId>
+                <artifactId>blob-export-guice</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
index de4204b..ca551d4 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.webadmin.vault.routes;
 
+import java.io.IOException;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -79,10 +80,9 @@ class DeletedMessagesVaultExportTask implements Task {
     public Result run() {
         try {
             Runnable messageToShareCallback = totalExportedMessages::incrementAndGet;
-            exportService.export(userExportFrom, exportQuery, exportTo, messageToShareCallback)
-                .block();
+            exportService.export(userExportFrom, exportQuery, exportTo, messageToShareCallback);
             return Result.COMPLETED;
-        } catch (Exception e) {
+        } catch (IOException e) {
             LOGGER.error("Error happens when exporting deleted messages from {} to {}", userExportFrom.asString(), exportTo.asString());
             return Result.PARTIAL;
         }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
index 2e3c014..8dc77d1 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
@@ -19,19 +19,11 @@
 
 package org.apache.james.webadmin.vault.routes;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.util.Collection;
-import java.util.UUID;
-import java.util.function.Function;
-import java.util.stream.Stream;
 
 import javax.inject.Inject;
 
+import org.apache.commons.io.FileUtils;
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStore;
 import org.apache.james.blob.export.api.BlobExportMechanism;
@@ -42,29 +34,14 @@ import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.DeletedMessageZipper;
 import org.apache.james.vault.search.Query;
 
-import com.github.fge.lambdas.Throwing;
-import com.github.fge.lambdas.functions.ThrowingFunction;
-import com.github.steveash.guavate.Guavate;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.ByteSource;
+import com.google.common.io.FileBackedOutputStream;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Schedulers;
 
 class ExportService {
-
-    private class ZippedData {
-        private final long contentLength;
-        private final InputStream inputStream;
-
-        private ZippedData(long contentLength, InputStream content) {
-            this.contentLength = contentLength;
-            this.inputStream = content;
-        }
-    }
-
-    private static final String TEMPORARY_FILE_EXTENSION = ".temp";
-
     private final BlobExportMechanism blobExport;
     private final BlobStore blobStore;
     private final DeletedMessageZipper zipper;
@@ -79,53 +56,31 @@ class ExportService {
         this.vault = vault;
     }
 
-    Mono<Void> export(User user, Query exportQuery, MailAddress exportToAddress,
-                      Runnable messageToExportCallback) {
+    void export(User user, Query exportQuery, MailAddress exportToAddress, Runnable messageToExportCallback) throws IOException {
+        Flux<DeletedMessage> matchedMessages = Flux.from(vault.search(user, exportQuery))
+            .doOnNext(any -> messageToExportCallback.run());
 
-        return matchingMessages(user, exportQuery)
-            .doOnNext(any -> messageToExportCallback.run())
-            .collect(Guavate.toImmutableList())
-            .map(Collection::stream)
-            .flatMap(messages -> Mono.fromCallable(() -> zipData(user, messages)))
-            .flatMap(sneakyThrow(zippedData -> blobStore.save(zippedData.inputStream, zippedData.contentLength)))
-            .flatMap(blobId -> exportTo(user, exportToAddress, blobId))
-            .then();
-    }
+        BlobId blobId = zipToBlob(user, matchedMessages);
 
-    private Flux<DeletedMessage> matchingMessages(User user, Query exportQuery) {
-        return Flux.from(vault.search(user, exportQuery))
-            .publishOn(Schedulers.elastic());
-    }
-
-    private ZippedData zipData(User user, Stream<DeletedMessage> messages) throws IOException {
-        File tempFile = temporaryFile();
-        FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
-
-        zipper.zip(message -> loadMessageContent(user, message), messages, fileOutputStream);
-        return new ZippedData(tempFile.length(), new FileInputStream(tempFile));
-    }
-
-    private File temporaryFile() throws IOException {
-        String tempFileName = UUID.randomUUID().toString();
-        File tempFile = Files.createTempFile(tempFileName, TEMPORARY_FILE_EXTENSION).toFile();
-        tempFile.deleteOnExit();
-        return tempFile;
+        blobExport.blobId(blobId)
+            .with(exportToAddress)
+            .explanation(exportMessage(user))
+            .export();
     }
 
-    private InputStream loadMessageContent(User user, DeletedMessage message) {
-        return Mono.from(vault.loadMimeMessage(user, message.getMessageId()))
-            .block();
+    private BlobId zipToBlob(User user, Flux<DeletedMessage> messages) throws IOException {
+        try (FileBackedOutputStream fileOutputStream = new FileBackedOutputStream(FileUtils.ONE_MB_BI.intValue())) {
+            zipper.zip(contentLoader(user), messages.toStream(), fileOutputStream);
+            ByteSource byteSource = fileOutputStream.asByteSource();
+            return blobStore.save(byteSource.openStream(), byteSource.size()).block();
+        }
     }
 
-    private Mono<Void> exportTo(User user, MailAddress exportToAddress, BlobId blobId) {
-        return Mono.fromRunnable(() -> blobExport
-            .blobId(blobId)
-            .with(exportToAddress)
-            .explanation(String.format("Some deleted messages from user %s has been shared to you", user.asString()))
-            .export());
+    private DeletedMessageZipper.DeletedMessageContentLoader contentLoader(User user) {
+        return message -> Mono.from(vault.loadMimeMessage(user, message.getMessageId())).blockOptional();
     }
 
-    private <T, R> Function<T, R> sneakyThrow(ThrowingFunction<T, R> function) {
-        return Throwing.function(function).sneakyThrow();
+    private String exportMessage(User user) {
+        return String.format("Some deleted messages from user %s has been shared to you", user.asString());
     }
 }
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 fcc563f..82f84be 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
@@ -58,6 +58,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.apache.commons.configuration.DefaultConfigurationBuilder;
@@ -1839,7 +1840,7 @@ class DeletedMessagesVaultRoutesTest {
 
         private byte[] zippedMessagesData() throws IOException {
             ByteArrayOutputStream expectedZippedData = new ByteArrayOutputStream();
-            zipper.zip(message -> new ByteArrayInputStream(CONTENT),
+            zipper.zip(message -> Optional.of(new ByteArrayInputStream(CONTENT)),
                 Stream.of(DELETED_MESSAGE, DELETED_MESSAGE_2),
                 expectedZippedData);
             return expectedZippedData.toByteArray();


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


[james-project] 19/23: MAILBOX-388 Add a VaultConfiguration object

Posted by bt...@apache.org.
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 948f85b23dee285728101d80a9c1a2fda3d99488
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 12:01:31 2019 +0700

    MAILBOX-388 Add a VaultConfiguration object
---
 .../apache/james/vault/RetentionConfiguration.java | 68 ++++++++++++++++++
 .../james/vault/RetentionConfigurationTest.java    | 81 ++++++++++++++++++++++
 2 files changed, 149 insertions(+)

diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/RetentionConfiguration.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/RetentionConfiguration.java
new file mode 100644
index 0000000..93dd6b6
--- /dev/null
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/RetentionConfiguration.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.vault;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.commons.configuration.Configuration;
+import org.apache.james.util.DurationParser;
+
+import com.google.common.base.Preconditions;
+
+public class RetentionConfiguration {
+    public static final RetentionConfiguration DEFAULT = new RetentionConfiguration(ChronoUnit.YEARS.getDuration());
+
+    public static RetentionConfiguration from(Configuration propertiesConfiguration) {
+        return Optional.ofNullable(propertiesConfiguration.getString("retentionPeriod"))
+            .map(string -> DurationParser.parse(string, ChronoUnit.DAYS))
+            .map(RetentionConfiguration::new)
+            .orElse(DEFAULT);
+    }
+
+    private final Duration retentionPeriod;
+
+    RetentionConfiguration(Duration retentionPeriod) {
+        Preconditions.checkNotNull(retentionPeriod);
+
+        this.retentionPeriod = retentionPeriod;
+    }
+
+    public Duration getRetentionPeriod() {
+        return retentionPeriod;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof RetentionConfiguration) {
+            RetentionConfiguration that = (RetentionConfiguration) o;
+
+            return Objects.equals(this.retentionPeriod, that.retentionPeriod);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(retentionPeriod);
+    }
+}
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/RetentionConfigurationTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/RetentionConfigurationTest.java
new file mode 100644
index 0000000..7a15011
--- /dev/null
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/RetentionConfigurationTest.java
@@ -0,0 +1,81 @@
+/****************************************************************
+ * 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.vault;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.time.Duration;
+
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class RetentionConfigurationTest {
+    @Test
+    void ShouldMatchBeanContract() {
+        EqualsVerifier.forClass(RetentionConfiguration.class)
+            .verify();
+    }
+
+    @Test
+    void constructorShouldThrowWhenNull() {
+        assertThatThrownBy(() -> new RetentionConfiguration(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void fromShouldThrowWhenNull() {
+        assertThatThrownBy(() -> RetentionConfiguration.from(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void fromShouldReturnConfiguredRetentionTime() {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("retentionPeriod", "15d");
+
+        assertThat(RetentionConfiguration.from(configuration)).isEqualTo(new RetentionConfiguration(Duration.ofDays(15)));
+    }
+
+    @Test
+    void fromShouldHandleHours() {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("retentionPeriod", "15h");
+
+        assertThat(RetentionConfiguration.from(configuration)).isEqualTo(new RetentionConfiguration(Duration.ofHours(15)));
+    }
+
+    @Test
+    void fromShouldUseDaysAsADefaultUnit() {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("retentionPeriod", "15");
+
+        assertThat(RetentionConfiguration.from(configuration)).isEqualTo(new RetentionConfiguration(Duration.ofDays(15)));
+    }
+
+    @Test
+    void fromShouldReturnDefaultWhenNoRetentionTime() {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+
+        assertThat(RetentionConfiguration.from(configuration)).isEqualTo(RetentionConfiguration.DEFAULT);
+    }
+}
\ No newline at end of file


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


[james-project] 14/23: MAILBOX-388 FunctionUtils: turn toPredicate into identityPredicate

Posted by bt...@apache.org.
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 85d3b926cceafa1789ec9cb50e547903b2e84a0f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 13:49:24 2019 +0700

    MAILBOX-388 FunctionUtils: turn toPredicate into identityPredicate
    
    This is the only use case. Such an API change thus makes the API simpler to use and more explicit.
---
 .../james/mailbox/cassandra/mail/CassandraACLMapper.java   |  5 +++--
 .../mailbox/cassandra/mail/CassandraMessageIdMapper.java   |  3 +--
 .../mailbox/cassandra/mail/CassandraModSeqProvider.java    |  4 ++--
 .../apache/james/transport/matchers/SMTPIsAuthNetwork.java |  3 +--
 .../main/java/org/apache/james/util/FunctionalUtils.java   |  5 ++---
 .../java/org/apache/james/util/FunctionalUtilsTest.java    | 14 +++++++-------
 .../james/sieve/cassandra/CassandraSieveRepository.java    |  3 ++-
 7 files changed, 18 insertions(+), 19 deletions(-)

diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraACLMapper.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraACLMapper.java
index 9fb8bbb..2698c4c 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraACLMapper.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraACLMapper.java
@@ -49,6 +49,7 @@ import com.datastax.driver.core.PreparedStatement;
 import com.datastax.driver.core.Row;
 import com.datastax.driver.core.Session;
 import com.fasterxml.jackson.core.JsonProcessingException;
+
 import reactor.core.publisher.Mono;
 
 public class CassandraACLMapper {
@@ -162,7 +163,7 @@ public class CassandraACLMapper {
                 .setString(CassandraACLTable.ACL, convertAclToJson(aclWithVersion.mailboxACL))
                 .setLong(CassandraACLTable.VERSION, aclWithVersion.version + 1)
                 .setLong(OLD_VERSION, aclWithVersion.version))
-            .filter(FunctionalUtils.toPredicate(Function.identity()))
+            .filter(FunctionalUtils.identityPredicate())
             .map(any -> aclWithVersion.mailboxACL);
     }
 
@@ -171,7 +172,7 @@ public class CassandraACLMapper {
             conditionalInsertStatement.bind()
                     .setUUID(CassandraACLTable.ID, cassandraId.asUuid())
                     .setString(CassandraACLTable.ACL, convertAclToJson(acl))))
-            .filter(FunctionalUtils.toPredicate(Function.identity()))
+            .filter(FunctionalUtils.identityPredicate())
             .map(any -> acl);
     }
 
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageIdMapper.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageIdMapper.java
index f74b2ca..830ae98 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageIdMapper.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageIdMapper.java
@@ -23,7 +23,6 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.function.Function;
 
 import javax.mail.Flags;
 
@@ -289,7 +288,7 @@ public class CassandraMessageIdMapper implements MessageIdMapper {
 
     private Mono<Pair<Flags, ComposedMessageIdWithMetaData>> updateFlags(ComposedMessageIdWithMetaData oldComposedId, ComposedMessageIdWithMetaData newComposedId) {
         return imapUidDAO.updateMetadata(newComposedId, oldComposedId.getModSeq())
-            .filter(FunctionalUtils.toPredicate(Function.identity()))
+            .filter(FunctionalUtils.identityPredicate())
             .flatMap(any -> messageIdDAO.updateMetadata(newComposedId)
                 .thenReturn(Pair.of(oldComposedId.getFlags(), newComposedId)));
     }
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraModSeqProvider.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraModSeqProvider.java
index 25347bb..1d2780f 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraModSeqProvider.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraModSeqProvider.java
@@ -32,7 +32,6 @@ import static org.apache.james.mailbox.cassandra.table.CassandraMessageModseqTab
 import java.time.Duration;
 import java.util.Optional;
 import java.util.concurrent.CompletionException;
-import java.util.function.Function;
 import java.util.function.Supplier;
 
 import javax.inject.Inject;
@@ -50,6 +49,7 @@ import org.apache.james.util.FunctionalUtils;
 import com.datastax.driver.core.PreparedStatement;
 import com.datastax.driver.core.Session;
 import com.google.common.base.MoreObjects;
+
 import reactor.core.publisher.Mono;
 
 public class CassandraModSeqProvider implements ModSeqProvider {
@@ -172,7 +172,7 @@ public class CassandraModSeqProvider implements ModSeqProvider {
 
     private Mono<ModSeq> successToModSeq(ModSeq modSeq, Boolean success) {
         return Mono.just(success)
-            .filter(FunctionalUtils.toPredicate(Function.identity()))
+            .filter(FunctionalUtils.identityPredicate())
             .map(any -> modSeq);
     }
 
diff --git a/mailet/standard/src/main/java/org/apache/james/transport/matchers/SMTPIsAuthNetwork.java b/mailet/standard/src/main/java/org/apache/james/transport/matchers/SMTPIsAuthNetwork.java
index 7ea745f..107c6cb 100644
--- a/mailet/standard/src/main/java/org/apache/james/transport/matchers/SMTPIsAuthNetwork.java
+++ b/mailet/standard/src/main/java/org/apache/james/transport/matchers/SMTPIsAuthNetwork.java
@@ -20,7 +20,6 @@
 package org.apache.james.transport.matchers;
 
 import java.util.Collection;
-import java.util.function.Function;
 
 import org.apache.james.core.MailAddress;
 import org.apache.james.util.FunctionalUtils;
@@ -53,7 +52,7 @@ public class SMTPIsAuthNetwork extends GenericMatcher {
     public Collection<MailAddress> match(Mail mail) {
         return AttributeUtils
             .getValueAndCastFromMail(mail, SMTP_AUTH_NETWORK_NAME, Boolean.class)
-            .filter(FunctionalUtils.toPredicate(Function.identity()))
+            .filter(FunctionalUtils.identityPredicate())
             .map(any -> mail.getRecipients())
             .orElse(ImmutableList.of());
     }
diff --git a/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java b/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java
index 651f8ba..30c0b63 100644
--- a/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java
+++ b/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java
@@ -19,7 +19,6 @@
 package org.apache.james.util;
 
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.UnaryOperator;
 
@@ -38,7 +37,7 @@ public class FunctionalUtils {
         };
     }
 
-    public static <T> Predicate<T> toPredicate(Function<T, Boolean> function) {
-        return value -> function.apply(value);
+    public static Predicate<Boolean> identityPredicate() {
+        return b -> b;
     }
 }
diff --git a/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java b/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java
index 6c736b0..c62653f 100644
--- a/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java
+++ b/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java
@@ -22,7 +22,6 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.function.Consumer;
 import java.util.function.Function;
-import java.util.function.Predicate;
 
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -85,14 +84,15 @@ class FunctionalUtilsTest {
     }
 
     @Nested
-    class ToPredicate {
+    class IdentityPredicate {
         @Test
-        void shouldKeepProperty() {
-            Function<Integer, Boolean> function = value -> value % 42 == 0;
-            Predicate<Integer> predicate = FunctionalUtils.toPredicate(function);
+        void shouldKeepTrue() {
+            assertThat(FunctionalUtils.identityPredicate().test(true)).isTrue();
+        }
 
-            assertThat(predicate.test(5)).isFalse();
-            assertThat(predicate.test(42)).isTrue();
+        @Test
+        void shouldDiscardFalse() {
+            assertThat(FunctionalUtils.identityPredicate().test(false)).isFalse();
         }
     }
 }
\ No newline at end of file
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/sieve/cassandra/CassandraSieveRepository.java b/server/data/data-cassandra/src/main/java/org/apache/james/sieve/cassandra/CassandraSieveRepository.java
index c01f9d3..2c866ba 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/sieve/cassandra/CassandraSieveRepository.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/sieve/cassandra/CassandraSieveRepository.java
@@ -46,6 +46,7 @@ import org.apache.james.sieverepository.api.exception.ScriptNotFoundException;
 import org.apache.james.util.FunctionalUtils;
 
 import com.github.steveash.guavate.Guavate;
+
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -167,7 +168,7 @@ public class CassandraSieveRepository implements SieveRepository {
         Mono<Boolean> activateNewScript =
             unactivateOldScript(user)
                 .then(updateScriptActivation(user, name, true))
-                .filter(FunctionalUtils.toPredicate(Function.identity()))
+                .filter(FunctionalUtils.identityPredicate())
                 .flatMap(any -> cassandraActiveScriptDAO.activate(user, name).thenReturn(any));
 
         if (!activateNewScript.blockOptional().isPresent()) {


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


[james-project] 13/23: MAILBOX-388 Add runnable -> function util

Posted by bt...@apache.org.
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 1fe0d84086f81fb28a559f306434e50468c897b0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 13:38:10 2019 +0700

    MAILBOX-388 Add runnable -> function util
---
 .../org/apache/james/util/FunctionalUtils.java     |  7 ++++++
 .../org/apache/james/util/FunctionalUtilsTest.java | 28 +++++++++++++++++++++-
 2 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java b/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java
index 54c8305..651f8ba 100644
--- a/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java
+++ b/server/container/util/src/main/java/org/apache/james/util/FunctionalUtils.java
@@ -31,6 +31,13 @@ public class FunctionalUtils {
         };
     }
 
+    public static <T> UnaryOperator<T> identityWithSideEffect(Runnable runnable) {
+        return argument -> {
+            runnable.run();
+            return argument;
+        };
+    }
+
     public static <T> Predicate<T> toPredicate(Function<T, Boolean> function) {
         return value -> function.apply(value);
     }
diff --git a/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java b/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java
index 2147ef4..6c736b0 100644
--- a/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java
+++ b/server/container/util/src/test/java/org/apache/james/util/FunctionalUtilsTest.java
@@ -32,15 +32,41 @@ class FunctionalUtilsTest {
     @Nested
     class ToFunction {
         @Test
-        void shouldCallConsumerAndReturnTheGivenParameter() {
+        void toFunctionShouldReturnTheGivenParameter() {
             Counter counter = new Counter(26);
             Consumer<Integer> consumer = counter::increment;
             Function<Integer, Integer> function = FunctionalUtils.toFunction(consumer);
 
             assertThat(function.apply(16)).isEqualTo(16);
+        }
+
+        @Test
+        void toFunctionShouldCallConsumer() {
+            Counter counter = new Counter(26);
+            Consumer<Integer> consumer = counter::increment;
+            FunctionalUtils.toFunction(consumer).apply(16);
+
             assertThat(counter.getCounter()).isEqualTo(42);
         }
 
+        @Test
+        void identityWithSideEffectShouldReturnTheGivenParameterForRunnable() {
+            Counter counter = new Counter(26);
+            Runnable runnable = () -> counter.increment(1);
+            Function<Integer, Integer> function = FunctionalUtils.identityWithSideEffect(runnable);
+
+            assertThat(function.apply(16)).isEqualTo(16);
+        }
+
+        @Test
+        void identityWithSideEffectShouldCallRunnable() {
+            Counter counter = new Counter(26);
+            Runnable runnable = () -> counter.increment(1);
+            FunctionalUtils.identityWithSideEffect(runnable).apply(23);
+
+            assertThat(counter.getCounter()).isEqualTo(27);
+        }
+
         private class Counter {
             private Integer counter;
 


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


[james-project] 07/23: JAMES-2685 LocalFileBlobExportModule for blob sharing

Posted by bt...@apache.org.
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 f52c3b63e6dd31b686445a3ac6644d5af23f3a95
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Thu Mar 21 11:39:11 2019 +0700

    JAMES-2685 LocalFileBlobExportModule for blob sharing
    
    to be used in integration tests
---
 pom.xml                                            | 10 ++++
 .../export/file/LocalFileBlobExportMechanism.java  |  7 +++
 .../file/LocalFileBlobExportMechanismTest.java     |  5 +-
 .../pom.xml                                        | 20 ++------
 .../LocalFileBlobExportMechanismModule.java        | 37 +++++++++++++++
 server/container/guice/blob-memory-guice/pom.xml   | 11 -----
 server/container/guice/cassandra-guice/pom.xml     |  4 ++
 .../org/apache/james/CassandraJamesServerMain.java |  4 +-
 server/container/guice/memory-guice/pom.xml        |  4 ++
 .../org/apache/james/MemoryJamesServerMain.java    |  2 +
 server/container/guice/pom.xml                     |  1 +
 .../memory-jmap-integration-testing/pom.xml        |  5 ++
 .../vault/routes/DeletedMessagesVaultRoutes.java   |  5 +-
 .../james/webadmin/vault/routes/ExportService.java | 53 ++++++++++++----------
 14 files changed, 109 insertions(+), 59 deletions(-)

diff --git a/pom.xml b/pom.xml
index f478bff..0a66da5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1103,6 +1103,16 @@
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
+                <artifactId>blob-export-guice</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
+                <artifactId>blob-export-file</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
                 <artifactId>blob-memory</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/server/blob/blob-export-file/src/main/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanism.java b/server/blob/blob-export-file/src/main/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanism.java
index f731c9a..656dc45 100644
--- a/server/blob/blob-export-file/src/main/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanism.java
+++ b/server/blob/blob-export-file/src/main/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanism.java
@@ -23,6 +23,8 @@ import java.io.File;
 import java.io.IOException;
 import java.net.UnknownHostException;
 
+import javax.inject.Inject;
+
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.james.blob.api.BlobId;
@@ -43,6 +45,10 @@ public class LocalFileBlobExportMechanism implements BlobExportMechanism {
     static final String CORRESPONDING_FILE_HEADER = "corresponding-file";
 
     public static class Configuration {
+
+        private static final String DEFAULT_DIRECTORY_LOCATION = "file://var/blobExporting";
+        public static final Configuration DEFAULT_CONFIGURATION = new Configuration(DEFAULT_DIRECTORY_LOCATION);
+
         private final String exportDirectory;
 
         public Configuration(String exportDirectory) {
@@ -56,6 +62,7 @@ public class LocalFileBlobExportMechanism implements BlobExportMechanism {
     private final DNSService dnsService;
     private final Configuration configuration;
 
+    @Inject
     LocalFileBlobExportMechanism(MailetContext mailetContext, BlobStore blobStore, FileSystem fileSystem, DNSService dnsService, Configuration configuration) {
         this.mailetContext = mailetContext;
         this.blobStore = blobStore;
diff --git a/server/blob/blob-export-file/src/test/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanismTest.java b/server/blob/blob-export-file/src/test/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanismTest.java
index 35983a0..83cd0e6 100644
--- a/server/blob/blob-export-file/src/test/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanismTest.java
+++ b/server/blob/blob-export-file/src/test/java/org/apache/james/blob/export/file/LocalFileBlobExportMechanismTest.java
@@ -64,9 +64,8 @@ class LocalFileBlobExportMechanismTest {
         DNSService dnsService = mock(DNSService.class);
         when(dnsService.getLocalHost()).thenReturn(localHost);
 
-        LocalFileBlobExportMechanism.Configuration blobExportConfiguration = new LocalFileBlobExportMechanism.Configuration("file://var/blobExporting");
-
-        testee = new LocalFileBlobExportMechanism(mailetContext, blobStore, fileSystem, dnsService, blobExportConfiguration);
+        testee = new LocalFileBlobExportMechanism(mailetContext, blobStore, fileSystem, dnsService,
+            LocalFileBlobExportMechanism.Configuration.DEFAULT_CONFIGURATION);
     }
 
     @Test
diff --git a/server/container/guice/blob-memory-guice/pom.xml b/server/container/guice/blob-export-guice/pom.xml
similarity index 73%
copy from server/container/guice/blob-memory-guice/pom.xml
copy to server/container/guice/blob-export-guice/pom.xml
index fbe487e..29a358b 100644
--- a/server/container/guice/blob-memory-guice/pom.xml
+++ b/server/container/guice/blob-export-guice/pom.xml
@@ -28,35 +28,23 @@
         <relativePath>../pom.xml</relativePath>
     </parent>
 
-    <artifactId>blob-memory-guice</artifactId>
+    <artifactId>blob-export-guice</artifactId>
     <packaging>jar</packaging>
 
-    <name>Apache James :: Server :: Blob Memory - guice injection</name>
-    <description>Blob modules on memory storage</description>
+    <name>Apache James :: Server :: Blob Exporting Mechanisms - guice injection</name>
 
     <dependencies>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>blob-api</artifactId>
+            <artifactId>blob-export-api</artifactId>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
-            <artifactId>blob-memory</artifactId>
+            <artifactId>blob-export-file</artifactId>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-common</artifactId>
         </dependency>
     </dependencies>
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <configuration>
-                    <reuseForks>true</reuseForks>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
 </project>
\ No newline at end of file
diff --git a/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/LocalFileBlobExportMechanismModule.java b/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/LocalFileBlobExportMechanismModule.java
new file mode 100644
index 0000000..7d38201
--- /dev/null
+++ b/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/LocalFileBlobExportMechanismModule.java
@@ -0,0 +1,37 @@
+/****************************************************************
+ * 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.modules;
+
+import org.apache.james.blob.export.api.BlobExportMechanism;
+import org.apache.james.blob.export.file.LocalFileBlobExportMechanism;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+public class LocalFileBlobExportMechanismModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+        bind(LocalFileBlobExportMechanism.Configuration.class).toInstance(LocalFileBlobExportMechanism.Configuration.DEFAULT_CONFIGURATION);
+
+        bind(LocalFileBlobExportMechanism.class).in(Scopes.SINGLETON);
+        bind(BlobExportMechanism.class).to(LocalFileBlobExportMechanism.class);
+    }
+}
diff --git a/server/container/guice/blob-memory-guice/pom.xml b/server/container/guice/blob-memory-guice/pom.xml
index fbe487e..e409945 100644
--- a/server/container/guice/blob-memory-guice/pom.xml
+++ b/server/container/guice/blob-memory-guice/pom.xml
@@ -48,15 +48,4 @@
             <artifactId>james-server-guice-common</artifactId>
         </dependency>
     </dependencies>
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <configuration>
-                    <reuseForks>true</reuseForks>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
 </project>
\ No newline at end of file
diff --git a/server/container/guice/cassandra-guice/pom.xml b/server/container/guice/cassandra-guice/pom.xml
index 01127fa..206e9bd 100644
--- a/server/container/guice/cassandra-guice/pom.xml
+++ b/server/container/guice/cassandra-guice/pom.xml
@@ -112,6 +112,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>blob-export-guice</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>event-sourcing-event-store-cassandra</artifactId>
         </dependency>
         <dependency>
diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/CassandraJamesServerMain.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/CassandraJamesServerMain.java
index a25e71d..f5f74d5 100644
--- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/CassandraJamesServerMain.java
+++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/CassandraJamesServerMain.java
@@ -19,6 +19,7 @@
 
 package org.apache.james;
 
+import org.apache.james.modules.LocalFileBlobExportMechanismModule;
 import org.apache.james.modules.MailboxModule;
 import org.apache.james.modules.activemq.ActiveMQQueueModule;
 import org.apache.james.modules.data.CassandraDLPConfigurationStoreModule;
@@ -124,7 +125,8 @@ public class CassandraJamesServerMain {
         CASSANDRA_SERVER_CORE_MODULE,
         CASSANDRA_MAILBOX_MODULE,
         PROTOCOLS,
-        PLUGINS);
+        PLUGINS,
+        new LocalFileBlobExportMechanismModule());
 
     public static void main(String[] args) throws Exception {
         Configuration configuration = Configuration.builder()
diff --git a/server/container/guice/memory-guice/pom.xml b/server/container/guice/memory-guice/pom.xml
index 792c017..6e3ae2f 100644
--- a/server/container/guice/memory-guice/pom.xml
+++ b/server/container/guice/memory-guice/pom.xml
@@ -65,6 +65,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>blob-export-guice</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>blob-memory-guice</artifactId>
         </dependency>
         <dependency>
diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java
index 6e0f235..c885f49 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java
+++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java
@@ -21,6 +21,7 @@ package org.apache.james;
 
 import org.apache.commons.configuration.DefaultConfigurationBuilder;
 import org.apache.james.modules.BlobMemoryModule;
+import org.apache.james.modules.LocalFileBlobExportMechanismModule;
 import org.apache.james.modules.MailboxModule;
 import org.apache.james.modules.data.MemoryDataJmapModule;
 import org.apache.james.modules.data.MemoryDataModule;
@@ -82,6 +83,7 @@ public class MemoryJamesServerMain {
     public static final Module IN_MEMORY_SERVER_MODULE = Modules.combine(
         new BlobMemoryModule(),
         new DeletedMessageVaultModule(),
+        new LocalFileBlobExportMechanismModule(),
         new MailboxModule(),
         new MemoryDataModule(),
         new MemoryEventStoreModule(),
diff --git a/server/container/guice/pom.xml b/server/container/guice/pom.xml
index 5948b80..f0f0ef0 100644
--- a/server/container/guice/pom.xml
+++ b/server/container/guice/pom.xml
@@ -34,6 +34,7 @@
 
     <modules>
         <module>blob-api-guice</module>
+        <module>blob-export-guice</module>
         <module>blob-memory-guice</module>
         <module>blob-objectstorage-guice</module>
         <module>cassandra-guice</module>
diff --git a/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/pom.xml b/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/pom.xml
index cf4901c..02531d3 100644
--- a/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/pom.xml
+++ b/server/protocols/jmap-integration-testing/memory-jmap-integration-testing/pom.xml
@@ -52,6 +52,11 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>blob-export-guice</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-dnsservice-test</artifactId>
         </dependency>
         <dependency>
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 940e3a8..7c1fa72 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
@@ -164,13 +164,12 @@ public class DeletedMessagesVaultRoutes implements Routes {
             name = "exportTo",
             paramType = "query",
             example = "?exportTo=user@james.org",
-            value = "Compulsory if action is export. Needs to be a valid mail address to represent for the destination " +
-                "where deleted messages content is export to")
+            value = "Compulsory if action is export. Needs to be a valid mail address. The content of the vault matching the query will be sent to that address")
     })
     @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.NOT_FOUND_404, message = "Not found - requested user is not existed in the system"),
+        @ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "Not found - requested user does not exist"),
         @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) throws JsonExtractException {
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
index bab02b8..2e3c014 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/ExportService.java
@@ -19,11 +19,14 @@
 
 package org.apache.james.webadmin.vault.routes;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
+import java.nio.file.Files;
 import java.util.Collection;
+import java.util.UUID;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
@@ -38,8 +41,6 @@ import org.apache.james.vault.DeletedMessage;
 import org.apache.james.vault.DeletedMessageVault;
 import org.apache.james.vault.DeletedMessageZipper;
 import org.apache.james.vault.search.Query;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
 import com.github.fge.lambdas.functions.ThrowingFunction;
@@ -52,7 +53,17 @@ import reactor.core.scheduler.Schedulers;
 
 class ExportService {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(ExportService.class);
+    private class ZippedData {
+        private final long contentLength;
+        private final InputStream inputStream;
+
+        private ZippedData(long contentLength, InputStream content) {
+            this.contentLength = contentLength;
+            this.inputStream = content;
+        }
+    }
+
+    private static final String TEMPORARY_FILE_EXTENSION = ".temp";
 
     private final BlobExportMechanism blobExport;
     private final BlobStore blobStore;
@@ -75,8 +86,8 @@ class ExportService {
             .doOnNext(any -> messageToExportCallback.run())
             .collect(Guavate.toImmutableList())
             .map(Collection::stream)
-            .map(sneakyThrow(messages -> zipData(user, messages)))
-            .flatMap(sneakyThrow(zippedStream -> blobStore.save(zippedStream, zippedStream.available())))
+            .flatMap(messages -> Mono.fromCallable(() -> zipData(user, messages)))
+            .flatMap(sneakyThrow(zippedData -> blobStore.save(zippedData.inputStream, zippedData.contentLength)))
             .flatMap(blobId -> exportTo(user, exportToAddress, blobId))
             .then();
     }
@@ -86,27 +97,19 @@ class ExportService {
             .publishOn(Schedulers.elastic());
     }
 
-    private PipedInputStream zipData(User user, Stream<DeletedMessage> messages) throws IOException {
-        PipedOutputStream outputStream = new PipedOutputStream();
-        PipedInputStream inputStream = new PipedInputStream();
-        inputStream.connect(outputStream);
+    private ZippedData zipData(User user, Stream<DeletedMessage> messages) throws IOException {
+        File tempFile = temporaryFile();
+        FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
 
-        asyncZipData(user, messages, outputStream).subscribe();
-
-        return inputStream;
+        zipper.zip(message -> loadMessageContent(user, message), messages, fileOutputStream);
+        return new ZippedData(tempFile.length(), new FileInputStream(tempFile));
     }
 
-    private Mono<Void> asyncZipData(User user, Stream<DeletedMessage> messages, PipedOutputStream outputStream) {
-        return Mono.fromRunnable(Throwing.runnable(() -> zipper.zip(message -> loadMessageContent(user, message), messages, outputStream)).sneakyThrow())
-            .doOnSuccessOrError(Throwing.biConsumer((result, throwable) -> {
-                if (throwable != null) {
-                    LOGGER.error("Error happens when zipping deleted messages", throwable);
-                }
-                outputStream.flush();
-                outputStream.close();
-            }))
-            .subscribeOn(Schedulers.elastic())
-            .then();
+    private File temporaryFile() throws IOException {
+        String tempFileName = UUID.randomUUID().toString();
+        File tempFile = Files.createTempFile(tempFileName, TEMPORARY_FILE_EXTENSION).toFile();
+        tempFile.deleteOnExit();
+        return tempFile;
     }
 
     private InputStream loadMessageContent(User user, DeletedMessage message) {


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


[james-project] 16/23: MAILBOX-388 MailRepositoryPath::parts

Posted by bt...@apache.org.
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 c394009bfe449f7218b01b5a77593a097c411736
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 15:28:20 2019 +0700

    MAILBOX-388 MailRepositoryPath::parts
---
 .../mailrepository/api/MailRepositoryPath.java     |  8 ++++++
 .../mailrepository/api/MailRepositoryPathTest.java | 30 ++++++++++++++++++++++
 2 files changed, 38 insertions(+)

diff --git a/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java b/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java
index f2d8686..e64ce9c 100644
--- a/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java
+++ b/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java
@@ -23,12 +23,14 @@ import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 import java.util.Objects;
 
 import org.apache.commons.lang3.StringUtils;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
 
 public class MailRepositoryPath implements Comparable<MailRepositoryPath> {
 
@@ -74,6 +76,12 @@ public class MailRepositoryPath implements Comparable<MailRepositoryPath> {
         return value.startsWith(other.value + PATH_DELIMITER);
     }
 
+    public List<String> parts() {
+        return Splitter.on(PATH_DELIMITER)
+            .omitEmptyStrings()
+            .splitToList(value);
+    }
+
     public String asString() {
         return value;
     }
diff --git a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java
index 31201e7..51dc6b6 100644
--- a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java
+++ b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java
@@ -158,4 +158,34 @@ class MailRepositoryPathTest {
         assertThat(MailRepositoryPath.from("abcghi").hasPrefix(MailRepositoryPath.from("abc")))
             .isFalse();
     }
+
+    @Test
+    void partsShouldSplitValue() {
+        assertThat(MailRepositoryPath.from("abc/ghi/klm").parts())
+            .containsExactly("abc", "ghi", "klm");
+    }
+
+    @Test
+    void partsShouldOmmitEmptyPart() {
+        assertThat(MailRepositoryPath.from("abc/ghi//klm").parts())
+            .containsExactly("abc", "ghi", "klm");
+    }
+
+    @Test
+    void partsShouldOmmitEmptyFirstPart() {
+        assertThat(MailRepositoryPath.from("/ghi/klm").parts())
+            .containsExactly("ghi", "klm");
+    }
+
+    @Test
+    void partsShouldreturnValueWhenNoSeparator() {
+        assertThat(MailRepositoryPath.from("abc").parts())
+            .containsExactly("abc");
+    }
+
+    @Test
+    void partsShouldEmptyWhenEmpty() {
+        assertThat(MailRepositoryPath.from("").parts())
+            .isEmpty();
+    }
 }
\ No newline at end of file


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


[james-project] 04/23: JAMES-2685 DMV Route webadmin markdown

Posted by bt...@apache.org.
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 c0c1ca3d89b277de9a9320cc18340d3248babb34
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Mon Mar 18 13:08:47 2019 +0700

    JAMES-2685 DMV Route webadmin markdown
---
 src/site/markdown/server/manage-webadmin.md | 42 +++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index f3a65f3..55e6e41 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -2552,6 +2552,7 @@ To move deleted messages in the vault, you need to specifically configure the De
 Here are the following actions available on the 'Deleted Messages Vault'
 
  - [Restore Deleted Messages](#Restore_deleted_messages)
+ - [Export Deleted Messages](#Export_deleted_messages)
 
  Note that the 'Deleted Messages Vault' feature is only supported on top of Cassandra-Guice.
 
@@ -2669,6 +2670,7 @@ Response code:
    - can not parse the JSON body
    - Json query object contains unsupported operator, fieldName
    - Json query object values violate parsing rules 
+ - 404: User not found
 
 The scheduled task will have the following type `deletedMessages/restore` and the following `additionalInformation`:
 
@@ -2685,6 +2687,46 @@ while:
  - errorRestoreCount: number of messages that failed to restore
  - user: owner of deleted messages need to restore
 
+### Export Deleted Messages
+
+Retrieve deleted messages matched with requested query from an user then share the content to a targeted mail address (exportTo)
+
+```
+curl -XPOST http://ip:port/deletedMessages/user/userExportFrom@domain.ext?action=export&exportTo=userReceiving@domain.ext
+
+BODY: is the json query has the same structure with Restore Deleted Messages section
+```
+**Note**: Json query passing into the body follows the same rules & restrictions like in [Restore Deleted Messages](#Restore_deleted_messages)
+
+Response code:
+
+ - 201: Task for exporting has been created
+ - 400: Bad request: 
+   - exportTo query param is not present
+   - exportTo query param is not a valid mail address
+   - 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 
+ - 404: User not found
+
+The scheduled task will have the following type `deletedMessages/export` and the following `additionalInformation`:
+
+```
+{
+  "userExportFrom": "userToRestore@domain.ext",
+  "exportTo": "userReceiving@domain.ext",
+  "totalExportedMessages": 1432
+}
+```
+
+while:
+ - userExportFrom: export deleted messages from this user
+ - exportTo: content of deleted messages have been shared to this mail address
+ - totalExportedMessages: number of deleted messages match with json query, then being shared to sharee
+
 ## 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


[james-project] 06/23: JAMES-2685 BlobMemoryModule for MemoryBlobStore

Posted by bt...@apache.org.
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 994c58fb99b2b1bdd4512167b828032219a6d5e5
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Tue Mar 19 11:45:15 2019 +0700

    JAMES-2685 BlobMemoryModule for MemoryBlobStore
    
    To be used in memory integration tests, more details, it is for injecting
    BlobStore to ExportService of DeletedMessageVaultRoute
---
 pom.xml                                            |  5 ++
 .../apache/james/blob/memory/MemoryBlobStore.java  |  4 ++
 server/container/guice/blob-memory-guice/pom.xml   | 62 ++++++++++++++++++++++
 .../org/apache/james/modules/BlobMemoryModule.java | 40 ++++++++++++++
 server/container/guice/memory-guice/pom.xml        |  4 ++
 .../org/apache/james/MemoryJamesServerMain.java    |  6 ++-
 server/container/guice/pom.xml                     |  1 +
 7 files changed, 120 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 23f959f..f478bff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1108,6 +1108,11 @@
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
+                <artifactId>blob-memory-guice</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
                 <artifactId>blob-objectstorage</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStore.java b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStore.java
index 2e203ef..c47d83f 100644
--- a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStore.java
+++ b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStore.java
@@ -25,18 +25,22 @@ import java.io.InputStream;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 
+import javax.inject.Inject;
+
 import org.apache.commons.io.IOUtils;
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStore;
 import org.apache.james.blob.api.ObjectStoreException;
 
 import com.google.common.base.Preconditions;
+
 import reactor.core.publisher.Mono;
 
 public class MemoryBlobStore implements BlobStore {
     private final ConcurrentHashMap<BlobId, byte[]> blobs;
     private final BlobId.Factory factory;
 
+    @Inject
     public MemoryBlobStore(BlobId.Factory factory) {
         this.factory = factory;
         blobs = new ConcurrentHashMap<>();
diff --git a/server/container/guice/blob-memory-guice/pom.xml b/server/container/guice/blob-memory-guice/pom.xml
new file mode 100644
index 0000000..fbe487e
--- /dev/null
+++ b/server/container/guice/blob-memory-guice/pom.xml
@@ -0,0 +1,62 @@
+<?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-guice</artifactId>
+        <version>3.4.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>blob-memory-guice</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Apache James :: Server :: Blob Memory - guice injection</name>
+    <description>Blob modules on memory storage</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>blob-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>blob-memory</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-common</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <reuseForks>true</reuseForks>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/server/container/guice/blob-memory-guice/src/main/java/org/apache/james/modules/BlobMemoryModule.java b/server/container/guice/blob-memory-guice/src/main/java/org/apache/james/modules/BlobMemoryModule.java
new file mode 100644
index 0000000..a9ab646
--- /dev/null
+++ b/server/container/guice/blob-memory-guice/src/main/java/org/apache/james/modules/BlobMemoryModule.java
@@ -0,0 +1,40 @@
+/****************************************************************
+ * 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.modules;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.blob.memory.MemoryBlobStore;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+public class BlobMemoryModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+        bind(HashBlobId.Factory.class).in(Scopes.SINGLETON);
+        bind(BlobId.Factory.class).to(HashBlobId.Factory.class);
+
+        bind(MemoryBlobStore.class).in(Scopes.SINGLETON);
+        bind(BlobStore.class).to(MemoryBlobStore.class);
+    }
+}
diff --git a/server/container/guice/memory-guice/pom.xml b/server/container/guice/memory-guice/pom.xml
index e785511..792c017 100644
--- a/server/container/guice/memory-guice/pom.xml
+++ b/server/container/guice/memory-guice/pom.xml
@@ -65,6 +65,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>blob-memory-guice</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>event-sourcing-event-store-memory</artifactId>
         </dependency>
         <dependency>
diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java
index cd951da..6e0f235 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java
+++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/MemoryJamesServerMain.java
@@ -20,6 +20,7 @@
 package org.apache.james;
 
 import org.apache.commons.configuration.DefaultConfigurationBuilder;
+import org.apache.james.modules.BlobMemoryModule;
 import org.apache.james.modules.MailboxModule;
 import org.apache.james.modules.data.MemoryDataJmapModule;
 import org.apache.james.modules.data.MemoryDataModule;
@@ -79,12 +80,13 @@ public class MemoryJamesServerMain {
         new JMAPServerModule());
 
     public static final Module IN_MEMORY_SERVER_MODULE = Modules.combine(
+        new BlobMemoryModule(),
         new DeletedMessageVaultModule(),
+        new MailboxModule(),
         new MemoryDataModule(),
         new MemoryEventStoreModule(),
         new MemoryMailboxModule(),
-        new MemoryMailQueueModule(),
-        new MailboxModule());
+        new MemoryMailQueueModule());
 
     public static final Module SMTP_ONLY_MODULE = Modules.combine(
         MemoryJamesServerMain.IN_MEMORY_SERVER_MODULE,
diff --git a/server/container/guice/pom.xml b/server/container/guice/pom.xml
index efc27bc..5948b80 100644
--- a/server/container/guice/pom.xml
+++ b/server/container/guice/pom.xml
@@ -34,6 +34,7 @@
 
     <modules>
         <module>blob-api-guice</module>
+        <module>blob-memory-guice</module>
         <module>blob-objectstorage-guice</module>
         <module>cassandra-guice</module>
         <module>cassandra-ldap-guice</module>


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


[james-project] 15/23: MAILBOX-388 MailRepositoryPath::hasPrefix

Posted by bt...@apache.org.
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 dde54c8273f6cbc4f06d0efba04eda0608bd8cf6
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Mar 21 15:23:33 2019 +0700

    MAILBOX-388 MailRepositoryPath::hasPrefix
---
 .../mailrepository/api/MailRepositoryPath.java     | 10 ++++
 .../mailrepository/api/MailRepositoryPathTest.java | 66 ++++++++++++++++++++++
 2 files changed, 76 insertions(+)

diff --git a/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java b/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java
index 5bb9d79..f2d8686 100644
--- a/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java
+++ b/server/mailrepository/mailrepository-api/src/main/java/org/apache/james/mailrepository/api/MailRepositoryPath.java
@@ -64,6 +64,16 @@ public class MailRepositoryPath implements Comparable<MailRepositoryPath> {
         return from(value + PATH_DELIMITER + suffix);
     }
 
+    public boolean hasPrefix(MailRepositoryPath other) {
+        if (value.isEmpty()) {
+            return false;
+        }
+        if (other.value.isEmpty()) {
+            return true;
+        }
+        return value.startsWith(other.value + PATH_DELIMITER);
+    }
+
     public String asString() {
         return value;
     }
diff --git a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java
index bb16d0f..31201e7 100644
--- a/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java
+++ b/server/mailrepository/mailrepository-api/src/test/java/org/apache/james/mailrepository/api/MailRepositoryPathTest.java
@@ -92,4 +92,70 @@ class MailRepositoryPathTest {
         assertThatThrownBy(() -> MailRepositoryPath.from("abc").subPath("/def"))
             .isInstanceOf(IllegalArgumentException.class);
     }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenEquals() {
+        assertThat(MailRepositoryPath.from("abc/def").hasPrefix(MailRepositoryPath.from("abc/def")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenSiblings() {
+        assertThat(MailRepositoryPath.from("abc/def").hasPrefix(MailRepositoryPath.from("abc/ghi")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenAncestor() {
+        assertThat(MailRepositoryPath.from("abc").hasPrefix(MailRepositoryPath.from("abc/ghi")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenDescendant() {
+        assertThat(MailRepositoryPath.from("abc/ghi").hasPrefix(MailRepositoryPath.from("abc")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenDescendantStartingWithSlash() {
+        assertThat(MailRepositoryPath.from("/abc/ghi").hasPrefix(MailRepositoryPath.from("/abc")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenDescendantAdditionalFirstSlash() {
+        assertThat(MailRepositoryPath.from("abc/ghi").hasPrefix(MailRepositoryPath.from("/abc")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenDescendantMissingFirstSlash() {
+        assertThat(MailRepositoryPath.from("/abc/ghi").hasPrefix(MailRepositoryPath.from("abc")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenFarDescendant() {
+        assertThat(MailRepositoryPath.from("abc/ghi/klm").hasPrefix(MailRepositoryPath.from("abc")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnTrueWhenEmpty() {
+        assertThat(MailRepositoryPath.from("abc").hasPrefix(MailRepositoryPath.from("")))
+            .isTrue();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenBothEmpty() {
+        assertThat(MailRepositoryPath.from("").hasPrefix(MailRepositoryPath.from("")))
+            .isFalse();
+    }
+
+    @Test
+    void hasPrefixShouldReturnFalseWhenMissingSlah() {
+        assertThat(MailRepositoryPath.from("abcghi").hasPrefix(MailRepositoryPath.from("abc")))
+            .isFalse();
+    }
 }
\ No newline at end of file


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