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/05/16 08:48:15 UTC

[james-project] branch master updated (f31eedc -> 895b50b)

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 f31eedc  JAMES-2756 MessageSearcher should decode value before searching
     new cb8d222  JAMES-2149 Support listing sources for domains in RRT
     new abaa002  JAMES-2149 Domain routes should handle aliases
     new 9370dc6  JAMES-2149 Integration tests for domain aliases
     new df68593  JAMES-2149 Website documentation for Domain aliases
     new 4d86c00  JAMES-2149 When I remove a domain, the corresponding aliases are also deleted
     new c901923  JAMES-2149 Factorize RRT error processor definitions
     new 41e29c5  JAMES-2149 RRT processing should fail when DomainAlias loop
     new 49e309c  JAMES-2149 User alias should be applied before domain mapping
     new bbfb2fb  JAMES-2149 Extract 'returnNoContent' is a static method
     new a0545c7  JAMES-2149 Introduce DomainAliasService
     new 16914c8  JAMES-2763 StartUpChecksPerformer implementation
     new 1be5ce5  JAMES-2763 Plug StartUpChecksPerformer to GuiceJamesServer
     new 89d880c  JAMES-2719 Copy of backends-es module to a new backends-es-v6 module
     new f803a50  JAMES-2719 Update version of ES 6 docker image to 6.7.2
     new eb5fc15  JAMES-2719 Update version of ES to 6.7.2 in the backends module
     new 7f756a8  JAMES-2719 Migrate ES backend code to ES6 syntax
     new eed7a8b  JAMES-2719 Migrate ES backend tests to ES6 syntax
     new b79ce0b  JAMES-2719 Migrate and fix ClientProviderImplConnectionTest class with ES6 syntax
     new 2d5473f  JAMES-2719 Refactor IndexCreationFactory to an immutable structure
     new 00026aa  JAMES-2719 Add bean contract for AliasName and ElasticSearchConfiguration in ES backend module
     new 45e3224  JAMES-2719 Refactor delete by query on ES backend
     new 2013287  JAMES-2763 CassandraSchemaVersion StartUpCheck
     new 895b50b  JAMES-2770 RRT rewriting can lead to duplicated mails

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:
 backends-common/elasticsearch-v6/pom.xml           | 102 +++++++
 .../org/apache/james/backends/es/v6/AliasName.java |  39 ++-
 .../james/backends/es/v6/ClientProvider.java       |  14 +-
 .../james/backends/es/v6/ClientProviderImpl.java   |  80 +++++
 .../es/v6/DeleteByQueryActionListener.java         |  27 +-
 .../backends/es/v6/ElasticSearchConfiguration.java | 240 +++++++++++++++
 .../james/backends/es/v6/ElasticSearchIndexer.java | 129 ++++++++
 .../james/backends/es/v6/IndexCreationFactory.java | 189 ++++++++++++
 .../org/apache/james/backends/es/v6/IndexName.java |  39 ++-
 .../james/backends/es/v6/NodeMappingFactory.java   |  81 +++++
 .../apache/james/backends/es/v6/ReadAliasName.java |  14 +-
 .../org/apache/james/backends/es/v6/TypeName.java  |  20 +-
 .../backends/es/v6/UpdatedRepresentation.java      |  69 +++++
 .../james/backends/es/v6/WriteAliasName.java       |  14 +-
 .../apache/james/backends/es/v6/AliasNameTest.java |  20 +-
 .../es/v6/ClientProviderImplConnectionTest.java    | 103 +++++++
 .../backends/es/v6/ClientProviderImplTest.java     | 142 +++++++++
 .../james/backends/es/v6/DockerElasticSearch.java  | 119 ++++++++
 .../backends/es/v6/DockerElasticSearchRule.java    |  33 ++-
 .../es/v6/DockerElasticSearchSingleton.java        |  16 +-
 .../es/v6/ElasticSearchConfigurationTest.java      | 327 +++++++++++++++++++++
 .../backends/es/v6/ElasticSearchIndexerTest.java   | 257 ++++++++++++++++
 .../backends/es/v6/IndexCreationFactoryTest.java   |  69 +++++
 .../backends/es/v6/NodeMappingFactoryTest.java     | 100 +++++++
 .../src/test/resources/logback-test.xml            |   0
 backends-common/pom.xml                            |   1 +
 .../apache/james/modules/BlobExportImplChoice.java |   2 +-
 .../CassandraSchemaVersionStartUpCheck.java        | 116 ++++++++
 .../modules/mailbox/CassandraSessionModule.java    |  54 +---
 .../CassandraSchemaVersionStartUpCheckTest.java}   |  42 ++-
 .../java/org/apache/james/GuiceJamesServer.java    |   2 +
 .../org/apache/james/StartUpChecksPerformer.java   | 215 ++++++++++++++
 .../apache/james/modules/CommonServicesModule.java |   1 +
 .../apache/james/modules/StartUpChecksModule.java} |  21 +-
 .../apache/james/StartUpChecksPerformerTest.java   |  92 ++++++
 .../james/GuiceJamesServerStartUpCheckTest.java    | 189 ++++++++++++
 .../james/rrt/api/RecipientRewriteTable.java       |   3 +-
 .../rrt/lib/AbstractRecipientRewriteTableTest.java |  37 ++-
 .../mailets/configuration/CommonProcessors.java    |  34 +++
 .../james/transport/mailets/AliasMappingTest.java  |  50 ++--
 .../james/transport/mailets/DomainMappingTest.java | 107 +++++++
 .../transport/mailets/GroupMappingRelayTest.java   |  35 +--
 .../james/transport/mailets/GroupMappingTest.java  |  38 +--
 .../mailets/RecipientRewriteTableProcessor.java    |  24 +-
 .../RecipientRewriteTableProcessorTest.java        |  21 ++
 .../apache/james/webadmin/routes/TasksRoutes.java  |   5 +-
 .../apache/james/webadmin/utils/Responses.java}    |  20 +-
 .../james/webadmin/dto/DomainAliasResponse.java}   |  41 ++-
 .../webadmin/routes/DLPConfigurationRoutes.java    |   8 +-
 .../james/webadmin/routes/DomainsRoutes.java       | 167 +++++++++--
 .../james/webadmin/routes/SieveQuotaRoutes.java    |  21 +-
 .../apache/james/webadmin/routes/UserRoutes.java   |   5 +-
 .../james/webadmin/service/DomainAliasService.java |  98 ++++++
 .../apache/james/webadmin/service/UserService.java |   3 +-
 .../webadmin/dto/DomainAliasResponseTest.java}     |  21 +-
 .../james/webadmin/routes/DomainsRoutesTest.java   | 296 ++++++++++++++++++-
 .../james/webadmin/routes/DomainQuotaRoutes.java   |  22 +-
 .../webadmin/routes/EventDeadLettersRoutes.java    |   6 +-
 .../james/webadmin/routes/GlobalQuotaRoutes.java   |  16 +-
 .../james/webadmin/routes/UserMailboxesRoutes.java |  13 +-
 .../james/webadmin/routes/UserQuotaRoutes.java     |  22 +-
 .../james/webadmin/routes/MailQueueRoutes.java     |   6 +-
 .../webadmin/routes/MailRepositoriesRoutes.java    |   7 +-
 .../java/org/apache/james/util/docker/Images.java  |   2 +-
 src/site/markdown/server/manage-webadmin.md        |  64 ++++
 65 files changed, 3748 insertions(+), 422 deletions(-)
 create mode 100644 backends-common/elasticsearch-v6/pom.xml
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/AliasName.java (66%)
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java (73%)
 create mode 100644 backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryActionListener.java (65%)
 create mode 100644 backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java
 create mode 100644 backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
 create mode 100644 backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexName.java (66%)
 create mode 100644 backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ReadAliasName.java (73%)
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/TypeName.java (73%)
 create mode 100644 backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/UpdatedRepresentation.java
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/WriteAliasName.java (73%)
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/AliasNameTest.java (73%)
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplTest.java
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java (64%)
 copy server/testing/src/main/java/org/apache/james/util/docker/Images.java => backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchSingleton.java (73%)
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
 create mode 100644 backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java
 copy backends-common/{elasticsearch => elasticsearch-v6}/src/test/resources/logback-test.xml (100%)
 create mode 100644 server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheck.java
 rename server/container/guice/cassandra-guice/src/test/java/org/apache/james/{CassandraVersionCheckingTest.java => modules/mailbox/CassandraSchemaVersionStartUpCheckTest.java} (77%)
 create mode 100644 server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java
 copy server/{testing/src/main/java/org/apache/james/util/docker/Images.java => container/guice/guice-common/src/main/java/org/apache/james/modules/StartUpChecksModule.java} (73%)
 create mode 100644 server/container/guice/guice-common/src/test/java/org/apache/james/StartUpChecksPerformerTest.java
 create mode 100644 server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceJamesServerStartUpCheckTest.java
 copy server/{testing/src/main/java/org/apache/james/util/docker/Images.java => protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/Responses.java} (73%)
 copy server/{testing/src/main/java/org/apache/james/util/docker/Images.java => protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/DomainAliasResponse.java} (62%)
 create mode 100644 server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DomainAliasService.java
 copy server/{testing/src/main/java/org/apache/james/util/docker/Images.java => protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/dto/DomainAliasResponseTest.java} (73%)


---------------------------------------------------------------------
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: JAMES-2763 CassandraSchemaVersion StartUpCheck

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 201328786d3a5e4fcb33f18c2763010fe9ccd32e
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Wed May 15 16:56:16 2019 +0700

    JAMES-2763 CassandraSchemaVersion StartUpCheck
---
 .../CassandraSchemaVersionStartUpCheck.java        | 116 +++++++++++++++++++++
 .../modules/mailbox/CassandraSessionModule.java    |  54 +---------
 .../CassandraSchemaVersionStartUpCheckTest.java}   |  42 +++++---
 .../org/apache/james/StartUpChecksPerformer.java   |  15 ++-
 4 files changed, 159 insertions(+), 68 deletions(-)

diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheck.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheck.java
new file mode 100644
index 0000000..0e6b24a
--- /dev/null
+++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheck.java
@@ -0,0 +1,116 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.modules.mailbox;
+
+import org.apache.james.StartUpChecksPerformer;
+import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+
+public class CassandraSchemaVersionStartUpCheck implements StartUpChecksPerformer.StartUpCheck {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(CassandraSchemaVersionStartUpCheck.class);
+    static final String CHECK_NAME = "CassandraSchemaVersionStartUpCheck";
+
+    private final CassandraSchemaVersionManager versionManager;
+
+    @Inject
+    public CassandraSchemaVersionStartUpCheck(CassandraSchemaVersionManager versionManager) {
+        this.versionManager = versionManager;
+    }
+
+    @Override
+    public CheckResult check() {
+        CassandraSchemaVersionManager.SchemaState schemaState = versionManager.computeSchemaState();
+        switch (schemaState) {
+            case TOO_OLD:
+                return checkTooOldState();
+            case TOO_RECENT:
+                return checkTooRecentState();
+            case UP_TO_DATE:
+                return checkUpToDateState();
+            case UPGRADABLE:
+                return checkUpgradeAbleState();
+            default:
+                String unknownSchemaStateMessage = "Unknown schema state " + schemaState;
+                LOGGER.error(unknownSchemaStateMessage);
+                return CheckResult.builder()
+                    .checkName(CHECK_NAME)
+                    .resultType(ResultType.BAD)
+                    .description(unknownSchemaStateMessage)
+                    .build();
+        }
+    }
+
+    private CheckResult checkUpgradeAbleState() {
+        String upgradeVersionMessage =
+            String.format("Current schema version is %d. Recommended version is %d",
+                versionManager.computeVersion().getValue(),
+                versionManager.getMaximumSupportedVersion().getValue());
+        LOGGER.warn(upgradeVersionMessage);
+        return CheckResult.builder()
+            .checkName(CHECK_NAME)
+            .resultType(ResultType.GOOD)
+            .description(upgradeVersionMessage)
+            .build();
+    }
+
+    private CheckResult checkUpToDateState() {
+        String message = "Schema version is up-to-date";
+        LOGGER.info(message);
+        return CheckResult.builder()
+            .checkName(CHECK_NAME)
+            .resultType(ResultType.GOOD)
+            .description(message)
+            .build();
+    }
+
+    private CheckResult checkTooRecentState() {
+        String versionExceedMaximumSupportedMessage =
+            String.format("Current schema version is %d whereas the maximum supported version is %d. " +
+                "Recommended version is %d.",
+                versionManager.computeVersion().getValue(),
+                versionManager.getMaximumSupportedVersion().getValue(),
+                versionManager.getMaximumSupportedVersion().getValue());
+        LOGGER.error(versionExceedMaximumSupportedMessage);
+        return CheckResult.builder()
+            .checkName(CHECK_NAME)
+            .resultType(ResultType.BAD)
+            .description(versionExceedMaximumSupportedMessage)
+            .build();
+    }
+
+    private CheckResult checkTooOldState() {
+        String versionToOldMessage =
+            String.format("Current schema version is %d whereas minimum required version is %d. " +
+                "Recommended version is %d",
+                versionManager.computeVersion().getValue(),
+                versionManager.getMinimumSupportedVersion().getValue(),
+                versionManager.getMaximumSupportedVersion().getValue());
+        LOGGER.error(versionToOldMessage);
+        return CheckResult.builder()
+            .checkName(CHECK_NAME)
+            .resultType(ResultType.BAD)
+            .description(versionToOldMessage)
+            .build();
+    }
+}
diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java
index 1613dfe..951a328 100644
--- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java
+++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java
@@ -19,11 +19,11 @@
 package org.apache.james.modules.mailbox;
 
 import java.io.FileNotFoundException;
-import java.util.List;
 import java.util.Set;
 
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.configuration.ConfigurationException;
+import org.apache.james.StartUpChecksPerformer;
 import org.apache.james.backends.cassandra.components.CassandraModule;
 import org.apache.james.backends.cassandra.init.CassandraZonedDateTimeModule;
 import org.apache.james.backends.cassandra.init.SessionWithInitializedTablesFactory;
@@ -33,14 +33,11 @@ import org.apache.james.backends.cassandra.utils.CassandraHealthCheck;
 import org.apache.james.backends.cassandra.utils.CassandraUtils;
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO;
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager;
-import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager.SchemaState;
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionModule;
 import org.apache.james.core.healthcheck.HealthCheck;
-import org.apache.james.lifecycle.api.Startable;
 import org.apache.james.mailbox.store.BatchSizes;
 import org.apache.james.server.CassandraProbe;
 import org.apache.james.util.Host;
-import org.apache.james.utils.ConfigurationPerformer;
 import org.apache.james.utils.GuiceProbe;
 import org.apache.james.utils.PropertiesProvider;
 import org.slf4j.Logger;
@@ -49,9 +46,7 @@ import org.slf4j.LoggerFactory;
 import com.datastax.driver.core.Cluster;
 import com.datastax.driver.core.Session;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
 import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
 import com.google.inject.Provides;
 import com.google.inject.Scopes;
 import com.google.inject.Singleton;
@@ -79,7 +74,8 @@ public class CassandraSessionModule extends AbstractModule {
         bind(CassandraSchemaVersionManager.class).in(Scopes.SINGLETON);
         bind(CassandraSchemaVersionDAO.class).in(Scopes.SINGLETON);
 
-        Multibinder.newSetBinder(binder(), ConfigurationPerformer.class).addBinding().to(CassandraSchemaChecker.class);
+        Multibinder.newSetBinder(binder(), StartUpChecksPerformer.StartUpCheck.class)
+            .addBinding().to(CassandraSchemaVersionStartUpCheck.class);
 
         Multibinder.newSetBinder(binder(), GuiceProbe.class).addBinding().to(CassandraProbe.class);
 
@@ -136,48 +132,4 @@ public class CassandraSessionModule extends AbstractModule {
                 .build();
         }
     }
-
-    public static class CassandraSchemaChecker implements ConfigurationPerformer {
-        private final CassandraSchemaVersionManager versionManager;
-
-        @Inject
-        public CassandraSchemaChecker(CassandraSchemaVersionManager versionManager) {
-            this.versionManager = versionManager;
-        }
-
-        @Override
-        public void initModule() {
-            SchemaState schemaState = versionManager.computeSchemaState();
-            switch (schemaState) {
-                case TOO_OLD:
-                    throw new IllegalStateException(
-                        String.format("Current schema version is %d whereas minimum required version is %d. " +
-                            "Recommended version is %d",
-                            versionManager.computeVersion().getValue(),
-                            versionManager.getMinimumSupportedVersion().getValue(),
-                            versionManager.getMaximumSupportedVersion().getValue()));
-                case TOO_RECENT:
-                    throw new IllegalStateException(
-                        String.format("Current schema version is %d whereas the minimum supported version is %d. " +
-                            "Recommended version is %d.",
-                            versionManager.computeVersion().getValue(),
-                            versionManager.getMinimumSupportedVersion().getValue(),
-                            versionManager.getMaximumSupportedVersion().getValue()));
-                case UP_TO_DATE:
-                    LOGGER.info("Schema version is up-to-date");
-                    return;
-                case UPGRADABLE:
-                    LOGGER.warn("Current schema version is {}. Recommended version is {}", versionManager.computeVersion(),
-                        versionManager.getMaximumSupportedVersion());
-                    return;
-                default:
-                    throw new IllegalStateException("Unknown schema state " + schemaState);
-            }
-        }
-
-        @Override
-        public List<Class<? extends Startable>> forClasses() {
-            return ImmutableList.of();
-        }
-    }
 }
diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraVersionCheckingTest.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheckTest.java
similarity index 77%
rename from server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraVersionCheckingTest.java
rename to server/container/guice/cassandra-guice/src/test/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheckTest.java
index d7cb7d1..a653310 100644
--- a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/CassandraVersionCheckingTest.java
+++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/modules/mailbox/CassandraSchemaVersionStartUpCheckTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james;
+package org.apache.james.modules.mailbox;
 
 import static org.apache.james.CassandraJamesServerMain.ALL_BUT_JMX_CASSANDRA_MODULE;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -31,6 +31,12 @@ import java.nio.channels.SocketChannel;
 import java.nio.charset.Charset;
 import java.util.Optional;
 
+import org.apache.james.CassandraExtension;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.StartUpChecksPerformer.StartUpChecksException;
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO;
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager;
 import org.apache.james.backends.cassandra.versions.SchemaVersion;
@@ -45,9 +51,10 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 
 import reactor.core.publisher.Mono;
 
