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:28 UTC

[james-project] 13/23: JAMES-2719 Copy of backends-es module to a new backends-es-v6 module

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