-class CassandraVersionCheckingTest {
+class CassandraSchemaVersionStartUpCheckTest {
     private static final int LIMIT_TO_10_MESSAGES = 10;
     private static final String LOCAL_HOST = "127.0.0.1";
+    private static final String EXPECTED_SERVER_CONNECTED_MESSAGE = "* OK JAMES IMAP4rev1 Server";
     private static final SchemaVersion MIN_VERSION = new SchemaVersion(2);
     private static final SchemaVersion MAX_VERSION = new SchemaVersion(4);
 
@@ -85,7 +92,9 @@ class CassandraVersionCheckingTest {
         when(versionDAO.getCurrentSchemaVersion())
             .thenReturn(Mono.just(Optional.of(MAX_VERSION)));
 
-        assertThatServerStartCorrectly(server);
+        server.start();
+        assertThat(responseAfterConnectTo(server))
+            .startsWith(EXPECTED_SERVER_CONNECTED_MESSAGE);
     }
 
     @Test
@@ -93,7 +102,9 @@ class CassandraVersionCheckingTest {
         when(versionDAO.getCurrentSchemaVersion())
             .thenReturn(Mono.just(Optional.of(MIN_VERSION.next())));
 
-        assertThatServerStartCorrectly(server);
+        server.start();
+        assertThat(responseAfterConnectTo(server))
+            .startsWith(EXPECTED_SERVER_CONNECTED_MESSAGE);
     }
 
     @Test
@@ -101,7 +112,9 @@ class CassandraVersionCheckingTest {
         when(versionDAO.getCurrentSchemaVersion())
             .thenReturn(Mono.just(Optional.of(MIN_VERSION)));
 
-        assertThatServerStartCorrectly(server);
+        server.start();
+        assertThat(responseAfterConnectTo(server))
+            .startsWith(EXPECTED_SERVER_CONNECTED_MESSAGE);
     }
 
     @Test
@@ -109,7 +122,11 @@ class CassandraVersionCheckingTest {
         when(versionDAO.getCurrentSchemaVersion())
             .thenReturn(Mono.just(Optional.of(MIN_VERSION.previous())));
 
-        assertThatThrownBy(server::start).isInstanceOf(IllegalStateException.class);
+        assertThatThrownBy(server::start)
+            .isInstanceOfSatisfying(
+                StartUpChecksException.class,
+                exception -> assertThat(exception.badCheckNames())
+                    .containsOnly(CassandraSchemaVersionStartUpCheck.CHECK_NAME));
     }
 
     @Test
@@ -117,17 +134,16 @@ class CassandraVersionCheckingTest {
         when(versionDAO.getCurrentSchemaVersion())
             .thenReturn(Mono.just(Optional.of(MAX_VERSION.next())));
 
-        assertThatThrownBy(server::start).isInstanceOf(IllegalStateException.class);
+        assertThatThrownBy(server::start)
+            .isInstanceOfSatisfying(
+                StartUpChecksException.class,
+                exception -> assertThat(exception.badCheckNames())
+                    .containsOnly(CassandraSchemaVersionStartUpCheck.CHECK_NAME));
     }
 
-    private void assertThatServerStartCorrectly(GuiceJamesServer server) throws Exception {
-        server.start();
+    private String responseAfterConnectTo(GuiceJamesServer server) throws IOException {
         socketChannel.connect(new InetSocketAddress(LOCAL_HOST, server.getProbe(ImapGuiceProbe.class).getImapPort()));
-        assertThat(getServerConnectionResponse(socketChannel))
-            .startsWith("* OK JAMES IMAP4rev1 Server");
-    }
 
-    private String getServerConnectionResponse(SocketChannel socketChannel) throws IOException {
         ByteBuffer byteBuffer = ByteBuffer.allocate(1000);
         socketChannel.read(byteBuffer);
         byte[] bytes = byteBuffer.array();
diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java b/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java
index 02d895e..e047e97 100644
--- a/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java
+++ b/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java
@@ -65,6 +65,13 @@ public class StartUpChecksPerformer {
         public List<StartUpCheck.CheckResult> getBadChecks() {
             return badChecks;
         }
+
+        @VisibleForTesting
+        public List<String> badCheckNames() {
+            return badChecks.stream()
+                .map(StartUpCheck.CheckResult::getName)
+                .collect(Guavate.toImmutableList());
+        }
     }
 
     public interface StartUpCheck {
@@ -75,19 +82,19 @@ public class StartUpChecksPerformer {
 
         class CheckResult {
 
-            static class Builder {
+            public static class Builder {
 
                 @FunctionalInterface
-                interface RequireCheckName {
+                public interface RequireCheckName {
                     RequireResultType checkName(String name);
                 }
 
                 @FunctionalInterface
-                interface RequireResultType {
+                public interface RequireResultType {
                     ReadyToBuild resultType(ResultType resultType);
                 }
 
-                static class ReadyToBuild {
+                public static class ReadyToBuild {
                     private final String name;
                     private final ResultType resultType;
                     private Optional<String> description;


---------------------------------------------------------------------
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: JAMES-2719 Copy of backends-es module to a new backends-es-v6 module

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 89d880ca5438274144782fda3d6e52038a7266d7
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Mon May 13 17:59:31 2019 +0700

    JAMES-2719 Copy of backends-es module to a new backends-es-v6 module
---
 backends-common/elasticsearch-v6/pom.xml           | 104 +++++++
 .../org/apache/james/backends/es/v6/AliasName.java |  49 ++++
 .../james/backends/es/v6/ClientProvider.java       |  26 ++
 .../james/backends/es/v6/ClientProviderImpl.java   |  86 ++++++
 .../backends/es/v6/DeleteByQueryPerformer.java     |  84 ++++++
 .../backends/es/v6/ElasticSearchConfiguration.java | 240 ++++++++++++++++
 .../james/backends/es/v6/ElasticSearchIndexer.java | 116 ++++++++
 .../james/backends/es/v6/IndexCreationFactory.java | 158 ++++++++++
 .../org/apache/james/backends/es/v6/IndexName.java |  49 ++++
 .../james/backends/es/v6/NodeMappingFactory.java   |  75 +++++
 .../apache/james/backends/es/v6/ReadAliasName.java |  26 ++
 .../org/apache/james/backends/es/v6/TypeName.java  |  32 +++
 .../backends/es/v6/UpdatedRepresentation.java      |  69 +++++
 .../james/backends/es/v6/WriteAliasName.java       |  26 ++
 .../backends/es/v6/search/ScrollIterable.java      |  81 ++++++
 .../es/v6/ClientProviderImplConnectionTest.java    |  96 +++++++
 .../backends/es/v6/ClientProviderImplTest.java     | 142 +++++++++
 .../james/backends/es/v6/DockerElasticSearch.java  | 126 ++++++++
 .../backends/es/v6/DockerElasticSearchRule.java    |  50 ++++
 .../es/v6/DockerElasticSearchSingleton.java        |  28 ++
 .../es/v6/ElasticSearchConfigurationTest.java      | 319 +++++++++++++++++++++
 .../backends/es/v6/ElasticSearchIndexerTest.java   | 248 ++++++++++++++++
 .../backends/es/v6/IndexCreationFactoryTest.java   |  68 +++++
 .../backends/es/v6/NodeMappingFactoryTest.java     | 101 +++++++
 .../backends/es/v6/search/ScrollIterableTest.java  | 205 +++++++++++++
 .../es/v6/utils/TestingClientProvider.java         |  37 +++
 .../src/test/resources/logback-test.xml            |  12 +
 backends-common/pom.xml                            |   1 +
 28 files changed, 2654 insertions(+)

diff --git a/backends-common/elasticsearch-v6/pom.xml b/backends-common/elasticsearch-v6/pom.xml
new file mode 100644
index 0000000..cc661e3
--- /dev/null
+++ b/backends-common/elasticsearch-v6/pom.xml
@@ -0,0 +1,104 @@
+<?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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.james</groupId>
+        <artifactId>james-backends-common</artifactId>
+        <version>3.4.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>apache-james-backends-es-v6</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.github.fge</groupId>
+            <artifactId>throwing-lambdas</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-configuration</groupId>
+            <artifactId>commons-configuration</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.elasticsearch</groupId>
+            <artifactId>elasticsearch</artifactId>
+            <version>2.2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.elasticsearch</groupId>
+            <artifactId>elasticsearch</artifactId>
+            <version>2.2.1</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/AliasName.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/AliasName.java
new file mode 100644
index 0000000..754ac6e
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/AliasName.java
@@ -0,0 +1,49 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.util.Objects;
+
+public class AliasName {
+    private final String value;
+
+    protected AliasName(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof AliasName) {
+            AliasName aliasName = (AliasName) o;
+
+            return Objects.equals(this.value, aliasName.value);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(value);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java
new file mode 100644
index 0000000..0145d0a
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java
@@ -0,0 +1,26 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import org.elasticsearch.client.Client;
+
+public interface ClientProvider {
+
+    Client get();
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java
new file mode 100644
index 0000000..aac59a5
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java
@@ -0,0 +1,86 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.net.InetAddress;
+import java.util.Optional;
+
+import org.apache.james.util.Host;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.client.transport.TransportClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.transport.InetSocketTransportAddress;
+
+import com.github.fge.lambdas.Throwing;
+import com.github.fge.lambdas.consumers.ConsumerChainer;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+public class ClientProviderImpl implements ClientProvider {
+
+    public static ClientProviderImpl forHost(String address, Integer port, Optional<String> clusterName) {
+        return new ClientProviderImpl(ImmutableList.of(Host.from(address, port)), clusterName);
+    }
+
+    public static ClientProviderImpl fromHostsString(String hostsString, Optional<String> clusterName) {
+        Preconditions.checkNotNull(hostsString, "HostString should not be null");
+        return new ClientProviderImpl(Host.parseHosts(hostsString), clusterName);
+    }
+
+    public static ClientProviderImpl fromHosts(ImmutableList<Host> hosts, Optional<String> clusterName) {
+        Preconditions.checkNotNull(hosts, "Hosts should not be null");
+        return new ClientProviderImpl(hosts, clusterName);
+    }
+
+    private static final String CLUSTER_NAME_SETTING = "cluster.name";
+
+    private final ImmutableList<Host> hosts;
+    private final Optional<String> clusterName;
+
+    private ClientProviderImpl(ImmutableList<Host> hosts, Optional<String> clusterName) {
+        Preconditions.checkArgument(!hosts.isEmpty(), "You should provide at least one host");
+        this.hosts = hosts;
+        this.clusterName = clusterName;
+    }
+
+
+    @Override
+    public Client get() {
+        TransportClient transportClient = TransportClient.builder()
+                .settings(settings())
+                .build();
+        ConsumerChainer<Host> consumer = Throwing.consumer(host -> transportClient
+            .addTransportAddress(
+                new InetSocketTransportAddress(
+                    InetAddress.getByName(host.getHostName()),
+                    host.getPort())));
+        hosts.forEach(consumer.sneakyThrow());
+        return transportClient;
+    }
+
+    @VisibleForTesting Settings settings() {
+        if (clusterName.isPresent()) {
+            return Settings.builder()
+                    .put(CLUSTER_NAME_SETTING, clusterName.get())
+                    .build();
+        }
+        return Settings.EMPTY;
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
new file mode 100644
index 0000000..05fd04e
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
@@ -0,0 +1,84 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.james.backends.es.v6.search.ScrollIterable;
+import org.elasticsearch.action.ListenableActionFuture;
+import org.elasticsearch.action.bulk.BulkRequestBuilder;
+import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.SearchHit;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public class DeleteByQueryPerformer {
+    public static final TimeValue TIMEOUT = new TimeValue(60000);
+
+    private final Client client;
+    private final ExecutorService executor;
+    private final int batchSize;
+    private final WriteAliasName aliasName;
+    private final TypeName typeName;
+
+    @VisibleForTesting
+    public DeleteByQueryPerformer(Client client, ExecutorService executor, int batchSize, WriteAliasName aliasName, TypeName typeName) {
+        this.client = client;
+        this.executor = executor;
+        this.batchSize = batchSize;
+        this.aliasName = aliasName;
+        this.typeName = typeName;
+    }
+
+    public Future<Void> perform(QueryBuilder queryBuilder) {
+        return executor.submit(() -> doDeleteByQuery(queryBuilder));
+    }
+
+    protected Void doDeleteByQuery(QueryBuilder queryBuilder) {
+        new ScrollIterable(client,
+            client.prepareSearch(aliasName.getValue())
+                .setTypes(typeName.getValue())
+                .setScroll(TIMEOUT)
+                .setNoFields()
+                .setQuery(queryBuilder)
+                .setSize(batchSize))
+            .stream()
+            .map(searchResponse -> deleteRetrievedIds(client, searchResponse))
+            .forEach(ListenableActionFuture::actionGet);
+        return null;
+    }
+
+    private ListenableActionFuture<BulkResponse> deleteRetrievedIds(Client client, SearchResponse searchResponse) {
+        BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
+        for (SearchHit hit : searchResponse.getHits()) {
+            bulkRequestBuilder.add(client.prepareDelete()
+                .setIndex(aliasName.getValue())
+                .setType(typeName.getValue())
+                .setId(hit.getId()));
+        }
+        return bulkRequestBuilder.execute();
+    }
+
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java
new file mode 100644
index 0000000..f490941
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java
@@ -0,0 +1,240 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.james.util.Host;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+public class ElasticSearchConfiguration {
+
+
+    public static class Builder {
+
+        private final ImmutableList.Builder<Host> hosts;
+        private Optional<String> clusterName;
+        private Optional<Integer> nbShards;
+        private Optional<Integer> nbReplica;
+        private Optional<Integer> minDelay;
+        private Optional<Integer> maxRetries;
+
+        public Builder() {
+            hosts = ImmutableList.builder();
+            clusterName = Optional.empty();
+            nbShards = Optional.empty();
+            nbReplica = Optional.empty();
+            minDelay = Optional.empty();
+            maxRetries = Optional.empty();
+        }
+
+        public Builder addHost(Host host) {
+            this.hosts.add(host);
+            return this;
+        }
+
+        public Builder clusterName(String clusterName) {
+            this.clusterName = Optional.ofNullable(clusterName);
+            return this;
+        }
+
+        public Builder addHosts(Collection<Host> hosts) {
+            this.hosts.addAll(hosts);
+            return this;
+        }
+
+        public Builder nbShards(int nbShards) {
+            Preconditions.checkArgument(nbShards > 0, "You need the number of shards to be strictly positive");
+            this.nbShards = Optional.of(nbShards);
+            return this;
+        }
+
+        public Builder nbReplica(int nbReplica) {
+            Preconditions.checkArgument(nbReplica >= 0, "You need the number of replica to be positive");
+            this.nbReplica = Optional.of(nbReplica);
+            return this;
+        }
+
+        public Builder minDelay(Optional<Integer> minDelay) {
+            this.minDelay = minDelay;
+            return this;
+        }
+
+        public Builder maxRetries(Optional<Integer> maxRetries) {
+            this.maxRetries = maxRetries;
+            return this;
+        }
+
+        public ElasticSearchConfiguration build() {
+            ImmutableList<Host> hosts = this.hosts.build();
+            Preconditions.checkState(!hosts.isEmpty(), "You need to specify ElasticSearch host");
+            return new ElasticSearchConfiguration(
+                hosts,
+                clusterName,
+                nbShards.orElse(DEFAULT_NB_SHARDS),
+                nbReplica.orElse(DEFAULT_NB_REPLICA),
+                minDelay.orElse(DEFAULT_CONNECTION_MIN_DELAY),
+                maxRetries.orElse(DEFAULT_CONNECTION_MAX_RETRIES));
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final String ELASTICSEARCH_HOSTS = "elasticsearch.hosts";
+    public static final String ELASTICSEARCH_CLUSTER_NAME = "elasticsearch.clusterName";
+    public static final String ELASTICSEARCH_MASTER_HOST = "elasticsearch.masterHost";
+    public static final String ELASTICSEARCH_PORT = "elasticsearch.port";
+    public static final String ELASTICSEARCH_NB_REPLICA = "elasticsearch.nb.replica";
+    public static final String ELASTICSEARCH_NB_SHARDS = "elasticsearch.nb.shards";
+    public static final String ELASTICSEARCH_RETRY_CONNECTION_MIN_DELAY = "elasticsearch.retryConnection.minDelay";
+    public static final String ELASTICSEARCH_RETRY_CONNECTION_MAX_RETRIES = "elasticsearch.retryConnection.maxRetries";
+
+    public static final int DEFAULT_CONNECTION_MAX_RETRIES = 7;
+    public static final int DEFAULT_CONNECTION_MIN_DELAY = 3000;
+    public static final int DEFAULT_NB_SHARDS = 5;
+    public static final int DEFAULT_NB_REPLICA = 1;
+    public static final int DEFAULT_PORT = 9300;
+    private static final String LOCALHOST = "127.0.0.1";
+    public static final Optional<Integer> DEFAULT_PORT_AS_OPTIONAL = Optional.of(DEFAULT_PORT);
+
+    public static final ElasticSearchConfiguration DEFAULT_CONFIGURATION = builder()
+        .addHost(Host.from(LOCALHOST, DEFAULT_PORT))
+        .build();
+
+    public static ElasticSearchConfiguration fromProperties(Configuration configuration) throws ConfigurationException {
+        return builder()
+            .addHosts(getHosts(configuration))
+            .clusterName(configuration.getString(ELASTICSEARCH_CLUSTER_NAME))
+            .nbShards(configuration.getInteger(ELASTICSEARCH_NB_SHARDS, DEFAULT_NB_SHARDS))
+            .nbReplica(configuration.getInteger(ELASTICSEARCH_NB_REPLICA, DEFAULT_NB_REPLICA))
+            .minDelay(Optional.ofNullable(configuration.getInteger(ELASTICSEARCH_RETRY_CONNECTION_MIN_DELAY, null)))
+            .maxRetries(Optional.ofNullable(configuration.getInteger(ELASTICSEARCH_RETRY_CONNECTION_MAX_RETRIES, null)))
+            .build();
+    }
+
+    private static ImmutableList<Host> getHosts(Configuration propertiesReader) throws ConfigurationException {
+        AbstractConfiguration.setDefaultListDelimiter(',');
+        Optional<String> masterHost = Optional.ofNullable(
+            propertiesReader.getString(ELASTICSEARCH_MASTER_HOST, null));
+        Optional<Integer> masterPort = Optional.ofNullable(
+            propertiesReader.getInteger(ELASTICSEARCH_PORT, null));
+        List<String> multiHosts = Arrays.asList(propertiesReader.getStringArray(ELASTICSEARCH_HOSTS));
+
+        validateHostsConfigurationOptions(masterHost, masterPort, multiHosts);
+
+        if (masterHost.isPresent()) {
+            return ImmutableList.of(
+                Host.from(masterHost.get(),
+                masterPort.get()));
+        } else {
+            return multiHosts.stream()
+                .map(ipAndPort -> Host.parse(ipAndPort, DEFAULT_PORT_AS_OPTIONAL))
+                .collect(Guavate.toImmutableList());
+        }
+    }
+
+    @VisibleForTesting
+    static void validateHostsConfigurationOptions(Optional<String> masterHost,
+                                                  Optional<Integer> masterPort,
+                                                  List<String> multiHosts) throws ConfigurationException {
+        if (masterHost.isPresent() != masterPort.isPresent()) {
+            throw new ConfigurationException(ELASTICSEARCH_MASTER_HOST + " and " + ELASTICSEARCH_PORT + " should be specified together");
+        }
+        if (!multiHosts.isEmpty() && masterHost.isPresent()) {
+            throw new ConfigurationException("You should choose between mono host set up and " + ELASTICSEARCH_HOSTS);
+        }
+        if (multiHosts.isEmpty() && !masterHost.isPresent()) {
+            throw new ConfigurationException("You should specify either (" + ELASTICSEARCH_MASTER_HOST + " and " + ELASTICSEARCH_PORT + ") or " + ELASTICSEARCH_HOSTS);
+        }
+    }
+
+    private final ImmutableList<Host> hosts;
+    private final Optional<String> clusterName;
+    private final int nbShards;
+    private final int nbReplica;
+    private final int minDelay;
+    private final int maxRetries;
+
+    private ElasticSearchConfiguration(ImmutableList<Host> hosts, Optional<String> clusterName, int nbShards, int nbReplica, int minDelay, int maxRetries) {
+        this.hosts = hosts;
+        this.clusterName = clusterName;
+        this.nbShards = nbShards;
+        this.nbReplica = nbReplica;
+        this.minDelay = minDelay;
+        this.maxRetries = maxRetries;
+    }
+
+    public ImmutableList<Host> getHosts() {
+        return hosts;
+    }
+
+    public Optional<String> getClusterName() {
+        return clusterName;
+    }
+
+    public int getNbShards() {
+        return nbShards;
+    }
+
+    public int getNbReplica() {
+        return nbReplica;
+    }
+
+    public int getMinDelay() {
+        return minDelay;
+    }
+
+    public int getMaxRetries() {
+        return maxRetries;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof ElasticSearchConfiguration) {
+            ElasticSearchConfiguration that = (ElasticSearchConfiguration) o;
+
+            return Objects.equals(this.nbShards, that.nbShards)
+                && Objects.equals(this.clusterName, that.clusterName)
+                && Objects.equals(this.nbReplica, that.nbReplica)
+                && Objects.equals(this.minDelay, that.minDelay)
+                && Objects.equals(this.maxRetries, that.maxRetries)
+                && Objects.equals(this.hosts, that.hosts);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(hosts, clusterName, nbShards, nbReplica, minDelay, maxRetries);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
new file mode 100644
index 0000000..8572e4d
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
@@ -0,0 +1,116 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+package org.apache.james.backends.es.v6;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.commons.lang3.StringUtils;
+import org.elasticsearch.action.bulk.BulkRequestBuilder;
+import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.index.IndexResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.ValidationException;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+public class ElasticSearchIndexer {
+    private static final int DEBUG_MAX_LENGTH_CONTENT = 1000;
+    private static final int DEFAULT_BATCH_SIZE = 100;
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchIndexer.class);
+
+    private final Client client;
+    private final DeleteByQueryPerformer deleteByQueryPerformer;
+    private final AliasName aliasName;
+    private final TypeName typeName;
+
+    public ElasticSearchIndexer(Client client, ExecutorService executor,
+                                WriteAliasName aliasName,
+                                TypeName typeName) {
+        this(client, executor, aliasName, typeName, DEFAULT_BATCH_SIZE);
+    }
+
+    @VisibleForTesting
+    public ElasticSearchIndexer(Client client, ExecutorService executor,
+                                WriteAliasName aliasName,
+                                TypeName typeName,
+                                int batchSize) {
+        this.client = client;
+        this.deleteByQueryPerformer = new DeleteByQueryPerformer(client, executor, batchSize, aliasName, typeName);
+        this.aliasName = aliasName;
+        this.typeName = typeName;
+    }
+
+    public IndexResponse index(String id, String content) {
+        checkArgument(content);
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Indexing {}: {}", id, StringUtils.left(content, DEBUG_MAX_LENGTH_CONTENT));
+        }
+        return client.prepareIndex(aliasName.getValue(), typeName.getValue(), id)
+            .setSource(content)
+            .get();
+    }
+
+    public Optional<BulkResponse> update(List<UpdatedRepresentation> updatedDocumentParts) {
+        try {
+            Preconditions.checkNotNull(updatedDocumentParts);
+            BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
+            updatedDocumentParts.forEach(updatedDocumentPart -> bulkRequestBuilder.add(
+                client.prepareUpdate(
+                    aliasName.getValue(),
+                    typeName.getValue(),
+                    updatedDocumentPart.getId())
+                    .setDoc(updatedDocumentPart.getUpdatedDocumentPart())));
+            return Optional.of(bulkRequestBuilder.get());
+        } catch (ValidationException e) {
+            LOGGER.warn("Error while updating index", e);
+            return Optional.empty();
+        }
+    }
+
+    public Optional<BulkResponse> delete(List<String> ids) {
+        try {
+            BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
+            ids.forEach(id -> bulkRequestBuilder.add(
+                client.prepareDelete(
+                    aliasName.getValue(),
+                    typeName.getValue(),
+                    id)));
+            return Optional.of(bulkRequestBuilder.get());
+        } catch (ValidationException e) {
+            LOGGER.warn("Error while deleting index", e);
+            return Optional.empty();
+        }
+    }
+
+    public Future<Void> deleteAllMatchingQuery(QueryBuilder queryBuilder) {
+        return deleteByQueryPerformer.perform(queryBuilder);
+    }
+
+    private void checkArgument(String content) {
+        Preconditions.checkArgument(content != null, "content should be provided");
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
new file mode 100644
index 0000000..bdce72c
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
@@ -0,0 +1,158 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import javax.inject.Inject;
+
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.indices.IndexAlreadyExistsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+
+public class IndexCreationFactory {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(IndexCreationFactory.class);
+    public static final String CASE_INSENSITIVE = "case_insensitive";
+    public static final String KEEP_MAIL_AND_URL = "keep_mail_and_url";
+    public static final String SNOWBALL_KEEP_MAIL_AND_URL = "snowball_keep_mail_and_token";
+    public static final String ENGLISH_SNOWBALL = "english_snowball";
+
+    private IndexName indexName;
+    private ArrayList<AliasName> aliases;
+    private int nbShards;
+    private int nbReplica;
+
+    @Inject
+    public IndexCreationFactory(ElasticSearchConfiguration configuration) {
+        indexName = null;
+        aliases = new ArrayList<>();
+        nbShards = configuration.getNbShards();
+        nbReplica = configuration.getNbReplica();
+    }
+
+    public IndexCreationFactory useIndex(IndexName indexName) {
+        Preconditions.checkNotNull(indexName);
+        this.indexName = indexName;
+        return this;
+    }
+
+    public IndexCreationFactory addAlias(AliasName aliasName) {
+        Preconditions.checkNotNull(aliasName);
+        this.aliases.add(aliasName);
+        return this;
+    }
+
+    public Client createIndexAndAliases(Client client) {
+        Preconditions.checkNotNull(indexName);
+        try {
+            createIndexIfNeeded(client, indexName, generateSetting(nbShards, nbReplica));
+            aliases.forEach(alias -> createAliasIfNeeded(client, indexName, alias));
+        } catch (IOException e) {
+            LOGGER.error("Error while creating index : ", e);
+        }
+        return client;
+    }
+
+    private void createAliasIfNeeded(Client client, IndexName indexName, AliasName aliasName) {
+        if (!aliasExist(client, aliasName)) {
+            client.admin()
+                .indices()
+                .aliases(new IndicesAliasesRequest()
+                    .addAlias(aliasName.getValue(), indexName.getValue()))
+                .actionGet();
+        }
+    }
+
+    private boolean aliasExist(Client client, AliasName aliasName) {
+        return client.admin()
+            .indices()
+            .aliasesExist(new GetAliasesRequest()
+                .aliases(aliasName.getValue()))
+            .actionGet()
+            .exists();
+    }
+
+    private void createIndexIfNeeded(Client client, IndexName indexName, XContentBuilder settings) {
+        try {
+            client.admin()
+                .indices()
+                .prepareCreate(indexName.getValue())
+                .setSettings(settings)
+                .execute()
+                .actionGet();
+        } catch (IndexAlreadyExistsException exception) {
+            LOGGER.info("Index [{}] already exist", indexName);
+        }
+    }
+
+    private XContentBuilder generateSetting(int nbShards, int nbReplica) throws IOException {
+        return jsonBuilder()
+            .startObject()
+                .field("number_of_shards", nbShards)
+                .field("number_of_replicas", nbReplica)
+                .startObject("analysis")
+                    .startObject("analyzer")
+                        .startObject(CASE_INSENSITIVE)
+                            .field("tokenizer", "keyword")
+                            .startArray("filter")
+                                .value("lowercase")
+                            .endArray()
+                        .endObject()
+                    .endObject()
+                    .startObject("analyzer")
+                        .startObject(KEEP_MAIL_AND_URL)
+                            .field("tokenizer", "uax_url_email")
+                            .startArray("filter")
+                                .value("lowercase")
+                                .value("stop")
+                            .endArray()
+                        .endObject()
+                    .endObject()
+                    .startObject("filter")
+                        .startObject(ENGLISH_SNOWBALL)
+                            .field("type", "snowball")
+                            .field("language", "English")
+                        .endObject()
+                    .endObject()
+                    .startObject("analyzer")
+                        .startObject(SNOWBALL_KEEP_MAIL_AND_URL)
+                        .field("tokenizer", "uax_url_email")
+                            .startArray("filter")
+                                .value("lowercase")
+                                .value("stop")
+                                .value(ENGLISH_SNOWBALL)
+                            .endArray()
+                        .endObject()
+                    .endObject()
+                .endObject()
+            .endObject();
+    }
+
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexName.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexName.java
new file mode 100644
index 0000000..39c3f28
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexName.java
@@ -0,0 +1,49 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.util.Objects;
+
+public class IndexName {
+    private final String value;
+
+    public IndexName(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof IndexName) {
+            IndexName indexName = (IndexName) o;
+
+            return Objects.equals(this.value, indexName.value);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(value);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java
new file mode 100644
index 0000000..01b0bf0
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java
@@ -0,0 +1,75 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import org.apache.james.util.streams.Iterators;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+public class NodeMappingFactory {
+
+    public static final String BOOLEAN = "boolean";
+    public static final String TYPE = "type";
+    public static final String LONG = "long";
+    public static final String DOUBLE = "double";
+    public static final String INDEX = "index";
+    public static final String NOT_ANALYZED = "not_analyzed";
+    public static final String STRING = "string";
+    public static final String PROPERTIES = "properties";
+    public static final String DATE = "date";
+    public static final String FORMAT = "format";
+    public static final String NESTED = "nested";
+    public static final String FIELDS = "fields";
+    public static final String RAW = "raw";
+    public static final String SPLIT_EMAIL = "splitEmail";
+    public static final String ANALYZER = "analyzer";
+    public static final String SEARCH_ANALYZER = "search_analyzer";
+    public static final String SNOWBALL = "snowball";
+    public static final String IGNORE_ABOVE = "ignore_above";
+
+    public static Client applyMapping(Client client, IndexName indexName, TypeName typeName, XContentBuilder mappingsSources) {
+        if (!mappingAlreadyExist(client, indexName, typeName)) {
+            createMapping(client, indexName, typeName, mappingsSources);
+        }
+        return client;
+    }
+
+    public static boolean mappingAlreadyExist(Client client, IndexName indexName, TypeName typeName) {
+        return Iterators.toStream(client.admin()
+            .indices()
+            .prepareGetMappings(indexName.getValue())
+            .execute()
+            .actionGet()
+            .getMappings()
+            .valuesIt())
+            .anyMatch(mapping -> mapping.keys().contains(typeName.getValue()));
+    }
+
+    public static void createMapping(Client client, IndexName indexName, TypeName typeName, XContentBuilder mappingsSources) {
+        client.admin()
+            .indices()
+            .preparePutMapping(indexName.getValue())
+            .setType(typeName.getValue())
+            .setSource(mappingsSources)
+            .execute()
+            .actionGet();
+    }
+
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ReadAliasName.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ReadAliasName.java
new file mode 100644
index 0000000..8763664
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ReadAliasName.java
@@ -0,0 +1,26 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+public class ReadAliasName extends AliasName {
+    public ReadAliasName(String value) {
+        super(value);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/TypeName.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/TypeName.java
new file mode 100644
index 0000000..7f3dbf7
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/TypeName.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+public class TypeName {
+    private final String value;
+
+    public TypeName(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/UpdatedRepresentation.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/UpdatedRepresentation.java
new file mode 100644
index 0000000..1d919c9
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/UpdatedRepresentation.java
@@ -0,0 +1,69 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.util.Objects;
+
+import org.elasticsearch.common.Strings;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+public class UpdatedRepresentation {
+    private final String id;
+    private final String updatedDocumentPart;
+
+    public UpdatedRepresentation(String id, String updatedDocumentPart) {
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(id), "Updated id must be specified " + id);
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(updatedDocumentPart), "Updated document must be specified");
+        this.id = id;
+        this.updatedDocumentPart = updatedDocumentPart;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getUpdatedDocumentPart() {
+        return updatedDocumentPart;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof UpdatedRepresentation) {
+            UpdatedRepresentation other = (UpdatedRepresentation) o;
+            return Objects.equals(id, other.id)
+                && Objects.equals(updatedDocumentPart, other.updatedDocumentPart);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(id, updatedDocumentPart);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("id", id)
+            .add("updatedDocumentPart", updatedDocumentPart)
+            .toString();
+    }
+}
\ No newline at end of file
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/WriteAliasName.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/WriteAliasName.java
new file mode 100644
index 0000000..0947ad7
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/WriteAliasName.java
@@ -0,0 +1,26 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+public class WriteAliasName extends AliasName {
+    public WriteAliasName(String value) {
+        super(value);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/search/ScrollIterable.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/search/ScrollIterable.java
new file mode 100644
index 0000000..eca5bae
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/search/ScrollIterable.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.backends.es.v6.search;
+
+import java.util.Iterator;
+import java.util.stream.Stream;
+
+import org.apache.james.util.streams.Iterators;
+import org.elasticsearch.action.ListenableActionFuture;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.unit.TimeValue;
+
+public class ScrollIterable implements Iterable<SearchResponse> {
+
+    private static final TimeValue TIMEOUT = new TimeValue(60000);
+    private final Client client;
+    private final SearchRequestBuilder searchRequestBuilder;
+
+    public ScrollIterable(Client client, SearchRequestBuilder searchRequestBuilder) {
+        this.client = client;
+        this.searchRequestBuilder = searchRequestBuilder;
+    }
+
+    @Override
+    public Iterator<SearchResponse> iterator() {
+        return new ScrollIterator(client, searchRequestBuilder);
+    }
+
+    public Stream<SearchResponse> stream() {
+        return Iterators.toStream(iterator());
+    }
+
+    public static class ScrollIterator implements Iterator<SearchResponse> {
+
+        private final Client client;
+        private ListenableActionFuture<SearchResponse> searchResponseFuture;
+
+        public ScrollIterator(Client client, SearchRequestBuilder searchRequestBuilder) {
+            this.client = client;
+            this.searchResponseFuture = searchRequestBuilder.execute();
+        }
+
+        @Override
+        public boolean hasNext() {
+            return !allSearchResponsesConsumed(searchResponseFuture.actionGet());
+        }
+
+        @Override
+        public SearchResponse next() {
+            SearchResponse result = searchResponseFuture.actionGet();
+            searchResponseFuture =  client.prepareSearchScroll(result.getScrollId())
+                .setScroll(TIMEOUT)
+                .execute();
+            return result;
+        }
+
+        private boolean allSearchResponsesConsumed(SearchResponse searchResponse) {
+            return searchResponse.getHits().getHits().length == 0;
+        }
+    }
+
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java
new file mode 100644
index 0000000..a30d96e
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java
@@ -0,0 +1,96 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.james.util.docker.DockerGenericContainer;
+import org.awaitility.Awaitility;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Ignore("JAMES-1952")
+public class ClientProviderImplConnectionTest {
+    private static final Logger LOGGER = LoggerFactory.getLogger(ClientProviderImplConnectionTest.class);
+    private static final String DOCKER_ES_IMAGE = "elasticsearch:2.2.1";
+    private static final int ES_APPLICATIVE_PORT = 9300;
+
+    @Rule
+    public DockerGenericContainer es1 = new DockerGenericContainer(DOCKER_ES_IMAGE)
+        .withAffinityToContainer()
+        .withExposedPorts(ES_APPLICATIVE_PORT);
+
+    @Rule
+    public DockerGenericContainer es2 = new DockerGenericContainer(DOCKER_ES_IMAGE)
+        .withAffinityToContainer()
+        .withExposedPorts(ES_APPLICATIVE_PORT);
+
+    @Test
+    public void connectingASingleServerShouldWork() throws Exception {
+        Awaitility.await()
+            .atMost(1, TimeUnit.MINUTES)
+            .pollInterval(5, TimeUnit.SECONDS)
+            .until(() -> isConnected(ClientProviderImpl.forHost(es1.getContainerIp(), 9300, Optional.empty())));
+    }
+
+    @Test
+    public void connectingAClusterShouldWork() throws Exception {
+        Awaitility.await()
+            .atMost(1, TimeUnit.MINUTES)
+            .pollInterval(5, TimeUnit.SECONDS)
+            .until(() -> isConnected(
+                ClientProviderImpl.fromHostsString(
+                    es1.getContainerIp() + ":" + ES_APPLICATIVE_PORT + ","
+                        + es2.getContainerIp() + ":" + ES_APPLICATIVE_PORT,
+                    Optional.empty())));
+    }
+
+    @Test
+    public void connectingAClusterWithAFailedNodeShouldWork() throws Exception {
+        es2.stop();
+
+        Awaitility.await()
+            .atMost(1, TimeUnit.MINUTES)
+            .pollInterval(5, TimeUnit.SECONDS)
+            .until(() -> isConnected(
+                ClientProviderImpl.fromHostsString(
+                    es1.getContainerIp() + ":" + ES_APPLICATIVE_PORT + ","
+                        + es2.getContainerIp() + ":" + ES_APPLICATIVE_PORT,
+                    Optional.empty())));
+    }
+
+    private boolean isConnected(ClientProvider clientProvider) {
+        try (Client client = clientProvider.get()) {
+            client.prepareSearch()
+                .setQuery(QueryBuilders.existsQuery("any"))
+                .get();
+            return true;
+        } catch (Exception e) {
+            LOGGER.info("Caught exception while trying to connect", e);
+            return false;
+        }
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplTest.java
new file mode 100644
index 0000000..8079425
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplTest.java
@@ -0,0 +1,142 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Optional;
+
+import org.elasticsearch.common.settings.Settings;
+import org.junit.Test;
+
+public class ClientProviderImplTest {
+
+    @Test
+    public void fromHostsStringShouldThrowOnNullString() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString(null, Optional.empty()))
+                .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnEmptyString() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void forHostShouldThrowOnNullHost() {
+        assertThatThrownBy(() -> ClientProviderImpl.forHost(null, 9200, Optional.empty()))
+                .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    public void forHostShouldThrowOnEmptyHost() {
+        assertThatThrownBy(() -> ClientProviderImpl.forHost("", 9200, Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void forHostShouldThrowOnNegativePort() {
+        assertThatThrownBy(() -> ClientProviderImpl.forHost("localhost", -1, Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void forHostShouldThrowOnZeroPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.forHost("localhost", 0, Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void forHostShouldThrowOnTooBigPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.forHost("localhost", 65536, Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldEmptyAddress() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString(":9200", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnAbsentPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowWhenTooMuchParts() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:9200:9200", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnEmptyPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:", Optional.empty()))
+                .isInstanceOf(NumberFormatException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnInvalidPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:invalid", Optional.empty()))
+                .isInstanceOf(NumberFormatException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnNegativePort() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:-1", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnZeroPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:0", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowOnTooBigPort() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:65536", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void fromHostsStringShouldThrowIfOneHostIsInvalid() {
+        assertThatThrownBy(() -> ClientProviderImpl.fromHostsString("localhost:9200,localhost", Optional.empty()))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void settingsShouldBeEmptyWhenClusterNameIsEmpty() {
+        ClientProviderImpl clientProvider = ClientProviderImpl.fromHostsString("localhost:9200", Optional.empty());
+
+        assertThat(clientProvider.settings()).isEqualTo(Settings.EMPTY);
+    }
+
+    @Test
+    public void settingsShouldContainClusterNameSettingWhenClusterNameIsGiven() {
+        String clusterName = "myClusterName";
+        ClientProviderImpl clientProvider = ClientProviderImpl.fromHostsString("localhost:9200", Optional.of(clusterName));
+
+        assertThat(clientProvider.settings().get("cluster.name")).isEqualTo(clusterName);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java
new file mode 100644
index 0000000..6e159f7
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java
@@ -0,0 +1,126 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Optional;
+
+import org.apache.http.HttpStatus;
+import org.apache.james.util.Host;
+import org.apache.james.util.docker.DockerGenericContainer;
+import org.apache.james.util.docker.Images;
+import org.apache.james.util.docker.RateLimiters;
+import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
+
+import com.google.common.collect.ImmutableList;
+
+import feign.Feign;
+import feign.Logger;
+import feign.RequestLine;
+import feign.Response;
+import feign.slf4j.Slf4jLogger;
+
+public class DockerElasticSearch {
+
+    interface ElasticSearchAPI {
+
+        static ElasticSearchAPI from(Host esHttpHost) {
+            return Feign.builder()
+                .logger(new Slf4jLogger(ElasticSearchAPI.class))
+                .logLevel(Logger.Level.FULL)
+                .target(ElasticSearchAPI.class, "http://" + esHttpHost.getHostName() + ":" + esHttpHost.getPort());
+        }
+
+        @RequestLine("DELETE /_all")
+        Response deleteAllIndexes();
+
+        @RequestLine("POST /_flush?force&wait_if_ongoing=true")
+        Response flush();
+    }
+
+    private static final int ES_HTTP_PORT = 9200;
+    private static final int ES_TCP_PORT = 9300;
+
+    private final DockerGenericContainer eSContainer;
+
+    public DockerElasticSearch() {
+        this.eSContainer = new DockerGenericContainer(Images.ELASTICSEARCH_2)
+            .withExposedPorts(ES_HTTP_PORT, ES_TCP_PORT)
+            .waitingFor(new HostPortWaitStrategy().withRateLimiter(RateLimiters.TWENTIES_PER_SECOND));
+    }
+
+    public void start() {
+        if (!eSContainer.isRunning()) {
+            eSContainer.start();
+        }
+    }
+
+    public void stop() {
+        eSContainer.stop();
+    }
+
+    public int getHttpPort() {
+        return eSContainer.getMappedPort(ES_HTTP_PORT);
+    }
+
+    public int getTcpPort() {
+        return eSContainer.getMappedPort(ES_TCP_PORT);
+    }
+
+    public String getIp() {
+        return eSContainer.getHostIp();
+    }
+
+    public Host getTcpHost() {
+        return Host.from(getIp(), getTcpPort());
+    }
+
+    public Host getHttpHost() {
+        return Host.from(getIp(), getHttpPort());
+    }
+
+    public void pause() {
+        eSContainer.pause();
+    }
+
+    public void unpause() {
+        eSContainer.unpause();
+    }
+
+    public void cleanUpData() {
+        assertThat(esAPI().deleteAllIndexes().status())
+            .isEqualTo(HttpStatus.SC_OK);
+    }
+
+    public void awaitForElasticSearch() {
+        assertThat(esAPI().flush().status())
+            .isEqualTo(HttpStatus.SC_OK);
+    }
+
+    public ClientProvider clientProvider() {
+        Optional<String> noClusterName = Optional.empty();
+        return ClientProviderImpl.fromHosts(ImmutableList.of(getTcpHost()), noClusterName);
+    }
+
+    private ElasticSearchAPI esAPI() {
+        return ElasticSearchAPI.from(getHttpHost());
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java
new file mode 100644
index 0000000..c5b19e5
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java
@@ -0,0 +1,50 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import org.apache.james.util.Host;
+import org.junit.rules.ExternalResource;
+
+public class DockerElasticSearchRule extends ExternalResource {
+
+    private final DockerElasticSearch dockerElasticSearch = DockerElasticSearchSingleton.INSTANCE;
+
+    @Override
+    protected void before() throws Throwable {
+        dockerElasticSearch.start();
+    }
+
+    @Override
+    protected void after() {
+        dockerElasticSearch.cleanUpData();
+    }
+
+    public ClientProvider clientProvider() {
+        return dockerElasticSearch.clientProvider();
+    }
+    
+    public void awaitForElasticSearch() {
+        dockerElasticSearch.awaitForElasticSearch();
+    }
+
+    public Host getTcpHost() {
+        return dockerElasticSearch.getTcpHost();
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchSingleton.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchSingleton.java
new file mode 100644
index 0000000..4fa6677
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchSingleton.java
@@ -0,0 +1,28 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+public class DockerElasticSearchSingleton {
+    public static DockerElasticSearch INSTANCE = new DockerElasticSearch();
+
+    static {
+        INSTANCE.start();
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java
new file mode 100644
index 0000000..43d80ef
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java
@@ -0,0 +1,319 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Optional;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.james.util.Host;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class ElasticSearchConfigurationTest {
+
+    @Test
+    public void getNbReplicaShouldReturnConfiguredValue() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        int value = 36;
+        configuration.addProperty("elasticsearch.nb.replica", value);
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getNbReplica())
+            .isEqualTo(value);
+    }
+
+    @Test
+    public void getNbReplicaShouldReturnDefaultValueWhenMissing() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getNbReplica())
+            .isEqualTo(ElasticSearchConfiguration.DEFAULT_NB_REPLICA);
+    }
+
+    @Test
+    public void getNbShardsShouldReturnConfiguredValue() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        int value = 36;
+        configuration.addProperty("elasticsearch.nb.shards", value);
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getNbShards())
+            .isEqualTo(value);
+    }
+
+    @Test
+    public void getNbShardsShouldReturnDefaultValueWhenMissing() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getNbShards())
+            .isEqualTo(ElasticSearchConfiguration.DEFAULT_NB_SHARDS);
+    }
+
+    @Test
+    public void getMaxRetriesShouldReturnConfiguredValue() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        int value = 36;
+        configuration.addProperty("elasticsearch.retryConnection.maxRetries", value);
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getMaxRetries())
+            .isEqualTo(value);
+    }
+
+    @Test
+    public void getMaxRetriesShouldReturnDefaultValueWhenMissing() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getMaxRetries())
+            .isEqualTo(ElasticSearchConfiguration.DEFAULT_CONNECTION_MAX_RETRIES);
+    }
+
+    @Test
+    public void getMinDelayShouldReturnConfiguredValue() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        int value = 36;
+        configuration.addProperty("elasticsearch.retryConnection.minDelay", value);
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getMinDelay())
+            .isEqualTo(value);
+    }
+
+    @Test
+    public void getMinDelayShouldReturnDefaultValueWhenMissing() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getMinDelay())
+            .isEqualTo(ElasticSearchConfiguration.DEFAULT_CONNECTION_MIN_DELAY);
+    }
+
+    @Test
+    public void getHostsShouldReturnConfiguredHostsWhenNoPort() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        String hostname = "myHost";
+        configuration.addProperty("elasticsearch.hosts", hostname);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getHosts())
+            .containsOnly(Host.from(hostname, ElasticSearchConfiguration.DEFAULT_PORT));
+    }
+
+    @Test
+    public void getHostsShouldReturnConfiguredHostsWhenListIsUsed() throws ConfigurationException {
+        String hostname = "myHost";
+        String hostname2 = "myOtherHost";
+        int port = 2154;
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("elasticsearch.hosts", hostname + "," + hostname2 + ":" + port);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getHosts())
+            .containsOnly(Host.from(hostname, ElasticSearchConfiguration.DEFAULT_PORT),
+                Host.from(hostname2, port));
+    }
+
+    @Test
+    public void getHostsShouldReturnConfiguredHosts() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        String hostname = "myHost";
+        int port = 2154;
+        configuration.addProperty("elasticsearch.hosts", hostname + ":" + port);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getHosts())
+            .containsOnly(Host.from(hostname, port));
+    }
+
+    @Test
+    public void getHostsShouldReturnConfiguredMasterHost() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        String hostname = "myHost";
+        configuration.addProperty("elasticsearch.masterHost", hostname);
+        int port = 9300;
+        configuration.addProperty("elasticsearch.port", port);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getHosts())
+            .containsOnly(Host.from(hostname, port));
+    }
+
+    @Test
+    public void clusterNameShouldBeEmptyWhenNotGiven() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        String hostname = "myHost";
+        configuration.addProperty("elasticsearch.masterHost", hostname);
+        int port = 9300;
+        configuration.addProperty("elasticsearch.port", port);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getClusterName())
+                .isEmpty();
+    }
+
+    @Test
+    public void clusterNameShouldBeEmptyWhenNull() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        String hostname = "myHost";
+        configuration.addProperty("elasticsearch.masterHost", hostname);
+        int port = 9300;
+        configuration.addProperty("elasticsearch.port", port);
+        configuration.addProperty("elasticsearch.clusterName", null);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getClusterName())
+                .isEmpty();
+    }
+
+    @Test
+    public void clusterNameShouldKeepTheValueWhenGiven() throws ConfigurationException {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        String hostname = "myHost";
+        configuration.addProperty("elasticsearch.masterHost", hostname);
+        int port = 9300;
+        configuration.addProperty("elasticsearch.port", port);
+        String clusterName = "myClusterName";
+        configuration.addProperty("elasticsearch.clusterName", clusterName);
+
+        ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration);
+
+        assertThat(elasticSearchConfiguration.getClusterName())
+                .contains(clusterName);
+    }
+
+    @Test
+    public void validateHostsConfigurationOptionsShouldThrowWhenNoHostSpecify() {
+        assertThatThrownBy(() ->
+            ElasticSearchConfiguration.validateHostsConfigurationOptions(
+                Optional.empty(),
+                Optional.empty(),
+                ImmutableList.of()))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("You should specify either (" + ElasticSearchConfiguration.ELASTICSEARCH_MASTER_HOST +
+                " and " + ElasticSearchConfiguration.ELASTICSEARCH_PORT +
+                ") or " + ElasticSearchConfiguration.ELASTICSEARCH_HOSTS);
+    }
+
+    @Test
+    public void validateHostsConfigurationOptionsShouldThrowWhenMonoAndMultiHostSpecified() {
+        assertThatThrownBy(() ->
+            ElasticSearchConfiguration.validateHostsConfigurationOptions(
+                Optional.of("localhost"),
+                Optional.of(9200),
+                ImmutableList.of("localhost:9200")))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage("You should choose between mono host set up and " + ElasticSearchConfiguration.ELASTICSEARCH_HOSTS);
+    }
+
+    @Test
+    public void validateHostsConfigurationOptionsShouldThrowWhenMonoHostWithoutPort() {
+        assertThatThrownBy(() ->
+            ElasticSearchConfiguration.validateHostsConfigurationOptions(
+                Optional.of("localhost"),
+                Optional.empty(),
+                ImmutableList.of()))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessage(ElasticSearchConfiguration.ELASTICSEARCH_MASTER_HOST +
+                " and " + ElasticSearchConfiguration.ELASTICSEARCH_PORT + " should be specified together");
+    }
+
+    @Test
+    public void validateHostsConfigurationOptionsShouldThrowWhenMonoHostWithoutAddress() {
+        assertThatThrownBy(() ->
+        ElasticSearchConfiguration.validateHostsConfigurationOptions(
+            Optional.empty(),
+            Optional.of(9200),
+            ImmutableList.of()))
+        .isInstanceOf(ConfigurationException.class)
+        .hasMessage(ElasticSearchConfiguration.ELASTICSEARCH_MASTER_HOST + " and " +
+            ElasticSearchConfiguration.ELASTICSEARCH_PORT + " should be specified together");
+    }
+
+    @Test
+    public void validateHostsConfigurationOptionsShouldAcceptMonoHostConfiguration() throws Exception {
+        ElasticSearchConfiguration.validateHostsConfigurationOptions(
+            Optional.of("localhost"),
+            Optional.of(9200),
+            ImmutableList.of());
+    }
+
+    @Test
+    public void validateHostsConfigurationOptionsShouldAcceptMultiHostConfiguration() throws Exception {
+        ElasticSearchConfiguration.validateHostsConfigurationOptions(
+            Optional.empty(),
+            Optional.empty(),
+            ImmutableList.of("localhost:9200"));
+    }
+
+
+    @Test
+    public void nbReplicaShouldThrowWhenNegative() {
+        assertThatThrownBy(() ->
+                ElasticSearchConfiguration.builder()
+                        .nbReplica(-1))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void nbShardsShouldThrowWhenNegative() {
+        assertThatThrownBy(() ->
+                ElasticSearchConfiguration.builder()
+                        .nbShards(-1))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void nbShardsShouldThrowWhenZero() {
+        assertThatThrownBy(() ->
+                ElasticSearchConfiguration.builder()
+                        .nbShards(0))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
new file mode 100644
index 0000000..4346958
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
@@ -0,0 +1,248 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+
+import java.util.concurrent.Executors;
+
+import org.apache.james.util.concurrent.NamedThreadFactory;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class ElasticSearchIndexerTest {
+
+    private static final int MINIMUM_BATCH_SIZE = 1;
+    private static final IndexName INDEX_NAME = new IndexName("index_name");
+    private static final WriteAliasName ALIAS_NAME = new WriteAliasName("alias_name");
+    private static final TypeName TYPE_NAME = new TypeName("type_name");
+
+    @Rule
+    public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
+    private ElasticSearchIndexer testee;
+
+    @Before
+    public void setup() {
+        new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+            .useIndex(INDEX_NAME)
+            .addAlias(ALIAS_NAME)
+            .createIndexAndAliases(getESClient());
+        testee = new ElasticSearchIndexer(getESClient(),
+            Executors.newSingleThreadExecutor(NamedThreadFactory.withClassName(getClass())),
+            ALIAS_NAME, TYPE_NAME, MINIMUM_BATCH_SIZE);
+    }
+
+    private Client getESClient() {
+        return elasticSearch.clientProvider().get();
+    }
+
+    @Test
+    public void indexMessageShouldWork() {
+        String messageId = "1";
+        String content = "{\"message\": \"trying out Elasticsearch\"}";
+        
+        testee.index(messageId, content);
+        elasticSearch.awaitForElasticSearch();
+        
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                    .setTypes(TYPE_NAME.getValue())
+                    .setQuery(QueryBuilders.matchQuery("message", "trying"))
+                    .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
+        }
+    }
+    
+    @Test
+    public void indexMessageShouldThrowWhenJsonIsNull() {
+        assertThatThrownBy(() -> testee.index("1", null))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+    
+    @Test
+    public void updateMessages() {
+        String messageId = "1";
+        String content = "{\"message\": \"trying out Elasticsearch\",\"field\":\"Should be unchanged\"}";
+
+        testee.index(messageId, content);
+        elasticSearch.awaitForElasticSearch();
+
+        testee.update(ImmutableList.of(new UpdatedRepresentation(messageId, "{\"message\": \"mastering out Elasticsearch\"}")));
+        elasticSearch.awaitForElasticSearch();
+
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setQuery(QueryBuilders.matchQuery("message", "mastering"))
+                .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
+        }
+
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setQuery(QueryBuilders.matchQuery("field", "unchanged"))
+                .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
+        }
+    }
+
+    @Test
+    public void updateMessageShouldThrowWhenJsonIsNull() {
+        assertThatThrownBy(() -> testee.update(ImmutableList.of(new UpdatedRepresentation("1", null))))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void updateMessageShouldThrowWhenIdIsNull() {
+        assertThatThrownBy(() -> testee.update(ImmutableList.of(new UpdatedRepresentation(null, "{\"message\": \"mastering out Elasticsearch\"}"))))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void updateMessageShouldThrowWhenJsonIsEmpty() {
+        assertThatThrownBy(() -> testee.update(ImmutableList.of(new UpdatedRepresentation("1", ""))))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void updateMessageShouldThrowWhenIdIsEmpty() {
+        assertThatThrownBy(() -> testee.update(ImmutableList.of(new UpdatedRepresentation("", "{\"message\": \"mastering out Elasticsearch\"}"))))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void deleteByQueryShouldWorkOnSingleMessage() throws Exception {
+        String messageId = "1:2";
+        String content = "{\"message\": \"trying out Elasticsearch\", \"property\":\"1\"}";
+
+        testee.index(messageId, content);
+        elasticSearch.awaitForElasticSearch();
+        
+        testee.deleteAllMatchingQuery(termQuery("property", "1")).get();
+        elasticSearch.awaitForElasticSearch();
+        
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                    .setTypes(TYPE_NAME.getValue())
+                    .setQuery(QueryBuilders.matchAllQuery())
+                    .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void deleteByQueryShouldWorkWhenMultipleMessages() throws Exception {
+        String messageId = "1:1";
+        String content = "{\"message\": \"trying out Elasticsearch\", \"property\":\"1\"}";
+        
+        testee.index(messageId, content);
+        
+        String messageId2 = "1:2";
+        String content2 = "{\"message\": \"trying out Elasticsearch 2\", \"property\":\"1\"}";
+        
+        testee.index(messageId2, content2);
+        
+        String messageId3 = "2:3";
+        String content3 = "{\"message\": \"trying out Elasticsearch 3\", \"property\":\"2\"}";
+        
+        testee.index(messageId3, content3);
+        elasticSearch.awaitForElasticSearch();
+
+        testee.deleteAllMatchingQuery(termQuery("property", "1")).get();
+        elasticSearch.awaitForElasticSearch();
+        
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                    .setTypes(TYPE_NAME.getValue())
+                    .setQuery(QueryBuilders.matchAllQuery())
+                    .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
+        }
+    }
+    
+    @Test
+    public void deleteMessage() {
+        String messageId = "1:2";
+        String content = "{\"message\": \"trying out Elasticsearch\"}";
+
+        testee.index(messageId, content);
+        elasticSearch.awaitForElasticSearch();
+
+        testee.delete(ImmutableList.of(messageId));
+        elasticSearch.awaitForElasticSearch();
+        
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                    .setTypes(TYPE_NAME.getValue())
+                    .setQuery(QueryBuilders.matchAllQuery())
+                    .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void deleteShouldWorkWhenMultipleMessages() {
+        String messageId = "1:1";
+        String content = "{\"message\": \"trying out Elasticsearch\", \"mailboxId\":\"1\"}";
+
+        testee.index(messageId, content);
+
+        String messageId2 = "1:2";
+        String content2 = "{\"message\": \"trying out Elasticsearch 2\", \"mailboxId\":\"1\"}";
+
+        testee.index(messageId2, content2);
+
+        String messageId3 = "2:3";
+        String content3 = "{\"message\": \"trying out Elasticsearch 3\", \"mailboxId\":\"2\"}";
+
+        testee.index(messageId3, content3);
+        elasticSearch.awaitForElasticSearch();
+
+        testee.delete(ImmutableList.of(messageId, messageId3));
+        elasticSearch.awaitForElasticSearch();
+
+        try (Client client = getESClient()) {
+            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setQuery(QueryBuilders.matchAllQuery())
+                .get();
+            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
+        }
+    }
+    
+    @Test
+    public void updateMessagesShouldNotThrowWhenEmptyList() {
+        testee.update(ImmutableList.of());
+    }
+    
+    @Test
+    public void deleteMessagesShouldNotThrowWhenEmptyList() {
+        testee.delete(ImmutableList.of());
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
new file mode 100644
index 0000000..a5417eb
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.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.backends.es.v6;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class IndexCreationFactoryTest {
+    public static final IndexName INDEX_NAME = new IndexName("index");
+    public static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
+
+    @Rule
+    public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
+    private ClientProvider clientProvider;
+
+    @Before
+    public void setUp() {
+        clientProvider = elasticSearch.clientProvider();
+        new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+            .useIndex(INDEX_NAME)
+            .addAlias(ALIAS_NAME)
+            .createIndexAndAliases(clientProvider.get());
+    }
+
+    @Test
+    public void createIndexAndAliasShouldNotThrowWhenCalledSeveralTime() {
+        new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+            .useIndex(INDEX_NAME)
+            .addAlias(ALIAS_NAME)
+            .createIndexAndAliases(clientProvider.get());
+    }
+
+    @Test
+    public void useIndexShouldThrowWhenNull() {
+        assertThatThrownBy(() ->
+            new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+                .useIndex(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    public void addAliasShouldThrowWhenNull() {
+        assertThatThrownBy(() ->
+            new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+                .addAlias(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+}
\ No newline at end of file
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java
new file mode 100644
index 0000000..bcbefef
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java
@@ -0,0 +1,101 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class NodeMappingFactoryTest {
+    public static final String MESSAGE = "message";
+    public static final IndexName INDEX_NAME = new IndexName("index");
+    public static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
+    public static final TypeName TYPE_NAME = new TypeName("type");
+
+    @Rule
+    public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
+    private ClientProvider clientProvider;
+
+    @Before
+    public void setUp() throws Exception {
+        clientProvider = elasticSearch.clientProvider();
+        new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+            .useIndex(INDEX_NAME)
+            .addAlias(ALIAS_NAME)
+            .createIndexAndAliases(clientProvider.get());
+        NodeMappingFactory.applyMapping(clientProvider.get(),
+            INDEX_NAME,
+            TYPE_NAME,
+            getMappingsSources());
+    }
+
+    @Test
+    public void applyMappingShouldNotThrowWhenCalledSeveralTime() throws Exception {
+        NodeMappingFactory.applyMapping(clientProvider.get(),
+            INDEX_NAME,
+            TYPE_NAME,
+            getMappingsSources());
+    }
+
+    @Test
+    public void applyMappingShouldNotThrowWhenIndexerChanges() throws Exception {
+        NodeMappingFactory.applyMapping(clientProvider.get(),
+            INDEX_NAME,
+            TYPE_NAME,
+            getMappingsSources());
+
+        elasticSearch.awaitForElasticSearch();
+
+        NodeMappingFactory.applyMapping(clientProvider.get(),
+            INDEX_NAME,
+            TYPE_NAME,
+            getOtherMappingsSources());
+    }
+
+    private XContentBuilder getMappingsSources() throws Exception {
+        return jsonBuilder()
+            .startObject()
+                .startObject(TYPE_NAME.getValue())
+                    .startObject(NodeMappingFactory.PROPERTIES)
+                        .startObject(MESSAGE)
+                            .field(NodeMappingFactory.TYPE, NodeMappingFactory.STRING)
+                        .endObject()
+                    .endObject()
+                .endObject()
+            .endObject();
+    }
+
+    private XContentBuilder getOtherMappingsSources() throws Exception {
+        return jsonBuilder()
+            .startObject()
+                .startObject(TYPE_NAME.getValue())
+                    .startObject(NodeMappingFactory.PROPERTIES)
+                        .startObject(MESSAGE)
+                            .field(NodeMappingFactory.TYPE, NodeMappingFactory.STRING)
+                            .field(NodeMappingFactory.INDEX, NodeMappingFactory.NOT_ANALYZED)
+                        .endObject()
+                    .endObject()
+                .endObject()
+            .endObject();
+    }
+}
\ No newline at end of file
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/search/ScrollIterableTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/search/ScrollIterableTest.java
new file mode 100644
index 0000000..4810975
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/search/ScrollIterableTest.java
@@ -0,0 +1,205 @@
+/****************************************************************
+ * 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.backends.es.v6.search;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.james.backends.es.v6.ClientProvider;
+import org.apache.james.backends.es.v6.DockerElasticSearchRule;
+import org.apache.james.backends.es.v6.ElasticSearchConfiguration;
+import org.apache.james.backends.es.v6.IndexCreationFactory;
+import org.apache.james.backends.es.v6.IndexName;
+import org.apache.james.backends.es.v6.NodeMappingFactory;
+import org.apache.james.backends.es.v6.ReadAliasName;
+import org.apache.james.backends.es.v6.TypeName;
+import org.awaitility.Duration;
+import org.awaitility.core.ConditionFactory;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.SearchHit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class ScrollIterableTest {
+
+    private static final TimeValue TIMEOUT = new TimeValue(6000);
+    private static final int SIZE = 2;
+    private static final String MESSAGE = "message";
+    private static final IndexName INDEX_NAME = new IndexName("index");
+    private static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
+    private static final TypeName TYPE_NAME = new TypeName("messages");
+
+    private static final ConditionFactory WAIT_CONDITION = await().timeout(Duration.FIVE_SECONDS);
+
+    @Rule
+    public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
+    private ClientProvider clientProvider;
+
+    @Before
+    public void setUp() throws Exception {
+        clientProvider = elasticSearch.clientProvider();
+        new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+            .useIndex(INDEX_NAME)
+            .addAlias(ALIAS_NAME)
+            .createIndexAndAliases(clientProvider.get());
+        elasticSearch.awaitForElasticSearch();
+        NodeMappingFactory.applyMapping(clientProvider.get(), INDEX_NAME, TYPE_NAME, getMappingsSources());
+    }
+
+    private XContentBuilder getMappingsSources() throws IOException {
+        return jsonBuilder()
+            .startObject()
+                .startObject(TYPE_NAME.getValue())
+                    .startObject(NodeMappingFactory.PROPERTIES)
+                        .startObject(MESSAGE)
+                            .field(NodeMappingFactory.TYPE, NodeMappingFactory.STRING)
+                        .endObject()
+                    .endObject()
+                .endObject()
+            .endObject();
+    }
+
+    @Test
+    public void scrollIterableShouldWorkWhenEmpty() {
+        try (Client client = clientProvider.get()) {
+            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setScroll(TIMEOUT)
+                .setQuery(matchAllQuery())
+                .setSize(SIZE);
+
+            assertThat(new ScrollIterable(client, searchRequestBuilder))
+                .isEmpty();
+        }
+    }
+
+    @Test
+    public void scrollIterableShouldWorkWhenOneElement() {
+        try (Client client = clientProvider.get()) {
+            String id = "1";
+            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id)
+                .setSource(MESSAGE, "Sample message")
+                .execute();
+
+            elasticSearch.awaitForElasticSearch();
+            WAIT_CONDITION.untilAsserted(() -> hasIdsInIndex(client, id));
+
+            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setScroll(TIMEOUT)
+                .setQuery(matchAllQuery())
+                .setSize(SIZE);
+
+            assertThat(convertToIdList(new ScrollIterable(client, searchRequestBuilder)))
+                .containsOnly(id);
+        }
+    }
+
+    @Test
+    public void scrollIterableShouldWorkWhenSizeElement() {
+        try (Client client = clientProvider.get()) {
+            String id1 = "1";
+            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id1)
+                .setSource(MESSAGE, "Sample message")
+                .execute();
+
+            String id2 = "2";
+            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id2)
+                .setSource(MESSAGE, "Sample message")
+                .execute();
+
+            elasticSearch.awaitForElasticSearch();
+            WAIT_CONDITION.untilAsserted(() -> hasIdsInIndex(client, id1, id2));
+
+            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setScroll(TIMEOUT)
+                .setQuery(matchAllQuery())
+                .setSize(SIZE);
+
+            assertThat(convertToIdList(new ScrollIterable(client, searchRequestBuilder)))
+                .containsOnly(id1, id2);
+        }
+    }
+
+    @Test
+    public void scrollIterableShouldWorkWhenMoreThanSizeElement() {
+        try (Client client = clientProvider.get()) {
+            String id1 = "1";
+            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id1)
+                .setSource(MESSAGE, "Sample message")
+                .execute();
+
+            String id2 = "2";
+            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id2)
+                .setSource(MESSAGE, "Sample message")
+                .execute();
+
+            String id3 = "3";
+            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id3)
+                .setSource(MESSAGE, "Sample message")
+                .execute();
+
+            elasticSearch.awaitForElasticSearch();
+            WAIT_CONDITION.untilAsserted(() -> hasIdsInIndex(client, id1, id2, id3));
+
+            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
+                .setTypes(TYPE_NAME.getValue())
+                .setScroll(TIMEOUT)
+                .setQuery(matchAllQuery())
+                .setSize(SIZE);
+
+            assertThat(convertToIdList(new ScrollIterable(client, searchRequestBuilder)))
+                .containsOnly(id1, id2, id3);
+        }
+    }
+
+    private List<String> convertToIdList(ScrollIterable scrollIterable) {
+        return scrollIterable.stream()
+            .flatMap(searchResponse -> Arrays.stream(searchResponse.getHits().getHits()))
+            .map(SearchHit::getId)
+            .collect(Collectors.toList());
+    }
+
+    private void hasIdsInIndex(Client client, String... ids) {
+        SearchHit[] hits = client.prepareSearch(INDEX_NAME.getValue())
+            .setQuery(QueryBuilders.idsQuery(TYPE_NAME.getValue()).addIds(ids))
+            .execute()
+            .actionGet()
+            .getHits()
+            .hits();
+
+        assertThat(hits)
+            .extracting(SearchHit::getId)
+            .contains(ids);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/utils/TestingClientProvider.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/utils/TestingClientProvider.java
new file mode 100644
index 0000000..967b80e
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/utils/TestingClientProvider.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.backends.es.v6.utils;
+
+import org.apache.james.backends.es.v6.ClientProvider;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.node.Node;
+
+public class TestingClientProvider implements ClientProvider {
+
+    private final Node node;
+
+    public TestingClientProvider(Node node) {
+        this.node = node;
+    }
+    
+    @Override
+    public Client get() {
+        return node.client();
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/resources/logback-test.xml b/backends-common/elasticsearch-v6/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..dd2d81e
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/resources/logback-test.xml
@@ -0,0 +1,12 @@
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%date %-5level [%thread] - [%logger]- %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <root level="WARN">
+        <appender-ref ref="STDOUT" />
+    </root>
+</configuration>
\ No newline at end of file
diff --git a/backends-common/pom.xml b/backends-common/pom.xml
index 5cab887..f6a96ff 100644
--- a/backends-common/pom.xml
+++ b/backends-common/pom.xml
@@ -38,6 +38,7 @@
         <module>elasticsearch</module>
         <module>jpa</module>
         <module>rabbitmq</module>
+        <module>elasticsearch-v6</module>
     </modules>
 
 </project>


---------------------------------------------------------------------
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: JAMES-2719 Migrate ES backend code to ES6 syntax

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 7f756a80bdb93b5a3168755fdee9b1546a23d561
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed May 15 15:50:40 2019 +0700

    JAMES-2719 Migrate ES backend code to ES6 syntax
---
 .../james/backends/es/v6/ClientProvider.java       |   4 +-
 .../james/backends/es/v6/ClientProviderImpl.java   |  28 ++---
 .../backends/es/v6/DeleteByQueryPerformer.java     |  46 +++-----
 .../backends/es/v6/ElasticSearchConfiguration.java |   2 +-
 .../james/backends/es/v6/ElasticSearchIndexer.java |  48 ++++----
 .../james/backends/es/v6/IndexCreationFactory.java | 131 +++++++++++----------
 .../james/backends/es/v6/NodeMappingFactory.java   |  46 ++++----
 .../backends/es/v6/search/ScrollIterable.java      |  81 -------------
 8 files changed, 149 insertions(+), 237 deletions(-)

diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java
index 0145d0a..9ba5d21 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProvider.java
@@ -18,9 +18,9 @@
  ****************************************************************/
 package org.apache.james.backends.es.v6;
 
-import org.elasticsearch.client.Client;
+import org.elasticsearch.client.RestHighLevelClient;
 
 public interface ClientProvider {
 
-    Client get();
+    RestHighLevelClient get();
 }
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java
index aac59a5..074296b 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ClientProviderImpl.java
@@ -18,17 +18,14 @@
  ****************************************************************/
 package org.apache.james.backends.es.v6;
 
-import java.net.InetAddress;
 import java.util.Optional;
 
+import org.apache.http.HttpHost;
 import org.apache.james.util.Host;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.client.transport.TransportClient;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.transport.InetSocketTransportAddress;
 
-import com.github.fge.lambdas.Throwing;
-import com.github.fge.lambdas.consumers.ConsumerChainer;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -50,6 +47,7 @@ public class ClientProviderImpl implements ClientProvider {
     }
 
     private static final String CLUSTER_NAME_SETTING = "cluster.name";
+    private static final String HTTP_HOST_SCHEME = "http";
 
     private final ImmutableList<Host> hosts;
     private final Optional<String> clusterName;
@@ -60,19 +58,15 @@ public class ClientProviderImpl implements ClientProvider {
         this.clusterName = clusterName;
     }
 
+    private HttpHost[] hostsToHttpHosts() {
+        return hosts.stream()
+            .map(host -> new HttpHost(host.getHostName(), host.getPort(), HTTP_HOST_SCHEME))
+            .toArray(HttpHost[]::new);
+    }
 
     @Override
-    public Client get() {
-        TransportClient transportClient = TransportClient.builder()
-                .settings(settings())
-                .build();
-        ConsumerChainer<Host> consumer = Throwing.consumer(host -> transportClient
-            .addTransportAddress(
-                new InetSocketTransportAddress(
-                    InetAddress.getByName(host.getHostName()),
-                    host.getPort())));
-        hosts.forEach(consumer.sneakyThrow());
-        return transportClient;
+    public RestHighLevelClient get() {
+        return new RestHighLevelClient(RestClient.builder(hostsToHttpHosts()));
     }
 
     @VisibleForTesting Settings settings() {
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
index 05fd04e..a912c0f 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
@@ -19,32 +19,29 @@
 
 package org.apache.james.backends.es.v6;
 
+import java.io.IOException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 
-import org.apache.james.backends.es.v6.search.ScrollIterable;
-import org.elasticsearch.action.ListenableActionFuture;
-import org.elasticsearch.action.bulk.BulkRequestBuilder;
-import org.elasticsearch.action.bulk.BulkResponse;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.client.Client;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.index.reindex.DeleteByQueryRequest;
 
 import com.google.common.annotations.VisibleForTesting;
 
 public class DeleteByQueryPerformer {
     public static final TimeValue TIMEOUT = new TimeValue(60000);
 
-    private final Client client;
+    private final RestHighLevelClient client;
     private final ExecutorService executor;
     private final int batchSize;
     private final WriteAliasName aliasName;
     private final TypeName typeName;
 
     @VisibleForTesting
-    public DeleteByQueryPerformer(Client client, ExecutorService executor, int batchSize, WriteAliasName aliasName, TypeName typeName) {
+    public DeleteByQueryPerformer(RestHighLevelClient client, ExecutorService executor, int batchSize, WriteAliasName aliasName, TypeName typeName) {
         this.client = client;
         this.executor = executor;
         this.batchSize = batchSize;
@@ -56,29 +53,14 @@ public class DeleteByQueryPerformer {
         return executor.submit(() -> doDeleteByQuery(queryBuilder));
     }
 
-    protected Void doDeleteByQuery(QueryBuilder queryBuilder) {
-        new ScrollIterable(client,
-            client.prepareSearch(aliasName.getValue())
-                .setTypes(typeName.getValue())
-                .setScroll(TIMEOUT)
-                .setNoFields()
-                .setQuery(queryBuilder)
-                .setSize(batchSize))
-            .stream()
-            .map(searchResponse -> deleteRetrievedIds(client, searchResponse))
-            .forEach(ListenableActionFuture::actionGet);
-        return null;
-    }
+    protected Void doDeleteByQuery(QueryBuilder queryBuilder) throws IOException {
+        DeleteByQueryRequest request = new DeleteByQueryRequest(aliasName.getValue())
+            .setDocTypes(typeName.getValue())
+            .setScroll(TIMEOUT)
+            .setQuery(queryBuilder)
+            .setBatchSize(batchSize);
 
-    private ListenableActionFuture<BulkResponse> deleteRetrievedIds(Client client, SearchResponse searchResponse) {
-        BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
-        for (SearchHit hit : searchResponse.getHits()) {
-            bulkRequestBuilder.add(client.prepareDelete()
-                .setIndex(aliasName.getValue())
-                .setType(typeName.getValue())
-                .setId(hit.getId()));
-        }
-        return bulkRequestBuilder.execute();
+        client.deleteByQuery(request, RequestOptions.DEFAULT);
+        return null;
     }
-
 }
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java
index f490941..032203e 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchConfiguration.java
@@ -123,7 +123,7 @@ public class ElasticSearchConfiguration {
     public static final int DEFAULT_CONNECTION_MIN_DELAY = 3000;
     public static final int DEFAULT_NB_SHARDS = 5;
     public static final int DEFAULT_NB_REPLICA = 1;
-    public static final int DEFAULT_PORT = 9300;
+    public static final int DEFAULT_PORT = 9200;
     private static final String LOCALHOST = "127.0.0.1";
     public static final Optional<Integer> DEFAULT_PORT_AS_OPTIONAL = Optional.of(DEFAULT_PORT);
 
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
index 8572e4d..492ec37 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
@@ -18,17 +18,23 @@
  ****************************************************************/
 package org.apache.james.backends.es.v6;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 
 import org.apache.commons.lang3.StringUtils;
-import org.elasticsearch.action.bulk.BulkRequestBuilder;
+import org.elasticsearch.action.bulk.BulkRequest;
 import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.delete.DeleteRequest;
+import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.index.IndexResponse;
-import org.elasticsearch.client.Client;
+import org.elasticsearch.action.update.UpdateRequest;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.common.ValidationException;
+import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,19 +48,19 @@ public class ElasticSearchIndexer {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchIndexer.class);
 
-    private final Client client;
+    private final RestHighLevelClient client;
     private final DeleteByQueryPerformer deleteByQueryPerformer;
     private final AliasName aliasName;
     private final TypeName typeName;
 
-    public ElasticSearchIndexer(Client client, ExecutorService executor,
+    public ElasticSearchIndexer(RestHighLevelClient client, ExecutorService executor,
                                 WriteAliasName aliasName,
                                 TypeName typeName) {
         this(client, executor, aliasName, typeName, DEFAULT_BATCH_SIZE);
     }
 
     @VisibleForTesting
-    public ElasticSearchIndexer(Client client, ExecutorService executor,
+    public ElasticSearchIndexer(RestHighLevelClient client, ExecutorService executor,
                                 WriteAliasName aliasName,
                                 TypeName typeName,
                                 int batchSize) {
@@ -64,42 +70,42 @@ public class ElasticSearchIndexer {
         this.typeName = typeName;
     }
 
-    public IndexResponse index(String id, String content) {
+    public IndexResponse index(String id, String content) throws IOException {
         checkArgument(content);
         if (LOGGER.isDebugEnabled()) {
             LOGGER.debug("Indexing {}: {}", id, StringUtils.left(content, DEBUG_MAX_LENGTH_CONTENT));
         }
-        return client.prepareIndex(aliasName.getValue(), typeName.getValue(), id)
-            .setSource(content)
-            .get();
+        return client.index(
+            new IndexRequest(aliasName.getValue(), typeName.getValue(), id)
+                .source(content, XContentType.JSON),
+            RequestOptions.DEFAULT);
     }
 
-    public Optional<BulkResponse> update(List<UpdatedRepresentation> updatedDocumentParts) {
+    public Optional<BulkResponse> update(List<UpdatedRepresentation> updatedDocumentParts) throws IOException {
         try {
             Preconditions.checkNotNull(updatedDocumentParts);
-            BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
-            updatedDocumentParts.forEach(updatedDocumentPart -> bulkRequestBuilder.add(
-                client.prepareUpdate(
-                    aliasName.getValue(),
+            BulkRequest request = new BulkRequest();
+            updatedDocumentParts.forEach(updatedDocumentPart -> request.add(
+                new UpdateRequest(aliasName.getValue(),
                     typeName.getValue(),
                     updatedDocumentPart.getId())
-                    .setDoc(updatedDocumentPart.getUpdatedDocumentPart())));
-            return Optional.of(bulkRequestBuilder.get());
+                .doc(updatedDocumentPart.getUpdatedDocumentPart(), XContentType.JSON)));
+            return Optional.of(client.bulk(request, RequestOptions.DEFAULT));
         } catch (ValidationException e) {
             LOGGER.warn("Error while updating index", e);
             return Optional.empty();
         }
     }
 
-    public Optional<BulkResponse> delete(List<String> ids) {
+    public Optional<BulkResponse> delete(List<String> ids) throws IOException {
         try {
-            BulkRequestBuilder bulkRequestBuilder = client.prepareBulk();
-            ids.forEach(id -> bulkRequestBuilder.add(
-                client.prepareDelete(
+            BulkRequest request = new BulkRequest();
+            ids.forEach(id -> request.add(
+                new DeleteRequest(
                     aliasName.getValue(),
                     typeName.getValue(),
                     id)));
-            return Optional.of(bulkRequestBuilder.get());
+            return Optional.of(client.bulk(request, RequestOptions.DEFAULT));
         } catch (ValidationException e) {
             LOGGER.warn("Error while deleting index", e);
             return Optional.empty();
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
index bdce72c..1a520fa 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.backends.es.v6;
 
+import static org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 
 import java.io.IOException;
@@ -26,23 +27,27 @@ import java.util.ArrayList;
 
 import javax.inject.Inject;
 
+import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
-import org.elasticsearch.client.Client;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.indices.CreateIndexRequest;
 import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.indices.IndexAlreadyExistsException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.fge.lambdas.Throwing;
 import com.google.common.base.Preconditions;
 
 public class IndexCreationFactory {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(IndexCreationFactory.class);
-    public static final String CASE_INSENSITIVE = "case_insensitive";
-    public static final String KEEP_MAIL_AND_URL = "keep_mail_and_url";
-    public static final String SNOWBALL_KEEP_MAIL_AND_URL = "snowball_keep_mail_and_token";
-    public static final String ENGLISH_SNOWBALL = "english_snowball";
+    private static final String INDEX_ALREADY_EXISTS_EXCEPTION_MESSAGE = "type=resource_already_exists_exception";
+    private static final String CASE_INSENSITIVE = "case_insensitive";
+    private static final String KEEP_MAIL_AND_URL = "keep_mail_and_url";
+    private static final String SNOWBALL_KEEP_MAIL_AND_URL = "snowball_keep_mail_and_token";
+    private static final String ENGLISH_SNOWBALL = "english_snowball";
 
     private IndexName indexName;
     private ArrayList<AliasName> aliases;
@@ -69,86 +74,86 @@ public class IndexCreationFactory {
         return this;
     }
 
-    public Client createIndexAndAliases(Client client) {
+    public RestHighLevelClient createIndexAndAliases(RestHighLevelClient client) {
         Preconditions.checkNotNull(indexName);
         try {
             createIndexIfNeeded(client, indexName, generateSetting(nbShards, nbReplica));
-            aliases.forEach(alias -> createAliasIfNeeded(client, indexName, alias));
+            aliases.forEach(Throwing.consumer(alias -> createAliasIfNeeded(client, indexName, alias)));
         } catch (IOException e) {
             LOGGER.error("Error while creating index : ", e);
         }
         return client;
     }
 
-    private void createAliasIfNeeded(Client client, IndexName indexName, AliasName aliasName) {
+    private void createAliasIfNeeded(RestHighLevelClient client, IndexName indexName, AliasName aliasName) throws IOException {
         if (!aliasExist(client, aliasName)) {
-            client.admin()
-                .indices()
-                .aliases(new IndicesAliasesRequest()
-                    .addAlias(aliasName.getValue(), indexName.getValue()))
-                .actionGet();
+            client.indices()
+                .updateAliases(
+                    new IndicesAliasesRequest().addAliasAction(
+                        new AliasActions(AliasActions.Type.ADD)
+                            .index(indexName.getValue())
+                            .alias(aliasName.getValue())),
+                    RequestOptions.DEFAULT);
         }
     }
 
-    private boolean aliasExist(Client client, AliasName aliasName) {
-        return client.admin()
-            .indices()
-            .aliasesExist(new GetAliasesRequest()
-                .aliases(aliasName.getValue()))
-            .actionGet()
-            .exists();
+    private boolean aliasExist(RestHighLevelClient client, AliasName aliasName) throws IOException {
+        return client.indices()
+            .existsAlias(new GetAliasesRequest().aliases(aliasName.getValue()),
+                RequestOptions.DEFAULT);
     }
 
-    private void createIndexIfNeeded(Client client, IndexName indexName, XContentBuilder settings) {
+    private void createIndexIfNeeded(RestHighLevelClient client, IndexName indexName, XContentBuilder settings) throws IOException {
         try {
-            client.admin()
-                .indices()
-                .prepareCreate(indexName.getValue())
-                .setSettings(settings)
-                .execute()
-                .actionGet();
-        } catch (IndexAlreadyExistsException exception) {
-            LOGGER.info("Index [{}] already exist", indexName);
+            client.indices()
+                .create(
+                    new CreateIndexRequest(indexName.getValue())
+                        .source(settings),
+                    RequestOptions.DEFAULT);
+        } catch (ElasticsearchStatusException exception) {
+            if (exception.getMessage().contains(INDEX_ALREADY_EXISTS_EXCEPTION_MESSAGE)) {
+                LOGGER.info("Index [{}] already exist", indexName);
+            } else {
+                throw exception;
+            }
         }
     }
 
     private XContentBuilder generateSetting(int nbShards, int nbReplica) throws IOException {
         return jsonBuilder()
             .startObject()
-                .field("number_of_shards", nbShards)
-                .field("number_of_replicas", nbReplica)
-                .startObject("analysis")
-                    .startObject("analyzer")
-                        .startObject(CASE_INSENSITIVE)
-                            .field("tokenizer", "keyword")
-                            .startArray("filter")
-                                .value("lowercase")
-                            .endArray()
+                .startObject("settings")
+                    .field("number_of_shards", nbShards)
+                    .field("number_of_replicas", nbReplica)
+                    .startObject("analysis")
+                        .startObject("analyzer")
+                            .startObject(CASE_INSENSITIVE)
+                                .field("tokenizer", "keyword")
+                                .startArray("filter")
+                                    .value("lowercase")
+                                .endArray()
+                            .endObject()
+                            .startObject(KEEP_MAIL_AND_URL)
+                                .field("tokenizer", "uax_url_email")
+                                .startArray("filter")
+                                    .value("lowercase")
+                                    .value("stop")
+                                .endArray()
+                            .endObject()
+                            .startObject(SNOWBALL_KEEP_MAIL_AND_URL)
+                                .field("tokenizer", "uax_url_email")
+                                .startArray("filter")
+                                    .value("lowercase")
+                                    .value("stop")
+                                    .value(ENGLISH_SNOWBALL)
+                                .endArray()
+                            .endObject()
                         .endObject()
-                    .endObject()
-                    .startObject("analyzer")
-                        .startObject(KEEP_MAIL_AND_URL)
-                            .field("tokenizer", "uax_url_email")
-                            .startArray("filter")
-                                .value("lowercase")
-                                .value("stop")
-                            .endArray()
-                        .endObject()
-                    .endObject()
-                    .startObject("filter")
-                        .startObject(ENGLISH_SNOWBALL)
-                            .field("type", "snowball")
-                            .field("language", "English")
-                        .endObject()
-                    .endObject()
-                    .startObject("analyzer")
-                        .startObject(SNOWBALL_KEEP_MAIL_AND_URL)
-                        .field("tokenizer", "uax_url_email")
-                            .startArray("filter")
-                                .value("lowercase")
-                                .value("stop")
-                                .value(ENGLISH_SNOWBALL)
-                            .endArray()
+                        .startObject("filter")
+                            .startObject(ENGLISH_SNOWBALL)
+                                .field("type", "snowball")
+                                .field("language", "English")
+                            .endObject()
                         .endObject()
                     .endObject()
                 .endObject()
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java
index 01b0bf0..eda3c50 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/NodeMappingFactory.java
@@ -19,8 +19,13 @@
 
 package org.apache.james.backends.es.v6;
 
+import java.io.IOException;
+
 import org.apache.james.util.streams.Iterators;
-import org.elasticsearch.client.Client;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.indices.GetMappingsRequest;
+import org.elasticsearch.client.indices.PutMappingRequest;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 
 public class NodeMappingFactory {
@@ -32,6 +37,7 @@ public class NodeMappingFactory {
     public static final String INDEX = "index";
     public static final String NOT_ANALYZED = "not_analyzed";
     public static final String STRING = "string";
+    public static final String TEXT = "text";
     public static final String PROPERTIES = "properties";
     public static final String DATE = "date";
     public static final String FORMAT = "format";
@@ -44,32 +50,32 @@ public class NodeMappingFactory {
     public static final String SNOWBALL = "snowball";
     public static final String IGNORE_ABOVE = "ignore_above";
 
-    public static Client applyMapping(Client client, IndexName indexName, TypeName typeName, XContentBuilder mappingsSources) {
+    public static RestHighLevelClient applyMapping(RestHighLevelClient client, IndexName indexName, TypeName typeName, XContentBuilder mappingsSources) throws IOException {
         if (!mappingAlreadyExist(client, indexName, typeName)) {
-            createMapping(client, indexName, typeName, mappingsSources);
+            createMapping(client, indexName, mappingsSources);
         }
         return client;
     }
 
-    public static boolean mappingAlreadyExist(Client client, IndexName indexName, TypeName typeName) {
-        return Iterators.toStream(client.admin()
-            .indices()
-            .prepareGetMappings(indexName.getValue())
-            .execute()
-            .actionGet()
-            .getMappings()
-            .valuesIt())
-            .anyMatch(mapping -> mapping.keys().contains(typeName.getValue()));
+    public static boolean mappingAlreadyExist(RestHighLevelClient client, IndexName indexName, TypeName typeName) throws IOException {
+        return Iterators.toStream(client.indices()
+            .getMapping(
+                new GetMappingsRequest()
+                    .indices(indexName.getValue()),
+                RequestOptions.DEFAULT)
+            .mappings()
+            .values()
+            .iterator())
+            .anyMatch(mapping -> mapping.type().contains(typeName.getValue()));
     }
 
-    public static void createMapping(Client client, IndexName indexName, TypeName typeName, XContentBuilder mappingsSources) {
-        client.admin()
-            .indices()
-            .preparePutMapping(indexName.getValue())
-            .setType(typeName.getValue())
-            .setSource(mappingsSources)
-            .execute()
-            .actionGet();
+    public static void createMapping(RestHighLevelClient client, IndexName indexName, XContentBuilder mappingsSources) throws IOException {
+        PutMappingRequest request = new PutMappingRequest(indexName.getValue())
+            .source(mappingsSources);
+        client.indices().putMapping(
+            new PutMappingRequest(indexName.getValue())
+                .source(mappingsSources),
+            RequestOptions.DEFAULT);
     }
 
 }
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/search/ScrollIterable.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/search/ScrollIterable.java
deleted file mode 100644
index eca5bae..0000000
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/search/ScrollIterable.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/****************************************************************
- * 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.backends.es.v6.search;
-
-import java.util.Iterator;
-import java.util.stream.Stream;
-
-import org.apache.james.util.streams.Iterators;
-import org.elasticsearch.action.ListenableActionFuture;
-import org.elasticsearch.action.search.SearchRequestBuilder;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.common.unit.TimeValue;
-
-public class ScrollIterable implements Iterable<SearchResponse> {
-
-    private static final TimeValue TIMEOUT = new TimeValue(60000);
-    private final Client client;
-    private final SearchRequestBuilder searchRequestBuilder;
-
-    public ScrollIterable(Client client, SearchRequestBuilder searchRequestBuilder) {
-        this.client = client;
-        this.searchRequestBuilder = searchRequestBuilder;
-    }
-
-    @Override
-    public Iterator<SearchResponse> iterator() {
-        return new ScrollIterator(client, searchRequestBuilder);
-    }
-
-    public Stream<SearchResponse> stream() {
-        return Iterators.toStream(iterator());
-    }
-
-    public static class ScrollIterator implements Iterator<SearchResponse> {
-
-        private final Client client;
-        private ListenableActionFuture<SearchResponse> searchResponseFuture;
-
-        public ScrollIterator(Client client, SearchRequestBuilder searchRequestBuilder) {
-            this.client = client;
-            this.searchResponseFuture = searchRequestBuilder.execute();
-        }
-
-        @Override
-        public boolean hasNext() {
-            return !allSearchResponsesConsumed(searchResponseFuture.actionGet());
-        }
-
-        @Override
-        public SearchResponse next() {
-            SearchResponse result = searchResponseFuture.actionGet();
-            searchResponseFuture =  client.prepareSearchScroll(result.getScrollId())
-                .setScroll(TIMEOUT)
-                .execute();
-            return result;
-        }
-
-        private boolean allSearchResponsesConsumed(SearchResponse searchResponse) {
-            return searchResponse.getHits().getHits().length == 0;
-        }
-    }
-
-}


---------------------------------------------------------------------
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-2763 Plug StartUpChecksPerformer to GuiceJamesServer

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 1be5ce56c3287e63fe899fa69eecbae16dc7606f
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Tue May 14 11:40:53 2019 +0700

    JAMES-2763 Plug StartUpChecksPerformer to GuiceJamesServer
---
 .../apache/james/modules/BlobExportImplChoice.java |   2 +-
 .../java/org/apache/james/GuiceJamesServer.java    |   2 +
 .../apache/james/modules/CommonServicesModule.java |   1 +
 .../apache/james/modules/StartUpChecksModule.java  |  33 ++++
 .../james/GuiceJamesServerStartUpCheckTest.java    | 189 +++++++++++++++++++++
 5 files changed, 226 insertions(+), 1 deletion(-)

diff --git a/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/BlobExportImplChoice.java b/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/BlobExportImplChoice.java
index 8f4f201..c7c3df4 100644
--- a/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/BlobExportImplChoice.java
+++ b/server/container/guice/blob-export-guice/src/main/java/org/apache/james/modules/BlobExportImplChoice.java
@@ -29,7 +29,7 @@ import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
-enum  BlobExportImplChoice {
+public enum  BlobExportImplChoice {
     LOCAL_FILE("localFile"),
     LINSHARE("linshare");
 
diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/GuiceJamesServer.java b/server/container/guice/guice-common/src/main/java/org/apache/james/GuiceJamesServer.java
index d355840..d1d5eed 100644
--- a/server/container/guice/guice-common/src/main/java/org/apache/james/GuiceJamesServer.java
+++ b/server/container/guice/guice-common/src/main/java/org/apache/james/GuiceJamesServer.java
@@ -78,6 +78,8 @@ public class GuiceJamesServer {
     public void start() throws Exception {
         Injector injector = Guice.createInjector(module);
         preDestroy = injector.getInstance(Key.get(new TypeLiteral<Stager<PreDestroy>>() {}));
+        injector.getInstance(StartUpChecksPerformer.class)
+            .performCheck();
         injector.getInstance(ConfigurationsPerformer.class).initModules();
         guiceProbeProvider = injector.getInstance(GuiceProbeProvider.class);
         isStartedProbe.notifyStarted();
diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/modules/CommonServicesModule.java b/server/container/guice/guice-common/src/main/java/org/apache/james/modules/CommonServicesModule.java
index 075d905..7ed083e 100644
--- a/server/container/guice/guice-common/src/main/java/org/apache/james/modules/CommonServicesModule.java
+++ b/server/container/guice/guice-common/src/main/java/org/apache/james/modules/CommonServicesModule.java
@@ -54,6 +54,7 @@ public class CommonServicesModule extends AbstractModule {
     
     @Override
     protected void configure() {
+        install(new StartUpChecksModule());
         install(new StartablesModule());
         install(new PreDestroyModule());
         install(new DNSServiceModule());
diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/modules/StartUpChecksModule.java b/server/container/guice/guice-common/src/main/java/org/apache/james/modules/StartUpChecksModule.java
new file mode 100644
index 0000000..9f7484d
--- /dev/null
+++ b/server/container/guice/guice-common/src/main/java/org/apache/james/modules/StartUpChecksModule.java
@@ -0,0 +1,33 @@
+/****************************************************************
+ * 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.StartUpChecksPerformer;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+
+public class StartUpChecksModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+        Multibinder.newSetBinder(binder(), StartUpChecksPerformer.StartUpCheck.class);
+    }
+}
diff --git a/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceJamesServerStartUpCheckTest.java b/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceJamesServerStartUpCheckTest.java
new file mode 100644
index 0000000..08daaf4
--- /dev/null
+++ b/server/container/guice/memory-guice/src/test/java/org/apache/james/GuiceJamesServerStartUpCheckTest.java
@@ -0,0 +1,189 @@
+/****************************************************************
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.james.StartUpChecksPerformer.StartUpCheck;
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.store.search.PDFTextExtractor;
+import org.apache.james.modules.BlobExportImplChoice;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.google.inject.Inject;
+import com.google.inject.multibindings.Multibinder;
+
+class GuiceJamesServerStartUpCheckTest {
+
+    private static class NoopStartUpCheck implements StartUpCheck {
+
+        private static final String CHECK_NAME = "NoopStartUpCheck";
+
+        @Override
+        public CheckResult check() {
+            return CheckResult.builder()
+                .checkName(CHECK_NAME)
+                .resultType(ResultType.GOOD)
+                .build();
+        }
+    }
+
+    private static class FailingStartUpCheck implements StartUpCheck {
+
+        private static final String CHECK_NAME = "FaillingStartUpCheck";
+
+        @Override
+        public CheckResult check() {
+            return CheckResult.builder()
+                .checkName(CHECK_NAME)
+                .resultType(ResultType.BAD)
+                .description("Failing by intention")
+                .build();
+        }
+    }
+
+    private static class TestBlobExportMechanismStartUpCheck implements StartUpCheck {
+
+        private static final String CHECK_NAME = "TestBlobExportMechanismStartUpCheck";
+
+        private final BlobExportImplChoice blobExportImplChoice;
+
+        @Inject
+        private TestBlobExportMechanismStartUpCheck(BlobExportImplChoice blobExportImplChoice) {
+            // do no thing, just verify that start up checks are able to be injected by guice
+            this.blobExportImplChoice = blobExportImplChoice;
+        }
+
+        @Override
+        public CheckResult check() {
+            return CheckResult.builder()
+                .checkName(CHECK_NAME)
+                .resultType(ResultType.GOOD)
+                .build();
+        }
+    }
+
+    private static final int LIMIT_TO_10_MESSAGES = 10;
+
+    interface StartUpCheckSuccessContract {
+
+        @Test
+        default void serverShouldStartSuccessfully(GuiceJamesServer server) throws Exception {
+            server.start();
+
+            assertThat(server.isStarted()).isTrue();
+        }
+    }
+
+    @Nested
+    class WithStartUpCheckDoesntRequireGuiceComponents implements StartUpCheckSuccessContract {
+
+        @RegisterExtension
+        JamesServerExtension jamesServerExtension = new JamesServerBuilder()
+            .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+                .combineWith(MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE)
+                .overrideWith(new TestJMAPServerModule(LIMIT_TO_10_MESSAGES))
+                .overrideWith(binder -> binder.bind(TextExtractor.class).to(PDFTextExtractor.class))
+                .overrideWith(binder -> Multibinder
+                    .newSetBinder(binder, StartUpCheck.class)
+                    .addBinding().to(NoopStartUpCheck.class)))
+            .disableAutoStart()
+            .build();
+    }
+
+    @Nested
+    class WithStartUpCheckRequireGuiceComponents implements StartUpCheckSuccessContract {
+
+        @RegisterExtension
+        JamesServerExtension jamesServerExtension = new JamesServerBuilder()
+            .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+                .combineWith(MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE)
+                .overrideWith(new TestJMAPServerModule(LIMIT_TO_10_MESSAGES))
+                .overrideWith(binder -> binder.bind(TextExtractor.class).to(PDFTextExtractor.class))
+                .overrideWith(binder -> Multibinder
+                    .newSetBinder(binder, StartUpCheck.class)
+                    .addBinding().to(TestBlobExportMechanismStartUpCheck.class)))
+            .disableAutoStart()
+            .build();
+    }
+
+    @Nested
+    class WithNoStartUpCheck implements StartUpCheckSuccessContract {
+
+        @RegisterExtension
+        JamesServerExtension jamesServerExtension = new JamesServerBuilder()
+            .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+                .combineWith(MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE)
+                .overrideWith(new TestJMAPServerModule(LIMIT_TO_10_MESSAGES))
+                .overrideWith(binder -> binder.bind(TextExtractor.class).to(PDFTextExtractor.class)))
+            .disableAutoStart()
+            .build();
+    }
+
+    @Nested
+    class StartUpCheckFails {
+
+        @RegisterExtension
+        JamesServerExtension jamesServerExtension = new JamesServerBuilder()
+            .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+                .combineWith(MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE)
+                .overrideWith(new TestJMAPServerModule(LIMIT_TO_10_MESSAGES))
+                .overrideWith(binder -> binder.bind(TextExtractor.class).to(PDFTextExtractor.class))
+                .overrideWith(binder -> {
+                    Multibinder<StartUpCheck> setBinder = Multibinder
+                        .newSetBinder(binder, StartUpCheck.class);
+
+                    setBinder.addBinding().to(NoopStartUpCheck.class);
+                    setBinder.addBinding().to(FailingStartUpCheck.class);
+                }))
+            .disableAutoStart()
+            .build();
+
+        @Test
+        void startUpCheckFailsShouldThrowAnExceptionCarryingOnlyBadChecks(GuiceJamesServer server) throws Exception {
+            assertThatThrownBy(server::start)
+                .isInstanceOfSatisfying(
+                    StartUpChecksPerformer.StartUpChecksException.class,
+                    exception -> assertThat(nameOfStartUpChecks(exception.getBadChecks()))
+                        .containsOnly(FailingStartUpCheck.CHECK_NAME));
+        }
+
+        @Test
+        void serverShouldNotStartWhenAStartUpCheckFails(GuiceJamesServer server) throws Exception {
+            assertThatThrownBy(server::start)
+                .isInstanceOf(StartUpChecksPerformer.StartUpChecksException.class);
+
+            assertThat(server.isStarted())
+                .isFalse();
+        }
+
+        private Stream<String> nameOfStartUpChecks(List<StartUpCheck.CheckResult> checkResults) {
+            return checkResults.stream()
+                .map(StartUpCheck.CheckResult::getName);
+        }
+    }
+}


---------------------------------------------------------------------
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-2149 Domain routes should handle aliases

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 abaa0021c6c9f2d17d84f84052b77f0959c984d0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon May 13 17:50:40 2019 +0700

    JAMES-2149 Domain routes should handle aliases
    
    Add the following CRUD routes and their unit tests:
    
    GET /domains/domain.tld/aliases
    PUT /domains/domain.tld/aliases/alias.domain.tld
    DELETE /domains/domain.tld/aliases/alias.domain.tld
---
 .../james/webadmin/dto/DomainAliasResponse.java    |  51 ++++
 .../james/webadmin/routes/DomainsRoutes.java       | 162 +++++++++++--
 .../webadmin/dto/DomainAliasResponseTest.java      |  31 +++
 .../james/webadmin/routes/DomainsRoutesTest.java   | 257 ++++++++++++++++++++-
 4 files changed, 479 insertions(+), 22 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/DomainAliasResponse.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/DomainAliasResponse.java
new file mode 100644
index 0000000..141faf9
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/DomainAliasResponse.java
@@ -0,0 +1,51 @@
+/****************************************************************
+ * 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.dto;
+
+import java.util.Objects;
+
+import org.apache.james.rrt.lib.MappingSource;
+
+public class DomainAliasResponse {
+    private final MappingSource source;
+
+    public DomainAliasResponse(MappingSource source) {
+        this.source = source;
+    }
+
+    public String getSource() {
+        return source.getFixedDomain();
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof DomainAliasResponse) {
+            DomainAliasResponse that = (DomainAliasResponse) o;
+
+            return Objects.equals(this.source, that.source);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(source);
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
index 321b2c7..8625498 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
@@ -34,8 +34,13 @@ import javax.ws.rs.Produces;
 import org.apache.james.core.Domain;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.lib.Mapping;
+import org.apache.james.rrt.lib.MappingSource;
 import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.dto.DomainAliasResponse;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonTransformer;
@@ -43,13 +48,18 @@ import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
+import spark.HaltException;
 import spark.Request;
 import spark.Response;
 import spark.Service;
@@ -58,22 +68,31 @@ import spark.Service;
 @Path(DomainsRoutes.DOMAINS)
 @Produces("application/json")
 public class DomainsRoutes implements Routes {
-
-    private static final String DOMAIN_NAME = ":domainName";
-    private static final Logger LOGGER = LoggerFactory.getLogger(DomainsRoutes.class);
+    @FunctionalInterface
+    interface MappingOperation {
+        void perform(MappingSource mappingSource, Mapping mapping) throws RecipientRewriteTableException;
+    }
 
     public static final String DOMAINS = "/domains";
-    public static final String SPECIFIC_DOMAIN = DOMAINS + SEPARATOR + DOMAIN_NAME;
-    public static final int MAXIMUM_DOMAIN_SIZE = 256;
-
+    private static final Logger LOGGER = LoggerFactory.getLogger(DomainsRoutes.class);
+    private static final String DOMAIN_NAME = ":domainName";
+    private static final String SOURCE_DOMAIN = ":sourceDomain";
+    private static final String DESTINATION_DOMAIN = ":destinationDomain";
+    private static final String SPECIFIC_DOMAIN = DOMAINS + SEPARATOR + DOMAIN_NAME;
+    private static final String ALIASES = "aliases";
+    private static final String DOMAIN_ALIASES = SPECIFIC_DOMAIN + SEPARATOR + ALIASES;
+    private static final String SPECIFIC_ALIAS = DOMAINS + SEPARATOR + DESTINATION_DOMAIN + SEPARATOR + ALIASES + SEPARATOR + SOURCE_DOMAIN;
+    private static final int MAXIMUM_DOMAIN_SIZE = 256;
 
     private final DomainList domainList;
+    private final RecipientRewriteTable recipientRewriteTable;
     private final JsonTransformer jsonTransformer;
     private Service service;
 
     @Inject
-    public DomainsRoutes(DomainList domainList, JsonTransformer jsonTransformer) {
+    public DomainsRoutes(DomainList domainList, RecipientRewriteTable recipientRewriteTable, JsonTransformer jsonTransformer) {
         this.domainList = domainList;
+        this.recipientRewriteTable = recipientRewriteTable;
         this.jsonTransformer = jsonTransformer;
     }
 
@@ -86,13 +105,16 @@ public class DomainsRoutes implements Routes {
     public void define(Service service) {
         this.service = service;
 
+        // Domain endpoints
         defineGetDomains();
-
         defineDomainExists();
-
         defineAddDomain();
-
         defineDeleteDomain();
+
+        // Domain aliases endpoints
+        defineListAliases(service);
+        defineAddAlias(service);
+        defineRemoveAlias(service);
     }
 
     @DELETE
@@ -156,9 +178,59 @@ public class DomainsRoutes implements Routes {
             jsonTransformer);
     }
 
+    @GET
+    @Path("/{domainName}/aliases")
+    @ApiOperation(value = "Getting all aliases for a domain")
+    @ApiImplicitParams({
+        @ApiImplicitParam(required = true, dataType = "string", name = "domainName", paramType = "path")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+        @ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The domain does not exist."),
+        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+            message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineListAliases(Service service) {
+        service.get(DOMAIN_ALIASES, this::listDomainAliases, jsonTransformer);
+    }
+
+    @DELETE
+    @Path("/{destinationDomain}/aliases/{sourceDomain}")
+    @ApiOperation(value = "Remove an alias for a specific domain")
+    @ApiImplicitParams({
+        @ApiImplicitParam(required = true, dataType = "string", name = "sourceDomain", paramType = "path"),
+        @ApiImplicitParam(required = true, dataType = "string", name = "destinationDomain", paramType = "path")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK", response = List.class),
+        @ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The domain does not exist."),
+        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+            message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineRemoveAlias(Service service) {
+        service.delete(SPECIFIC_ALIAS, this::removeDomainAlias, jsonTransformer);
+    }
+
+    @PUT
+    @Path("/{destinationDomain}/aliases/{sourceDomain}")
+    @ApiOperation(value = "Add an alias for a specific domain")
+    @ApiImplicitParams({
+        @ApiImplicitParam(required = true, dataType = "string", name = "sourceDomain", paramType = "path"),
+        @ApiImplicitParam(required = true, dataType = "string", name = "destinationDomain", paramType = "path")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK", response = List.class),
+        @ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The domain does not exist."),
+        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+            message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineAddAlias(Service service) {
+        service.put(SPECIFIC_ALIAS, this::addDomainAlias, jsonTransformer);
+    }
+
     private String removeDomain(Request request, Response response) {
         try {
-            Domain domain = checkValidDomain(request);
+            Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
             domainList.removeDomain(domain);
         } catch (DomainListException e) {
             LOGGER.info("{} did not exists", request.params(DOMAIN_NAME));
@@ -168,7 +240,7 @@ public class DomainsRoutes implements Routes {
     }
 
     private String addDomain(Request request, Response response) {
-        Domain domain = checkValidDomain(request);
+        Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
         try {
             addDomain(domain);
             response.status(204);
@@ -192,8 +264,7 @@ public class DomainsRoutes implements Routes {
         return Constants.EMPTY_BODY;
     }
 
-    private Domain checkValidDomain(Request request) {
-        String domainName = request.params(DOMAIN_NAME);
+    private Domain checkValidDomain(String domainName) {
         try {
             return Domain.of(domainName);
         } catch (IllegalArgumentException e) {
@@ -212,16 +283,67 @@ public class DomainsRoutes implements Routes {
     }
 
     private Response exists(Request request, Response response) throws DomainListException {
-        Domain domain = checkValidDomain(request);
+        Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
+
         if (!domainList.containsDomain(domain)) {
-            throw ErrorResponder.builder()
-                .statusCode(HttpStatus.NOT_FOUND_404)
-                .type(ErrorType.INVALID_ARGUMENT)
-                .message("The domain list does not contain: " + domain.name())
-                .haltError();
+            throw domainNotFound(domain);
         } else {
             response.status(HttpStatus.NO_CONTENT_204);
             return response;
         }
     }
+
+    private ImmutableSet<DomainAliasResponse> listDomainAliases(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
+        Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
+
+        if (!hasAliases(domain)) {
+            throw domainHasNoAliases(domain);
+        } else {
+            return recipientRewriteTable.listSources(Mapping.domain(domain))
+                .map(DomainAliasResponse::new)
+                .collect(Guavate.toImmutableSet());
+        }
+    }
+
+    private String addDomainAlias(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
+        return performOperationOnAlias(request, response, recipientRewriteTable::addMapping);
+    }
+
+    private String removeDomainAlias(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
+        return performOperationOnAlias(request, response, recipientRewriteTable::removeMapping);
+    }
+
+    private String performOperationOnAlias(Request request, Response response, MappingOperation operation) throws DomainListException, RecipientRewriteTableException {
+        Domain sourceDomain = checkValidDomain(request.params(SOURCE_DOMAIN));
+        Domain destinationDomain = checkValidDomain(request.params(DESTINATION_DOMAIN));
+
+        if (!domainList.containsDomain(sourceDomain)) {
+            throw domainNotFound(sourceDomain);
+        }
+
+        operation.perform(MappingSource.fromDomain(sourceDomain), Mapping.domain(destinationDomain));
+        response.status(HttpStatus.NO_CONTENT_204);
+        return Constants.EMPTY_BODY;
+    }
+
+    private boolean hasAliases(Domain domain) throws DomainListException, RecipientRewriteTableException {
+        return domainList.containsDomain(domain)
+            || recipientRewriteTable.listSources(Mapping.domain(domain)).findFirst().isPresent();
+    }
+
+    private HaltException domainNotFound(Domain domain) {
+        return ErrorResponder.builder()
+            .statusCode(HttpStatus.NOT_FOUND_404)
+            .type(ErrorType.INVALID_ARGUMENT)
+            .message("The domain list does not contain: " + domain.name())
+            .haltError();
+    }
+
+    private HaltException domainHasNoAliases(Domain domain) {
+        return ErrorResponder.builder()
+            .statusCode(HttpStatus.NOT_FOUND_404)
+            .type(ErrorType.INVALID_ARGUMENT)
+            .message("The following domain is not in the domain list and has no registered local aliases: " + domain.name())
+            .haltError();
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/dto/DomainAliasResponseTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/dto/DomainAliasResponseTest.java
new file mode 100644
index 0000000..f8fb107
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/dto/DomainAliasResponseTest.java
@@ -0,0 +1,31 @@
+/****************************************************************
+ * 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.dto;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class DomainAliasResponseTest {
+    @Test
+    void shouldMatchBeanContract() {
+        EqualsVerifier.forClass(DomainAliasResponse.class).verify();
+    }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
index 82b7dde..d5f28bd 100644
--- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
@@ -25,6 +25,9 @@ import static io.restassured.RestAssured.with;
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
@@ -40,6 +43,7 @@ import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
 import org.apache.james.domainlist.memory.MemoryDomainList;
 import org.apache.james.metrics.logger.DefaultMetricFactory;
+import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
 import org.apache.james.webadmin.WebAdminServer;
 import org.apache.james.webadmin.WebAdminUtils;
 import org.apache.james.webadmin.utils.JsonTransformer;
@@ -54,14 +58,17 @@ import io.restassured.http.ContentType;
 
 
 public class DomainsRoutesTest {
-    public static final String DOMAIN = "domain";
+    private static final String DOMAIN = "domain";
+    private static final String ALIAS_DOMAIN = "alias.domain";
+    private static final String ALIAS_DOMAIN_2 = "alias.domain.bis";
+    public static final String EXTERNAL_DOMAIN = "external.domain.tld";
 
     private WebAdminServer webAdminServer;
 
     private void createServer(DomainList domainList) throws Exception {
         webAdminServer = WebAdminUtils.createWebAdminServer(
             new DefaultMetricFactory(),
-            new DomainsRoutes(domainList, new JsonTransformer()));
+            new DomainsRoutes(domainList, new MemoryRecipientRewriteTable(), new JsonTransformer()));
         webAdminServer.configure(NO_CONFIGURATION);
         webAdminServer.await();
 
@@ -255,6 +262,252 @@ public class DomainsRoutesTest {
                 .statusCode(HttpStatus.NOT_FOUND_404);
         }
 
+        @Nested
+        class DomainAlias{
+            @Test
+            void getAliasesShouldReturnNotFoundWhenDomainDoesNotExist() {
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.NOT_FOUND_404)
+                    .body("statusCode", is(HttpStatus.NOT_FOUND_404))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("The following domain is not in the domain list and has no registered local aliases: domain"));
+            }
+
+            @Test
+            void getAliasesShouldReturnEmptyWhenNone() {
+                with().put(DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body(".", hasSize(0));
+            }
+
+            @Test
+            void getAliasesShouldReturnCreatedAliases() {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+                with().put(ALIAS_DOMAIN_2);
+
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN_2);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", containsInAnyOrder(ALIAS_DOMAIN, ALIAS_DOMAIN_2));
+            }
+
+            @Test
+            void putShouldBeIdempotent() {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", containsInAnyOrder(ALIAS_DOMAIN));
+            }
+
+            @Test
+            void deleteShouldNotFailOnNonExistingEvents() {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+
+                with().delete(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("", hasSize(0));
+            }
+
+            @Test
+            void putShouldLowercaseDomain() {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+
+                with().put(DOMAIN + "/aliases/" + "Alias.Domain");
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", containsInAnyOrder(ALIAS_DOMAIN));
+            }
+
+            @Test
+            void getAliasesShouldNotReturnDeletedAliases() {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+                with().delete(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body(".", hasSize(0));
+            }
+
+            @Test
+            void deleteShouldReturnNotFoundWhenAliasDomainDoesNotExist() {
+                with().put(DOMAIN);
+
+                when()
+                    .delete(DOMAIN + "/aliases/" + ALIAS_DOMAIN)
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.NOT_FOUND_404)
+                    .body("statusCode", is(HttpStatus.NOT_FOUND_404))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("The domain list does not contain: " + ALIAS_DOMAIN));
+            }
+
+            @Test
+            void putShouldReturnNotFoundWhenAliasDomainDoesNotExist() {
+                with().put(DOMAIN);
+
+                when()
+                    .put(DOMAIN + "/aliases/" + ALIAS_DOMAIN)
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.NOT_FOUND_404)
+                    .body("statusCode", is(HttpStatus.NOT_FOUND_404))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("The domain list does not contain: " + ALIAS_DOMAIN));
+            }
+
+            @Test
+            void putShouldNotFailOnExternalDomainAlias() {
+                with().put(DOMAIN);
+
+                when()
+                    .put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN).prettyPeek()
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.NO_CONTENT_204);
+            }
+
+            @Test
+            void deleteShouldNotFailOnExternalDomainDestinationForAnAlias() {
+                with().put(DOMAIN);
+
+                when()
+                    .delete(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN)
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.NO_CONTENT_204);
+            }
+
+            @Test
+            void getAliasesShouldListAliasesForExternalDomains() {
+                with().put(DOMAIN);
+
+                with().put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN);
+
+                when()
+                    .get(EXTERNAL_DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", containsInAnyOrder(DOMAIN));
+            }
+
+            @Test
+            void deleteShouldRemoveExternalDomainAlias() {
+                with().put(DOMAIN);
+
+                with().put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN);
+
+                with().delete(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", hasSize(0));
+            }
+
+            @Test
+            void putShouldReturnBadRequestWhenDestinationDomainIsInvalid() {
+                when()
+                    .put("invalid@domain/aliases/" + ALIAS_DOMAIN)
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.BAD_REQUEST_400)
+                    .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("Invalid request for domain creation invalid@domain"));
+            }
+
+            @Test
+            void putShouldReturnBadRequestWhenSourceDomainIsInvalid() {
+                when()
+                    .put("domain/aliases/invalid@alias.domain")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.BAD_REQUEST_400)
+                    .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("Invalid request for domain creation invalid@alias.domain"));
+            }
+
+            @Test
+            void deleteShouldReturnBadRequestWhenDestinationDomainIsInvalid() {
+                when()
+                    .delete("invalid@domain/aliases/" + ALIAS_DOMAIN)
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.BAD_REQUEST_400)
+                    .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("Invalid request for domain creation invalid@domain"));
+            }
+
+            @Test
+            void deleteShouldReturnBadRequestWhenSourceDomainIsInvalid() {
+                when()
+                    .delete("domain/aliases/invalid@alias.domain")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.BAD_REQUEST_400)
+                    .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("Invalid request for domain creation invalid@alias.domain"));
+            }
+
+            @Test
+            void getAliasesShouldReturnBadRequestWhenDomainIsInvalid() {
+                when()
+                    .get("invalid@domain/aliases")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.BAD_REQUEST_400)
+                    .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("Invalid request for domain creation invalid@domain"));
+            }
+        }
+
     }
 
     @Nested


---------------------------------------------------------------------
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-2149 Factorize RRT error processor definitions

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 c901923a96eae1a9b30e805f950b2bfcf09634b3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue May 14 17:41:59 2019 +0700

    JAMES-2149 Factorize RRT error processor definitions
---
 .../mailets/configuration/CommonProcessors.java    | 34 +++++++++++++++++++
 .../james/transport/mailets/AliasMappingTest.java  | 31 ++----------------
 .../transport/mailets/GroupMappingRelayTest.java   | 35 ++------------------
 .../james/transport/mailets/GroupMappingTest.java  | 38 ++--------------------
 4 files changed, 41 insertions(+), 97 deletions(-)

diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/CommonProcessors.java b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/CommonProcessors.java
index 503b940..5ccda3f 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/CommonProcessors.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/mailets/configuration/CommonProcessors.java
@@ -37,6 +37,7 @@ import org.apache.james.transport.mailets.ToProcessor;
 import org.apache.james.transport.mailets.ToRepository;
 import org.apache.james.transport.matchers.All;
 import org.apache.james.transport.matchers.HasMailAttribute;
+import org.apache.james.transport.matchers.IsSenderInRRTLoop;
 import org.apache.james.transport.matchers.RecipientIsLocal;
 import org.apache.james.transport.matchers.RelayLimit;
 import org.apache.james.transport.matchers.SMTPAuthSuccessful;
@@ -44,6 +45,8 @@ import org.apache.james.transport.matchers.SMTPAuthSuccessful;
 public class CommonProcessors {
 
     public static final MailRepositoryUrl ERROR_REPOSITORY = MailRepositoryUrl.from("file://var/mail/error/");
+    public static final MailRepositoryUrl RRT_ERROR_REPOSITORY = MailRepositoryUrl.from("file://var/mail/rrt-error/");
+    private static final String RRT_ERROR = "rrt-error";
 
     public static ProcessorConfiguration root() {
         return ProcessorConfiguration.root()
@@ -139,4 +142,35 @@ public class CommonProcessors {
                         .addProperty("passThrough", "false"))
                 .build();
     }
+
+    public static final ProcessorConfiguration.Builder rrtErrorEnabledTransport() {
+        return ProcessorConfiguration.transport()
+            .addMailet(MailetConfiguration.builder()
+                .matcher(All.class)
+                .mailet(RecipientRewriteTable.class)
+                .addProperty("errorProcessor", RRT_ERROR))
+            .addMailet(MailetConfiguration.builder()
+                .matcher(RecipientIsLocal.class)
+                .mailet(VacationMailet.class))
+            .addMailet(MailetConfiguration.builder()
+                .matcher(RecipientIsLocal.class)
+                .mailet(JMAPFiltering.class))
+            .addMailetsFrom(CommonProcessors.deliverOnlyTransport());
+    }
+
+    public static ProcessorConfiguration.Builder rrtErrorProcessor() {
+        return ProcessorConfiguration.builder()
+            .state(RRT_ERROR)
+            .addMailet(MailetConfiguration.builder()
+                .matcher(All.class)
+                .mailet(ToRepository.class)
+                .addProperty("passThrough", "true")
+                .addProperty("repositoryPath", RRT_ERROR_REPOSITORY.asString()))
+            .addMailet(MailetConfiguration.builder()
+                .matcher(IsSenderInRRTLoop.class)
+                .mailet(Null.class))
+            .addMailet(MailetConfiguration.builder()
+                .matcher(All.class)
+                .mailet(Bounce.class));
+    }
 }
diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java
index 8fb36f2..116b522 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java
@@ -28,23 +28,16 @@ import static org.assertj.core.api.Assertions.assertThat;
 import javax.mail.internet.MimeMessage;
 
 import org.apache.james.core.builder.MimeMessageBuilder;
-import org.apache.james.jmap.mailet.VacationMailet;
-import org.apache.james.jmap.mailet.filter.JMAPFiltering;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailets.TemporaryJamesServer;
 import org.apache.james.mailets.configuration.CommonProcessors;
-import org.apache.james.mailets.configuration.MailetConfiguration;
 import org.apache.james.mailets.configuration.MailetContainer;
-import org.apache.james.mailets.configuration.ProcessorConfiguration;
 import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.modules.MailboxProbeImpl;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
 import org.apache.james.probe.DataProbe;
-import org.apache.james.transport.matchers.All;
-import org.apache.james.transport.matchers.IsSenderInRRTLoop;
-import org.apache.james.transport.matchers.RecipientIsLocal;
 import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.IMAPMessageReader;
 import org.apache.james.utils.MailRepositoryProbeImpl;
@@ -100,28 +93,8 @@ public class AliasMappingTest {
     @Before
     public void setup() throws Exception {
         MailetContainer.Builder mailetContainer = TemporaryJamesServer.SIMPLE_MAILET_CONTAINER_CONFIGURATION
-            .putProcessor(ProcessorConfiguration.transport()
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(RecipientRewriteTable.class)
-                    .addProperty("errorProcessor", RRT_ERROR))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(RecipientIsLocal.class)
-                    .mailet(VacationMailet.class))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(RecipientIsLocal.class)
-                    .mailet(JMAPFiltering.class))
-                .addMailetsFrom(CommonProcessors.deliverOnlyTransport()))
-            .putProcessor(ProcessorConfiguration.builder()
-                .state(RRT_ERROR)
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(ToRepository.class)
-                    .addProperty("passThrough", "true")
-                    .addProperty("repositoryPath", RRT_ERROR_REPOSITORY.asString()))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(IsSenderInRRTLoop.class)
-                    .mailet(Null.class)));
+            .putProcessor(CommonProcessors.rrtErrorEnabledTransport())
+            .putProcessor(CommonProcessors.rrtErrorProcessor());
 
         jamesServer = TemporaryJamesServer.builder()
             .withMailetContainer(mailetContainer)
diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingRelayTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingRelayTest.java
index dfedd88..dcd0e01 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingRelayTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingRelayTest.java
@@ -28,19 +28,13 @@ import static org.hamcrest.Matchers.equalTo;
 import javax.mail.internet.MimeMessage;
 
 import org.apache.james.core.builder.MimeMessageBuilder;
-import org.apache.james.jmap.mailet.VacationMailet;
-import org.apache.james.jmap.mailet.filter.JMAPFiltering;
 import org.apache.james.mailets.TemporaryJamesServer;
 import org.apache.james.mailets.configuration.CommonProcessors;
 import org.apache.james.mailets.configuration.MailetConfiguration;
 import org.apache.james.mailets.configuration.MailetContainer;
-import org.apache.james.mailets.configuration.ProcessorConfiguration;
-import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
 import org.apache.james.probe.DataProbe;
 import org.apache.james.transport.matchers.All;
-import org.apache.james.transport.matchers.IsSenderInRRTLoop;
-import org.apache.james.transport.matchers.RecipientIsLocal;
 import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.FakeSmtp;
 import org.apache.james.utils.IMAPMessageReader;
@@ -67,8 +61,6 @@ public class GroupMappingRelayTest {
     private static final String GROUP_ON_DOMAIN1 = "group@" + DOMAIN1;
 
     private static final String MESSAGE_CONTENT = "any text";
-    public static final String RRT_ERROR = "rrt-error";
-    public static final MailRepositoryUrl RRT_ERROR_REPOSITORY = MailRepositoryUrl.from("file://var/mail/rrt-error/");
 
     private TemporaryJamesServer jamesServer;
     private MimeMessage message;
@@ -91,34 +83,11 @@ public class GroupMappingRelayTest {
     @Before
     public void setup() throws Exception {
         MailetContainer.Builder mailetContainer = TemporaryJamesServer.SIMPLE_MAILET_CONTAINER_CONFIGURATION
-            .putProcessor(ProcessorConfiguration.transport()
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(RecipientRewriteTable.class)
-                    .addProperty("errorProcessor", RRT_ERROR))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(RecipientIsLocal.class)
-                    .mailet(VacationMailet.class))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(RecipientIsLocal.class)
-                    .mailet(JMAPFiltering.class))
-                .addMailetsFrom(CommonProcessors.deliverOnlyTransport())
+            .putProcessor(CommonProcessors.rrtErrorEnabledTransport()
                 .addMailet(MailetConfiguration.remoteDeliveryBuilder()
                     .matcher(All.class)
                     .addProperty("gateway", fakeSmtp.getContainer().getContainerIp())))
-            .putProcessor(ProcessorConfiguration.builder()
-                .state(RRT_ERROR)
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(ToRepository.class)
-                    .addProperty("passThrough", "true")
-                    .addProperty("repositoryPath", RRT_ERROR_REPOSITORY.asString()))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(IsSenderInRRTLoop.class)
-                    .mailet(Null.class))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(Bounce.class)));
+            .putProcessor(CommonProcessors.rrtErrorProcessor());
 
         jamesServer = TemporaryJamesServer.builder()
             .withMailetContainer(mailetContainer)
diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingTest.java
index 0057506..a53d382 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/GroupMappingTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.transport.mailets;
 
+import static org.apache.james.mailets.configuration.CommonProcessors.RRT_ERROR_REPOSITORY;
 import static org.apache.james.mailets.configuration.Constants.DEFAULT_DOMAIN;
 import static org.apache.james.mailets.configuration.Constants.LOCALHOST_IP;
 import static org.apache.james.mailets.configuration.Constants.PASSWORD;
@@ -28,23 +29,15 @@ import static org.assertj.core.api.Assertions.assertThat;
 import javax.mail.internet.MimeMessage;
 
 import org.apache.james.core.builder.MimeMessageBuilder;
-import org.apache.james.jmap.mailet.VacationMailet;
-import org.apache.james.jmap.mailet.filter.JMAPFiltering;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailets.TemporaryJamesServer;
 import org.apache.james.mailets.configuration.CommonProcessors;
-import org.apache.james.mailets.configuration.MailetConfiguration;
 import org.apache.james.mailets.configuration.MailetContainer;
-import org.apache.james.mailets.configuration.ProcessorConfiguration;
-import org.apache.james.mailrepository.api.MailRepositoryUrl;
 import org.apache.james.modules.MailboxProbeImpl;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
 import org.apache.james.probe.DataProbe;
-import org.apache.james.transport.matchers.All;
-import org.apache.james.transport.matchers.IsSenderInRRTLoop;
-import org.apache.james.transport.matchers.RecipientIsLocal;
 import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.IMAPMessageReader;
 import org.apache.james.utils.MailRepositoryProbeImpl;
@@ -73,8 +66,6 @@ public class GroupMappingTest {
     private static final String USER_DOMAIN1 = "user@" + DOMAIN1;
     private static final String USER_DOMAIN2 = "user@" + DOMAIN2;
     private static final String MESSAGE_CONTENT = "any text";
-    public static final String RRT_ERROR = "rrt-error";
-    public static final MailRepositoryUrl RRT_ERROR_REPOSITORY = MailRepositoryUrl.from("file://var/mail/rrt-error/");
 
     private TemporaryJamesServer jamesServer;
     private MimeMessage message;
@@ -91,31 +82,8 @@ public class GroupMappingTest {
     @Before
     public void setup() throws Exception {
         MailetContainer.Builder mailetContainer = TemporaryJamesServer.SIMPLE_MAILET_CONTAINER_CONFIGURATION
-            .putProcessor(ProcessorConfiguration.transport()
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(RecipientRewriteTable.class)
-                    .addProperty("errorProcessor", RRT_ERROR))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(RecipientIsLocal.class)
-                    .mailet(VacationMailet.class))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(RecipientIsLocal.class)
-                    .mailet(JMAPFiltering.class))
-                .addMailetsFrom(CommonProcessors.deliverOnlyTransport()))
-            .putProcessor(ProcessorConfiguration.builder()
-                .state(RRT_ERROR)
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(ToRepository.class)
-                    .addProperty("passThrough", "true")
-                    .addProperty("repositoryPath", RRT_ERROR_REPOSITORY.asString()))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(IsSenderInRRTLoop.class)
-                    .mailet(Null.class))
-                .addMailet(MailetConfiguration.builder()
-                    .matcher(All.class)
-                    .mailet(Bounce.class)));
+            .putProcessor(CommonProcessors.rrtErrorEnabledTransport())
+            .putProcessor(CommonProcessors.rrtErrorProcessor());
 
         jamesServer = TemporaryJamesServer.builder()
             .withMailetContainer(mailetContainer)


---------------------------------------------------------------------
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-2149 Support listing sources for domains in RRT

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 cb8d22290e48188b87fa63cba9d764b7e340e51a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri May 10 18:11:11 2019 +0700

    JAMES-2149 Support listing sources for domains in RRT
---
 .../james/rrt/api/RecipientRewriteTable.java       |  3 +-
 .../rrt/lib/AbstractRecipientRewriteTableTest.java | 37 ++++++++++++++++++++--
 2 files changed, 36 insertions(+), 4 deletions(-)

diff --git a/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java b/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java
index 1f067c5..9de41fe 100644
--- a/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java
+++ b/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java
@@ -52,7 +52,8 @@ public interface RecipientRewriteTable {
         Mapping.Type.Group,
         Mapping.Type.Forward,
         Mapping.Type.Address,
-        Mapping.Type.Alias);
+        Mapping.Type.Alias,
+        Mapping.Type.Domain);
 
     void addMapping(MappingSource source, Mapping mapping) throws RecipientRewriteTableException;
 
diff --git a/server/data/data-library/src/test/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTableTest.java b/server/data/data-library/src/test/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTableTest.java
index be44328..d1fd669 100644
--- a/server/data/data-library/src/test/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTableTest.java
+++ b/server/data/data-library/src/test/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTableTest.java
@@ -426,13 +426,44 @@ public abstract class AbstractRecipientRewriteTableTest {
     }
 
     @Test
-    public void listSourcesShouldThrowExceptionWhenHasDomainMapping() throws Exception {
+    public void listSourcesShouldHandleDomainMapping() throws Exception {
         Mapping mapping = Mapping.domain(Domain.of("domain"));
 
         virtualUserTable.addMapping(SOURCE, mapping);
 
-        assertThatThrownBy(() -> virtualUserTable.listSources(mapping))
-            .isInstanceOf(IllegalArgumentException.class);
+        assertThat(virtualUserTable.listSources(mapping))
+            .containsExactly(SOURCE);
+    }
+
+    @Test
+    public void listSourcesShouldReturnEmptyWhenNoDomainAlias() throws Exception {
+        Mapping mapping = Mapping.domain(Domain.of("domain"));
+
+        assertThat(virtualUserTable.listSources(mapping)).isEmpty();
+    }
+
+    @Test
+    public void listSourcesShouldHandleDomainSource() throws Exception {
+        Mapping mapping = Mapping.domain(Domain.of("domain"));
+
+        MappingSource source = MappingSource.fromDomain(Domain.of("source.org"));
+        virtualUserTable.addMapping(source, mapping);
+
+        assertThat(virtualUserTable.listSources(mapping))
+            .containsExactly(source);
+    }
+
+    @Test
+    public void listSourcesShouldHandleDomainSources() throws Exception {
+        Mapping mapping = Mapping.domain(Domain.of("domain"));
+
+        MappingSource source1 = MappingSource.fromDomain(Domain.of("source1.org"));
+        MappingSource source2 = MappingSource.fromDomain(Domain.of("source2.org"));
+        virtualUserTable.addMapping(source1, mapping);
+        virtualUserTable.addMapping(source2, mapping);
+
+        assertThat(virtualUserTable.listSources(mapping))
+            .containsExactlyInAnyOrder(source1, source2);
     }
 
     @Test


---------------------------------------------------------------------
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-2149 Integration tests for domain aliases

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 9370dc6680e774c803d3685338293e8b99c96ea4
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue May 14 10:30:52 2019 +0700

    JAMES-2149 Integration tests for domain aliases
---
 .../james/transport/mailets/DomainMappingTest.java     | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
index 01005c4..23cd506 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
@@ -113,4 +113,22 @@ public class DomainMappingTest {
             .awaitMessage(awaitAtMostOneMinute);
         assertThat(imapMessageReader.readFirstMessage()).contains(MESSAGE_CONTENT);
     }
+
+    @Test
+    public void messageShouldRedirectToUserOfTheDestinationDomainWhenSentToTheAliasDomain() throws Exception {
+        webAdminApi.put("/domains/" + DOMAIN1 + "/aliases/" + DOMAIN2);
+
+        messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .sendMessage(FakeMail.builder()
+                .name("name")
+                .mimeMessage(message)
+                .sender(SENDER)
+                .recipient(USER_DOMAIN2));
+
+        imapMessageReader.connect(LOCALHOST_IP, jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+            .login(USER_DOMAIN1, PASSWORD)
+            .select(IMAPMessageReader.INBOX)
+            .awaitMessage(awaitAtMostOneMinute);
+        assertThat(imapMessageReader.readFirstMessage()).contains(MESSAGE_CONTENT);
+    }
 }


---------------------------------------------------------------------
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: JAMES-2719 Update version of ES 6 docker image to 6.7.2

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 f803a50d98d09ffbefb99b367a070d76523034aa
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed May 15 10:36:37 2019 +0700

    JAMES-2719 Update version of ES 6 docker image to 6.7.2
---
 server/testing/src/main/java/org/apache/james/util/docker/Images.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/testing/src/main/java/org/apache/james/util/docker/Images.java b/server/testing/src/main/java/org/apache/james/util/docker/Images.java
index 34062f7..1feb8b8 100644
--- a/server/testing/src/main/java/org/apache/james/util/docker/Images.java
+++ b/server/testing/src/main/java/org/apache/james/util/docker/Images.java
@@ -23,7 +23,7 @@ public interface Images {
     String FAKE_SMTP = "weave/rest-smtp-sink:latest";
     String RABBITMQ = "rabbitmq:3.7.7-management";
     String ELASTICSEARCH_2 = "elasticsearch:2.4.6";
-    String ELASTICSEARCH_6 = "elasticsearch:6.5.1";
+    String ELASTICSEARCH_6 = "elasticsearch:6.7.2";
     String NGINX = "nginx:1.15.1";
     String TIKA = "logicalspark/docker-tikaserver:1.20";
     String SPAMASSASSIN = "dinkel/spamassassin:3.4.0";


---------------------------------------------------------------------
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: JAMES-2719 Migrate and fix ClientProviderImplConnectionTest class with ES6 syntax

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 b79ce0bcc3ebcb7eacf0bfa7e61aac3c7b7f9cae
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed May 15 15:52:04 2019 +0700

    JAMES-2719 Migrate and fix ClientProviderImplConnectionTest class with ES6 syntax
---
 .../es/v6/ClientProviderImplConnectionTest.java    | 43 +++++++++++++---------
 1 file changed, 25 insertions(+), 18 deletions(-)

diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java
index a30d96e..0a36d6a 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ClientProviderImplConnectionTest.java
@@ -23,41 +23,45 @@ import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.james.util.docker.DockerGenericContainer;
+import org.apache.james.util.docker.Images;
 import org.awaitility.Awaitility;
-import org.elasticsearch.client.Client;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.index.query.QueryBuilders;
-import org.junit.Ignore;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-@Ignore("JAMES-1952")
 public class ClientProviderImplConnectionTest {
     private static final Logger LOGGER = LoggerFactory.getLogger(ClientProviderImplConnectionTest.class);
-    private static final String DOCKER_ES_IMAGE = "elasticsearch:2.2.1";
-    private static final int ES_APPLICATIVE_PORT = 9300;
+    private static final int ES_APPLICATIVE_PORT = 9200;
 
-    @Rule
-    public DockerGenericContainer es1 = new DockerGenericContainer(DOCKER_ES_IMAGE)
+    @ClassRule
+    public static DockerGenericContainer es1 = new DockerGenericContainer(Images.ELASTICSEARCH_6)
+        .withEnv("discovery.type", "single-node")
         .withAffinityToContainer()
         .withExposedPorts(ES_APPLICATIVE_PORT);
 
     @Rule
-    public DockerGenericContainer es2 = new DockerGenericContainer(DOCKER_ES_IMAGE)
+    public DockerGenericContainer es2 = new DockerGenericContainer(Images.ELASTICSEARCH_6)
+        .withEnv("discovery.type", "single-node")
         .withAffinityToContainer()
         .withExposedPorts(ES_APPLICATIVE_PORT);
 
     @Test
-    public void connectingASingleServerShouldWork() throws Exception {
+    public void connectingASingleServerShouldWork() {
         Awaitility.await()
             .atMost(1, TimeUnit.MINUTES)
             .pollInterval(5, TimeUnit.SECONDS)
-            .until(() -> isConnected(ClientProviderImpl.forHost(es1.getContainerIp(), 9300, Optional.empty())));
+            .until(() -> isConnected(ClientProviderImpl.forHost(es1.getContainerIp(), ES_APPLICATIVE_PORT, Optional.empty())));
     }
 
     @Test
-    public void connectingAClusterShouldWork() throws Exception {
+    public void connectingAClusterShouldWork() {
         Awaitility.await()
             .atMost(1, TimeUnit.MINUTES)
             .pollInterval(5, TimeUnit.SECONDS)
@@ -69,7 +73,9 @@ public class ClientProviderImplConnectionTest {
     }
 
     @Test
-    public void connectingAClusterWithAFailedNodeShouldWork() throws Exception {
+    public void connectingAClusterWithAFailedNodeShouldWork() {
+        String es1Ip = es1.getContainerIp();
+        String es2Ip = es2.getContainerIp();
         es2.stop();
 
         Awaitility.await()
@@ -77,16 +83,17 @@ public class ClientProviderImplConnectionTest {
             .pollInterval(5, TimeUnit.SECONDS)
             .until(() -> isConnected(
                 ClientProviderImpl.fromHostsString(
-                    es1.getContainerIp() + ":" + ES_APPLICATIVE_PORT + ","
-                        + es2.getContainerIp() + ":" + ES_APPLICATIVE_PORT,
+                    es1Ip + ":" + ES_APPLICATIVE_PORT + ","
+                        + es2Ip + ":" + ES_APPLICATIVE_PORT,
                     Optional.empty())));
     }
 
     private boolean isConnected(ClientProvider clientProvider) {
-        try (Client client = clientProvider.get()) {
-            client.prepareSearch()
-                .setQuery(QueryBuilders.existsQuery("any"))
-                .get();
+        try (RestHighLevelClient client = clientProvider.get()) {
+            client.search(
+                new SearchRequest()
+                    .source(new SearchSourceBuilder().query(QueryBuilders.existsQuery("any"))),
+                RequestOptions.DEFAULT);
             return true;
         } catch (Exception e) {
             LOGGER.info("Caught exception while trying to connect", e);


---------------------------------------------------------------------
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-2149 When I remove a domain, the corresponding aliases are also deleted

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 4d86c003ff9c9772ab8869eec8d87c3365d9769f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue May 14 17:26:44 2019 +0700

    JAMES-2149 When I remove a domain, the corresponding aliases are also deleted
---
 .../james/webadmin/routes/DomainsRoutes.java       | 12 +++++++-
 .../james/webadmin/routes/DomainsRoutesTest.java   | 36 ++++++++++++++++++++++
 2 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
index 8625498..cd047f9 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
@@ -48,6 +48,7 @@ import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.fge.lambdas.Throwing;
 import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -228,10 +229,12 @@ public class DomainsRoutes implements Routes {
         service.put(SPECIFIC_ALIAS, this::addDomainAlias, jsonTransformer);
     }
 
-    private String removeDomain(Request request, Response response) {
+    private String removeDomain(Request request, Response response) throws RecipientRewriteTableException {
         try {
             Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
             domainList.removeDomain(domain);
+
+            removeCorrespondingDomainAliases(domain);
         } catch (DomainListException e) {
             LOGGER.info("{} did not exists", request.params(DOMAIN_NAME));
         }
@@ -239,6 +242,13 @@ public class DomainsRoutes implements Routes {
         return Constants.EMPTY_BODY;
     }
 
+    private void removeCorrespondingDomainAliases(Domain domain) throws RecipientRewriteTableException {
+        MappingSource mappingSource = MappingSource.fromDomain(domain);
+        recipientRewriteTable.getStoredMappings(mappingSource)
+            .asStream()
+            .forEach(Throwing.<Mapping>consumer(mapping -> recipientRewriteTable.removeMapping(mappingSource, mapping)).sneakyThrow());
+    }
+
     private String addDomain(Request request, Response response) {
         Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
         try {
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
index d5f28bd..dea3fc9 100644
--- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
@@ -62,6 +62,7 @@ public class DomainsRoutesTest {
     private static final String ALIAS_DOMAIN = "alias.domain";
     private static final String ALIAS_DOMAIN_2 = "alias.domain.bis";
     public static final String EXTERNAL_DOMAIN = "external.domain.tld";
+    public static final String DOMAIN_2 = "domain2";
 
     private WebAdminServer webAdminServer;
 
@@ -506,6 +507,41 @@ public class DomainsRoutesTest {
                     .body("type", is("InvalidArgument"))
                     .body("message", is("Invalid request for domain creation invalid@domain"));
             }
+
+            @Test
+            void deleteSourceDomainShouldRemoveTheCorrespondingAlias() {
+                with().put(ALIAS_DOMAIN_2);
+                with().put(ALIAS_DOMAIN);
+
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN_2);
+
+                with().delete(ALIAS_DOMAIN_2);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                    .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", containsInAnyOrder(ALIAS_DOMAIN));
+            }
+
+            @Test
+            void deleteDestinationDomainShouldHaveNoImpactOnAliasesAliases() {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+
+                with().put(DOMAIN + "/aliases/" + ALIAS_DOMAIN);
+
+                with().delete(DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/aliases")
+                    .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body("source", containsInAnyOrder(ALIAS_DOMAIN));
+            }
         }
 
     }


---------------------------------------------------------------------
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: JAMES-2719 Migrate ES backend tests to ES6 syntax

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 eed7a8bd8d45ba8245feac5de406c9fa67b5660e
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed May 15 15:51:07 2019 +0700

    JAMES-2719 Migrate ES backend tests to ES6 syntax
---
 .../james/backends/es/v6/DockerElasticSearch.java  |  17 +-
 .../backends/es/v6/DockerElasticSearchRule.java    |   5 -
 .../backends/es/v6/ElasticSearchIndexerTest.java   |  96 +++++-----
 .../backends/es/v6/IndexCreationFactoryTest.java   |   4 +-
 .../backends/es/v6/NodeMappingFactoryTest.java     |  35 ++--
 .../backends/es/v6/search/ScrollIterableTest.java  | 205 ---------------------
 .../es/v6/utils/TestingClientProvider.java         |  37 ----
 7 files changed, 77 insertions(+), 322 deletions(-)

diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java
index 6e159f7..413e7ef 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearch.java
@@ -57,13 +57,14 @@ public class DockerElasticSearch {
     }
 
     private static final int ES_HTTP_PORT = 9200;
-    private static final int ES_TCP_PORT = 9300;
 
     private final DockerGenericContainer eSContainer;
 
     public DockerElasticSearch() {
-        this.eSContainer = new DockerGenericContainer(Images.ELASTICSEARCH_2)
-            .withExposedPorts(ES_HTTP_PORT, ES_TCP_PORT)
+        this.eSContainer = new DockerGenericContainer(Images.ELASTICSEARCH_6)
+            .withEnv("discovery.type", "single-node")
+            .withAffinityToContainer()
+            .withExposedPorts(ES_HTTP_PORT)
             .waitingFor(new HostPortWaitStrategy().withRateLimiter(RateLimiters.TWENTIES_PER_SECOND));
     }
 
@@ -81,18 +82,10 @@ public class DockerElasticSearch {
         return eSContainer.getMappedPort(ES_HTTP_PORT);
     }
 
-    public int getTcpPort() {
-        return eSContainer.getMappedPort(ES_TCP_PORT);
-    }
-
     public String getIp() {
         return eSContainer.getHostIp();
     }
 
-    public Host getTcpHost() {
-        return Host.from(getIp(), getTcpPort());
-    }
-
     public Host getHttpHost() {
         return Host.from(getIp(), getHttpPort());
     }
@@ -117,7 +110,7 @@ public class DockerElasticSearch {
 
     public ClientProvider clientProvider() {
         Optional<String> noClusterName = Optional.empty();
-        return ClientProviderImpl.fromHosts(ImmutableList.of(getTcpHost()), noClusterName);
+        return ClientProviderImpl.fromHosts(ImmutableList.of(getHttpHost()), noClusterName);
     }
 
     private ElasticSearchAPI esAPI() {
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java
index c5b19e5..e945e31 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/DockerElasticSearchRule.java
@@ -19,7 +19,6 @@
 
 package org.apache.james.backends.es.v6;
 
-import org.apache.james.util.Host;
 import org.junit.rules.ExternalResource;
 
 public class DockerElasticSearchRule extends ExternalResource {
@@ -43,8 +42,4 @@ public class DockerElasticSearchRule extends ExternalResource {
     public void awaitForElasticSearch() {
         dockerElasticSearch.awaitForElasticSearch();
     }
-
-    public Host getTcpHost() {
-        return dockerElasticSearch.getTcpHost();
-    }
 }
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
index 4346958..fc2219e 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
@@ -26,9 +26,12 @@ import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 import java.util.concurrent.Executors;
 
 import org.apache.james.util.concurrent.NamedThreadFactory;
+import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.client.Client;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -57,23 +60,24 @@ public class ElasticSearchIndexerTest {
             ALIAS_NAME, TYPE_NAME, MINIMUM_BATCH_SIZE);
     }
 
-    private Client getESClient() {
+    private RestHighLevelClient getESClient() {
         return elasticSearch.clientProvider().get();
     }
 
     @Test
-    public void indexMessageShouldWork() {
+    public void indexMessageShouldWork() throws Exception {
         String messageId = "1";
         String content = "{\"message\": \"trying out Elasticsearch\"}";
         
         testee.index(messageId, content);
         elasticSearch.awaitForElasticSearch();
         
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                    .setTypes(TYPE_NAME.getValue())
-                    .setQuery(QueryBuilders.matchQuery("message", "trying"))
-                    .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchQuery("message", "trying"))),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
         }
     }
@@ -85,7 +89,7 @@ public class ElasticSearchIndexerTest {
     }
     
     @Test
-    public void updateMessages() {
+    public void updateMessages() throws Exception {
         String messageId = "1";
         String content = "{\"message\": \"trying out Elasticsearch\",\"field\":\"Should be unchanged\"}";
 
@@ -95,19 +99,21 @@ public class ElasticSearchIndexerTest {
         testee.update(ImmutableList.of(new UpdatedRepresentation(messageId, "{\"message\": \"mastering out Elasticsearch\"}")));
         elasticSearch.awaitForElasticSearch();
 
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setQuery(QueryBuilders.matchQuery("message", "mastering"))
-                .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchQuery("message", "mastering"))),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
         }
 
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setQuery(QueryBuilders.matchQuery("field", "unchanged"))
-                .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchQuery("field", "unchanged"))),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
         }
     }
@@ -147,11 +153,12 @@ public class ElasticSearchIndexerTest {
         testee.deleteAllMatchingQuery(termQuery("property", "1")).get();
         elasticSearch.awaitForElasticSearch();
         
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                    .setTypes(TYPE_NAME.getValue())
-                    .setQuery(QueryBuilders.matchAllQuery())
-                    .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(0);
         }
     }
@@ -177,17 +184,18 @@ public class ElasticSearchIndexerTest {
         testee.deleteAllMatchingQuery(termQuery("property", "1")).get();
         elasticSearch.awaitForElasticSearch();
         
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                    .setTypes(TYPE_NAME.getValue())
-                    .setQuery(QueryBuilders.matchAllQuery())
-                    .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
         }
     }
     
     @Test
-    public void deleteMessage() {
+    public void deleteMessage() throws Exception {
         String messageId = "1:2";
         String content = "{\"message\": \"trying out Elasticsearch\"}";
 
@@ -197,17 +205,18 @@ public class ElasticSearchIndexerTest {
         testee.delete(ImmutableList.of(messageId));
         elasticSearch.awaitForElasticSearch();
         
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                    .setTypes(TYPE_NAME.getValue())
-                    .setQuery(QueryBuilders.matchAllQuery())
-                    .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(0);
         }
     }
 
     @Test
-    public void deleteShouldWorkWhenMultipleMessages() {
+    public void deleteShouldWorkWhenMultipleMessages() throws Exception {
         String messageId = "1:1";
         String content = "{\"message\": \"trying out Elasticsearch\", \"mailboxId\":\"1\"}";
 
@@ -227,22 +236,23 @@ public class ElasticSearchIndexerTest {
         testee.delete(ImmutableList.of(messageId, messageId3));
         elasticSearch.awaitForElasticSearch();
 
-        try (Client client = getESClient()) {
-            SearchResponse searchResponse = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setQuery(QueryBuilders.matchAllQuery())
-                .get();
+        try (RestHighLevelClient client = getESClient()) {
+            SearchResponse searchResponse = client.search(
+                new SearchRequest(INDEX_NAME.getValue())
+                    .types(TYPE_NAME.getValue())
+                    .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
+                RequestOptions.DEFAULT);
             assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
         }
     }
     
     @Test
-    public void updateMessagesShouldNotThrowWhenEmptyList() {
+    public void updateMessagesShouldNotThrowWhenEmptyList() throws Exception {
         testee.update(ImmutableList.of());
     }
     
     @Test
-    public void deleteMessagesShouldNotThrowWhenEmptyList() {
+    public void deleteMessagesShouldNotThrowWhenEmptyList() throws Exception {
         testee.delete(ImmutableList.of());
     }
 }
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
index a5417eb..4f1efd3 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
@@ -26,8 +26,8 @@ import org.junit.Rule;
 import org.junit.Test;
 
 public class IndexCreationFactoryTest {
-    public static final IndexName INDEX_NAME = new IndexName("index");
-    public static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
+    private static final IndexName INDEX_NAME = new IndexName("index");
+    private static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
 
     @Rule
     public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java
index bcbefef..95207ff 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/NodeMappingFactoryTest.java
@@ -19,18 +19,20 @@
 
 package org.apache.james.backends.es.v6;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 
+import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
 public class NodeMappingFactoryTest {
-    public static final String MESSAGE = "message";
-    public static final IndexName INDEX_NAME = new IndexName("index");
-    public static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
-    public static final TypeName TYPE_NAME = new TypeName("type");
+    private static final String MESSAGE = "message";
+    private static final IndexName INDEX_NAME = new IndexName("index");
+    private static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
+    private static final TypeName TYPE_NAME = new TypeName("type");
 
     @Rule
     public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
@@ -58,7 +60,7 @@ public class NodeMappingFactoryTest {
     }
 
     @Test
-    public void applyMappingShouldNotThrowWhenIndexerChanges() throws Exception {
+    public void applyMappingShouldThrowWhenTryingIndexerChanges() throws Exception {
         NodeMappingFactory.applyMapping(clientProvider.get(),
             INDEX_NAME,
             TYPE_NAME,
@@ -66,20 +68,19 @@ public class NodeMappingFactoryTest {
 
         elasticSearch.awaitForElasticSearch();
 
-        NodeMappingFactory.applyMapping(clientProvider.get(),
+        assertThatThrownBy(() ->NodeMappingFactory.applyMapping(clientProvider.get(),
             INDEX_NAME,
             TYPE_NAME,
-            getOtherMappingsSources());
+            getOtherMappingsSources()))
+        .isInstanceOf(ElasticsearchStatusException.class);
     }
 
     private XContentBuilder getMappingsSources() throws Exception {
         return jsonBuilder()
             .startObject()
-                .startObject(TYPE_NAME.getValue())
-                    .startObject(NodeMappingFactory.PROPERTIES)
-                        .startObject(MESSAGE)
-                            .field(NodeMappingFactory.TYPE, NodeMappingFactory.STRING)
-                        .endObject()
+                .startObject(NodeMappingFactory.PROPERTIES)
+                    .startObject(MESSAGE)
+                        .field(NodeMappingFactory.TYPE, NodeMappingFactory.TEXT)
                     .endObject()
                 .endObject()
             .endObject();
@@ -88,12 +89,10 @@ public class NodeMappingFactoryTest {
     private XContentBuilder getOtherMappingsSources() throws Exception {
         return jsonBuilder()
             .startObject()
-                .startObject(TYPE_NAME.getValue())
-                    .startObject(NodeMappingFactory.PROPERTIES)
-                        .startObject(MESSAGE)
-                            .field(NodeMappingFactory.TYPE, NodeMappingFactory.STRING)
-                            .field(NodeMappingFactory.INDEX, NodeMappingFactory.NOT_ANALYZED)
-                        .endObject()
+                .startObject(NodeMappingFactory.PROPERTIES)
+                    .startObject(MESSAGE)
+                        .field(NodeMappingFactory.TYPE, NodeMappingFactory.TEXT)
+                        .field(NodeMappingFactory.INDEX, false)
                     .endObject()
                 .endObject()
             .endObject();
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/search/ScrollIterableTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/search/ScrollIterableTest.java
deleted file mode 100644
index 4810975..0000000
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/search/ScrollIterableTest.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/****************************************************************
- * 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.backends.es.v6.search;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
-import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
-import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.apache.james.backends.es.v6.ClientProvider;
-import org.apache.james.backends.es.v6.DockerElasticSearchRule;
-import org.apache.james.backends.es.v6.ElasticSearchConfiguration;
-import org.apache.james.backends.es.v6.IndexCreationFactory;
-import org.apache.james.backends.es.v6.IndexName;
-import org.apache.james.backends.es.v6.NodeMappingFactory;
-import org.apache.james.backends.es.v6.ReadAliasName;
-import org.apache.james.backends.es.v6.TypeName;
-import org.awaitility.Duration;
-import org.awaitility.core.ConditionFactory;
-import org.elasticsearch.action.search.SearchRequestBuilder;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.index.query.QueryBuilders;
-import org.elasticsearch.search.SearchHit;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class ScrollIterableTest {
-
-    private static final TimeValue TIMEOUT = new TimeValue(6000);
-    private static final int SIZE = 2;
-    private static final String MESSAGE = "message";
-    private static final IndexName INDEX_NAME = new IndexName("index");
-    private static final ReadAliasName ALIAS_NAME = new ReadAliasName("alias");
-    private static final TypeName TYPE_NAME = new TypeName("messages");
-
-    private static final ConditionFactory WAIT_CONDITION = await().timeout(Duration.FIVE_SECONDS);
-
-    @Rule
-    public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
-    private ClientProvider clientProvider;
-
-    @Before
-    public void setUp() throws Exception {
-        clientProvider = elasticSearch.clientProvider();
-        new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
-            .useIndex(INDEX_NAME)
-            .addAlias(ALIAS_NAME)
-            .createIndexAndAliases(clientProvider.get());
-        elasticSearch.awaitForElasticSearch();
-        NodeMappingFactory.applyMapping(clientProvider.get(), INDEX_NAME, TYPE_NAME, getMappingsSources());
-    }
-
-    private XContentBuilder getMappingsSources() throws IOException {
-        return jsonBuilder()
-            .startObject()
-                .startObject(TYPE_NAME.getValue())
-                    .startObject(NodeMappingFactory.PROPERTIES)
-                        .startObject(MESSAGE)
-                            .field(NodeMappingFactory.TYPE, NodeMappingFactory.STRING)
-                        .endObject()
-                    .endObject()
-                .endObject()
-            .endObject();
-    }
-
-    @Test
-    public void scrollIterableShouldWorkWhenEmpty() {
-        try (Client client = clientProvider.get()) {
-            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setScroll(TIMEOUT)
-                .setQuery(matchAllQuery())
-                .setSize(SIZE);
-
-            assertThat(new ScrollIterable(client, searchRequestBuilder))
-                .isEmpty();
-        }
-    }
-
-    @Test
-    public void scrollIterableShouldWorkWhenOneElement() {
-        try (Client client = clientProvider.get()) {
-            String id = "1";
-            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id)
-                .setSource(MESSAGE, "Sample message")
-                .execute();
-
-            elasticSearch.awaitForElasticSearch();
-            WAIT_CONDITION.untilAsserted(() -> hasIdsInIndex(client, id));
-
-            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setScroll(TIMEOUT)
-                .setQuery(matchAllQuery())
-                .setSize(SIZE);
-
-            assertThat(convertToIdList(new ScrollIterable(client, searchRequestBuilder)))
-                .containsOnly(id);
-        }
-    }
-
-    @Test
-    public void scrollIterableShouldWorkWhenSizeElement() {
-        try (Client client = clientProvider.get()) {
-            String id1 = "1";
-            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id1)
-                .setSource(MESSAGE, "Sample message")
-                .execute();
-
-            String id2 = "2";
-            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id2)
-                .setSource(MESSAGE, "Sample message")
-                .execute();
-
-            elasticSearch.awaitForElasticSearch();
-            WAIT_CONDITION.untilAsserted(() -> hasIdsInIndex(client, id1, id2));
-
-            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setScroll(TIMEOUT)
-                .setQuery(matchAllQuery())
-                .setSize(SIZE);
-
-            assertThat(convertToIdList(new ScrollIterable(client, searchRequestBuilder)))
-                .containsOnly(id1, id2);
-        }
-    }
-
-    @Test
-    public void scrollIterableShouldWorkWhenMoreThanSizeElement() {
-        try (Client client = clientProvider.get()) {
-            String id1 = "1";
-            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id1)
-                .setSource(MESSAGE, "Sample message")
-                .execute();
-
-            String id2 = "2";
-            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id2)
-                .setSource(MESSAGE, "Sample message")
-                .execute();
-
-            String id3 = "3";
-            client.prepareIndex(INDEX_NAME.getValue(), TYPE_NAME.getValue(), id3)
-                .setSource(MESSAGE, "Sample message")
-                .execute();
-
-            elasticSearch.awaitForElasticSearch();
-            WAIT_CONDITION.untilAsserted(() -> hasIdsInIndex(client, id1, id2, id3));
-
-            SearchRequestBuilder searchRequestBuilder = client.prepareSearch(INDEX_NAME.getValue())
-                .setTypes(TYPE_NAME.getValue())
-                .setScroll(TIMEOUT)
-                .setQuery(matchAllQuery())
-                .setSize(SIZE);
-
-            assertThat(convertToIdList(new ScrollIterable(client, searchRequestBuilder)))
-                .containsOnly(id1, id2, id3);
-        }
-    }
-
-    private List<String> convertToIdList(ScrollIterable scrollIterable) {
-        return scrollIterable.stream()
-            .flatMap(searchResponse -> Arrays.stream(searchResponse.getHits().getHits()))
-            .map(SearchHit::getId)
-            .collect(Collectors.toList());
-    }
-
-    private void hasIdsInIndex(Client client, String... ids) {
-        SearchHit[] hits = client.prepareSearch(INDEX_NAME.getValue())
-            .setQuery(QueryBuilders.idsQuery(TYPE_NAME.getValue()).addIds(ids))
-            .execute()
-            .actionGet()
-            .getHits()
-            .hits();
-
-        assertThat(hits)
-            .extracting(SearchHit::getId)
-            .contains(ids);
-    }
-}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/utils/TestingClientProvider.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/utils/TestingClientProvider.java
deleted file mode 100644
index 967b80e..0000000
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/utils/TestingClientProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/****************************************************************
- * 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.backends.es.v6.utils;
-
-import org.apache.james.backends.es.v6.ClientProvider;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.node.Node;
-
-public class TestingClientProvider implements ClientProvider {
-
-    private final Node node;
-
-    public TestingClientProvider(Node node) {
-        this.node = node;
-    }
-    
-    @Override
-    public Client get() {
-        return node.client();
-    }
-}


---------------------------------------------------------------------
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-2149 Extract 'returnNoContent' is a static method

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 bbfb2fb9fed8e64068793ed0739807c05eb6aa7a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed May 15 11:00:24 2019 +0700

    JAMES-2149 Extract 'returnNoContent' is a static method
---
 .../apache/james/webadmin/routes/TasksRoutes.java  |  5 ++--
 .../org/apache/james/webadmin/utils/Responses.java | 32 ++++++++++++++++++++++
 .../webadmin/routes/DLPConfigurationRoutes.java    |  8 ++----
 .../james/webadmin/routes/SieveQuotaRoutes.java    | 21 +++++---------
 .../apache/james/webadmin/routes/UserRoutes.java   |  5 ++--
 .../apache/james/webadmin/service/UserService.java |  3 +-
 .../james/webadmin/routes/DomainQuotaRoutes.java   | 22 ++++++---------
 .../webadmin/routes/EventDeadLettersRoutes.java    |  6 ++--
 .../james/webadmin/routes/GlobalQuotaRoutes.java   | 16 ++++-------
 .../james/webadmin/routes/UserMailboxesRoutes.java | 13 ++++-----
 .../james/webadmin/routes/UserQuotaRoutes.java     | 22 ++++++---------
 .../james/webadmin/routes/MailQueueRoutes.java     |  6 ++--
 .../webadmin/routes/MailRepositoriesRoutes.java    |  7 ++---
 13 files changed, 84 insertions(+), 82 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/TasksRoutes.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/TasksRoutes.java
index 02305b8..7dd1204 100644
--- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/TasksRoutes.java
+++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/TasksRoutes.java
@@ -34,11 +34,11 @@ import org.apache.james.task.TaskExecutionDetails;
 import org.apache.james.task.TaskId;
 import org.apache.james.task.TaskManager;
 import org.apache.james.task.TaskNotFoundException;
-import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.ExecutionDetailsDto;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 
 import io.swagger.annotations.Api;
@@ -166,8 +166,7 @@ public class TasksRoutes implements Routes {
     public Object cancel(Request req, Response response) {
         TaskId taskId = getTaskId(req);
         taskManager.cancel(taskId);
-        response.status(HttpStatus.NO_CONTENT_204);
-        return Constants.EMPTY_BODY;
+        return Responses.returnNoContent(response);
     }
 
     private TaskId getTaskId(Request req) {
diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/Responses.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/Responses.java
new file mode 100644
index 0000000..52d943a
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/Responses.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.utils;
+
+import org.apache.james.webadmin.Constants;
+import org.eclipse.jetty.http.HttpStatus;
+
+import spark.Response;
+
+public class Responses {
+    public static String returnNoContent(Response response) {
+        response.status(HttpStatus.NO_CONTENT_204);
+        return Constants.EMPTY_BODY;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DLPConfigurationRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DLPConfigurationRoutes.java
index 1b550b9..2a8769c 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DLPConfigurationRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DLPConfigurationRoutes.java
@@ -20,7 +20,6 @@
 package org.apache.james.webadmin.routes;
 
 import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
-import static org.apache.james.webadmin.Constants.EMPTY_BODY;
 import static org.apache.james.webadmin.Constants.JSON_CONTENT_TYPE;
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 
@@ -46,6 +45,7 @@ import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 
 import io.swagger.annotations.Api;
@@ -131,8 +131,7 @@ public class DLPConfigurationRoutes implements Routes {
 
             dlpConfigurationStore.store(senderDomain, rules);
 
-            response.status(HttpStatus.NO_CONTENT_204);
-            return EMPTY_BODY;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -197,8 +196,7 @@ public class DLPConfigurationRoutes implements Routes {
             Domain senderDomain = parseDomain(request);
             dlpConfigurationStore.clear(senderDomain);
 
-            response.status(HttpStatus.NO_CONTENT_204);
-            return EMPTY_BODY;
+            return Responses.returnNoContent(response);
         }, jsonTransformer);
     }
 
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/SieveQuotaRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/SieveQuotaRoutes.java
index d0d8cc5..5ae5def 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/SieveQuotaRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/SieveQuotaRoutes.java
@@ -19,7 +19,6 @@
 
 package org.apache.james.webadmin.routes;
 
-import static org.apache.james.webadmin.Constants.EMPTY_BODY;
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 
 import javax.inject.Inject;
@@ -33,12 +32,12 @@ import org.apache.james.core.User;
 import org.apache.james.core.quota.QuotaSize;
 import org.apache.james.sieverepository.api.SieveQuotaRepository;
 import org.apache.james.sieverepository.api.exception.QuotaNotFoundException;
-import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.JsonExtractException;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -107,8 +106,7 @@ public class SieveQuotaRoutes implements Routes {
                 response.status(HttpStatus.OK_200);
                 return sieveQuota.asLong();
             } catch (QuotaNotFoundException e) {
-                response.status(HttpStatus.NO_CONTENT_204);
-                return EMPTY_BODY;
+                return Responses.returnNoContent(response);
             }
         }, jsonTransformer);
     }
@@ -128,8 +126,7 @@ public class SieveQuotaRoutes implements Routes {
             try {
                 QuotaSize requestedSize = extractRequestedQuotaSizeFromRequest(request);
                 sieveQuotaRepository.setDefaultQuota(requestedSize);
-                response.status(HttpStatus.NO_CONTENT_204);
-                return Constants.EMPTY_BODY;
+                return Responses.returnNoContent(response);
             } catch (JsonExtractException e) {
                 LOGGER.info("Malformed JSON", e);
                 throw ErrorResponder.builder()
@@ -155,8 +152,7 @@ public class SieveQuotaRoutes implements Routes {
             } catch (QuotaNotFoundException e) {
                 // Do nothing
             }
-            response.status(HttpStatus.NO_CONTENT_204);
-            return Constants.EMPTY_BODY;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -178,8 +174,7 @@ public class SieveQuotaRoutes implements Routes {
                 response.status(HttpStatus.OK_200);
                 return userQuota.asLong();
             } catch (QuotaNotFoundException e) {
-                response.status(HttpStatus.NO_CONTENT_204);
-                return EMPTY_BODY;
+                return Responses.returnNoContent(response);
             }
         }, jsonTransformer);
     }
@@ -201,7 +196,7 @@ public class SieveQuotaRoutes implements Routes {
             try {
                 QuotaSize requestedSize = extractRequestedQuotaSizeFromRequest(request);
                 sieveQuotaRepository.setQuota(userId, requestedSize);
-                response.status(HttpStatus.NO_CONTENT_204);
+                return Responses.returnNoContent(response);
             } catch (JsonExtractException e) {
                 LOGGER.info("Malformed JSON", e);
                 throw ErrorResponder.builder()
@@ -211,7 +206,6 @@ public class SieveQuotaRoutes implements Routes {
                     .cause(e)
                     .haltError();
             }
-            return Constants.EMPTY_BODY;
         }, jsonTransformer);
     }
 
@@ -232,8 +226,7 @@ public class SieveQuotaRoutes implements Routes {
             } catch (QuotaNotFoundException e) {
                 // Do nothing
             }
-            response.status(HttpStatus.NO_CONTENT_204);
-            return Constants.EMPTY_BODY;
+            return Responses.returnNoContent(response);
         });
     }
 
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
index aa373a6..6054d0e 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
@@ -29,7 +29,6 @@ import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 
 import org.apache.james.user.api.UsersRepositoryException;
-import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.AddUserRequest;
 import org.apache.james.webadmin.dto.UserResponse;
@@ -39,6 +38,7 @@ import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonExtractException;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -142,8 +142,7 @@ public class UserRoutes implements Routes {
         String username = request.params(USER_NAME);
         try {
             userService.removeUser(username);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return Constants.EMPTY_BODY;
+            return Responses.returnNoContent(response);
         } catch (UsersRepositoryException e) {
             throw ErrorResponder.builder()
                 .statusCode(HttpStatus.NO_CONTENT_204)
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
index 3b09e4b..6f6fe41 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
@@ -30,6 +30,7 @@ import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.api.model.User;
 import org.apache.james.util.streams.Iterators;
 import org.apache.james.webadmin.dto.UserResponse;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -71,7 +72,7 @@ public class UserService {
         User user = usersRepository.getUserByName(username);
         try {
             upsert(user, username, password);
-            response.status(HttpStatus.NO_CONTENT_204);
+            return Responses.returnNoContent(response);
         } catch (UsersRepositoryException e) {
             LOGGER.info("Error creating or updating user : {}", e.getMessage());
             response.status(HttpStatus.CONFLICT_409);
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/DomainQuotaRoutes.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/DomainQuotaRoutes.java
index 8c33381..c8da44f 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/DomainQuotaRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/DomainQuotaRoutes.java
@@ -47,6 +47,7 @@ import org.apache.james.webadmin.utils.JsonExtractException;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.JsonTransformerModule;
+import org.apache.james.webadmin.utils.Responses;
 import org.apache.james.webadmin.validation.Quotas;
 import org.eclipse.jetty.http.HttpStatus;
 
@@ -133,8 +134,7 @@ public class DomainQuotaRoutes implements Routes {
             Domain domain = checkDomainExist(request);
             QuotaDTO quotaDTO = parseQuotaDTO(request);
             domainQuotaService.defineQuota(domain, quotaDTO);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         }));
     }
 
@@ -169,8 +169,7 @@ public class DomainQuotaRoutes implements Routes {
         service.delete(SIZE_ENDPOINT, (request, response) -> {
             Domain domain = checkDomainExist(request);
             domainQuotaService.remoteMaxQuotaSize(domain);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -193,8 +192,7 @@ public class DomainQuotaRoutes implements Routes {
             Domain domain = checkDomainExist(request);
             QuotaSize quotaSize = Quotas.quotaSize(request.body());
             domainQuotaService.setMaxSizeQuota(domain, quotaSize);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -215,8 +213,7 @@ public class DomainQuotaRoutes implements Routes {
             if (maxSizeQuota.isPresent()) {
                 return maxSizeQuota;
             }
-            response.status(HttpStatus.NO_CONTENT_204);
-            return null;
+            return Responses.returnNoContent(response);
         }, jsonTransformer);
     }
 
@@ -233,8 +230,7 @@ public class DomainQuotaRoutes implements Routes {
         service.delete(COUNT_ENDPOINT, (request, response) -> {
             Domain domain = checkDomainExist(request);
             domainQuotaService.remoteMaxQuotaCount(domain);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -257,8 +253,7 @@ public class DomainQuotaRoutes implements Routes {
             Domain domain = checkDomainExist(request);
             QuotaCount quotaCount = Quotas.quotaCount(request.body());
             domainQuotaService.setMaxCountQuota(domain, quotaCount);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -278,8 +273,7 @@ public class DomainQuotaRoutes implements Routes {
             if (maxCountQuota.isPresent()) {
                 return maxCountQuota;
             }
-            response.status(HttpStatus.NO_CONTENT_204);
-            return null;
+            return Responses.returnNoContent(response);
         }, jsonTransformer);
     }
 
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/EventDeadLettersRoutes.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/EventDeadLettersRoutes.java
index 8921a3b..6f21a88 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/EventDeadLettersRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/EventDeadLettersRoutes.java
@@ -42,6 +42,7 @@ import org.apache.james.webadmin.dto.TaskIdDto;
 import org.apache.james.webadmin.service.EventDeadLettersService;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 
 import io.swagger.annotations.Api;
@@ -252,13 +253,12 @@ public class EventDeadLettersRoutes implements Routes {
         @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Invalid group name or insertion id"),
         @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = INTERNAL_SERVER_ERROR)
     })
-    private Response deleteEvent(Request request, Response response) {
+    private String deleteEvent(Request request, Response response) {
         Group group = parseGroup(request);
         EventDeadLetters.InsertionId insertionId = parseInsertionId(request);
 
         eventDeadLettersService.deleteEvent(group, insertionId);
-        response.status(HttpStatus.NO_CONTENT_204);
-        return response;
+        return Responses.returnNoContent(response);
     }
 
     @POST
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/GlobalQuotaRoutes.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/GlobalQuotaRoutes.java
index 623ded1..c4df685 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/GlobalQuotaRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/GlobalQuotaRoutes.java
@@ -40,6 +40,7 @@ import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonExtractException;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.apache.james.webadmin.validation.Quotas;
 import org.eclipse.jetty.http.HttpStatus;
 
@@ -115,8 +116,7 @@ public class GlobalQuotaRoutes implements Routes {
             try {
                 QuotaDTO quotaDTO = jsonExtractor.parse(request.body());
                 globalQuotaService.defineQuota(quotaDTO);
-                response.status(HttpStatus.NO_CONTENT_204);
-                return response;
+                return Responses.returnNoContent(response);
             } catch (JsonExtractException e) {
                 throw ErrorResponder.builder()
                     .statusCode(HttpStatus.BAD_REQUEST_400)
@@ -158,8 +158,7 @@ public class GlobalQuotaRoutes implements Routes {
     public void defineDeleteQuotaSize() {
         service.delete(SIZE_ENDPOINT, (request, response) -> {
             globalQuotaService.deleteMaxSizeQuota();
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -178,8 +177,7 @@ public class GlobalQuotaRoutes implements Routes {
         service.put(SIZE_ENDPOINT, (request, response) -> {
             QuotaSize quotaSize = Quotas.quotaSize(request.body());
             globalQuotaService.defineMaxSizeQuota(quotaSize);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -213,8 +211,7 @@ public class GlobalQuotaRoutes implements Routes {
     public void defineDeleteQuotaCount() {
         service.delete(COUNT_ENDPOINT, (request, response) -> {
             globalQuotaService.deleteMaxCountQuota();
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -233,8 +230,7 @@ public class GlobalQuotaRoutes implements Routes {
         service.put(COUNT_ENDPOINT, (request, response) -> {
             QuotaCount quotaRequest = Quotas.quotaCount(request.body());
             globalQuotaService.defineMaxCountQuota(quotaRequest);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserMailboxesRoutes.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserMailboxesRoutes.java
index 97303c8..95f324d 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserMailboxesRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserMailboxesRoutes.java
@@ -33,6 +33,7 @@ import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.MailboxHaveChildrenException;
+import org.apache.james.webadmin.utils.Responses;
 import org.apache.james.webadmin.validation.MailboxName;
 import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
@@ -136,7 +137,7 @@ public class UserMailboxesRoutes implements Routes {
         service.delete(SPECIFIC_MAILBOX, (request, response) -> {
             try {
                 userMailboxesService.deleteMailbox(request.params(USER_NAME), new MailboxName(request.params(MAILBOX_NAME)));
-                response.status(HttpStatus.NO_CONTENT_204);
+                return Responses.returnNoContent(response);
             } catch (IllegalStateException e) {
                 LOGGER.info("Invalid delete on user mailbox", e);
                 throw ErrorResponder.builder()
@@ -162,7 +163,6 @@ public class UserMailboxesRoutes implements Routes {
                     .cause(e)
                     .haltError();
             }
-            return Constants.EMPTY_BODY;
         });
     }
 
@@ -181,7 +181,7 @@ public class UserMailboxesRoutes implements Routes {
         service.delete(USER_MAILBOXES_BASE, (request, response) -> {
             try {
                 userMailboxesService.deleteMailboxes(request.params(USER_NAME));
-                response.status(HttpStatus.NO_CONTENT_204);
+                return Responses.returnNoContent(response);
             } catch (IllegalStateException e) {
                 LOGGER.info("Invalid delete on user mailboxes", e);
                 throw ErrorResponder.builder()
@@ -191,7 +191,6 @@ public class UserMailboxesRoutes implements Routes {
                     .cause(e)
                     .haltError();
             }
-            return Constants.EMPTY_BODY;
         });
     }
 
@@ -213,7 +212,7 @@ public class UserMailboxesRoutes implements Routes {
         service.get(SPECIFIC_MAILBOX, (request, response) -> {
             try {
                 if (userMailboxesService.testMailboxExists(request.params(USER_NAME), new MailboxName(request.params(MAILBOX_NAME)))) {
-                    response.status(HttpStatus.NO_CONTENT_204);
+                    return Responses.returnNoContent(response);
                 } else {
                     throw ErrorResponder.builder()
                         .statusCode(HttpStatus.NOT_FOUND_404)
@@ -238,7 +237,6 @@ public class UserMailboxesRoutes implements Routes {
                     .cause(e)
                     .haltError();
             }
-            return Constants.EMPTY_BODY;
         });
     }
 
@@ -260,7 +258,7 @@ public class UserMailboxesRoutes implements Routes {
         service.put(SPECIFIC_MAILBOX, (request, response) -> {
             try {
                 userMailboxesService.createMailbox(request.params(USER_NAME), new MailboxName(request.params(MAILBOX_NAME)));
-                response.status(HttpStatus.NO_CONTENT_204);
+                return Responses.returnNoContent(response);
             } catch (IllegalStateException e) {
                 LOGGER.info("Invalid put on user mailbox", e);
                 throw ErrorResponder.builder()
@@ -278,7 +276,6 @@ public class UserMailboxesRoutes implements Routes {
                     .cause(e)
                     .haltError();
             }
-            return Constants.EMPTY_BODY;
         });
     }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java
index 11e4166..7b1d201 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java
@@ -54,6 +54,7 @@ import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.JsonTransformerModule;
 import org.apache.james.webadmin.utils.ParametersExtractor;
+import org.apache.james.webadmin.utils.Responses;
 import org.apache.james.webadmin.validation.Quotas;
 import org.eclipse.jetty.http.HttpStatus;
 
@@ -134,8 +135,7 @@ public class UserQuotaRoutes implements Routes {
             User user = checkUserExist(request);
             QuotaDTO quotaDTO = parseQuotaDTO(request);
             userQuotaService.defineQuota(user, quotaDTO);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         }));
     }
 
@@ -250,8 +250,7 @@ public class UserQuotaRoutes implements Routes {
         service.delete(SIZE_ENDPOINT, (request, response) -> {
             User user = checkUserExist(request);
             userQuotaService.deleteMaxSizeQuota(user);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -273,8 +272,7 @@ public class UserQuotaRoutes implements Routes {
             User user = checkUserExist(request);
             QuotaSize quotaSize = Quotas.quotaSize(request.body());
             userQuotaService.defineMaxSizeQuota(user, quotaSize);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -294,8 +292,7 @@ public class UserQuotaRoutes implements Routes {
             if (maxSizeQuota.isPresent()) {
                 return maxSizeQuota;
             }
-            response.status(HttpStatus.NO_CONTENT_204);
-            return null;
+            return Responses.returnNoContent(response);
         }, jsonTransformer);
     }
 
@@ -311,8 +308,7 @@ public class UserQuotaRoutes implements Routes {
         service.delete(COUNT_ENDPOINT, (request, response) -> {
             User user = checkUserExist(request);
             userQuotaService.deleteMaxCountQuota(user);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -334,8 +330,7 @@ public class UserQuotaRoutes implements Routes {
             User user = checkUserExist(request);
             QuotaCount quotaCount = Quotas.quotaCount(request.body());
             userQuotaService.defineMaxCountQuota(user, quotaCount);
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         });
     }
 
@@ -355,8 +350,7 @@ public class UserQuotaRoutes implements Routes {
             if (maxCountQuota.isPresent()) {
                 return maxCountQuota;
             }
-            response.status(HttpStatus.NO_CONTENT_204);
-            return null;
+            return Responses.returnNoContent(response);
         }, jsonTransformer);
     }
 
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/routes/MailQueueRoutes.java b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/routes/MailQueueRoutes.java
index 7fe0788..1b0b86e 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/routes/MailQueueRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/routes/MailQueueRoutes.java
@@ -53,6 +53,7 @@ import org.apache.james.webadmin.utils.JsonExtractException;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.ParametersExtractor;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 
 import com.github.fge.lambdas.Throwing;
@@ -382,15 +383,14 @@ public class MailQueueRoutes implements Routes {
             jsonTransformer);
     }
 
-    private Response forceDelayedMailsDelivery(Request request, Response response) throws JsonExtractException, MailQueueException {
+    private String forceDelayedMailsDelivery(Request request, Response response) throws JsonExtractException, MailQueueException {
         assertDelayedParamIsTrue(request);
         assertPayloadContainsDelayedEntry(request);
         ManageableMailQueue mailQueue = assertMailQueueExists(request);
 
         mailQueue.flush();
 
-        response.status(HttpStatus.NO_CONTENT_204);
-        return response;
+        return Responses.returnNoContent(response);
     }
 
     private ManageableMailQueue assertMailQueueExists(Request request) {
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java
index 5260653..7a956a1 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java
@@ -61,6 +61,7 @@ import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.ParametersExtractor;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 
 import com.github.steveash.guavate.Guavate;
@@ -148,8 +149,7 @@ public class MailRepositoriesRoutes implements Routes {
             String protocol = request.queryParams("protocol");
             try {
                 repositoryStoreService.createMailRepository(path, protocol);
-                response.status(HttpStatus.NO_CONTENT_204);
-                return Constants.EMPTY_BODY;
+                return Responses.returnNoContent(response);
             } catch (MailRepositoryStore.MailRepositoryStoreException e) {
                 throw ErrorResponder.builder()
                     .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
@@ -359,9 +359,8 @@ public class MailRepositoriesRoutes implements Routes {
             MailRepositoryPath path = decodedRepositoryPath(request);
             MailKey mailKey = new MailKey(request.params("mailKey"));
             try {
-                response.status(HttpStatus.NO_CONTENT_204);
                 repositoryStoreService.deleteMail(path, mailKey);
-                return Constants.EMPTY_BODY;
+                return Responses.returnNoContent(response);
             } catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
                 throw ErrorResponder.builder()
                     .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)


---------------------------------------------------------------------
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: JAMES-2719 Refactor IndexCreationFactory to an immutable structure

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 2d5473fd86aff3ce91fb64ada9e7fc4d5a39864f
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu May 16 10:31:26 2019 +0700

    JAMES-2719 Refactor IndexCreationFactory to an immutable structure
---
 .../james/backends/es/v6/IndexCreationFactory.java | 224 ++++++++++++---------
 .../backends/es/v6/IndexCreationFactoryTest.java   |   1 +
 2 files changed, 126 insertions(+), 99 deletions(-)

diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
index 1a520fa..2cba4ad 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/IndexCreationFactory.java
@@ -23,7 +23,6 @@ import static org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 
 import java.io.IOException;
-import java.util.ArrayList;
 
 import javax.inject.Inject;
 
@@ -39,125 +38,152 @@ import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 
 public class IndexCreationFactory {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(IndexCreationFactory.class);
-    private static final String INDEX_ALREADY_EXISTS_EXCEPTION_MESSAGE = "type=resource_already_exists_exception";
-    private static final String CASE_INSENSITIVE = "case_insensitive";
-    private static final String KEEP_MAIL_AND_URL = "keep_mail_and_url";
-    private static final String SNOWBALL_KEEP_MAIL_AND_URL = "snowball_keep_mail_and_token";
-    private static final String ENGLISH_SNOWBALL = "english_snowball";
+    static class AliasSpecificationStep {
+        private final int nbShards;
+        private final int nbReplica;
+        private final IndexName indexName;
+        private final ImmutableList.Builder<AliasName> aliases;
+
+        AliasSpecificationStep(int nbShards, int nbReplica, IndexName indexName) {
+            this.nbShards = nbShards;
+            this.nbReplica = nbReplica;
+            this.indexName = indexName;
+            this.aliases = ImmutableList.builder();
+        }
 
-    private IndexName indexName;
-    private ArrayList<AliasName> aliases;
-    private int nbShards;
-    private int nbReplica;
+        public AliasSpecificationStep addAlias(AliasName aliasName) {
+            Preconditions.checkNotNull(aliasName);
+            this.aliases.add(aliasName);
+            return this;
+        }
 
-    @Inject
-    public IndexCreationFactory(ElasticSearchConfiguration configuration) {
-        indexName = null;
-        aliases = new ArrayList<>();
-        nbShards = configuration.getNbShards();
-        nbReplica = configuration.getNbReplica();
+        public RestHighLevelClient createIndexAndAliases(RestHighLevelClient client) {
+            return new IndexCreationPerformer(nbShards, nbReplica, indexName, aliases.build()).createIndexAndAliases(client);
+        }
     }
 
-    public IndexCreationFactory useIndex(IndexName indexName) {
-        Preconditions.checkNotNull(indexName);
-        this.indexName = indexName;
-        return this;
-    }
+    static class IndexCreationPerformer {
+        private final int nbShards;
+        private final int nbReplica;
+        private final IndexName indexName;
+        private final ImmutableList<AliasName> aliases;
+
+        public IndexCreationPerformer(int nbShards, int nbReplica, IndexName indexName, ImmutableList<AliasName> aliases) {
+            this.nbShards = nbShards;
+            this.nbReplica = nbReplica;
+            this.indexName = indexName;
+            this.aliases = aliases;
+        }
 
-    public IndexCreationFactory addAlias(AliasName aliasName) {
-        Preconditions.checkNotNull(aliasName);
-        this.aliases.add(aliasName);
-        return this;
-    }
+        public RestHighLevelClient createIndexAndAliases(RestHighLevelClient client) {
+            Preconditions.checkNotNull(indexName);
+            try {
+                createIndexIfNeeded(client, indexName, generateSetting(nbShards, nbReplica));
+                aliases.forEach(Throwing.consumer(alias -> createAliasIfNeeded(client, indexName, alias)));
+            } catch (IOException e) {
+                LOGGER.error("Error while creating index : ", e);
+            }
+            return client;
+        }
 
-    public RestHighLevelClient createIndexAndAliases(RestHighLevelClient client) {
-        Preconditions.checkNotNull(indexName);
-        try {
-            createIndexIfNeeded(client, indexName, generateSetting(nbShards, nbReplica));
-            aliases.forEach(Throwing.consumer(alias -> createAliasIfNeeded(client, indexName, alias)));
-        } catch (IOException e) {
-            LOGGER.error("Error while creating index : ", e);
+        private void createAliasIfNeeded(RestHighLevelClient client, IndexName indexName, AliasName aliasName) throws IOException {
+            if (!aliasExist(client, aliasName)) {
+                client.indices()
+                    .updateAliases(
+                        new IndicesAliasesRequest().addAliasAction(
+                            new AliasActions(AliasActions.Type.ADD)
+                                .index(indexName.getValue())
+                                .alias(aliasName.getValue())),
+                        RequestOptions.DEFAULT);
+            }
         }
-        return client;
-    }
 
-    private void createAliasIfNeeded(RestHighLevelClient client, IndexName indexName, AliasName aliasName) throws IOException {
-        if (!aliasExist(client, aliasName)) {
-            client.indices()
-                .updateAliases(
-                    new IndicesAliasesRequest().addAliasAction(
-                        new AliasActions(AliasActions.Type.ADD)
-                            .index(indexName.getValue())
-                            .alias(aliasName.getValue())),
+        private boolean aliasExist(RestHighLevelClient client, AliasName aliasName) throws IOException {
+            return client.indices()
+                .existsAlias(new GetAliasesRequest().aliases(aliasName.getValue()),
                     RequestOptions.DEFAULT);
         }
-    }
 
-    private boolean aliasExist(RestHighLevelClient client, AliasName aliasName) throws IOException {
-        return client.indices()
-            .existsAlias(new GetAliasesRequest().aliases(aliasName.getValue()),
-                RequestOptions.DEFAULT);
-    }
-
-    private void createIndexIfNeeded(RestHighLevelClient client, IndexName indexName, XContentBuilder settings) throws IOException {
-        try {
-            client.indices()
-                .create(
-                    new CreateIndexRequest(indexName.getValue())
-                        .source(settings),
-                    RequestOptions.DEFAULT);
-        } catch (ElasticsearchStatusException exception) {
-            if (exception.getMessage().contains(INDEX_ALREADY_EXISTS_EXCEPTION_MESSAGE)) {
-                LOGGER.info("Index [{}] already exist", indexName);
-            } else {
-                throw exception;
+        private void createIndexIfNeeded(RestHighLevelClient client, IndexName indexName, XContentBuilder settings) throws IOException {
+            try {
+                client.indices()
+                    .create(
+                        new CreateIndexRequest(indexName.getValue())
+                            .source(settings),
+                        RequestOptions.DEFAULT);
+            } catch (ElasticsearchStatusException exception) {
+                if (exception.getMessage().contains(INDEX_ALREADY_EXISTS_EXCEPTION_MESSAGE)) {
+                    LOGGER.info("Index [{}] already exist", indexName);
+                } else {
+                    throw exception;
+                }
             }
         }
-    }
 
-    private XContentBuilder generateSetting(int nbShards, int nbReplica) throws IOException {
-        return jsonBuilder()
-            .startObject()
-                .startObject("settings")
-                    .field("number_of_shards", nbShards)
-                    .field("number_of_replicas", nbReplica)
-                    .startObject("analysis")
-                        .startObject("analyzer")
-                            .startObject(CASE_INSENSITIVE)
-                                .field("tokenizer", "keyword")
-                                .startArray("filter")
-                                    .value("lowercase")
-                                .endArray()
+        private XContentBuilder generateSetting(int nbShards, int nbReplica) throws IOException {
+            return jsonBuilder()
+                .startObject()
+                    .startObject("settings")
+                        .field("number_of_shards", nbShards)
+                        .field("number_of_replicas", nbReplica)
+                        .startObject("analysis")
+                            .startObject("analyzer")
+                                .startObject(CASE_INSENSITIVE)
+                                    .field("tokenizer", "keyword")
+                                    .startArray("filter")
+                                        .value("lowercase")
+                                    .endArray()
+                                .endObject()
+                                .startObject(KEEP_MAIL_AND_URL)
+                                    .field("tokenizer", "uax_url_email")
+                                    .startArray("filter")
+                                        .value("lowercase")
+                                        .value("stop")
+                                    .endArray()
+                                .endObject()
+                                .startObject(SNOWBALL_KEEP_MAIL_AND_URL)
+                                    .field("tokenizer", "uax_url_email")
+                                    .startArray("filter")
+                                        .value("lowercase")
+                                        .value("stop")
+                                        .value(ENGLISH_SNOWBALL)
+                                    .endArray()
+                                .endObject()
                             .endObject()
-                            .startObject(KEEP_MAIL_AND_URL)
-                                .field("tokenizer", "uax_url_email")
-                                .startArray("filter")
-                                    .value("lowercase")
-                                    .value("stop")
-                                .endArray()
-                            .endObject()
-                            .startObject(SNOWBALL_KEEP_MAIL_AND_URL)
-                                .field("tokenizer", "uax_url_email")
-                                .startArray("filter")
-                                    .value("lowercase")
-                                    .value("stop")
-                                    .value(ENGLISH_SNOWBALL)
-                                .endArray()
-                            .endObject()
-                        .endObject()
-                        .startObject("filter")
-                            .startObject(ENGLISH_SNOWBALL)
-                                .field("type", "snowball")
-                                .field("language", "English")
+                            .startObject("filter")
+                                .startObject(ENGLISH_SNOWBALL)
+                                    .field("type", "snowball")
+                                    .field("language", "English")
+                                .endObject()
                             .endObject()
                         .endObject()
                     .endObject()
-                .endObject()
-            .endObject();
+                .endObject();
+        }
     }
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(IndexCreationFactory.class);
+    private static final String INDEX_ALREADY_EXISTS_EXCEPTION_MESSAGE = "type=resource_already_exists_exception";
+    private static final String CASE_INSENSITIVE = "case_insensitive";
+    private static final String KEEP_MAIL_AND_URL = "keep_mail_and_url";
+    private static final String SNOWBALL_KEEP_MAIL_AND_URL = "snowball_keep_mail_and_token";
+    private static final String ENGLISH_SNOWBALL = "english_snowball";
+
+    private final int nbShards;
+    private final int nbReplica;
+
+    @Inject
+    public IndexCreationFactory(ElasticSearchConfiguration configuration) {
+        this.nbShards = configuration.getNbShards();
+        this.nbReplica = configuration.getNbReplica();
+    }
+
+    public AliasSpecificationStep useIndex(IndexName indexName) {
+        Preconditions.checkNotNull(indexName);
+        return new AliasSpecificationStep(nbShards, nbReplica, indexName);
+    }
 }
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
index 4f1efd3..49be066 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/IndexCreationFactoryTest.java
@@ -62,6 +62,7 @@ public class IndexCreationFactoryTest {
     public void addAliasShouldThrowWhenNull() {
         assertThatThrownBy(() ->
             new IndexCreationFactory(ElasticSearchConfiguration.DEFAULT_CONFIGURATION)
+                .useIndex(INDEX_NAME)
                 .addAlias(null))
             .isInstanceOf(NullPointerException.class);
     }


---------------------------------------------------------------------
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: JAMES-2770 RRT rewriting can lead to duplicated mails

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 895b50b1fbcb375bfc815d88518fc0985dcaafa6
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed May 15 11:56:05 2019 +0700

    JAMES-2770 RRT rewriting can lead to duplicated mails
---
 .../james/transport/mailets/AliasMappingTest.java  | 19 +++++++++++++++++
 .../mailets/RecipientRewriteTableProcessor.java    | 24 ++++++++++++----------
 .../RecipientRewriteTableProcessorTest.java        | 21 +++++++++++++++++++
 3 files changed, 53 insertions(+), 11 deletions(-)

diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java
index 116b522..0fe2f42 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/AliasMappingTest.java
@@ -343,4 +343,23 @@ public class AliasMappingTest {
                 .getRepositoryMailCount(RRT_ERROR_REPOSITORY) == 1);
     }
 
+    @Test
+    public void userShouldNotReceiveDuplicatesWhenUserAndAliasRegisteredToAGroup() throws Exception {
+        webAdminApi.put(AliasRoutes.ROOT_PATH + "/" + BOB_ADDRESS + "/sources/" + BOB_ALIAS);
+        webAdminApi.put(GroupsRoutes.ROOT_PATH + "/" + GROUP_ADDRESS + "/" + BOB_ADDRESS);
+        webAdminApi.put(GroupsRoutes.ROOT_PATH + "/" + GROUP_ADDRESS + "/" + BOB_ALIAS);
+
+        messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .sendMessage(FakeMail.builder()
+                .name("name")
+                .mimeMessage(message)
+                .sender(ALICE_ADDRESS)
+                .recipient(GROUP_ADDRESS));
+
+        imapMessageReader.connect(LOCALHOST_IP, jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+            .login(BOB_ADDRESS, PASSWORD)
+            .select(IMAPMessageReader.INBOX)
+            .awaitMessageCount(awaitAtMostOneMinute, 1);
+    }
+
 }
diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessor.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessor.java
index 18ead89..dd890c4 100644
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessor.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessor.java
@@ -22,6 +22,7 @@ package org.apache.james.transport.mailets;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -49,52 +50,53 @@ import com.github.fge.lambdas.Throwing;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 public class RecipientRewriteTableProcessor {
     private static final Logger LOGGER = LoggerFactory.getLogger(RecipientRewriteTableProcessor.class);
 
     private static class RrtExecutionResult {
         private static RrtExecutionResult empty() {
-            return new RrtExecutionResult(ImmutableList.of(), ImmutableList.of());
+            return new RrtExecutionResult(ImmutableSet.of(), ImmutableSet.of());
         }
 
         private static RrtExecutionResult error(MailAddress mailAddress) {
-            return new RrtExecutionResult(ImmutableList.of(), ImmutableList.of(mailAddress));
+            return new RrtExecutionResult(ImmutableSet.of(), ImmutableSet.of(mailAddress));
         }
 
         private static RrtExecutionResult success(MailAddress mailAddress) {
-            return new RrtExecutionResult(ImmutableList.of(mailAddress), ImmutableList.of());
+            return new RrtExecutionResult(ImmutableSet.of(mailAddress), ImmutableSet.of());
         }
 
         private static RrtExecutionResult success(List<MailAddress> mailAddresses) {
-            return new RrtExecutionResult(ImmutableList.copyOf(mailAddresses), ImmutableList.of());
+            return new RrtExecutionResult(ImmutableSet.copyOf(mailAddresses), ImmutableSet.of());
         }
 
         private static RrtExecutionResult merge(RrtExecutionResult result1, RrtExecutionResult result2) {
             return new RrtExecutionResult(
-                ImmutableList.<MailAddress>builder()
+                ImmutableSet.<MailAddress>builder()
                     .addAll(result1.getNewRecipients())
                     .addAll(result2.getNewRecipients())
                     .build(),
-                ImmutableList.<MailAddress>builder()
+                ImmutableSet.<MailAddress>builder()
                     .addAll(result1.getRecipientWithError())
                     .addAll(result2.getRecipientWithError())
                     .build());
         }
 
-        private final ImmutableList<MailAddress> newRecipients;
-        private final ImmutableList<MailAddress> recipientWithError;
+        private final ImmutableSet<MailAddress> newRecipients;
+        private final ImmutableSet<MailAddress> recipientWithError;
 
-        public RrtExecutionResult(ImmutableList<MailAddress> newRecipients, ImmutableList<MailAddress> recipientWithError) {
+        public RrtExecutionResult(ImmutableSet<MailAddress> newRecipients, ImmutableSet<MailAddress> recipientWithError) {
             this.newRecipients = newRecipients;
             this.recipientWithError = recipientWithError;
         }
 
-        public List<MailAddress> getNewRecipients() {
+        public Set<MailAddress> getNewRecipients() {
             return newRecipients;
         }
 
-        public List<MailAddress> getRecipientWithError() {
+        public Set<MailAddress> getRecipientWithError() {
             return recipientWithError;
         }
 
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessorTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessorTest.java
index 6290fab..746ebef 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessorTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/RecipientRewriteTableProcessorTest.java
@@ -35,6 +35,7 @@ import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
 import org.apache.james.rrt.api.RecipientRewriteTable.ErrorMappingException;
 import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.lib.Mapping;
 import org.apache.james.rrt.lib.MappingsImpl;
 import org.apache.james.util.MimeMessageUtil;
 import org.apache.mailet.Mail;
@@ -258,6 +259,26 @@ public class RecipientRewriteTableProcessorTest {
         assertThat(mailetContext.getSentMails()).containsOnly(expected);
         assertThat(mail.getRecipients()).containsOnly(MailAddressFixture.ANY_AT_LOCAL);
     }
+
+    @Test
+    public void processShouldNotDuplicateRewrittenMailAddresses() throws Exception {
+        when(virtualTableStore.getResolvedMappings(eq("other"), eq(Domain.of(MailAddressFixture.JAMES_LOCAL))))
+            .thenReturn(MappingsImpl.builder()
+                .add(Mapping.alias(MailAddressFixture.ANY_AT_LOCAL.asString()))
+                .add(Mapping.group(MailAddressFixture.ANY_AT_LOCAL.asString()))
+                .build());
+
+        mail = FakeMail.builder()
+            .name("mail")
+            .sender(MailAddressFixture.ANY_AT_JAMES)
+            .mimeMessage(message)
+            .recipients(MailAddressFixture.OTHER_AT_LOCAL)
+            .build();
+
+        processor.processMail(mail);
+
+        assertThat(mail.getRecipients()).containsExactly(MailAddressFixture.ANY_AT_LOCAL);
+    }
     
     @Test
     public void processShouldSendMailToAllErrorRecipientsWhenRecipientRewriteTableException() throws Exception {


---------------------------------------------------------------------
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-2149 Website documentation for Domain aliases

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 df68593cdbe9c5ff083e4131c7232ef4de3a5439
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue May 14 10:41:02 2019 +0700

    JAMES-2149 Website documentation for Domain aliases
---
 src/site/markdown/server/manage-webadmin.md | 64 +++++++++++++++++++++++++++++
 1 file changed, 64 insertions(+)

diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 05f87b5..77a3ee4 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -124,6 +124,9 @@ Response codes:
    - [Delete a domain](#Delete_a_domain)
    - [Test if a domain exists](#Test_if_a_domain_exists)
    - [Get the list of domains](#Get_the_list_of_domains)
+   - [Get the list of aliases for a domain](#Get_the_list_of_aliases_for_a_domain)
+   - [Create an alias for a domain](#Create_an_alias_for_a_domain)
+   - [Delete an alias for a domain](#Delete_an_alias_for_a_domain)
 
 ### Create a domain
 
@@ -180,6 +183,67 @@ Response codes:
 
  - 200: The domain list was successfully retrieved
 
+### Get the list of aliases for a domain
+
+```
+curl -XGET http://ip:port/domains/destination.domain.tld/aliases
+```
+
+Possible response:
+
+```
+[
+  {"source": "source1.domain.tld"},
+  {"source": "source2.domain.tld"}
+]
+```
+
+When sending an email to an email address having source1.domain.tld or source2.domain.tld as a domain part (exemple: benwa@source1.domain.tld), then
+the domain part will be rewritten into destination.domain.tld (so into benwa@destination.domain.tld).
+
+Response codes:
+
+ - 200: The domain aliases was successfully retrieved
+ - 400: destination.domain.tld has an invalid syntax
+ - 404: destination.domain.tld is not part of handled domains or does not have local domains as aliases.
+
+### Create an alias for a domain
+
+To create a domain alias execute the following query:
+
+```
+curl -XPUT http://ip:port/domains/destination.domain.tld/aliases/source.domain.tld
+```
+
+When sending an email to an email address having source.domain.tld as a domain part (exemple: benwa@source.domain.tld), then
+the domain part will be rewritten into destination.domain.tld (so into benwa@destination.domain.tld).
+
+
+Response codes:
+
+ - 204: The redirection now exists
+ - 400: source.domain.tld or destination.domain.tld have an invalid syntax
+ - 404: source.domain.tld are not part of handled domains.
+
+### Delete an alias for a domain
+
+
+To delete a domain alias execute the following query:
+
+```
+curl -XDELETE http://ip:port/domains/destination.domain.tld/aliases/source.domain.tld
+```
+
+When sending an email to an email address having source.domain.tld as a domain part (exemple: benwa@source.domain.tld), then
+the domain part will be rewritten into destination.domain.tld (so into benwa@destination.domain.tld).
+
+
+Response codes:
+
+ - 204: The redirection now exists
+ - 400: source.domain.tld or destination.domain.tld have an invalid syntax
+ - 404: source.domain.tld are not part of handled domains.
+
 ## Administrating users
 
    - [Create a user](#Create_a_user)


---------------------------------------------------------------------
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-2149 Introduce DomainAliasService

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 a0545c7102f6c618074f5c14323850dd4d9691ba
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed May 15 11:17:09 2019 +0700

    JAMES-2149 Introduce DomainAliasService
---
 .../james/webadmin/routes/DomainsRoutes.java       | 75 ++++++-----------
 .../james/webadmin/service/DomainAliasService.java | 98 ++++++++++++++++++++++
 .../james/webadmin/routes/DomainsRoutesTest.java   |  9 +-
 3 files changed, 129 insertions(+), 53 deletions(-)

diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
index cd047f9..de7300d 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
@@ -34,24 +34,19 @@ import javax.ws.rs.Produces;
 import org.apache.james.core.Domain;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
-import org.apache.james.rrt.api.RecipientRewriteTable;
 import org.apache.james.rrt.api.RecipientRewriteTableException;
-import org.apache.james.rrt.lib.Mapping;
-import org.apache.james.rrt.lib.MappingSource;
-import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.DomainAliasResponse;
+import org.apache.james.webadmin.service.DomainAliasService;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
 import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.github.fge.lambdas.Throwing;
-import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 import io.swagger.annotations.Api;
@@ -69,10 +64,6 @@ import spark.Service;
 @Path(DomainsRoutes.DOMAINS)
 @Produces("application/json")
 public class DomainsRoutes implements Routes {
-    @FunctionalInterface
-    interface MappingOperation {
-        void perform(MappingSource mappingSource, Mapping mapping) throws RecipientRewriteTableException;
-    }
 
     public static final String DOMAINS = "/domains";
     private static final Logger LOGGER = LoggerFactory.getLogger(DomainsRoutes.class);
@@ -86,14 +77,14 @@ public class DomainsRoutes implements Routes {
     private static final int MAXIMUM_DOMAIN_SIZE = 256;
 
     private final DomainList domainList;
-    private final RecipientRewriteTable recipientRewriteTable;
+    private final DomainAliasService domainAliasService;
     private final JsonTransformer jsonTransformer;
     private Service service;
 
     @Inject
-    public DomainsRoutes(DomainList domainList, RecipientRewriteTable recipientRewriteTable, JsonTransformer jsonTransformer) {
+    DomainsRoutes(DomainList domainList, DomainAliasService domainAliasService, JsonTransformer jsonTransformer) {
         this.domainList = domainList;
-        this.recipientRewriteTable = recipientRewriteTable;
+        this.domainAliasService = domainAliasService;
         this.jsonTransformer = jsonTransformer;
     }
 
@@ -234,26 +225,18 @@ public class DomainsRoutes implements Routes {
             Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
             domainList.removeDomain(domain);
 
-            removeCorrespondingDomainAliases(domain);
+            domainAliasService.removeCorrespondingDomainAliases(domain);
         } catch (DomainListException e) {
             LOGGER.info("{} did not exists", request.params(DOMAIN_NAME));
         }
-        response.status(HttpStatus.NO_CONTENT_204);
-        return Constants.EMPTY_BODY;
-    }
-
-    private void removeCorrespondingDomainAliases(Domain domain) throws RecipientRewriteTableException {
-        MappingSource mappingSource = MappingSource.fromDomain(domain);
-        recipientRewriteTable.getStoredMappings(mappingSource)
-            .asStream()
-            .forEach(Throwing.<Mapping>consumer(mapping -> recipientRewriteTable.removeMapping(mappingSource, mapping)).sneakyThrow());
+        return Responses.returnNoContent(response);
     }
 
     private String addDomain(Request request, Response response) {
         Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
         try {
             addDomain(domain);
-            response.status(204);
+            return Responses.returnNoContent(response);
         } catch (DomainListException e) {
             LOGGER.info("{} already exists", domain);
             throw ErrorResponder.builder()
@@ -271,7 +254,6 @@ public class DomainsRoutes implements Routes {
                 .cause(e)
                 .haltError();
         }
-        return Constants.EMPTY_BODY;
     }
 
     private Domain checkValidDomain(String domainName) {
@@ -292,53 +274,48 @@ public class DomainsRoutes implements Routes {
         domainList.addDomain(domain);
     }
 
-    private Response exists(Request request, Response response) throws DomainListException {
+    private String exists(Request request, Response response) throws DomainListException {
         Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
 
         if (!domainList.containsDomain(domain)) {
             throw domainNotFound(domain);
         } else {
-            response.status(HttpStatus.NO_CONTENT_204);
-            return response;
+            return Responses.returnNoContent(response);
         }
     }
 
     private ImmutableSet<DomainAliasResponse> listDomainAliases(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
         Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
 
-        if (!hasAliases(domain)) {
+        if (!domainAliasService.hasAliases(domain)) {
             throw domainHasNoAliases(domain);
         } else {
-            return recipientRewriteTable.listSources(Mapping.domain(domain))
-                .map(DomainAliasResponse::new)
-                .collect(Guavate.toImmutableSet());
+            return domainAliasService.listDomainAliases(domain);
         }
     }
 
     private String addDomainAlias(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
-        return performOperationOnAlias(request, response, recipientRewriteTable::addMapping);
-    }
+        Domain sourceDomain = checkValidDomain(request.params(SOURCE_DOMAIN));
+        Domain destinationDomain = checkValidDomain(request.params(DESTINATION_DOMAIN));
 
-    private String removeDomainAlias(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
-        return performOperationOnAlias(request, response, recipientRewriteTable::removeMapping);
+        try {
+            domainAliasService.addDomainAlias(sourceDomain, destinationDomain);
+            return Responses.returnNoContent(response);
+        } catch (DomainAliasService.DomainNotFound e) {
+            throw domainNotFound(e.getDomain());
+        }
     }
 
-    private String performOperationOnAlias(Request request, Response response, MappingOperation operation) throws DomainListException, RecipientRewriteTableException {
+    private String removeDomainAlias(Request request, Response response) throws DomainListException, RecipientRewriteTableException {
         Domain sourceDomain = checkValidDomain(request.params(SOURCE_DOMAIN));
         Domain destinationDomain = checkValidDomain(request.params(DESTINATION_DOMAIN));
 
-        if (!domainList.containsDomain(sourceDomain)) {
-            throw domainNotFound(sourceDomain);
+        try {
+            domainAliasService.removeDomainAlias(sourceDomain, destinationDomain);
+            return Responses.returnNoContent(response);
+        } catch (DomainAliasService.DomainNotFound e) {
+            throw domainNotFound(e.getDomain());
         }
-
-        operation.perform(MappingSource.fromDomain(sourceDomain), Mapping.domain(destinationDomain));
-        response.status(HttpStatus.NO_CONTENT_204);
-        return Constants.EMPTY_BODY;
-    }
-
-    private boolean hasAliases(Domain domain) throws DomainListException, RecipientRewriteTableException {
-        return domainList.containsDomain(domain)
-            || recipientRewriteTable.listSources(Mapping.domain(domain)).findFirst().isPresent();
     }
 
     private HaltException domainNotFound(Domain domain) {
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DomainAliasService.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DomainAliasService.java
new file mode 100644
index 0000000..afbc093
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DomainAliasService.java
@@ -0,0 +1,98 @@
+/****************************************************************
+ * 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.service;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Domain;
+import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.domainlist.api.DomainListException;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.lib.Mapping;
+import org.apache.james.rrt.lib.MappingSource;
+import org.apache.james.webadmin.dto.DomainAliasResponse;
+
+import com.github.fge.lambdas.Throwing;
+import com.github.steveash.guavate.Guavate;
+import com.google.common.collect.ImmutableSet;
+
+public class DomainAliasService {
+    public static class DomainNotFound extends RuntimeException {
+        private final Domain domain;
+
+        DomainNotFound(Domain domain) {
+            this.domain = domain;
+        }
+
+        public Domain getDomain() {
+            return domain;
+        }
+    }
+
+    @FunctionalInterface
+    interface MappingOperation {
+        void perform(MappingSource mappingSource, Mapping mapping) throws RecipientRewriteTableException;
+    }
+
+    private final RecipientRewriteTable recipientRewriteTable;
+    private final DomainList domainList;
+
+    @Inject
+    public DomainAliasService(RecipientRewriteTable recipientRewriteTable, DomainList domainList) {
+        this.recipientRewriteTable = recipientRewriteTable;
+        this.domainList = domainList;
+    }
+
+    public void removeCorrespondingDomainAliases(Domain domain) throws RecipientRewriteTableException {
+        MappingSource mappingSource = MappingSource.fromDomain(domain);
+        recipientRewriteTable.getStoredMappings(mappingSource)
+            .asStream()
+            .forEach(Throwing.<Mapping>consumer(mapping -> recipientRewriteTable.removeMapping(mappingSource, mapping)).sneakyThrow());
+    }
+
+    public ImmutableSet<DomainAliasResponse> listDomainAliases(Domain domain) throws RecipientRewriteTableException {
+        return recipientRewriteTable.listSources(Mapping.domain(domain))
+            .map(DomainAliasResponse::new)
+            .collect(Guavate.toImmutableSet());
+    }
+
+    public boolean hasAliases(Domain domain) throws DomainListException, RecipientRewriteTableException {
+        return domainList.containsDomain(domain)
+            || recipientRewriteTable.listSources(Mapping.domain(domain)).findFirst().isPresent();
+    }
+
+    public void addDomainAlias(Domain sourceDomain, Domain destinationDomain) throws DomainListException, RecipientRewriteTableException {
+        performOperationOnAlias(recipientRewriteTable::addMapping, sourceDomain, destinationDomain);
+    }
+
+    public void removeDomainAlias(Domain sourceDomain, Domain destinationDomain) throws DomainListException, RecipientRewriteTableException {
+        performOperationOnAlias(recipientRewriteTable::removeMapping, sourceDomain, destinationDomain);
+    }
+
+    private void performOperationOnAlias(MappingOperation operation, Domain sourceDomain, Domain destinationDomain) throws DomainListException, RecipientRewriteTableException {
+        if (!domainList.containsDomain(sourceDomain)) {
+            throw new DomainNotFound(sourceDomain);
+        }
+
+        operation.perform(MappingSource.fromDomain(sourceDomain), Mapping.domain(destinationDomain));
+    }
+
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
index dea3fc9..01dbe7e 100644
--- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
@@ -46,6 +46,7 @@ import org.apache.james.metrics.logger.DefaultMetricFactory;
 import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
 import org.apache.james.webadmin.WebAdminServer;
 import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.service.DomainAliasService;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.jupiter.api.AfterEach;
@@ -57,19 +58,19 @@ import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
 
 
-public class DomainsRoutesTest {
+class DomainsRoutesTest {
     private static final String DOMAIN = "domain";
     private static final String ALIAS_DOMAIN = "alias.domain";
     private static final String ALIAS_DOMAIN_2 = "alias.domain.bis";
-    public static final String EXTERNAL_DOMAIN = "external.domain.tld";
-    public static final String DOMAIN_2 = "domain2";
+    private static final String EXTERNAL_DOMAIN = "external.domain.tld";
 
     private WebAdminServer webAdminServer;
 
     private void createServer(DomainList domainList) throws Exception {
+        DomainAliasService domainAliasService = new DomainAliasService(new MemoryRecipientRewriteTable(), domainList);
         webAdminServer = WebAdminUtils.createWebAdminServer(
             new DefaultMetricFactory(),
-            new DomainsRoutes(domainList, new MemoryRecipientRewriteTable(), new JsonTransformer()));
+            new DomainsRoutes(domainList, domainAliasService, new JsonTransformer()));
         webAdminServer.configure(NO_CONFIGURATION);
         webAdminServer.await();
 


---------------------------------------------------------------------
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: JAMES-2719 Add bean contract for AliasName and ElasticSearchConfiguration in ES backend module

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 00026aa8b605581fbba588852571364a7550ea27
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu May 16 10:59:29 2019 +0700

    JAMES-2719 Add bean contract for AliasName and ElasticSearchConfiguration in ES backend module
---
 backends-common/elasticsearch-v6/pom.xml           |  5 ++++
 .../apache/james/backends/es/v6/AliasNameTest.java | 32 ++++++++++++++++++++++
 .../es/v6/ElasticSearchConfigurationTest.java      |  8 ++++++
 3 files changed, 45 insertions(+)

diff --git a/backends-common/elasticsearch-v6/pom.xml b/backends-common/elasticsearch-v6/pom.xml
index 2502b42..d062168 100644
--- a/backends-common/elasticsearch-v6/pom.xml
+++ b/backends-common/elasticsearch-v6/pom.xml
@@ -66,6 +66,11 @@
             <artifactId>feign-slf4j</artifactId>
         </dependency>
         <dependency>
+            <groupId>nl.jqno.equalsverifier</groupId>
+            <artifactId>equalsverifier</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
             <scope>test</scope>
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/AliasNameTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/AliasNameTest.java
new file mode 100644
index 0000000..3dc2f0c
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/AliasNameTest.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class AliasNameTest {
+    @Test
+    void aliasNameShouldRespectBeanContract() {
+        EqualsVerifier.forClass(AliasName.class)
+            .verify();
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java
index 43d80ef..1ba2a1a 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchConfigurationTest.java
@@ -31,9 +31,17 @@ import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
 
+import nl.jqno.equalsverifier.EqualsVerifier;
+
 public class ElasticSearchConfigurationTest {
 
     @Test
+    public void elasticSearchConfigurationShouldRespectBeanContract() {
+        EqualsVerifier.forClass(ElasticSearchConfiguration.class)
+            .verify();
+    }
+
+    @Test
     public void getNbReplicaShouldReturnConfiguredValue() throws ConfigurationException {
         PropertiesConfiguration configuration = new PropertiesConfiguration();
         int value = 36;


---------------------------------------------------------------------
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-2149 User alias should be applied before domain mapping

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 49e309c5db3eaa803e597f538478e5dd33055972
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue May 14 18:00:57 2019 +0700

    JAMES-2149 User alias should be applied before domain mapping
    
    If the domainAlias is applicable to the rewritten userAlias then it should be applied
---
 .../james/transport/mailets/DomainMappingTest.java | 42 ++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
index b4997d9..1a3eaff 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
@@ -178,4 +178,46 @@ public class DomainMappingTest {
             () -> jamesServer.getProbe(MailRepositoryProbeImpl.class)
                 .getRepositoryMailCount(RRT_ERROR_REPOSITORY) == 1);
     }
+
+    @Test
+    public void domainAliasShouldBeIgnoredWhenUserAlias() throws Exception {
+        jamesServer.getProbe(DataProbeImpl.class).addUser(BOB_DOMAIN1, PASSWORD);
+
+        webAdminApi.put("/address/aliases/" + BOB_DOMAIN1 + "/sources/" + USER_DOMAIN2);
+        webAdminApi.put("/domains/" + DOMAIN1 + "/aliases/" + DOMAIN2);
+
+        messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .sendMessage(FakeMail.builder()
+                .name("name")
+                .mimeMessage(message)
+                .sender(SENDER)
+                .recipient(USER_DOMAIN2));
+
+        imapMessageReader.connect(LOCALHOST_IP, jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+            .login(BOB_DOMAIN1, PASSWORD)
+            .select(IMAPMessageReader.INBOX)
+            .awaitMessage(awaitAtMostOneMinute);
+        assertThat(imapMessageReader.readFirstMessage()).contains(MESSAGE_CONTENT);
+    }
+
+    @Test
+    public void domainAliasShouldBeChainedIfApplicableAfterUserAliasRewrite() throws Exception {
+        jamesServer.getProbe(DataProbeImpl.class).addUser(BOB_DOMAIN1, PASSWORD);
+
+        webAdminApi.put("/address/aliases/" + BOB_DOMAIN2 + "/sources/" + USER_DOMAIN2);
+        webAdminApi.put("/domains/" + DOMAIN1 + "/aliases/" + DOMAIN2);
+
+        messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .sendMessage(FakeMail.builder()
+                .name("name")
+                .mimeMessage(message)
+                .sender(SENDER)
+                .recipient(USER_DOMAIN2));
+
+        imapMessageReader.connect(LOCALHOST_IP, jamesServer.getProbe(ImapGuiceProbe.class).getImapPort())
+            .login(BOB_DOMAIN1, PASSWORD)
+            .select(IMAPMessageReader.INBOX)
+            .awaitMessage(awaitAtMostOneMinute);
+        assertThat(imapMessageReader.readFirstMessage()).contains(MESSAGE_CONTENT);
+    }
 }


---------------------------------------------------------------------
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: JAMES-2719 Update version of ES to 6.7.2 in the backends module

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 eb5fc159d0bab07197c6cdd0b8d7088b9cca19cb
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed May 15 15:48:19 2019 +0700

    JAMES-2719 Update version of ES to 6.7.2 in the backends module
---
 backends-common/elasticsearch-v6/pom.xml | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/backends-common/elasticsearch-v6/pom.xml b/backends-common/elasticsearch-v6/pom.xml
index cc661e3..2502b42 100644
--- a/backends-common/elasticsearch-v6/pom.xml
+++ b/backends-common/elasticsearch-v6/pom.xml
@@ -75,16 +75,9 @@
             <artifactId>awaitility</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.elasticsearch</groupId>
-            <artifactId>elasticsearch</artifactId>
-            <version>2.2.1</version>
-        </dependency>
-        <dependency>
-            <groupId>org.elasticsearch</groupId>
-            <artifactId>elasticsearch</artifactId>
-            <version>2.2.1</version>
-            <type>test-jar</type>
-            <scope>test</scope>
+            <groupId>org.elasticsearch.client</groupId>
+            <artifactId>elasticsearch-rest-high-level-client</artifactId>
+            <version>6.7.2</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>


---------------------------------------------------------------------
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-2763 StartUpChecksPerformer implementation

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 16914c8aa68f216e7bf14e7ed3a0d7f0430cf779
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Tue May 14 10:40:52 2019 +0700

    JAMES-2763 StartUpChecksPerformer implementation
---
 .../org/apache/james/StartUpChecksPerformer.java   | 208 +++++++++++++++++++++
 .../apache/james/StartUpChecksPerformerTest.java   |  92 +++++++++
 2 files changed, 300 insertions(+)

diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java b/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java
new file mode 100644
index 0000000..02d895e
--- /dev/null
+++ b/server/container/guice/guice-common/src/main/java/org/apache/james/StartUpChecksPerformer.java
@@ -0,0 +1,208 @@
+/****************************************************************
+ * 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;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+
+import reactor.core.publisher.Flux;
+import reactor.core.scheduler.Schedulers;
+
+public class StartUpChecksPerformer {
+
+    public static class StartUpChecksException extends Exception {
+
+        private static String badChecksToString(List<StartUpCheck.CheckResult> badChecks) {
+            Preconditions.checkArgument(!badChecks.isEmpty(), "'badChecks' should not be empty");
+            Preconditions.checkArgument(badChecks
+                .stream()
+                .noneMatch(StartUpCheck.CheckResult::isGood),
+                "'badChecks' should not have any good check");
+
+            return Joiner.on("\n")
+                .join(badChecks);
+        }
+
+        private final List<StartUpCheck.CheckResult> badChecks;
+
+        StartUpChecksException(List<StartUpCheck.CheckResult> badChecks) {
+            super("StartUpChecks got bad results: " + badChecksToString(badChecks));
+
+            this.badChecks = badChecks;
+        }
+
+        @VisibleForTesting
+        public List<StartUpCheck.CheckResult> getBadChecks() {
+            return badChecks;
+        }
+    }
+
+    public interface StartUpCheck {
+
+        enum ResultType {
+            GOOD, BAD
+        }
+
+        class CheckResult {
+
+            static class Builder {
+
+                @FunctionalInterface
+                interface RequireCheckName {
+                    RequireResultType checkName(String name);
+                }
+
+                @FunctionalInterface
+                interface RequireResultType {
+                    ReadyToBuild resultType(ResultType resultType);
+                }
+
+                static class ReadyToBuild {
+                    private final String name;
+                    private final ResultType resultType;
+                    private Optional<String> description;
+
+                    ReadyToBuild(String name, ResultType resultType) {
+                        this.name = name;
+                        this.resultType = resultType;
+                        this.description = Optional.empty();
+                    }
+
+                    public ReadyToBuild description(String description) {
+                        this.description = Optional.ofNullable(description);
+                        return this;
+                    }
+
+                    public CheckResult build() {
+                        return new CheckResult(name, resultType, description);
+                    }
+                }
+
+            }
+
+            public static Builder.RequireCheckName builder() {
+                return name -> resultType -> new Builder.ReadyToBuild(name, resultType);
+            }
+
+            private final String name;
+            private final ResultType resultType;
+            private final Optional<String> description;
+
+            private CheckResult(String name, ResultType resultType, Optional<String> description) {
+                Preconditions.checkNotNull(name);
+                Preconditions.checkNotNull(resultType);
+                Preconditions.checkNotNull(description);
+
+                this.name = name;
+                this.resultType = resultType;
+                this.description = description;
+            }
+
+            public String getName() {
+                return name;
+            }
+
+            public ResultType getResultType() {
+                return resultType;
+            }
+
+            public Optional<String> getDescription() {
+                return description;
+            }
+
+            public boolean isBad() {
+                return resultType.equals(ResultType.BAD);
+            }
+
+            public boolean isGood() {
+                return resultType.equals(ResultType.GOOD);
+            }
+
+            @Override
+            public String toString() {
+                return MoreObjects.toStringHelper(this)
+                    .add("name", name)
+                    .add("resultType", resultType)
+                    .add("description", description)
+                    .toString();
+            }
+        }
+
+        CheckResult check();
+    }
+
+    static class StartUpChecks {
+
+        private final Set<StartUpCheck> startUpChecks;
+
+        @Inject
+        StartUpChecks(Set<StartUpCheck> startUpChecks) {
+            this.startUpChecks = startUpChecks;
+        }
+
+        public List<StartUpCheck.CheckResult> check() {
+            return Flux.fromIterable(startUpChecks)
+                .publishOn(Schedulers.elastic())
+                .map(StartUpCheck::check)
+                .collect(Guavate.toImmutableList())
+                .block();
+        }
+    }
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(StartUpChecksPerformer.class);
+
+    @VisibleForTesting
+    static StartUpChecksPerformer from(StartUpCheck... checks) {
+        return new StartUpChecksPerformer(new StartUpChecks(Sets.newHashSet(checks)));
+    }
+
+    private final StartUpChecks startUpChecks;
+
+    @Inject
+    public StartUpChecksPerformer(StartUpChecks startUpChecks) {
+        this.startUpChecks = startUpChecks;
+    }
+
+    public void performCheck() throws StartUpChecksException {
+        List<StartUpCheck.CheckResult> badChecks = startUpChecks.check()
+            .stream()
+            .filter(StartUpCheck.CheckResult::isBad)
+            .collect(Guavate.toImmutableList());
+
+        if (!badChecks.isEmpty()) {
+            throw new StartUpChecksException(badChecks);
+        }
+
+        LOGGER.info("StartUpChecks all succeeded");
+    }
+}
diff --git a/server/container/guice/guice-common/src/test/java/org/apache/james/StartUpChecksPerformerTest.java b/server/container/guice/guice-common/src/test/java/org/apache/james/StartUpChecksPerformerTest.java
new file mode 100644
index 0000000..c3750d2
--- /dev/null
+++ b/server/container/guice/guice-common/src/test/java/org/apache/james/StartUpChecksPerformerTest.java
@@ -0,0 +1,92 @@
+/****************************************************************
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.james.StartUpChecksPerformer.StartUpCheck.CheckResult;
+import org.apache.james.StartUpChecksPerformer.StartUpCheck.ResultType;
+import org.junit.jupiter.api.Test;
+
+class StartUpChecksPerformerTest {
+
+    private static final CheckResult GOOD_CHECK_1 = CheckResult.builder()
+        .checkName("good 1")
+        .resultType(ResultType.GOOD)
+        .build();
+    private static final CheckResult GOOD_CHECK_2 = CheckResult.builder()
+        .checkName("good 2")
+        .resultType(ResultType.GOOD)
+        .build();
+    private static final CheckResult BAD_CHECK_1 = CheckResult.builder()
+        .checkName("bad 1")
+        .resultType(ResultType.BAD)
+        .build();
+    private static final CheckResult BAD_CHECK_2 = CheckResult.builder()
+        .checkName("bad 2")
+        .resultType(ResultType.BAD)
+        .build();
+
+    @Test
+    void performCheckShouldNotThrowWhenAllChecksSucceed() {
+        StartUpChecksPerformer checksPerformer = StartUpChecksPerformer.from(
+            () -> GOOD_CHECK_1,
+            () -> GOOD_CHECK_2);
+
+        assertThatCode(checksPerformer::performCheck)
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    void performCheckShouldNotThrowWhenNoChecks() {
+        StartUpChecksPerformer checksPerformer = StartUpChecksPerformer.from();
+
+        assertThatCode(checksPerformer::performCheck)
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    void performCheckShouldThrowWhenThereIsOneCheckFails() {
+        StartUpChecksPerformer checksPerformer = StartUpChecksPerformer.from(
+            () -> GOOD_CHECK_1,
+            () -> GOOD_CHECK_2,
+            () -> BAD_CHECK_1);
+
+        assertThatThrownBy(checksPerformer::performCheck)
+            .isInstanceOf(StartUpChecksPerformer.StartUpChecksException.class);
+    }
+
+    @Test
+    void performCheckShouldThrowAnExceptionContainingAllBadChecksWhenThereAreBadChecks() {
+        StartUpChecksPerformer checksPerformer = StartUpChecksPerformer.from(
+            () -> GOOD_CHECK_1,
+            () -> GOOD_CHECK_2,
+            () -> BAD_CHECK_1,
+            () -> BAD_CHECK_2);
+
+        assertThatThrownBy(checksPerformer::performCheck)
+            .isInstanceOfSatisfying(
+                StartUpChecksPerformer.StartUpChecksException.class,
+                exception -> assertThat(exception.getBadChecks())
+                    .containsOnly(BAD_CHECK_1, BAD_CHECK_2));
+    }
+}
\ 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] 21/23: JAMES-2719 Refactor delete by query on ES backend

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 45e3224f2187d0cbab7523d433c669bffa3f7973
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu May 16 11:46:00 2019 +0700

    JAMES-2719 Refactor delete by query on ES backend
---
 .../es/v6/DeleteByQueryActionListener.java         | 39 +++++++++++++
 .../backends/es/v6/DeleteByQueryPerformer.java     | 66 ----------------------
 .../james/backends/es/v6/ElasticSearchIndexer.java | 25 +++++---
 .../backends/es/v6/ElasticSearchIndexerTest.java   | 39 +++++++------
 4 files changed, 74 insertions(+), 95 deletions(-)

diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryActionListener.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryActionListener.java
new file mode 100644
index 0000000..d89a9ec
--- /dev/null
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryActionListener.java
@@ -0,0 +1,39 @@
+/****************************************************************
+ * 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.backends.es.v6;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.index.reindex.BulkByScrollResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DeleteByQueryActionListener implements ActionListener<BulkByScrollResponse> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DeleteByQueryActionListener.class);
+
+    @Override
+    public void onResponse(BulkByScrollResponse bulkByScrollResponse) {
+
+    }
+
+    @Override
+    public void onFailure(Exception e) {
+        LOGGER.warn("Error during the ES delete by query operation: ", e);
+    }
+}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
deleted file mode 100644
index a912c0f..0000000
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/DeleteByQueryPerformer.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/****************************************************************
- * 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.backends.es.v6;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-import org.elasticsearch.client.RequestOptions;
-import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.index.reindex.DeleteByQueryRequest;
-
-import com.google.common.annotations.VisibleForTesting;
-
-public class DeleteByQueryPerformer {
-    public static final TimeValue TIMEOUT = new TimeValue(60000);
-
-    private final RestHighLevelClient client;
-    private final ExecutorService executor;
-    private final int batchSize;
-    private final WriteAliasName aliasName;
-    private final TypeName typeName;
-
-    @VisibleForTesting
-    public DeleteByQueryPerformer(RestHighLevelClient client, ExecutorService executor, int batchSize, WriteAliasName aliasName, TypeName typeName) {
-        this.client = client;
-        this.executor = executor;
-        this.batchSize = batchSize;
-        this.aliasName = aliasName;
-        this.typeName = typeName;
-    }
-
-    public Future<Void> perform(QueryBuilder queryBuilder) {
-        return executor.submit(() -> doDeleteByQuery(queryBuilder));
-    }
-
-    protected Void doDeleteByQuery(QueryBuilder queryBuilder) throws IOException {
-        DeleteByQueryRequest request = new DeleteByQueryRequest(aliasName.getValue())
-            .setDocTypes(typeName.getValue())
-            .setScroll(TIMEOUT)
-            .setQuery(queryBuilder)
-            .setBatchSize(batchSize);
-
-        client.deleteByQuery(request, RequestOptions.DEFAULT);
-        return null;
-    }
-}
diff --git a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
index 492ec37..79adb69 100644
--- a/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
+++ b/backends-common/elasticsearch-v6/src/main/java/org/apache/james/backends/es/v6/ElasticSearchIndexer.java
@@ -21,8 +21,6 @@ package org.apache.james.backends.es.v6;
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
 
 import org.apache.commons.lang3.StringUtils;
 import org.elasticsearch.action.bulk.BulkRequest;
@@ -34,8 +32,10 @@ import org.elasticsearch.action.update.UpdateRequest;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.common.ValidationException;
+import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.reindex.DeleteByQueryRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,29 +45,30 @@ import com.google.common.base.Preconditions;
 public class ElasticSearchIndexer {
     private static final int DEBUG_MAX_LENGTH_CONTENT = 1000;
     private static final int DEFAULT_BATCH_SIZE = 100;
+    private static final TimeValue TIMEOUT = new TimeValue(60000);
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchIndexer.class);
 
     private final RestHighLevelClient client;
-    private final DeleteByQueryPerformer deleteByQueryPerformer;
     private final AliasName aliasName;
     private final TypeName typeName;
+    private final int batchSize;
 
-    public ElasticSearchIndexer(RestHighLevelClient client, ExecutorService executor,
+    public ElasticSearchIndexer(RestHighLevelClient client,
                                 WriteAliasName aliasName,
                                 TypeName typeName) {
-        this(client, executor, aliasName, typeName, DEFAULT_BATCH_SIZE);
+        this(client, aliasName, typeName, DEFAULT_BATCH_SIZE);
     }
 
     @VisibleForTesting
-    public ElasticSearchIndexer(RestHighLevelClient client, ExecutorService executor,
+    public ElasticSearchIndexer(RestHighLevelClient client,
                                 WriteAliasName aliasName,
                                 TypeName typeName,
                                 int batchSize) {
         this.client = client;
-        this.deleteByQueryPerformer = new DeleteByQueryPerformer(client, executor, batchSize, aliasName, typeName);
         this.aliasName = aliasName;
         this.typeName = typeName;
+        this.batchSize = batchSize;
     }
 
     public IndexResponse index(String id, String content) throws IOException {
@@ -112,8 +113,14 @@ public class ElasticSearchIndexer {
         }
     }
 
-    public Future<Void> deleteAllMatchingQuery(QueryBuilder queryBuilder) {
-        return deleteByQueryPerformer.perform(queryBuilder);
+    public void deleteAllMatchingQuery(QueryBuilder queryBuilder) {
+        DeleteByQueryRequest request = new DeleteByQueryRequest(aliasName.getValue())
+            .setDocTypes(typeName.getValue())
+            .setScroll(TIMEOUT)
+            .setQuery(queryBuilder)
+            .setBatchSize(batchSize);
+
+        client.deleteByQueryAsync(request, RequestOptions.DEFAULT, new DeleteByQueryActionListener());
     }
 
     private void checkArgument(String content) {
diff --git a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
index fc2219e..08f02a2 100644
--- a/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
+++ b/backends-common/elasticsearch-v6/src/test/java/org/apache/james/backends/es/v6/ElasticSearchIndexerTest.java
@@ -21,11 +21,10 @@ package org.apache.james.backends.es.v6;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 
-import java.util.concurrent.Executors;
-
-import org.apache.james.util.concurrent.NamedThreadFactory;
+import org.awaitility.Duration;
 import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.client.RequestOptions;
@@ -55,9 +54,7 @@ public class ElasticSearchIndexerTest {
             .useIndex(INDEX_NAME)
             .addAlias(ALIAS_NAME)
             .createIndexAndAliases(getESClient());
-        testee = new ElasticSearchIndexer(getESClient(),
-            Executors.newSingleThreadExecutor(NamedThreadFactory.withClassName(getClass())),
-            ALIAS_NAME, TYPE_NAME, MINIMUM_BATCH_SIZE);
+        testee = new ElasticSearchIndexer(getESClient(), ALIAS_NAME, TYPE_NAME, MINIMUM_BATCH_SIZE);
     }
 
     private RestHighLevelClient getESClient() {
@@ -150,16 +147,17 @@ public class ElasticSearchIndexerTest {
         testee.index(messageId, content);
         elasticSearch.awaitForElasticSearch();
         
-        testee.deleteAllMatchingQuery(termQuery("property", "1")).get();
+        testee.deleteAllMatchingQuery(termQuery("property", "1"));
         elasticSearch.awaitForElasticSearch();
         
         try (RestHighLevelClient client = getESClient()) {
-            SearchResponse searchResponse = client.search(
-                new SearchRequest(INDEX_NAME.getValue())
-                    .types(TYPE_NAME.getValue())
-                    .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
-                RequestOptions.DEFAULT);
-            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(0);
+            await().atMost(Duration.TEN_SECONDS)
+                .until(() -> client.search(
+                        new SearchRequest(INDEX_NAME.getValue())
+                            .types(TYPE_NAME.getValue())
+                            .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
+                        RequestOptions.DEFAULT)
+                    .getHits().getTotalHits() == 0);
         }
     }
 
@@ -181,16 +179,17 @@ public class ElasticSearchIndexerTest {
         testee.index(messageId3, content3);
         elasticSearch.awaitForElasticSearch();
 
-        testee.deleteAllMatchingQuery(termQuery("property", "1")).get();
+        testee.deleteAllMatchingQuery(termQuery("property", "1"));
         elasticSearch.awaitForElasticSearch();
         
         try (RestHighLevelClient client = getESClient()) {
-            SearchResponse searchResponse = client.search(
-                new SearchRequest(INDEX_NAME.getValue())
-                    .types(TYPE_NAME.getValue())
-                    .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
-                RequestOptions.DEFAULT);
-            assertThat(searchResponse.getHits().getTotalHits()).isEqualTo(1);
+            await().atMost(Duration.TEN_SECONDS)
+                .until(() -> client.search(
+                    new SearchRequest(INDEX_NAME.getValue())
+                        .types(TYPE_NAME.getValue())
+                        .source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())),
+                    RequestOptions.DEFAULT)
+                    .getHits().getTotalHits() == 1);
         }
     }
     


---------------------------------------------------------------------
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-2149 RRT processing should fail when DomainAlias loop

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 41e29c5ed11f3276f3cab784f83dff24573a4762
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue May 14 18:00:19 2019 +0700

    JAMES-2149 RRT processing should fail when DomainAlias loop
---
 .../james/transport/mailets/DomainMappingTest.java | 47 ++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
index 23cd506..b4997d9 100644
--- a/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
+++ b/server/mailet/integration-testing/src/test/java/org/apache/james/transport/mailets/DomainMappingTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.transport.mailets;
 
+import static org.apache.james.mailets.configuration.CommonProcessors.RRT_ERROR_REPOSITORY;
 import static org.apache.james.mailets.configuration.Constants.DEFAULT_DOMAIN;
 import static org.apache.james.mailets.configuration.Constants.LOCALHOST_IP;
 import static org.apache.james.mailets.configuration.Constants.PASSWORD;
@@ -31,11 +32,14 @@ import org.apache.james.core.builder.MimeMessageBuilder;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailets.TemporaryJamesServer;
+import org.apache.james.mailets.configuration.CommonProcessors;
+import org.apache.james.mailets.configuration.MailetContainer;
 import org.apache.james.modules.MailboxProbeImpl;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
 import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.IMAPMessageReader;
+import org.apache.james.utils.MailRepositoryProbeImpl;
 import org.apache.james.utils.SMTPMessageSender;
 import org.apache.james.utils.WebAdminGuiceProbe;
 import org.apache.james.webadmin.WebAdminUtils;
@@ -56,6 +60,8 @@ public class DomainMappingTest {
     private static final String SENDER = SENDER_LOCAL_PART + "@" + DOMAIN1;
     private static final String USER_DOMAIN1 = "user@" + DOMAIN1;
     private static final String USER_DOMAIN2 = "user@" + DOMAIN2;
+    private static final String BOB_DOMAIN1 = "bob@" + DOMAIN1;
+    private static final String BOB_DOMAIN2 = "bob@" + DOMAIN2;
     private static final String MESSAGE_CONTENT = "any text";
 
     private TemporaryJamesServer jamesServer;
@@ -71,7 +77,12 @@ public class DomainMappingTest {
 
     @Before
     public void setup() throws Exception {
+        MailetContainer.Builder mailetContainer = TemporaryJamesServer.SIMPLE_MAILET_CONTAINER_CONFIGURATION
+            .putProcessor(CommonProcessors.rrtErrorEnabledTransport())
+            .putProcessor(CommonProcessors.rrtErrorProcessor());
+
         jamesServer = TemporaryJamesServer.builder()
+            .withMailetContainer(mailetContainer)
             .build(temporaryFolder);
 
         jamesServer.getProbe(DataProbeImpl.class).fluent()
@@ -131,4 +142,40 @@ public class DomainMappingTest {
             .awaitMessage(awaitAtMostOneMinute);
         assertThat(imapMessageReader.readFirstMessage()).contains(MESSAGE_CONTENT);
     }
+
+    @Test
+    public void mailShouldGoToRRTErrorMailRepositoryUponDomainLoop() throws Exception {
+        webAdminApi.put("/domains/" + DOMAIN1 + "/aliases/" + DOMAIN2);
+        webAdminApi.put("/domains/" + DOMAIN2 + "/aliases/" + DOMAIN1);
+
+        messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .sendMessage(FakeMail.builder()
+                .name("name")
+                .mimeMessage(message)
+                .sender(SENDER)
+                .recipient(USER_DOMAIN2));
+
+        awaitAtMostOneMinute.until(
+            () -> jamesServer.getProbe(MailRepositoryProbeImpl.class)
+                .getRepositoryMailCount(RRT_ERROR_REPOSITORY) == 1);
+    }
+
+    @Test
+    public void mailShouldGoToRRTErrorMailRepositoryUponLoopCombiningDomainAndAlias() throws Exception {
+        jamesServer.getProbe(DataProbeImpl.class).addUser(BOB_DOMAIN2, PASSWORD);
+
+        webAdminApi.put("/domains/" + DOMAIN1 + "/aliases/" + DOMAIN2);
+        webAdminApi.put("/address/aliases/" + BOB_DOMAIN2 + "/sources/" + BOB_DOMAIN1);
+
+        messageSender.connect(LOCALHOST_IP, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .sendMessage(FakeMail.builder()
+                .name("name")
+                .mimeMessage(message)
+                .sender(SENDER)
+                .recipient(BOB_DOMAIN1));
+
+        awaitAtMostOneMinute.until(
+            () -> jamesServer.getProbe(MailRepositoryProbeImpl.class)
+                .getRepositoryMailCount(RRT_ERROR_REPOSITORY) == 1);
+    }
 }


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