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/23 03:37:49 UTC
[james-project] 01/14: JAMES-2764 Copy of mailbox-elasticsearch
module to a new mailbox-elasticsearch-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 861b27ee70b0d79ccdcb8415dcc9715d1d60ba27
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu May 16 15:45:03 2019 +0700
JAMES-2764 Copy of mailbox-elasticsearch module to a new mailbox-elasticsearch-v6 module
---
mailbox/elasticsearch-v6/pom.xml | 203 ++++
.../v6/ElasticSearchMailboxConfiguration.java | 213 ++++
.../mailbox/elasticsearch/v6/IndexAttachments.java | 24 +
.../v6/MailboxElasticSearchConstants.java | 37 +
.../elasticsearch/v6/MailboxIndexCreationUtil.java | 56 +
.../elasticsearch/v6/MailboxMappingFactory.java | 365 ++++++
.../ElasticSearchListeningMessageSearchIndex.java | 196 +++
.../mailbox/elasticsearch/v6/json/EMailer.java | 75 ++
.../mailbox/elasticsearch/v6/json/EMailers.java | 52 +
.../elasticsearch/v6/json/HeaderCollection.java | 224 ++++
.../elasticsearch/v6/json/IndexableMessage.java | 471 ++++++++
.../v6/json/JsonMessageConstants.java | 83 ++
.../v6/json/MessageToElasticSearchJson.java | 86 ++
.../elasticsearch/v6/json/MessageUpdateJson.java | 79 ++
.../mailbox/elasticsearch/v6/json/MimePart.java | 303 +++++
.../v6/json/MimePartContainerBuilder.java | 50 +
.../elasticsearch/v6/json/MimePartParser.java | 129 ++
.../v6/json/RootMimePartContainerBuilder.java | 96 ++
.../elasticsearch/v6/json/Serializable.java | 25 +
.../mailbox/elasticsearch/v6/json/Subjects.java | 50 +
.../elasticsearch/v6/query/CriterionConverter.java | 310 +++++
.../v6/query/DateResolutionFormater.java | 73 ++
.../elasticsearch/v6/query/QueryConverter.java | 79 ++
.../elasticsearch/v6/query/SortConverter.java | 81 ++
.../v6/search/ElasticSearchSearcher.java | 138 +++
.../elasticsearch-v6/src/reporting-site/site.xml | 29 +
.../v6/ElasticSearchIntegrationTest.java | 223 ++++
.../v6/ElasticSearchMailboxConfigurationTest.java | 219 ++++
...asticSearchListeningMessageSearchIndexTest.java | 269 +++++
.../elasticsearch/v6/json/EMailersTest.java | 66 +
.../mailbox/elasticsearch/v6/json/FieldImpl.java | 65 +
.../v6/json/HeaderCollectionTest.java | 334 +++++
.../v6/json/IndexableMessageTest.java | 578 +++++++++
.../v6/json/MessageToElasticSearchJsonTest.java | 388 ++++++
.../elasticsearch/v6/json/MimePartTest.java | 50 +
.../elasticsearch/v6/json/SubjectsTest.java | 66 +
.../v6/query/DateResolutionFormaterTest.java | 99 ++
.../elasticsearch/v6/query/SearchQueryTest.java | 77 ++
.../src/test/resources/eml/bodyMakeTikaToFail.eml | 1272 ++++++++++++++++++++
.../test/resources/eml/emailWith3Attachments.eml | 50 +
.../src/test/resources/eml/mailWithHeaders.eml | 14 +
.../src/test/resources/logback-test.xml | 24 +
mailbox/pom.xml | 1 +
43 files changed, 7322 insertions(+)
diff --git a/mailbox/elasticsearch-v6/pom.xml b/mailbox/elasticsearch-v6/pom.xml
new file mode 100644
index 0000000..6bf0500
--- /dev/null
+++ b/mailbox/elasticsearch-v6/pom.xml
@@ -0,0 +1,203 @@
+<?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>apache-james-mailbox</artifactId>
+ <version>3.4.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>apache-james-mailbox-elasticsearch-v6</artifactId>
+ <name>Apache James :: Mailbox :: ElasticSearch :: v6</name>
+ <description>Apache James Mailbox IMAP search implementation using ElasticSearch v6</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-backends-es</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-backends-es</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-event-memory</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-memory</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-memory</artifactId>
+ <scope>test</scope>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-store</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-store</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-tika</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-tika</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>james-server-util</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james</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.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jdk8</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.github.steveash.guavate</groupId>
+ <artifactId>guavate</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.jayway.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.sun.mail</groupId>
+ <artifactId>javax.mail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.javacrumbs.json-unit</groupId>
+ <artifactId>json-unit-assertj</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </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.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <reuseForks>true</reuseForks>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchMailboxConfiguration.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchMailboxConfiguration.java
new file mode 100644
index 0000000..6ff086c
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchMailboxConfiguration.java
@@ -0,0 +1,213 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.commons.configuration.Configuration;
+import org.apache.james.backends.es.IndexName;
+import org.apache.james.backends.es.ReadAliasName;
+import org.apache.james.backends.es.WriteAliasName;
+import org.apache.james.util.OptionalUtils;
+
+public class ElasticSearchMailboxConfiguration {
+
+ public static class Builder {
+ private Optional<IndexName> indexMailboxName;
+ private Optional<ReadAliasName> readAliasMailboxName;
+ private Optional<WriteAliasName> writeAliasMailboxName;
+ private Optional<IndexAttachments> indexAttachment;
+
+ public Builder() {
+ indexMailboxName = Optional.empty();
+ readAliasMailboxName = Optional.empty();
+ writeAliasMailboxName = Optional.empty();
+ indexAttachment = Optional.empty();
+ }
+
+ public Builder indexMailboxName(IndexName indexMailboxName) {
+ return indexMailboxName(Optional.of(indexMailboxName));
+ }
+
+ public Builder indexMailboxName(Optional<IndexName> indexMailboxName) {
+ this.indexMailboxName = indexMailboxName;
+ return this;
+ }
+
+ public Builder readAliasMailboxName(ReadAliasName readAliasMailboxName) {
+ return readAliasMailboxName(Optional.of(readAliasMailboxName));
+ }
+
+ public Builder readAliasMailboxName(Optional<ReadAliasName> readAliasMailboxName) {
+ this.readAliasMailboxName = readAliasMailboxName;
+ return this;
+ }
+
+ public Builder writeAliasMailboxName(WriteAliasName writeAliasMailboxName) {
+ return writeAliasMailboxName(Optional.of(writeAliasMailboxName));
+ }
+
+ public Builder writeAliasMailboxName(Optional<WriteAliasName> writeAliasMailboxName) {
+ this.writeAliasMailboxName = writeAliasMailboxName;
+ return this;
+ }
+
+
+ public Builder indexAttachment(IndexAttachments indexAttachment) {
+ this.indexAttachment = Optional.of(indexAttachment);
+ return this;
+ }
+
+
+
+ public ElasticSearchMailboxConfiguration build() {
+ return new ElasticSearchMailboxConfiguration(
+ indexMailboxName.orElse(MailboxElasticSearchConstants.DEFAULT_MAILBOX_INDEX),
+ readAliasMailboxName.orElse(MailboxElasticSearchConstants.DEFAULT_MAILBOX_READ_ALIAS),
+ writeAliasMailboxName.orElse(MailboxElasticSearchConstants.DEFAULT_MAILBOX_WRITE_ALIAS),
+ indexAttachment.orElse(IndexAttachments.YES));
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final String ELASTICSEARCH_HOSTS = "elasticsearch.hosts";
+ public static final String ELASTICSEARCH_MASTER_HOST = "elasticsearch.masterHost";
+ public static final String ELASTICSEARCH_PORT = "elasticsearch.port";
+ public static final String ELASTICSEARCH_INDEX_NAME = "elasticsearch.index.name";
+ public static final String ELASTICSEARCH_INDEX_MAILBOX_NAME = "elasticsearch.index.mailbox.name";
+ 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_ALIAS_READ_NAME = "elasticsearch.alias.read.name";
+ public static final String ELASTICSEARCH_ALIAS_WRITE_NAME = "elasticsearch.alias.write.name";
+ public static final String ELASTICSEARCH_ALIAS_READ_MAILBOX_NAME = "elasticsearch.alias.read.mailbox.name";
+ public static final String ELASTICSEARCH_ALIAS_WRITE_MAILBOX_NAME = "elasticsearch.alias.write.mailbox.name";
+ public static final String ELASTICSEARCH_INDEX_QUOTA_RATIO_NAME = "elasticsearch.index.quota.ratio.name";
+ public static final String ELASTICSEARCH_ALIAS_READ_QUOTA_RATIO_NAME = "elasticsearch.alias.read.quota.ratio.name";
+ public static final String ELASTICSEARCH_ALIAS_WRITE_QUOTA_RATIO_NAME = "elasticsearch.alias.write.quota.ratio.name";
+ 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 String ELASTICSEARCH_INDEX_ATTACHMENTS = "elasticsearch.indexAttachments";
+
+ public static final int DEFAULT_CONNECTION_MAX_RETRIES = 7;
+ public static final int DEFAULT_CONNECTION_MIN_DELAY = 3000;
+ public static final boolean DEFAULT_INDEX_ATTACHMENTS = true;
+ public static final int DEFAULT_NB_SHARDS = 5;
+ public static final int DEFAULT_NB_REPLICA = 1;
+ public static final int DEFAULT_PORT = 9300;
+ public static final Optional<Integer> DEFAULT_PORT_AS_OPTIONAL = Optional.of(DEFAULT_PORT);
+
+ public static final ElasticSearchMailboxConfiguration DEFAULT_CONFIGURATION = builder().build();
+
+ public static ElasticSearchMailboxConfiguration fromProperties(Configuration configuration) {
+ return builder()
+ .indexMailboxName(computeMailboxIndexName(configuration))
+ .readAliasMailboxName(computeMailboxReadAlias(configuration))
+ .writeAliasMailboxName(computeMailboxWriteAlias(configuration))
+ .indexAttachment(provideIndexAttachments(configuration))
+ .build();
+ }
+
+ public static Optional<IndexName> computeMailboxIndexName(Configuration configuration) {
+ return OptionalUtils.or(
+ Optional.ofNullable(configuration.getString(ELASTICSEARCH_INDEX_MAILBOX_NAME))
+ .map(IndexName::new),
+ Optional.ofNullable(configuration.getString(ELASTICSEARCH_INDEX_NAME))
+ .map(IndexName::new));
+ }
+
+ public static Optional<WriteAliasName> computeMailboxWriteAlias(Configuration configuration) {
+ return OptionalUtils.or(
+ Optional.ofNullable(configuration.getString(ELASTICSEARCH_ALIAS_WRITE_MAILBOX_NAME))
+ .map(WriteAliasName::new),
+ Optional.ofNullable(configuration.getString(ELASTICSEARCH_ALIAS_WRITE_NAME))
+ .map(WriteAliasName::new));
+ }
+
+ public static Optional<ReadAliasName> computeMailboxReadAlias(Configuration configuration) {
+ return OptionalUtils.or(
+ Optional.ofNullable(configuration.getString(ELASTICSEARCH_ALIAS_READ_MAILBOX_NAME))
+ .map(ReadAliasName::new),
+ Optional.ofNullable(configuration.getString(ELASTICSEARCH_ALIAS_READ_NAME))
+ .map(ReadAliasName::new));
+ }
+
+
+ private static IndexAttachments provideIndexAttachments(Configuration configuration) {
+ if (configuration.getBoolean(ELASTICSEARCH_INDEX_ATTACHMENTS, DEFAULT_INDEX_ATTACHMENTS)) {
+ return IndexAttachments.YES;
+ }
+ return IndexAttachments.NO;
+ }
+
+
+
+
+ private final IndexName indexMailboxName;
+ private final ReadAliasName readAliasMailboxName;
+ private final WriteAliasName writeAliasMailboxName;
+ private final IndexAttachments indexAttachment;
+
+ private ElasticSearchMailboxConfiguration(IndexName indexMailboxName, ReadAliasName readAliasMailboxName,
+ WriteAliasName writeAliasMailboxName, IndexAttachments indexAttachment) {
+ this.indexMailboxName = indexMailboxName;
+ this.readAliasMailboxName = readAliasMailboxName;
+ this.writeAliasMailboxName = writeAliasMailboxName;
+ this.indexAttachment = indexAttachment;
+ }
+
+
+ public IndexName getIndexMailboxName() {
+ return indexMailboxName;
+ }
+
+ public ReadAliasName getReadAliasMailboxName() {
+ return readAliasMailboxName;
+ }
+
+ public WriteAliasName getWriteAliasMailboxName() {
+ return writeAliasMailboxName;
+ }
+
+ public IndexAttachments getIndexAttachment() {
+ return indexAttachment;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof ElasticSearchMailboxConfiguration) {
+ ElasticSearchMailboxConfiguration that = (ElasticSearchMailboxConfiguration) o;
+
+ return Objects.equals(this.indexAttachment, that.indexAttachment)
+ && Objects.equals(this.indexMailboxName, that.indexMailboxName)
+ && Objects.equals(this.readAliasMailboxName, that.readAliasMailboxName)
+ && Objects.equals(this.writeAliasMailboxName, that.writeAliasMailboxName);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(indexMailboxName, readAliasMailboxName, writeAliasMailboxName, indexAttachment, writeAliasMailboxName);
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/IndexAttachments.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/IndexAttachments.java
new file mode 100644
index 0000000..f827bd8
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/IndexAttachments.java
@@ -0,0 +1,24 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6;
+
+public enum IndexAttachments {
+ NO, YES
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxElasticSearchConstants.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxElasticSearchConstants.java
new file mode 100644
index 0000000..301127e
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxElasticSearchConstants.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.mailbox.elasticsearch.v6;
+
+import org.apache.james.backends.es.IndexName;
+import org.apache.james.backends.es.ReadAliasName;
+import org.apache.james.backends.es.TypeName;
+import org.apache.james.backends.es.WriteAliasName;
+
+public interface MailboxElasticSearchConstants {
+
+ interface InjectionNames {
+ String MAILBOX = "mailbox";
+ }
+
+ WriteAliasName DEFAULT_MAILBOX_WRITE_ALIAS = new WriteAliasName("mailboxWriteAlias");
+ ReadAliasName DEFAULT_MAILBOX_READ_ALIAS = new ReadAliasName("mailboxReadAlias");
+ IndexName DEFAULT_MAILBOX_INDEX = new IndexName("mailbox_v1");
+ TypeName MESSAGE_TYPE = new TypeName("message");
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxIndexCreationUtil.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxIndexCreationUtil.java
new file mode 100644
index 0000000..4d320e4
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxIndexCreationUtil.java
@@ -0,0 +1,56 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6;
+
+import org.apache.james.backends.es.ElasticSearchConfiguration;
+import org.apache.james.backends.es.IndexCreationFactory;
+import org.apache.james.backends.es.IndexName;
+import org.apache.james.backends.es.NodeMappingFactory;
+import org.apache.james.backends.es.ReadAliasName;
+import org.apache.james.backends.es.WriteAliasName;
+import org.elasticsearch.client.Client;
+
+public class MailboxIndexCreationUtil {
+
+ public static Client prepareClient(Client client,
+ ReadAliasName readAlias,
+ WriteAliasName writeAlias,
+ IndexName indexName,
+ ElasticSearchConfiguration configuration) {
+
+ return NodeMappingFactory.applyMapping(
+ new IndexCreationFactory(configuration)
+ .useIndex(indexName)
+ .addAlias(readAlias)
+ .addAlias(writeAlias)
+ .createIndexAndAliases(client),
+ indexName,
+ MailboxElasticSearchConstants.MESSAGE_TYPE,
+ MailboxMappingFactory.getMappingContent());
+ }
+
+ public static Client prepareDefaultClient(Client client, ElasticSearchConfiguration configuration) {
+ return prepareClient(client,
+ MailboxElasticSearchConstants.DEFAULT_MAILBOX_READ_ALIAS,
+ MailboxElasticSearchConstants.DEFAULT_MAILBOX_WRITE_ALIAS,
+ MailboxElasticSearchConstants.DEFAULT_MAILBOX_INDEX,
+ configuration);
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxMappingFactory.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxMappingFactory.java
new file mode 100644
index 0000000..2ecd6d3
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/MailboxMappingFactory.java
@@ -0,0 +1,365 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6;
+
+import static org.apache.james.backends.es.IndexCreationFactory.CASE_INSENSITIVE;
+import static org.apache.james.backends.es.IndexCreationFactory.KEEP_MAIL_AND_URL;
+import static org.apache.james.backends.es.IndexCreationFactory.SNOWBALL_KEEP_MAIL_AND_URL;
+import static org.apache.james.backends.es.NodeMappingFactory.ANALYZER;
+import static org.apache.james.backends.es.NodeMappingFactory.BOOLEAN;
+import static org.apache.james.backends.es.NodeMappingFactory.FIELDS;
+import static org.apache.james.backends.es.NodeMappingFactory.FORMAT;
+import static org.apache.james.backends.es.NodeMappingFactory.IGNORE_ABOVE;
+import static org.apache.james.backends.es.NodeMappingFactory.INDEX;
+import static org.apache.james.backends.es.NodeMappingFactory.LONG;
+import static org.apache.james.backends.es.NodeMappingFactory.NESTED;
+import static org.apache.james.backends.es.NodeMappingFactory.NOT_ANALYZED;
+import static org.apache.james.backends.es.NodeMappingFactory.PROPERTIES;
+import static org.apache.james.backends.es.NodeMappingFactory.RAW;
+import static org.apache.james.backends.es.NodeMappingFactory.SEARCH_ANALYZER;
+import static org.apache.james.backends.es.NodeMappingFactory.SNOWBALL;
+import static org.apache.james.backends.es.NodeMappingFactory.SPLIT_EMAIL;
+import static org.apache.james.backends.es.NodeMappingFactory.STRING;
+import static org.apache.james.backends.es.NodeMappingFactory.TYPE;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.BCC;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.CC;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.DATE;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.FROM;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.HAS_ATTACHMENT;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.HTML_BODY;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.IS_ANSWERED;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.IS_DELETED;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.IS_DRAFT;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.IS_FLAGGED;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.IS_RECENT;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.IS_UNREAD;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.MAILBOX_ID;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.MEDIA_TYPE;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.MESSAGE_ID;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.MIME_MESSAGE_ID;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.MODSEQ;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.SENT_DATE;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.SIZE;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.SUBJECT;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.SUBTYPE;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.TEXT;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.TEXT_BODY;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.TO;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.UID;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.USERS;
+import static org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.USER_FLAGS;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+
+import java.io.IOException;
+
+import org.apache.james.backends.es.NodeMappingFactory;
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.EMailer;
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants.Property;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+public class MailboxMappingFactory {
+
+ private static final int MAXIMUM_TERM_LENGTH = 4096;
+ private static final String STANDARD = "standard";
+
+ public static XContentBuilder getMappingContent() {
+ try {
+ return jsonBuilder()
+ .startObject()
+
+ .startObject(MailboxElasticSearchConstants.MESSAGE_TYPE.getValue())
+ .startObject(PROPERTIES)
+
+ .startObject(MESSAGE_ID)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(UID)
+ .field(TYPE, LONG)
+ .endObject()
+
+ .startObject(MODSEQ)
+ .field(TYPE, LONG)
+ .endObject()
+
+ .startObject(SIZE)
+ .field(TYPE, LONG)
+ .endObject()
+
+ .startObject(IS_ANSWERED)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(IS_DELETED)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(IS_DRAFT)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(IS_FLAGGED)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(IS_RECENT)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(IS_UNREAD)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(DATE)
+ .field(TYPE, NodeMappingFactory.DATE)
+ .field(FORMAT, "yyyy-MM-dd'T'HH:mm:ssZ")
+ .endObject()
+
+ .startObject(SENT_DATE)
+ .field(TYPE, NodeMappingFactory.DATE)
+ .field(FORMAT, "yyyy-MM-dd'T'HH:mm:ssZ")
+ .endObject()
+
+ .startObject(MEDIA_TYPE)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(SUBTYPE)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(USER_FLAGS)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(FROM)
+ .field(TYPE, NESTED)
+ .startObject(PROPERTIES)
+ .startObject(EMailer.NAME)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .startObject(EMailer.ADDRESS)
+ .field(TYPE, STRING)
+ .field(ANALYZER, STANDARD)
+ .field(SEARCH_ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(SUBJECT)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(TO)
+ .field(TYPE, NESTED)
+ .startObject(PROPERTIES)
+ .startObject(EMailer.NAME)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .startObject(EMailer.ADDRESS)
+ .field(TYPE, STRING)
+ .field(ANALYZER, STANDARD)
+ .field(SEARCH_ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(CC)
+ .field(TYPE, NESTED)
+ .startObject(PROPERTIES)
+ .startObject(EMailer.NAME)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .startObject(EMailer.ADDRESS)
+ .field(TYPE, STRING)
+ .field(ANALYZER, STANDARD)
+ .field(SEARCH_ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(BCC)
+ .field(TYPE, NESTED)
+ .startObject(PROPERTIES)
+ .startObject(EMailer.NAME)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .startObject(EMailer.ADDRESS)
+ .field(TYPE, STRING)
+ .field(ANALYZER, STANDARD)
+ .field(SEARCH_ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(MAILBOX_ID)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(MIME_MESSAGE_ID)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(USERS)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+
+ .startObject(PROPERTIES)
+ .field(TYPE, NESTED)
+ .startObject(PROPERTIES)
+ .startObject(Property.NAMESPACE)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+ .startObject(Property.NAME)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+ .startObject(Property.VALUE)
+ .field(TYPE, STRING)
+ .field(INDEX, NOT_ANALYZED)
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(TEXT_BODY)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(SPLIT_EMAIL)
+ .field(TYPE, STRING)
+ .field(ANALYZER, STANDARD)
+ .field(SEARCH_ANALYZER, KEEP_MAIL_AND_URL)
+ .endObject()
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .field(IGNORE_ABOVE, MAXIMUM_TERM_LENGTH)
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(HTML_BODY)
+ .field(TYPE, STRING)
+ .field(ANALYZER, KEEP_MAIL_AND_URL)
+ .startObject(FIELDS)
+ .startObject(SPLIT_EMAIL)
+ .field(TYPE, STRING)
+ .field(ANALYZER, STANDARD)
+ .field(SEARCH_ANALYZER, KEEP_MAIL_AND_URL)
+ .endObject()
+ .startObject(RAW)
+ .field(TYPE, STRING)
+ .field(ANALYZER, CASE_INSENSITIVE)
+ .field(IGNORE_ABOVE, MAXIMUM_TERM_LENGTH)
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .startObject(HAS_ATTACHMENT)
+ .field(TYPE, BOOLEAN)
+ .endObject()
+
+ .startObject(TEXT)
+ .field(TYPE, STRING)
+ .field(ANALYZER, SNOWBALL_KEEP_MAIL_AND_URL)
+ .field(IGNORE_ABOVE, MAXIMUM_TERM_LENGTH)
+ .startObject(FIELDS)
+ .startObject(SPLIT_EMAIL)
+ .field(TYPE, STRING)
+ .field(ANALYZER, SNOWBALL)
+ .field(SEARCH_ANALYZER, SNOWBALL_KEEP_MAIL_AND_URL)
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject()
+ .endObject();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/events/ElasticSearchListeningMessageSearchIndex.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/events/ElasticSearchListeningMessageSearchIndex.java
new file mode 100644
index 0000000..8ee2204
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/events/ElasticSearchListeningMessageSearchIndex.java
@@ -0,0 +1,196 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.events;
+
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.james.backends.es.ElasticSearchIndexer;
+import org.apache.james.backends.es.UpdatedRepresentation;
+import org.apache.james.mailbox.MailboxManager.MessageCapabilities;
+import org.apache.james.mailbox.MailboxManager.SearchCapabilities;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.mailbox.elasticsearch.v6.MailboxElasticSearchConstants;
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants;
+import org.apache.james.mailbox.elasticsearch.v6.json.MessageToElasticSearchJson;
+import org.apache.james.mailbox.elasticsearch.v6.search.ElasticSearchSearcher;
+import org.apache.james.mailbox.events.Group;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.Mailbox;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.SearchQuery;
+import org.apache.james.mailbox.model.UpdatedFlags;
+import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.SessionProvider;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.search.ListeningMessageSearchIndex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.github.fge.lambdas.Throwing;
+import com.github.steveash.guavate.Guavate;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+public class ElasticSearchListeningMessageSearchIndex extends ListeningMessageSearchIndex {
+ public static class ElasticSearchListeningMessageSearchIndexGroup extends Group {}
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchListeningMessageSearchIndex.class);
+ private static final String ID_SEPARATOR = ":";
+ private static final Group GROUP = new ElasticSearchListeningMessageSearchIndexGroup();
+
+ private final ElasticSearchIndexer elasticSearchIndexer;
+ private final ElasticSearchSearcher searcher;
+ private final MessageToElasticSearchJson messageToElasticSearchJson;
+
+ @Inject
+ public ElasticSearchListeningMessageSearchIndex(MailboxSessionMapperFactory factory,
+ @Named(MailboxElasticSearchConstants.InjectionNames.MAILBOX) ElasticSearchIndexer indexer,
+ ElasticSearchSearcher searcher, MessageToElasticSearchJson messageToElasticSearchJson,
+ SessionProvider sessionProvider) {
+ super(factory, sessionProvider);
+ this.elasticSearchIndexer = indexer;
+ this.messageToElasticSearchJson = messageToElasticSearchJson;
+ this.searcher = searcher;
+ }
+
+ @Override
+ public Group getDefaultGroup() {
+ return GROUP;
+ }
+
+ @Override
+ public EnumSet<SearchCapabilities> getSupportedCapabilities(EnumSet<MessageCapabilities> messageCapabilities) {
+ return EnumSet.of(
+ SearchCapabilities.MultimailboxSearch,
+ SearchCapabilities.Text,
+ SearchCapabilities.FullText,
+ SearchCapabilities.Attachment,
+ SearchCapabilities.AttachmentFileName,
+ SearchCapabilities.PartialEmailMatch);
+ }
+
+ @Override
+ public Iterator<MessageUid> search(MailboxSession session, Mailbox mailbox, SearchQuery searchQuery) throws MailboxException {
+ Preconditions.checkArgument(session != null, "'session' is mandatory");
+ Optional<Long> noLimit = Optional.empty();
+ return searcher
+ .search(ImmutableList.of(mailbox.getMailboxId()), searchQuery, noLimit)
+ .map(SearchResult::getMessageUid)
+ .iterator();
+ }
+
+ @Override
+ public List<MessageId> search(MailboxSession session, Collection<MailboxId> mailboxIds, SearchQuery searchQuery, long limit)
+ throws MailboxException {
+ Preconditions.checkArgument(session != null, "'session' is mandatory");
+
+ if (mailboxIds.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ return searcher.search(mailboxIds, searchQuery, Optional.empty())
+ .peek(this::logIfNoMessageId)
+ .map(SearchResult::getMessageId)
+ .map(Optional::get)
+ .distinct()
+ .limit(limit)
+ .collect(Guavate.toImmutableList());
+ }
+
+ @Override
+ public void add(MailboxSession session, Mailbox mailbox, MailboxMessage message) throws JsonProcessingException {
+ LOGGER.info("Indexing mailbox {}-{} of user {} on message {}",
+ mailbox.getName(),
+ mailbox.getMailboxId(),
+ session.getUser().asString(),
+ message.getUid());
+
+ String jsonContent = generateIndexedJson(mailbox, message, session);
+ elasticSearchIndexer.index(indexIdFor(mailbox, message.getUid()), jsonContent);
+ }
+
+ private String generateIndexedJson(Mailbox mailbox, MailboxMessage message, MailboxSession session) throws JsonProcessingException {
+ try {
+ return messageToElasticSearchJson.convertToJson(message, ImmutableList.of(session.getUser()));
+ } catch (Exception e) {
+ LOGGER.warn("Indexing mailbox {}-{} of user {} on message {} without attachments ",
+ mailbox.getName(),
+ mailbox.getMailboxId().serialize(),
+ session.getUser().asString(),
+ message.getUid(),
+ e);
+ return messageToElasticSearchJson.convertToJsonWithoutAttachment(message, ImmutableList.of(session.getUser()));
+ }
+ }
+
+ @Override
+ public void delete(MailboxSession session, Mailbox mailbox, Collection<MessageUid> expungedUids) {
+ elasticSearchIndexer.delete(expungedUids.stream()
+ .map(uid -> indexIdFor(mailbox, uid))
+ .collect(Guavate.toImmutableList()));
+ }
+
+ @Override
+ public void deleteAll(MailboxSession session, Mailbox mailbox) {
+ elasticSearchIndexer.deleteAllMatchingQuery(
+ termQuery(
+ JsonMessageConstants.MAILBOX_ID,
+ mailbox.getMailboxId().serialize()));
+ }
+
+ @Override
+ public void update(MailboxSession session, Mailbox mailbox, List<UpdatedFlags> updatedFlagsList) {
+ elasticSearchIndexer.update(updatedFlagsList.stream()
+ .map(Throwing.<UpdatedFlags, UpdatedRepresentation>function(
+ updatedFlags -> createUpdatedDocumentPartFromUpdatedFlags(mailbox, updatedFlags))
+ .sneakyThrow())
+ .collect(Guavate.toImmutableList()));
+ }
+
+ private UpdatedRepresentation createUpdatedDocumentPartFromUpdatedFlags(Mailbox mailbox, UpdatedFlags updatedFlags) throws JsonProcessingException {
+ return new UpdatedRepresentation(
+ indexIdFor(mailbox, updatedFlags.getUid()),
+ messageToElasticSearchJson.getUpdatedJsonMessagePart(
+ updatedFlags.getNewFlags(),
+ updatedFlags.getModSeq()));
+ }
+
+ private String indexIdFor(Mailbox mailbox, MessageUid uid) {
+ return String.join(ID_SEPARATOR, mailbox.getMailboxId().serialize(), String.valueOf(uid.asLong()));
+ }
+
+ private void logIfNoMessageId(SearchResult searchResult) {
+ if (!searchResult.getMessageId().isPresent()) {
+ LOGGER.error("No messageUid for {} in mailbox {}", searchResult.getMessageUid(), searchResult.getMailboxId());
+ }
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailer.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailer.java
new file mode 100644
index 0000000..83ff556
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailer.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.mailbox.elasticsearch.v6.json;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+
+public class EMailer implements Serializable {
+
+ private final String name;
+ private final String address;
+
+ public EMailer(String name, String address) {
+ this.name = name;
+ this.address = address;
+ }
+
+ @JsonProperty(JsonMessageConstants.EMailer.NAME)
+ public String getName() {
+ return name;
+ }
+
+ @JsonProperty(JsonMessageConstants.EMailer.ADDRESS)
+ public String getAddress() {
+ return address;
+ }
+
+ @Override
+ public String serialize() {
+ return Joiner.on(" ").join(name, address);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof EMailer) {
+ EMailer otherEMailer = (EMailer) o;
+ return Objects.equals(name, otherEMailer.name)
+ && Objects.equals(address, otherEMailer.address);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, address);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("name", name)
+ .add("address", address)
+ .toString();
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailers.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailers.java
new file mode 100644
index 0000000..e5e8e65
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailers.java
@@ -0,0 +1,52 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.base.Preconditions;
+
+public class EMailers implements Serializable {
+
+ public static EMailers from(Set<EMailer> emailers) {
+ Preconditions.checkNotNull(emailers, "'emailers' is mandatory");
+ return new EMailers(emailers);
+ }
+
+ private final Set<EMailer> emailers;
+
+ private EMailers(Set<EMailer> emailers) {
+ this.emailers = emailers;
+ }
+
+ @JsonValue
+ public Set<EMailer> getEmailers() {
+ return emailers;
+ }
+
+ @Override
+ public String serialize() {
+ return emailers.stream()
+ .map(EMailer::serialize)
+ .collect(Collectors.joining(" "));
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/HeaderCollection.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/HeaderCollection.java
new file mode 100644
index 0000000..83f2b52
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/HeaderCollection.java
@@ -0,0 +1,224 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import java.time.ZonedDateTime;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.james.mailbox.store.search.SearchUtil;
+import org.apache.james.mailbox.store.search.comparator.SentDateComparator;
+import org.apache.james.mime4j.dom.address.Address;
+import org.apache.james.mime4j.dom.address.Group;
+import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.field.address.LenientAddressParser;
+import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.util.MimeUtil;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+
+public class HeaderCollection {
+
+ public static class Builder {
+
+ private final Set<EMailer> toAddressSet;
+ private final Set<EMailer> fromAddressSet;
+ private final Set<EMailer> ccAddressSet;
+ private final Set<EMailer> bccAddressSet;
+ private final Set<EMailer> replyToAddressSet;
+ private final Set<String> subjectSet;
+ private final Multimap<String, String> headers;
+ private Optional<ZonedDateTime> sentDate;
+ private Optional<String> messageID;
+
+ private Builder() {
+ toAddressSet = new HashSet<>();
+ fromAddressSet = new HashSet<>();
+ ccAddressSet = new HashSet<>();
+ bccAddressSet = new HashSet<>();
+ replyToAddressSet = new HashSet<>();
+ subjectSet = new HashSet<>();
+ headers = ArrayListMultimap.create();
+ sentDate = Optional.empty();
+ messageID = Optional.empty();
+ }
+
+ public Builder add(Field field) {
+ Preconditions.checkNotNull(field);
+ String headerName = field.getName().toLowerCase(Locale.US);
+ String rawHeaderValue = field.getBody();
+ String sanitizedValue = MimeUtil.unscrambleHeaderValue(rawHeaderValue);
+
+ if (!headerName.contains(".")) {
+ headers.put(headerName, sanitizedValue);
+ }
+ handleSpecificHeader(headerName, sanitizedValue, rawHeaderValue);
+ return this;
+ }
+
+ public HeaderCollection build() {
+ return new HeaderCollection(
+ ImmutableSet.copyOf(toAddressSet),
+ ImmutableSet.copyOf(fromAddressSet),
+ ImmutableSet.copyOf(ccAddressSet),
+ ImmutableSet.copyOf(bccAddressSet),
+ ImmutableSet.copyOf(replyToAddressSet),
+ ImmutableSet.copyOf(subjectSet),
+ ImmutableMultimap.copyOf(headers),
+ sentDate, messageID);
+ }
+
+ private void handleSpecificHeader(String headerName, String headerValue, String rawHeaderValue) {
+ switch (headerName) {
+ case TO:
+ case FROM:
+ case CC:
+ case BCC:
+ case REPLY_TO:
+ manageAddressField(headerName, rawHeaderValue);
+ break;
+ case SUBJECT:
+ subjectSet.add(headerValue);
+ break;
+ case DATE:
+ sentDate = SentDateComparator.toISODate(headerValue);
+ break;
+ case MESSAGE_ID:
+ messageID = Optional.ofNullable(headerValue);
+ break;
+ }
+ }
+
+ private void manageAddressField(String headerName, String rawHeaderValue) {
+ LenientAddressParser.DEFAULT
+ .parseAddressList(rawHeaderValue)
+ .stream()
+ .flatMap(this::convertAddressToMailboxStream)
+ .map((mailbox) -> new EMailer(SearchUtil.getDisplayAddress(mailbox), mailbox.getAddress()))
+ .collect(Collectors.toCollection(() -> getAddressSet(headerName)));
+ }
+
+ private Stream<Mailbox> convertAddressToMailboxStream(Address address) {
+ if (address instanceof Mailbox) {
+ return Stream.of((Mailbox) address);
+ } else if (address instanceof Group) {
+ return ((Group) address).getMailboxes().stream();
+ }
+ return Stream.empty();
+ }
+
+ private Set<EMailer> getAddressSet(String headerName) {
+ switch (headerName) {
+ case TO:
+ return toAddressSet;
+ case FROM:
+ return fromAddressSet;
+ case CC:
+ return ccAddressSet;
+ case BCC:
+ return bccAddressSet;
+ case REPLY_TO:
+ return replyToAddressSet;
+ }
+ throw new RuntimeException(headerName + " is not a address header name");
+ }
+ }
+
+ public static final String TO = "to";
+ public static final String FROM = "from";
+ public static final String CC = "cc";
+ public static final String BCC = "bcc";
+ public static final String REPLY_TO = "reply-to";
+ public static final String SUBJECT = "subject";
+ public static final String DATE = "date";
+ public static final String MESSAGE_ID = "message-id";
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final ImmutableSet<EMailer> toAddressSet;
+ private final ImmutableSet<EMailer> fromAddressSet;
+ private final ImmutableSet<EMailer> ccAddressSet;
+ private final ImmutableSet<EMailer> bccAddressSet;
+ private final ImmutableSet<EMailer> replyToAddressSet;
+ private final ImmutableSet<String> subjectSet;
+ private final ImmutableMultimap<String, String> headers;
+ private final Optional<ZonedDateTime> sentDate;
+ private final Optional<String> messageID;
+
+ private HeaderCollection(ImmutableSet<EMailer> toAddressSet, ImmutableSet<EMailer> fromAddressSet,
+ ImmutableSet<EMailer> ccAddressSet, ImmutableSet<EMailer> bccAddressSet, ImmutableSet<EMailer> replyToAddressSet, ImmutableSet<String> subjectSet,
+ ImmutableMultimap<String, String> headers, Optional<ZonedDateTime> sentDate, Optional<String> messageID) {
+ this.toAddressSet = toAddressSet;
+ this.fromAddressSet = fromAddressSet;
+ this.ccAddressSet = ccAddressSet;
+ this.bccAddressSet = bccAddressSet;
+ this.replyToAddressSet = replyToAddressSet;
+ this.subjectSet = subjectSet;
+ this.headers = headers;
+ this.sentDate = sentDate;
+ this.messageID = messageID;
+ }
+
+ public Set<EMailer> getToAddressSet() {
+ return toAddressSet;
+ }
+
+ public Set<EMailer> getFromAddressSet() {
+ return fromAddressSet;
+ }
+
+ public Set<EMailer> getCcAddressSet() {
+ return ccAddressSet;
+ }
+
+ public Set<EMailer> getBccAddressSet() {
+ return bccAddressSet;
+ }
+
+ public Set<EMailer> getReplyToAddressSet() {
+ return replyToAddressSet;
+ }
+
+ public Set<String> getSubjectSet() {
+ return subjectSet;
+ }
+
+ public Optional<ZonedDateTime> getSentDate() {
+ return sentDate;
+ }
+
+ public Multimap<String, String> getHeaders() {
+ return headers;
+ }
+
+ public Optional<String> getMessageID() {
+ return messageID;
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/IndexableMessage.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/IndexableMessage.java
new file mode 100644
index 0000000..221aa91
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/IndexableMessage.java
@@ -0,0 +1,471 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.james.core.User;
+import org.apache.james.mailbox.elasticsearch.v6.IndexAttachments;
+import org.apache.james.mailbox.elasticsearch.v6.query.DateResolutionFormater;
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.Property;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleProperty;
+import org.apache.james.mailbox.store.search.SearchUtil;
+import org.apache.james.mime4j.MimeException;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.steveash.guavate.Guavate;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+
+public class IndexableMessage {
+
+ public static class Builder {
+ private static ZonedDateTime getSanitizedInternalDate(MailboxMessage message, ZoneId zoneId) {
+ if (message.getInternalDate() == null) {
+ return ZonedDateTime.now();
+ }
+ return ZonedDateTime.ofInstant(
+ Instant.ofEpochMilli(message.getInternalDate().getTime()),
+ zoneId);
+ }
+
+ private IndexAttachments indexAttachments;
+ private MailboxMessage message;
+ private TextExtractor textExtractor;
+ private List<User> users;
+
+ private ZoneId zoneId;
+
+ private Builder() {
+ }
+
+ public IndexableMessage build() {
+ Preconditions.checkNotNull(message.getMailboxId());
+ Preconditions.checkNotNull(users);
+ Preconditions.checkNotNull(textExtractor);
+ Preconditions.checkNotNull(indexAttachments);
+ Preconditions.checkNotNull(zoneId);
+ Preconditions.checkState(!users.isEmpty());
+
+ try {
+ return instanciateIndexedMessage();
+ } catch (IOException | MimeException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Builder extractor(TextExtractor textExtractor) {
+ this.textExtractor = textExtractor;
+ return this;
+ }
+
+ public Builder indexAttachments(IndexAttachments indexAttachments) {
+ this.indexAttachments = indexAttachments;
+ return this;
+ }
+
+ public Builder message(MailboxMessage message) {
+ this.message = message;
+ return this;
+ }
+
+ public Builder users(List<User> users) {
+ this.users = users;
+ return this;
+ }
+
+ public Builder zoneId(ZoneId zoneId) {
+ this.zoneId = zoneId;
+ return this;
+ }
+
+ private boolean computeHasAttachment(MailboxMessage message) {
+ return message.getProperties()
+ .stream()
+ .anyMatch(property -> property.equals(HAS_ATTACHMENT_PROPERTY));
+ }
+
+ private IndexableMessage instanciateIndexedMessage() throws IOException, MimeException {
+ String messageId = SearchUtil.getSerializedMessageIdIfSupportedByUnderlyingStorageOrNull(message);
+ MimePart parsingResult = new MimePartParser(message, textExtractor).parse();
+
+ List<String> stringifiedUsers = users.stream()
+ .map(User::asString)
+ .collect(Guavate.toImmutableList());
+
+ Optional<String> bodyText = parsingResult.locateFirstTextBody();
+ Optional<String> bodyHtml = parsingResult.locateFirstHtmlBody();
+
+ boolean hasAttachment = computeHasAttachment(message);
+ List<MimePart> attachments = setFlattenedAttachments(parsingResult, indexAttachments);
+
+ HeaderCollection headerCollection = parsingResult.getHeaderCollection();
+ ZonedDateTime internalDate = getSanitizedInternalDate(message, zoneId);
+
+ Multimap<String, String> headers = headerCollection.getHeaders();
+ Subjects subjects = Subjects.from(headerCollection.getSubjectSet());
+ EMailers from = EMailers.from(headerCollection.getFromAddressSet());
+ EMailers to = EMailers.from(headerCollection.getToAddressSet());
+ EMailers replyTo = EMailers.from(headerCollection.getReplyToAddressSet());
+ EMailers cc = EMailers.from(headerCollection.getCcAddressSet());
+ EMailers bcc = EMailers.from(headerCollection.getBccAddressSet());
+ String sentDate = DateResolutionFormater.DATE_TIME_FOMATTER.format(headerCollection.getSentDate().orElse(internalDate));
+ Optional<String> mimeMessageID = headerCollection.getMessageID();
+
+ String text = Stream.of(from.serialize(),
+ to.serialize(),
+ cc.serialize(),
+ bcc.serialize(),
+ subjects.serialize(),
+ bodyText.orElse(null),
+ bodyHtml.orElse(null))
+ .filter(str -> !Strings.isNullOrEmpty(str))
+ .collect(Collectors.joining(" "));
+
+ long uid = message.getUid().asLong();
+ String mailboxId = message.getMailboxId().serialize();
+ long modSeq = message.getModSeq();
+ long size = message.getFullContentOctets();
+ String date = DateResolutionFormater.DATE_TIME_FOMATTER.format(getSanitizedInternalDate(message, zoneId));
+ String mediaType = message.getMediaType();
+ String subType = message.getSubType();
+ boolean isAnswered = message.isAnswered();
+ boolean isDeleted = message.isDeleted();
+ boolean isDraft = message.isDraft();
+ boolean isFlagged = message.isFlagged();
+ boolean isRecent = message.isRecent();
+ boolean isUnRead = !message.isSeen();
+ String[] userFlags = message.createFlags().getUserFlags();
+ List<Property> properties = message.getProperties();
+
+ return new IndexableMessage(
+ attachments,
+ bcc,
+ bodyHtml,
+ bodyText,
+ cc,
+ date,
+ from,
+ hasAttachment,
+ headers,
+ isAnswered,
+ isDeleted,
+ isDraft,
+ isFlagged,
+ isRecent,
+ isUnRead,
+ mailboxId,
+ mediaType,
+ messageId,
+ modSeq,
+ properties,
+ replyTo,
+ sentDate,
+ size,
+ subjects,
+ subType,
+ text,
+ to,
+ uid,
+ userFlags,
+ stringifiedUsers,
+ mimeMessageID);
+ }
+
+ private List<MimePart> setFlattenedAttachments(MimePart parsingResult, IndexAttachments indexAttachments) {
+ if (IndexAttachments.YES.equals(indexAttachments)) {
+ return parsingResult.getAttachmentsStream()
+ .collect(Guavate.toImmutableList());
+ } else {
+ return ImmutableList.of();
+ }
+ }
+ }
+
+ public static final SimpleProperty HAS_ATTACHMENT_PROPERTY = new SimpleProperty(PropertyBuilder.JAMES_INTERNALS, PropertyBuilder.HAS_ATTACHMENT, "true");
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final List<MimePart> attachments;
+ private final EMailers bcc;
+ private final Optional<String> bodyHtml;
+ private final Optional<String> bodyText;
+ private final EMailers cc;
+ private final String date;
+ private final EMailers from;
+ private final boolean hasAttachment;
+ private final Multimap<String, String> headers;
+ private final boolean isAnswered;
+ private final boolean isDeleted;
+ private final boolean isDraft;
+ private final boolean isFlagged;
+ private final boolean isRecent;
+ private final boolean isUnRead;
+ private final String mailboxId;
+ private final String mediaType;
+ private final String messageId;
+ private final long modSeq;
+ private final List<Property> properties;
+ private final EMailers replyTo;
+ private final String sentDate;
+ private final long size;
+ private final Subjects subjects;
+ private final String subType;
+ private final String text;
+ private final EMailers to;
+ private final long uid;
+ private final String[] userFlags;
+ private final List<String> users;
+ private final Optional<String> mimeMessageID;
+
+ private IndexableMessage(
+ List<MimePart> attachments,
+ EMailers bcc,
+ Optional<String> bodyHtml,
+ Optional<String> bodyText,
+ EMailers cc,
+ String date,
+ EMailers from,
+ boolean hasAttachment,
+ Multimap<String, String> headers,
+ boolean isAnswered,
+ boolean isDeleted,
+ boolean isDraft,
+ boolean isFlagged,
+ boolean isRecent,
+ boolean isUnRead,
+ String mailboxId,
+ String mediaType,
+ String messageId,
+ long modSeq,
+ List<Property> properties,
+ EMailers replyTo,
+ String sentDate,
+ long size,
+ Subjects subjects,
+ String subType,
+ String text,
+ EMailers to,
+ long uid,
+ String[] userFlags,
+ List<String> users,
+ Optional<String> mimeMessageID) {
+ this.attachments = attachments;
+ this.bcc = bcc;
+ this.bodyHtml = bodyHtml;
+ this.bodyText = bodyText;
+ this.cc = cc;
+ this.date = date;
+ this.from = from;
+ this.hasAttachment = hasAttachment;
+ this.headers = headers;
+ this.isAnswered = isAnswered;
+ this.isDeleted = isDeleted;
+ this.isDraft = isDraft;
+ this.isFlagged = isFlagged;
+ this.isRecent = isRecent;
+ this.isUnRead = isUnRead;
+ this.mailboxId = mailboxId;
+ this.mediaType = mediaType;
+ this.messageId = messageId;
+ this.modSeq = modSeq;
+ this.properties = properties;
+ this.replyTo = replyTo;
+ this.sentDate = sentDate;
+ this.size = size;
+ this.subjects = subjects;
+ this.subType = subType;
+ this.text = text;
+ this.to = to;
+ this.uid = uid;
+ this.userFlags = userFlags;
+ this.users = users;
+ this.mimeMessageID = mimeMessageID;
+ }
+
+ @JsonProperty(JsonMessageConstants.ATTACHMENTS)
+ public List<MimePart> getAttachments() {
+ return attachments;
+ }
+
+ @JsonProperty(JsonMessageConstants.BCC)
+ public EMailers getBcc() {
+ return bcc;
+ }
+
+ @JsonProperty(JsonMessageConstants.HTML_BODY)
+ public Optional<String> getBodyHtml() {
+ return bodyHtml;
+ }
+
+ @JsonProperty(JsonMessageConstants.TEXT_BODY)
+ public Optional<String> getBodyText() {
+ return bodyText;
+ }
+
+ @JsonProperty(JsonMessageConstants.CC)
+ public EMailers getCc() {
+ return cc;
+ }
+
+ @JsonProperty(JsonMessageConstants.DATE)
+ public String getDate() {
+ return date;
+ }
+
+ @JsonProperty(JsonMessageConstants.FROM)
+ public EMailers getFrom() {
+ return from;
+ }
+
+ @JsonProperty(JsonMessageConstants.HAS_ATTACHMENT)
+ public boolean getHasAttachment() {
+ return hasAttachment;
+ }
+
+ @JsonProperty(JsonMessageConstants.HEADERS)
+ public Multimap<String, String> getHeaders() {
+ return headers;
+ }
+
+ @JsonProperty(JsonMessageConstants.MAILBOX_ID)
+ public String getMailboxId() {
+ return mailboxId;
+ }
+
+ @JsonProperty(JsonMessageConstants.MEDIA_TYPE)
+ public String getMediaType() {
+ return mediaType;
+ }
+
+ @JsonProperty(JsonMessageConstants.MESSAGE_ID)
+ public String getMessageId() {
+ return messageId;
+ }
+
+ @JsonProperty(JsonMessageConstants.MODSEQ)
+ public long getModSeq() {
+ return modSeq;
+ }
+
+ @JsonProperty(JsonMessageConstants.PROPERTIES)
+ public List<Property> getProperties() {
+ return properties;
+ }
+
+ @JsonProperty(JsonMessageConstants.REPLY_TO)
+ public EMailers getReplyTo() {
+ return replyTo;
+ }
+
+ @JsonProperty(JsonMessageConstants.SENT_DATE)
+ public String getSentDate() {
+ return sentDate;
+ }
+
+ @JsonProperty(JsonMessageConstants.SIZE)
+ public long getSize() {
+ return size;
+ }
+
+ @JsonProperty(JsonMessageConstants.SUBJECT)
+ public Subjects getSubjects() {
+ return subjects;
+ }
+
+ @JsonProperty(JsonMessageConstants.SUBTYPE)
+ public String getSubType() {
+ return subType;
+ }
+
+ @JsonProperty(JsonMessageConstants.TEXT)
+ public String getText() {
+ return text;
+ }
+
+ @JsonProperty(JsonMessageConstants.TO)
+ public EMailers getTo() {
+ return to;
+ }
+
+ @JsonProperty(JsonMessageConstants.UID)
+ public Long getUid() {
+ return uid;
+ }
+
+ @JsonProperty(JsonMessageConstants.USER_FLAGS)
+ public String[] getUserFlags() {
+ return userFlags;
+ }
+
+ @JsonProperty(JsonMessageConstants.USERS)
+ public List<String> getUsers() {
+ return users;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_ANSWERED)
+ public boolean isAnswered() {
+ return isAnswered;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_DELETED)
+ public boolean isDeleted() {
+ return isDeleted;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_DRAFT)
+ public boolean isDraft() {
+ return isDraft;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_FLAGGED)
+ public boolean isFlagged() {
+ return isFlagged;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_RECENT)
+ public boolean isRecent() {
+ return isRecent;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_UNREAD)
+ public boolean isUnRead() {
+ return isUnRead;
+ }
+
+ @JsonProperty(JsonMessageConstants.MIME_MESSAGE_ID)
+ public Optional<String> getMimeMessageID() {
+ return mimeMessageID;
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/JsonMessageConstants.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/JsonMessageConstants.java
new file mode 100644
index 0000000..e747c9f
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/JsonMessageConstants.java
@@ -0,0 +1,83 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+public interface JsonMessageConstants {
+
+ /*
+ Properties defined by JMAP
+ */
+ String MESSAGE_ID = "messageId";
+ String UID = "uid";
+ String MAILBOX_ID = "mailboxId";
+ String USERS = "users";
+ String IS_UNREAD = "isUnread";
+ String IS_FLAGGED = "isFlagged";
+ String IS_ANSWERED = "isAnswered";
+ String IS_DRAFT = "isDraft";
+ String HEADERS = "headers";
+ String FROM = "from";
+ String TO = "to";
+ String CC = "cc";
+ String BCC = "bcc";
+ String REPLY_TO = "replyTo";
+ String SUBJECT = "subject";
+ String DATE = "date";
+ String SIZE = "size";
+ String TEXT_BODY = "textBody";
+ String HTML_BODY = "htmlBody";
+ String SENT_DATE = "sentDate";
+ String ATTACHMENTS = "attachments";
+ String TEXT = "text";
+ String MIME_MESSAGE_ID = "mimeMessageID";
+
+ /*
+ James properties we can easily get
+ */
+ String PROPERTIES = "properties";
+ String MODSEQ = "modSeq";
+ String USER_FLAGS = "userFlags";
+ String IS_RECENT = "isRecent";
+ String IS_DELETED = "isDeleted";
+ String MEDIA_TYPE = "mediaType";
+ String SUBTYPE = "subtype";
+ String HAS_ATTACHMENT = "hasAttachment";
+
+ interface EMailer {
+ String NAME = "name";
+ String ADDRESS = "address";
+ }
+
+ interface Attachment {
+ String TEXT_CONTENT = "textContent";
+ String MEDIA_TYPE = "mediaType";
+ String SUBTYPE = "subtype";
+ String CONTENT_DISPOSITION = "contentDisposition";
+ String FILENAME = "fileName";
+ String FILE_EXTENSION = "fileExtension";
+ }
+
+ interface Property {
+ String NAMESPACE = "namespace";
+ String NAME = "name";
+ String VALUE = "value";
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageToElasticSearchJson.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageToElasticSearchJson.java
new file mode 100644
index 0000000..95d9c5d
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageToElasticSearchJson.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.mailbox.elasticsearch.v6.json;
+
+import java.time.ZoneId;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.mail.Flags;
+
+import org.apache.james.core.User;
+import org.apache.james.mailbox.elasticsearch.v6.IndexAttachments;
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.guava.GuavaModule;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.google.common.base.Preconditions;
+
+public class MessageToElasticSearchJson {
+
+ private final ObjectMapper mapper;
+ private final TextExtractor textExtractor;
+ private final ZoneId zoneId;
+ private final IndexAttachments indexAttachments;
+
+ public MessageToElasticSearchJson(TextExtractor textExtractor, ZoneId zoneId, IndexAttachments indexAttachments) {
+ this.textExtractor = textExtractor;
+ this.zoneId = zoneId;
+ this.indexAttachments = indexAttachments;
+ this.mapper = new ObjectMapper();
+ this.mapper.registerModule(new GuavaModule());
+ this.mapper.registerModule(new Jdk8Module());
+ }
+
+ @Inject
+ public MessageToElasticSearchJson(TextExtractor textExtractor, IndexAttachments indexAttachments) {
+ this(textExtractor, ZoneId.systemDefault(), indexAttachments);
+ }
+
+ public String convertToJson(MailboxMessage message, List<User> users) throws JsonProcessingException {
+ Preconditions.checkNotNull(message);
+
+ return mapper.writeValueAsString(IndexableMessage.builder()
+ .message(message)
+ .users(users)
+ .extractor(textExtractor)
+ .zoneId(zoneId)
+ .indexAttachments(indexAttachments)
+ .build());
+ }
+
+ public String convertToJsonWithoutAttachment(MailboxMessage message, List<User> users) throws JsonProcessingException {
+ return mapper.writeValueAsString(IndexableMessage.builder()
+ .message(message)
+ .users(users)
+ .extractor(textExtractor)
+ .zoneId(zoneId)
+ .indexAttachments(IndexAttachments.NO)
+ .build());
+ }
+
+ public String getUpdatedJsonMessagePart(Flags flags, long modSeq) throws JsonProcessingException {
+ Preconditions.checkNotNull(flags);
+ return mapper.writeValueAsString(new MessageUpdateJson(flags, modSeq));
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageUpdateJson.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageUpdateJson.java
new file mode 100644
index 0000000..f8b2510
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageUpdateJson.java
@@ -0,0 +1,79 @@
+
+
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import javax.mail.Flags;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MessageUpdateJson {
+
+ private final Flags flags;
+ private final long modSeq;
+
+ public MessageUpdateJson(Flags flags, long modSeq) {
+ this.flags = flags;
+ this.modSeq = modSeq;
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_ANSWERED)
+ public boolean isAnswered() {
+ return flags.contains(Flags.Flag.ANSWERED);
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_DELETED)
+ public boolean isDeleted() {
+ return flags.contains(Flags.Flag.DELETED);
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_DRAFT)
+ public boolean isDraft() {
+ return flags.contains(Flags.Flag.DRAFT);
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_FLAGGED)
+ public boolean isFlagged() {
+ return flags.contains(Flags.Flag.FLAGGED);
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_RECENT)
+ public boolean isRecent() {
+ return flags.contains(Flags.Flag.RECENT);
+ }
+
+ @JsonProperty(JsonMessageConstants.IS_UNREAD)
+ public boolean isUnRead() {
+ return ! flags.contains(Flags.Flag.SEEN);
+ }
+
+
+ @JsonProperty(JsonMessageConstants.USER_FLAGS)
+ public String[] getUserFlags() {
+ return flags.getUserFlags();
+ }
+
+ @JsonProperty(JsonMessageConstants.MODSEQ)
+ public long getModSeq() {
+ return modSeq;
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePart.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePart.java
new file mode 100644
index 0000000..3d700cb
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePart.java
@@ -0,0 +1,303 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mailbox.extractor.ParsedContent;
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.store.extractor.DefaultTextExtractor;
+import org.apache.james.mime4j.stream.Field;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+
+public class MimePart {
+
+ public static class Builder implements MimePartContainerBuilder {
+
+ private final HeaderCollection.Builder headerCollectionBuilder;
+ private Optional<InputStream> bodyContent;
+ private final List<MimePart> children;
+ private Optional<String> mediaType;
+ private Optional<String> subType;
+ private Optional<String> fileName;
+ private Optional<String> fileExtension;
+ private Optional<String> contentDisposition;
+ private Optional<Charset> charset;
+ private TextExtractor textExtractor;
+
+ private Builder() {
+ children = Lists.newArrayList();
+ headerCollectionBuilder = HeaderCollection.builder();
+ this.bodyContent = Optional.empty();
+ this.mediaType = Optional.empty();
+ this.subType = Optional.empty();
+ this.fileName = Optional.empty();
+ this.fileExtension = Optional.empty();
+ this.contentDisposition = Optional.empty();
+ this.charset = Optional.empty();
+ this.textExtractor = new DefaultTextExtractor();
+ }
+
+ @Override
+ public Builder addToHeaders(Field field) {
+ headerCollectionBuilder.add(field);
+ return this;
+ }
+
+ @Override
+ public Builder addBodyContent(InputStream bodyContent) {
+ this.bodyContent = Optional.of(bodyContent);
+ return this;
+ }
+
+ @Override
+ public Builder addChild(MimePart mimePart) {
+ children.add(mimePart);
+ return this;
+ }
+
+ @Override
+ public Builder addFileName(String fileName) {
+ this.fileName = Optional.ofNullable(fileName);
+ this.fileExtension = this.fileName.map(FilenameUtils::getExtension);
+ return this;
+ }
+
+ @Override
+ public Builder addMediaType(String mediaType) {
+ this.mediaType = Optional.ofNullable(mediaType);
+ return this;
+ }
+
+ @Override
+ public Builder addSubType(String subType) {
+ this.subType = Optional.ofNullable(subType);
+ return this;
+ }
+
+ @Override
+ public Builder addContentDisposition(String contentDisposition) {
+ this.contentDisposition = Optional.ofNullable(contentDisposition);
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder using(TextExtractor textExtractor) {
+ Preconditions.checkArgument(textExtractor != null, "Provided text extractor should not be null");
+ this.textExtractor = textExtractor;
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder charset(Charset charset) {
+ this.charset = Optional.of(charset);
+ return this;
+ }
+
+ @Override
+ public MimePart build() {
+ Optional<ParsedContent> parsedContent = parseContent(textExtractor);
+ return new MimePart(
+ headerCollectionBuilder.build(),
+ parsedContent.flatMap(ParsedContent::getTextualContent),
+ mediaType,
+ subType,
+ fileName,
+ fileExtension,
+ contentDisposition,
+ children);
+ }
+
+ private Optional<ParsedContent> parseContent(TextExtractor textExtractor) {
+ if (bodyContent.isPresent()) {
+ try {
+ return Optional.of(extractText(textExtractor, bodyContent.get()));
+ } catch (Throwable e) {
+ LOGGER.warn("Failed parsing attachment", e);
+ }
+ }
+ return Optional.empty();
+ }
+
+ private ParsedContent extractText(TextExtractor textExtractor, InputStream bodyContent) throws Exception {
+ if (isTextBody()) {
+ return new ParsedContent(
+ Optional.ofNullable(IOUtils.toString(bodyContent, charset.orElse(StandardCharsets.UTF_8))),
+ ImmutableMap.of());
+ }
+ return textExtractor.extractContent(
+ bodyContent,
+ computeContentType().orElse(null));
+ }
+
+ private Boolean isTextBody() {
+ return mediaType.map("text"::equals).orElse(false);
+ }
+
+ private Optional<String> computeContentType() {
+ if (mediaType.isPresent() && subType.isPresent()) {
+ return Optional.of(mediaType.get() + "/" + subType.get());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MimePart.class);
+
+ private final HeaderCollection headerCollection;
+ private final Optional<String> bodyTextContent;
+ private final Optional<String> mediaType;
+ private final Optional<String> subType;
+ private final Optional<String> fileName;
+ private final Optional<String> fileExtension;
+ private final Optional<String> contentDisposition;
+ private final List<MimePart> attachments;
+
+ private MimePart(HeaderCollection headerCollection, Optional<String> bodyTextContent, Optional<String> mediaType,
+ Optional<String> subType, Optional<String> fileName, Optional<String> fileExtension,
+ Optional<String> contentDisposition, List<MimePart> attachments) {
+ this.headerCollection = headerCollection;
+ this.mediaType = mediaType;
+ this.subType = subType;
+ this.fileName = fileName;
+ this.fileExtension = fileExtension;
+ this.contentDisposition = contentDisposition;
+ this.attachments = attachments;
+ this.bodyTextContent = bodyTextContent;
+ }
+
+ @JsonIgnore
+ public List<MimePart> getAttachments() {
+ return attachments;
+ }
+
+ @JsonIgnore
+ public HeaderCollection getHeaderCollection() {
+ return headerCollection;
+ }
+
+ @JsonProperty(JsonMessageConstants.HEADERS)
+ public Multimap<String, String> getHeaders() {
+ return headerCollection.getHeaders();
+ }
+
+ @JsonProperty(JsonMessageConstants.Attachment.FILENAME)
+ public Optional<String> getFileName() {
+ return fileName;
+ }
+
+ @JsonProperty(JsonMessageConstants.Attachment.FILE_EXTENSION)
+ public Optional<String> getFileExtension() {
+ return fileExtension;
+ }
+
+ @JsonProperty(JsonMessageConstants.Attachment.MEDIA_TYPE)
+ public Optional<String> getMediaType() {
+ return mediaType;
+ }
+
+ @JsonProperty(JsonMessageConstants.Attachment.SUBTYPE)
+ public Optional<String> getSubType() {
+ return subType;
+ }
+
+ @JsonProperty(JsonMessageConstants.Attachment.CONTENT_DISPOSITION)
+ public Optional<String> getContentDisposition() {
+ return contentDisposition;
+ }
+
+ @JsonProperty(JsonMessageConstants.Attachment.TEXT_CONTENT)
+ public Optional<String> getTextualBody() {
+ return bodyTextContent;
+ }
+
+ @JsonIgnore
+ public Optional<String> locateFirstTextBody() {
+ return firstBody(textAttachments()
+ .filter(this::isPlainSubType));
+ }
+
+ @JsonIgnore
+ public Optional<String> locateFirstHtmlBody() {
+ return firstBody(textAttachments()
+ .filter(this::isHtmlSubType));
+ }
+
+ private Optional<String> firstBody(Stream<MimePart> mimeParts) {
+ return mimeParts
+ .map((mimePart) -> mimePart.bodyTextContent)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst();
+ }
+
+ private Stream<MimePart> textAttachments() {
+ return Stream.concat(
+ Stream.of(this),
+ attachments.stream())
+ .filter(this::isTextMediaType);
+ }
+
+ private boolean isTextMediaType(MimePart mimePart) {
+ return mimePart.getMediaType()
+ .filter("text"::equals)
+ .isPresent();
+ }
+
+ private boolean isPlainSubType(MimePart mimePart) {
+ return mimePart.getSubType()
+ .filter("plain"::equals)
+ .isPresent();
+ }
+
+ private boolean isHtmlSubType(MimePart mimePart) {
+ return mimePart.getSubType()
+ .filter("html"::equals)
+ .isPresent();
+ }
+
+ @JsonIgnore
+ public Stream<MimePart> getAttachmentsStream() {
+ return attachments.stream()
+ .flatMap((mimePart) -> Stream.concat(Stream.of(mimePart), mimePart.getAttachmentsStream()));
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartContainerBuilder.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartContainerBuilder.java
new file mode 100644
index 0000000..5a12008
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartContainerBuilder.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.mailbox.elasticsearch.v6.json;
+
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mime4j.stream.Field;
+
+public interface MimePartContainerBuilder {
+
+ MimePart build();
+
+ MimePartContainerBuilder using(TextExtractor textExtractor);
+
+ MimePartContainerBuilder addToHeaders(Field field);
+
+ MimePartContainerBuilder addBodyContent(InputStream bodyContent);
+
+ MimePartContainerBuilder addChild(MimePart mimePart);
+
+ MimePartContainerBuilder addFileName(String fileName);
+
+ MimePartContainerBuilder charset(Charset charset);
+
+ MimePartContainerBuilder addMediaType(String mediaType);
+
+ MimePartContainerBuilder addSubType(String subType);
+
+ MimePartContainerBuilder addContentDisposition(String contentDisposition);
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartParser.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartParser.java
new file mode 100644
index 0000000..0f2a8ff
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartParser.java
@@ -0,0 +1,129 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.Optional;
+
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.store.mail.model.Message;
+import org.apache.james.mime4j.MimeException;
+import org.apache.james.mime4j.message.DefaultBodyDescriptorBuilder;
+import org.apache.james.mime4j.message.MaximalBodyDescriptor;
+import org.apache.james.mime4j.stream.EntityState;
+import org.apache.james.mime4j.stream.MimeConfig;
+import org.apache.james.mime4j.stream.MimeTokenStream;
+
+import com.google.common.base.Preconditions;
+
+public class MimePartParser {
+
+ private final Message message;
+ private final TextExtractor textExtractor;
+ private final MimeTokenStream stream;
+ private final Deque<MimePartContainerBuilder> builderStack;
+ private MimePart result;
+ private MimePartContainerBuilder currentlyBuildMimePart;
+
+ public MimePartParser(Message message, TextExtractor textExtractor) {
+ this.message = message;
+ this.textExtractor = textExtractor;
+ this.builderStack = new LinkedList<>();
+ this.currentlyBuildMimePart = new RootMimePartContainerBuilder();
+ this.stream = new MimeTokenStream(
+ MimeConfig.PERMISSIVE,
+ new DefaultBodyDescriptorBuilder());
+ }
+
+ public MimePart parse() throws IOException, MimeException {
+ stream.parse(message.getFullContent());
+ for (EntityState state = stream.getState(); state != EntityState.T_END_OF_STREAM; state = stream.next()) {
+ processMimePart(stream, state);
+ }
+ return result;
+ }
+
+ private void processMimePart(MimeTokenStream stream, EntityState state) {
+ switch (state) {
+ case T_START_MULTIPART:
+ case T_START_MESSAGE:
+ stackCurrent();
+ break;
+ case T_START_HEADER:
+ currentlyBuildMimePart = MimePart.builder();
+ break;
+ case T_FIELD:
+ currentlyBuildMimePart.addToHeaders(stream.getField());
+ break;
+ case T_BODY:
+ manageBodyExtraction(stream);
+ closeMimePart();
+ break;
+ case T_END_MULTIPART:
+ case T_END_MESSAGE:
+ unstackToCurrent();
+ closeMimePart();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void stackCurrent() {
+ builderStack.push(currentlyBuildMimePart);
+ currentlyBuildMimePart = null;
+ }
+
+ private void unstackToCurrent() {
+ currentlyBuildMimePart = builderStack.pop();
+ }
+
+ private void closeMimePart() {
+ MimePart bodyMimePart = currentlyBuildMimePart.using(textExtractor).build();
+ if (!builderStack.isEmpty()) {
+ builderStack.peek().addChild(bodyMimePart);
+ } else {
+ Preconditions.checkState(result == null);
+ result = bodyMimePart;
+ }
+ }
+
+ private void manageBodyExtraction(MimeTokenStream stream) {
+ extractMimePartBodyDescription(stream);
+ currentlyBuildMimePart.addBodyContent(stream.getDecodedInputStream());
+ }
+
+ private void extractMimePartBodyDescription(MimeTokenStream stream) {
+ MaximalBodyDescriptor descriptor = (MaximalBodyDescriptor) stream.getBodyDescriptor();
+
+ currentlyBuildMimePart.addMediaType(descriptor.getMediaType())
+ .addSubType(descriptor.getSubType())
+ .addContentDisposition(descriptor.getContentDispositionType())
+ .addFileName(descriptor.getContentDispositionFilename());
+
+ Optional.ofNullable(descriptor.getCharset())
+ .map(Charset::forName)
+ .ifPresent(currentlyBuildMimePart::charset);
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/RootMimePartContainerBuilder.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/RootMimePartContainerBuilder.java
new file mode 100644
index 0000000..415d96f
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/RootMimePartContainerBuilder.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.mailbox.elasticsearch.v6.json;
+
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mime4j.stream.Field;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RootMimePartContainerBuilder implements MimePartContainerBuilder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RootMimePartContainerBuilder.class);
+
+ private MimePart rootMimePart;
+
+ @Override
+ public MimePart build() {
+ return rootMimePart;
+ }
+
+ @Override public MimePartContainerBuilder using(TextExtractor textExtractor) {
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addToHeaders(Field field) {
+ LOGGER.warn("Trying to add headers to the Root MimePart container");
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addBodyContent(InputStream bodyContent) {
+ LOGGER.warn("Trying to add body content to the Root MimePart container");
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addChild(MimePart mimePart) {
+ if (rootMimePart == null) {
+ rootMimePart = mimePart;
+ } else {
+ LOGGER.warn("Trying to add several children to the Root MimePart container");
+ }
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addFileName(String fileName) {
+ LOGGER.warn("Trying to add fineName to the Root MimePart container");
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addMediaType(String mediaType) {
+ LOGGER.warn("Trying to add media type to the Root MimePart container");
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addSubType(String subType) {
+ LOGGER.warn("Trying to add sub type to the Root MimePart container");
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder addContentDisposition(String contentDisposition) {
+ LOGGER.warn("Trying to add content disposition to the Root MimePart container");
+ return this;
+ }
+
+ @Override
+ public MimePartContainerBuilder charset(Charset charset) {
+ LOGGER.warn("Trying to add content charset to the Root MimePart container");
+ return this;
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/Serializable.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/Serializable.java
new file mode 100644
index 0000000..5ad6334
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/Serializable.java
@@ -0,0 +1,25 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+public interface Serializable {
+
+ String serialize();
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/Subjects.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/Subjects.java
new file mode 100644
index 0000000..d962932
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/json/Subjects.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.mailbox.elasticsearch.v6.json;
+
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+
+public class Subjects implements Serializable {
+
+ public static Subjects from(Set<String> subjects) {
+ Preconditions.checkNotNull(subjects, "'subjects' is mandatory");
+ return new Subjects(subjects);
+ }
+
+ private final Set<String> subjects;
+
+ private Subjects(Set<String> subjects) {
+ this.subjects = subjects;
+ }
+
+ @JsonValue
+ public Set<String> getSubjects() {
+ return subjects;
+ }
+
+ @Override
+ public String serialize() {
+ return Joiner.on(" ").join(subjects);
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/CriterionConverter.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/CriterionConverter.java
new file mode 100644
index 0000000..19612cc
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/CriterionConverter.java
@@ -0,0 +1,310 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.query;
+
+import static org.apache.james.backends.es.NodeMappingFactory.RAW;
+import static org.apache.james.backends.es.NodeMappingFactory.SPLIT_EMAIL;
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.existsQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.nestedQuery;
+import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import javax.mail.Flags;
+
+import org.apache.james.mailbox.elasticsearch.v6.json.HeaderCollection;
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants;
+import org.apache.james.mailbox.model.SearchQuery;
+import org.apache.james.mailbox.model.SearchQuery.Criterion;
+import org.apache.james.mailbox.model.SearchQuery.HeaderOperator;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+
+public class CriterionConverter {
+
+ private final Map<Class<?>, Function<SearchQuery.Criterion, QueryBuilder>> criterionConverterMap;
+ private final Map<Class<?>, BiFunction<String, SearchQuery.HeaderOperator, QueryBuilder>> headerOperatorConverterMap;
+
+ public CriterionConverter() {
+ criterionConverterMap = new HashMap<>();
+ headerOperatorConverterMap = new HashMap<>();
+
+ registerCriterionConverters();
+ registerHeaderOperatorConverters();
+ }
+
+ private void registerCriterionConverters() {
+ registerCriterionConverter(SearchQuery.FlagCriterion.class, this::convertFlag);
+ registerCriterionConverter(SearchQuery.UidCriterion.class, this::convertUid);
+ registerCriterionConverter(SearchQuery.ConjunctionCriterion.class, this::convertConjunction);
+ registerCriterionConverter(SearchQuery.HeaderCriterion.class, this::convertHeader);
+ registerCriterionConverter(SearchQuery.TextCriterion.class, this::convertTextCriterion);
+ registerCriterionConverter(SearchQuery.CustomFlagCriterion.class, this::convertCustomFlagCriterion);
+
+ registerCriterionConverter(SearchQuery.AllCriterion.class,
+ criterion -> matchAllQuery());
+
+ registerCriterionConverter(SearchQuery.ModSeqCriterion.class,
+ criterion -> createNumericFilter(JsonMessageConstants.MODSEQ, criterion.getOperator()));
+
+ registerCriterionConverter(SearchQuery.SizeCriterion.class,
+ criterion -> createNumericFilter(JsonMessageConstants.SIZE, criterion.getOperator()));
+
+ registerCriterionConverter(SearchQuery.InternalDateCriterion.class,
+ criterion -> dateRangeFilter(JsonMessageConstants.DATE, criterion.getOperator()));
+
+ registerCriterionConverter(SearchQuery.AttachmentCriterion.class, this::convertAttachmentCriterion);
+ registerCriterionConverter(SearchQuery.MimeMessageIDCriterion.class, this::convertMimeMessageIDCriterion);
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends Criterion> void registerCriterionConverter(Class<T> type, Function<T, QueryBuilder> f) {
+ criterionConverterMap.put(type, (Function<Criterion, QueryBuilder>) f);
+ }
+
+ private void registerHeaderOperatorConverters() {
+
+ registerHeaderOperatorConverter(
+ SearchQuery.ExistsOperator.class,
+ (headerName, operator) ->
+ existsQuery(JsonMessageConstants.HEADERS + "." + headerName));
+
+ registerHeaderOperatorConverter(
+ SearchQuery.AddressOperator.class,
+ (headerName, operator) -> manageAddressFields(headerName, operator.getAddress()));
+
+ registerHeaderOperatorConverter(
+ SearchQuery.DateOperator.class,
+ (headerName, operator) -> dateRangeFilter(JsonMessageConstants.SENT_DATE, operator));
+
+ registerHeaderOperatorConverter(
+ SearchQuery.ContainsOperator.class,
+ (headerName, operator) -> matchQuery(JsonMessageConstants.HEADERS + "." + headerName,
+ operator.getValue()));
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends HeaderOperator> void registerHeaderOperatorConverter(Class<T> type, BiFunction<String, T, QueryBuilder> f) {
+ headerOperatorConverterMap.put(type, (BiFunction<String, HeaderOperator, QueryBuilder>) f);
+ }
+
+ public QueryBuilder convertCriterion(SearchQuery.Criterion criterion) {
+ return criterionConverterMap.get(criterion.getClass()).apply(criterion);
+ }
+
+ private QueryBuilder convertAttachmentCriterion(SearchQuery.AttachmentCriterion criterion) {
+ return termQuery(JsonMessageConstants.HAS_ATTACHMENT, criterion.getOperator().isSet());
+ }
+
+ private QueryBuilder convertMimeMessageIDCriterion(SearchQuery.MimeMessageIDCriterion criterion) {
+ return termQuery(JsonMessageConstants.MIME_MESSAGE_ID, criterion.getMessageID());
+ }
+
+ private QueryBuilder convertCustomFlagCriterion(SearchQuery.CustomFlagCriterion criterion) {
+ QueryBuilder termQueryBuilder = termQuery(JsonMessageConstants.USER_FLAGS, criterion.getFlag());
+ if (criterion.getOperator().isSet()) {
+ return termQueryBuilder;
+ } else {
+ return boolQuery().mustNot(termQueryBuilder);
+ }
+ }
+
+ private QueryBuilder convertTextCriterion(SearchQuery.TextCriterion textCriterion) {
+ switch (textCriterion.getType()) {
+ case BODY:
+ return boolQuery()
+ .should(matchQuery(JsonMessageConstants.TEXT_BODY, textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.TEXT_BODY + "." + SPLIT_EMAIL,
+ textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.HTML_BODY + "." + SPLIT_EMAIL,
+ textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.HTML_BODY, textCriterion.getOperator().getValue()));
+ case TEXT:
+ return boolQuery()
+ .should(matchQuery(JsonMessageConstants.TEXT, textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.TEXT + "." + SPLIT_EMAIL,
+ textCriterion.getOperator().getValue()));
+ case FULL:
+ return boolQuery()
+ .should(matchQuery(JsonMessageConstants.TEXT_BODY, textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.TEXT_BODY + "." + SPLIT_EMAIL,
+ textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.HTML_BODY + "." + SPLIT_EMAIL,
+ textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.HTML_BODY, textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.HTML_BODY, textCriterion.getOperator().getValue()))
+ .should(matchQuery(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT,
+ textCriterion.getOperator().getValue()));
+ case ATTACHMENTS:
+ return boolQuery()
+ .should(matchQuery(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT,
+ textCriterion.getOperator().getValue()));
+ case ATTACHMENT_FILE_NAME:
+ return boolQuery()
+ .should(termQuery(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.FILENAME,
+ textCriterion.getOperator().getValue()));
+ }
+ throw new RuntimeException("Unknown SCOPE for text criterion");
+ }
+
+ private QueryBuilder dateRangeFilter(String field, SearchQuery.DateOperator dateOperator) {
+ return boolQuery().filter(
+ convertDateOperator(field,
+ dateOperator.getType(),
+ DateResolutionFormater.DATE_TIME_FOMATTER.format(
+ DateResolutionFormater.computeLowerDate(
+ DateResolutionFormater.convertDateToZonedDateTime(dateOperator.getDate()),
+ dateOperator.getDateResultion())),
+ DateResolutionFormater.DATE_TIME_FOMATTER.format(
+ DateResolutionFormater.computeUpperDate(
+ DateResolutionFormater.convertDateToZonedDateTime(dateOperator.getDate()),
+ dateOperator.getDateResultion()))));
+ }
+
+ private BoolQueryBuilder convertConjunction(SearchQuery.ConjunctionCriterion criterion) {
+ return convertToBoolQuery(criterion.getCriteria().stream().map(this::convertCriterion),
+ convertConjunctionType(criterion.getType()));
+ }
+
+ private BiFunction<BoolQueryBuilder, QueryBuilder, BoolQueryBuilder> convertConjunctionType(SearchQuery.Conjunction type) {
+ switch (type) {
+ case AND:
+ return BoolQueryBuilder::must;
+ case OR:
+ return BoolQueryBuilder::should;
+ case NOR:
+ return BoolQueryBuilder::mustNot;
+ default:
+ throw new RuntimeException("Unexpected conjunction criteria " + type);
+ }
+ }
+
+ private BoolQueryBuilder convertToBoolQuery(Stream<QueryBuilder> stream, BiFunction<BoolQueryBuilder, QueryBuilder, BoolQueryBuilder> addCriterionToBoolQuery) {
+ return stream.collect(Collector.of(QueryBuilders::boolQuery,
+ addCriterionToBoolQuery::apply,
+ addCriterionToBoolQuery::apply));
+ }
+
+ private QueryBuilder convertFlag(SearchQuery.FlagCriterion flagCriterion) {
+ SearchQuery.BooleanOperator operator = flagCriterion.getOperator();
+ Flags.Flag flag = flagCriterion.getFlag();
+ if (flag.equals(Flags.Flag.DELETED)) {
+ return boolQuery().filter(termQuery(JsonMessageConstants.IS_DELETED, operator.isSet()));
+ }
+ if (flag.equals(Flags.Flag.ANSWERED)) {
+ return boolQuery().filter(termQuery(JsonMessageConstants.IS_ANSWERED, operator.isSet()));
+ }
+ if (flag.equals(Flags.Flag.DRAFT)) {
+ return boolQuery().filter(termQuery(JsonMessageConstants.IS_DRAFT, operator.isSet()));
+ }
+ if (flag.equals(Flags.Flag.SEEN)) {
+ return boolQuery().filter(termQuery(JsonMessageConstants.IS_UNREAD, !operator.isSet()));
+ }
+ if (flag.equals(Flags.Flag.RECENT)) {
+ return boolQuery().filter(termQuery(JsonMessageConstants.IS_RECENT, operator.isSet()));
+ }
+ if (flag.equals(Flags.Flag.FLAGGED)) {
+ return boolQuery().filter(termQuery(JsonMessageConstants.IS_FLAGGED, operator.isSet()));
+ }
+ throw new RuntimeException("Unknown flag used in Flag search criterion");
+ }
+
+ private QueryBuilder createNumericFilter(String fieldName, SearchQuery.NumericOperator operator) {
+ switch (operator.getType()) {
+ case EQUALS:
+ return boolQuery().filter(rangeQuery(fieldName).gte(operator.getValue()).lte(operator.getValue()));
+ case GREATER_THAN:
+ return boolQuery().filter(rangeQuery(fieldName).gte(operator.getValue()));
+ case LESS_THAN:
+ return boolQuery().filter(rangeQuery(fieldName).lte(operator.getValue()));
+ default:
+ throw new RuntimeException("A non existing numeric operator was triggered");
+ }
+ }
+
+ private BoolQueryBuilder convertUid(SearchQuery.UidCriterion uidCriterion) {
+ if (uidCriterion.getOperator().getRange().length == 0) {
+ return boolQuery();
+ }
+ return boolQuery().filter(
+ convertToBoolQuery(
+ Arrays.stream(uidCriterion.getOperator().getRange())
+ .map(this::uidRangeFilter), BoolQueryBuilder::should));
+ }
+
+ private QueryBuilder uidRangeFilter(SearchQuery.UidRange numericRange) {
+ return rangeQuery(JsonMessageConstants.UID)
+ .lte(numericRange.getHighValue().asLong())
+ .gte(numericRange.getLowValue().asLong());
+ }
+
+ private QueryBuilder convertHeader(SearchQuery.HeaderCriterion headerCriterion) {
+ return headerOperatorConverterMap.get(headerCriterion.getOperator().getClass())
+ .apply(
+ headerCriterion.getHeaderName().toLowerCase(Locale.US),
+ headerCriterion.getOperator());
+ }
+
+ private QueryBuilder manageAddressFields(String headerName, String value) {
+ return nestedQuery(getFieldNameFromHeaderName(headerName), boolQuery()
+ .should(matchQuery(getFieldNameFromHeaderName(headerName) + "." + JsonMessageConstants.EMailer.NAME, value))
+ .should(matchQuery(getFieldNameFromHeaderName(headerName) + "." + JsonMessageConstants.EMailer.ADDRESS, value))
+ .should(matchQuery(getFieldNameFromHeaderName(headerName) + "." + JsonMessageConstants.EMailer.ADDRESS + "." + RAW, value)));
+ }
+
+ private String getFieldNameFromHeaderName(String headerName) {
+ switch (headerName.toLowerCase(Locale.US)) {
+ case HeaderCollection.TO:
+ return JsonMessageConstants.TO;
+ case HeaderCollection.CC:
+ return JsonMessageConstants.CC;
+ case HeaderCollection.BCC:
+ return JsonMessageConstants.BCC;
+ case HeaderCollection.FROM:
+ return JsonMessageConstants.FROM;
+ }
+ throw new RuntimeException("Header not recognized as Addess Header : " + headerName);
+ }
+
+ private QueryBuilder convertDateOperator(String field, SearchQuery.DateComparator dateComparator, String lowDateString, String upDateString) {
+ switch (dateComparator) {
+ case BEFORE:
+ return rangeQuery(field).lte(upDateString);
+ case AFTER:
+ return rangeQuery(field).gte(lowDateString);
+ case ON:
+ return rangeQuery(field).lte(upDateString).gte(lowDateString);
+ }
+ throw new RuntimeException("Unknown date operator");
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/DateResolutionFormater.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/DateResolutionFormater.java
new file mode 100644
index 0000000..731b564
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/DateResolutionFormater.java
@@ -0,0 +1,73 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.query;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.Date;
+
+import org.apache.james.mailbox.model.SearchQuery;
+
+public class DateResolutionFormater {
+
+ public static DateTimeFormatter DATE_TIME_FOMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
+
+ public static ZonedDateTime computeUpperDate(ZonedDateTime date, SearchQuery.DateResolution resolution) {
+ return computeLowerDate(date, resolution).plus(1, convertDateResolutionField(resolution));
+ }
+
+ public static ZonedDateTime computeLowerDate(ZonedDateTime date, SearchQuery.DateResolution resolution) {
+ switch (resolution) {
+ case Year:
+ return date.truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
+ case Month:
+ return date.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1);
+ default:
+ return date.truncatedTo(convertDateResolutionField(resolution));
+ }
+ }
+
+ private static TemporalUnit convertDateResolutionField(SearchQuery.DateResolution resolution) {
+ switch (resolution) {
+ case Year:
+ return ChronoUnit.YEARS;
+ case Month:
+ return ChronoUnit.MONTHS;
+ case Day:
+ return ChronoUnit.DAYS;
+ case Hour:
+ return ChronoUnit.HOURS;
+ case Minute:
+ return ChronoUnit.MINUTES;
+ case Second:
+ return ChronoUnit.SECONDS;
+ default:
+ throw new RuntimeException("Unknown Date resolution used");
+ }
+ }
+
+ public static ZonedDateTime convertDateToZonedDateTime(Date date) {
+ return ZonedDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
+ }
+}
\ No newline at end of file
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/QueryConverter.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/QueryConverter.java
new file mode 100644
index 0000000..c06239b
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/QueryConverter.java
@@ -0,0 +1,79 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.query;
+
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.SearchQuery;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.collect.ImmutableList;
+
+public class QueryConverter {
+
+
+ private final CriterionConverter criterionConverter;
+
+ @Inject
+ public QueryConverter(CriterionConverter criterionConverter) {
+ this.criterionConverter = criterionConverter;
+ }
+
+ public QueryBuilder from(Collection<MailboxId> mailboxIds, SearchQuery query) {
+ BoolQueryBuilder boolQueryBuilder = boolQuery()
+ .must(generateQueryBuilder(query));
+
+ mailboxesQuery(mailboxIds).map(boolQueryBuilder::filter);
+ return boolQueryBuilder;
+ }
+
+ private QueryBuilder generateQueryBuilder(SearchQuery searchQuery) {
+ List<SearchQuery.Criterion> criteria = searchQuery.getCriterias();
+ if (criteria.isEmpty()) {
+ return criterionConverter.convertCriterion(SearchQuery.all());
+ } else if (criteria.size() == 1) {
+ return criterionConverter.convertCriterion(criteria.get(0));
+ } else {
+ return criterionConverter.convertCriterion(new SearchQuery.ConjunctionCriterion(SearchQuery.Conjunction.AND, criteria));
+ }
+ }
+
+ private Optional<QueryBuilder> mailboxesQuery(Collection<MailboxId> mailboxIds) {
+ if (mailboxIds.isEmpty()) {
+ return Optional.empty();
+ }
+ ImmutableList<String> ids = mailboxIds.stream()
+ .map(MailboxId::serialize)
+ .collect(Guavate.toImmutableList());
+ return Optional.of(termsQuery(JsonMessageConstants.MAILBOX_ID, ids));
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/SortConverter.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/SortConverter.java
new file mode 100644
index 0000000..52a2624
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/query/SortConverter.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.mailbox.elasticsearch.v6.query;
+
+import org.apache.james.backends.es.NodeMappingFactory;
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants;
+import org.apache.james.mailbox.model.SearchQuery;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
+
+public class SortConverter {
+
+ private static final String MIN = "min";
+ private static final String PATH_SEPARATOR = ".";
+
+ public static FieldSortBuilder convertSort(SearchQuery.Sort sort) {
+ return getSortClause(sort.getSortClause())
+ .order(getOrder(sort))
+ .sortMode(MIN);
+ }
+
+ private static FieldSortBuilder getSortClause(SearchQuery.Sort.SortClause clause) {
+ switch (clause) {
+ case Arrival :
+ return SortBuilders.fieldSort(JsonMessageConstants.DATE);
+ case MailboxCc :
+ return SortBuilders.fieldSort(JsonMessageConstants.CC + PATH_SEPARATOR + JsonMessageConstants.EMailer.ADDRESS)
+ .setNestedPath(JsonMessageConstants.CC);
+ case MailboxFrom :
+ return SortBuilders.fieldSort(JsonMessageConstants.FROM + PATH_SEPARATOR + JsonMessageConstants.EMailer.ADDRESS)
+ .setNestedPath(JsonMessageConstants.FROM);
+ case MailboxTo :
+ return SortBuilders.fieldSort(JsonMessageConstants.TO + PATH_SEPARATOR + JsonMessageConstants.EMailer.ADDRESS)
+ .setNestedPath(JsonMessageConstants.TO);
+ case BaseSubject :
+ return SortBuilders.fieldSort(JsonMessageConstants.SUBJECT + PATH_SEPARATOR + NodeMappingFactory.RAW);
+ case Size :
+ return SortBuilders.fieldSort(JsonMessageConstants.SIZE);
+ case SentDate :
+ return SortBuilders.fieldSort(JsonMessageConstants.SENT_DATE);
+ case Uid :
+ return SortBuilders.fieldSort(JsonMessageConstants.UID);
+ case DisplayFrom:
+ return SortBuilders.fieldSort(JsonMessageConstants.FROM + PATH_SEPARATOR + JsonMessageConstants.EMailer.NAME + PATH_SEPARATOR + NodeMappingFactory.RAW)
+ .setNestedPath(JsonMessageConstants.FROM);
+ case DisplayTo:
+ return SortBuilders.fieldSort(JsonMessageConstants.TO + PATH_SEPARATOR + JsonMessageConstants.EMailer.NAME + PATH_SEPARATOR + NodeMappingFactory.RAW)
+ .setNestedPath(JsonMessageConstants.TO);
+ case Id:
+ return SortBuilders.fieldSort(JsonMessageConstants.MESSAGE_ID);
+ default:
+ throw new RuntimeException("Sort is not implemented");
+ }
+ }
+
+ private static SortOrder getOrder(SearchQuery.Sort sort) {
+ if (sort.isReverse()) {
+ return SortOrder.DESC;
+ } else {
+ return SortOrder.ASC;
+ }
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/search/ElasticSearchSearcher.java b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/search/ElasticSearchSearcher.java
new file mode 100644
index 0000000..195326e
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/main/java/org/apache/james/mailbox/elasticsearch/v6/search/ElasticSearchSearcher.java
@@ -0,0 +1,138 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.search;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.apache.james.backends.es.AliasName;
+import org.apache.james.backends.es.ReadAliasName;
+import org.apache.james.backends.es.TypeName;
+import org.apache.james.backends.es.search.ScrollIterable;
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.mailbox.elasticsearch.v6.json.JsonMessageConstants;
+import org.apache.james.mailbox.elasticsearch.v6.query.QueryConverter;
+import org.apache.james.mailbox.elasticsearch.v6.query.SortConverter;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.SearchQuery;
+import org.apache.james.mailbox.store.search.MessageSearchIndex;
+import org.apache.james.util.streams.Iterators;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchHitField;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ElasticSearchSearcher {
+
+ public static final int DEFAULT_SEARCH_SIZE = 100;
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchSearcher.class);
+ private static final TimeValue TIMEOUT = new TimeValue(60000);
+
+ private final Client client;
+ private final QueryConverter queryConverter;
+ private final int size;
+ private final MailboxId.Factory mailboxIdFactory;
+ private final MessageId.Factory messageIdFactory;
+ private final AliasName aliasName;
+ private final TypeName typeName;
+
+ public ElasticSearchSearcher(Client client, QueryConverter queryConverter, int size,
+ MailboxId.Factory mailboxIdFactory, MessageId.Factory messageIdFactory,
+ ReadAliasName aliasName, TypeName typeName) {
+ this.client = client;
+ this.queryConverter = queryConverter;
+ this.size = size;
+ this.mailboxIdFactory = mailboxIdFactory;
+ this.messageIdFactory = messageIdFactory;
+ this.aliasName = aliasName;
+ this.typeName = typeName;
+ }
+
+ public Stream<MessageSearchIndex.SearchResult> search(Collection<MailboxId> mailboxIds, SearchQuery query,
+ Optional<Long> limit) throws MailboxException {
+ SearchRequestBuilder searchRequestBuilder = getSearchRequestBuilder(client, mailboxIds, query, limit);
+ Stream<MessageSearchIndex.SearchResult> pairStream = new ScrollIterable(client, searchRequestBuilder).stream()
+ .flatMap(this::transformResponseToUidStream);
+
+ return limit.map(pairStream::limit)
+ .orElse(pairStream);
+ }
+
+ private SearchRequestBuilder getSearchRequestBuilder(Client client, Collection<MailboxId> users,
+ SearchQuery query, Optional<Long> limit) {
+ return query.getSorts()
+ .stream()
+ .reduce(
+ client.prepareSearch(aliasName.getValue())
+ .setTypes(typeName.getValue())
+ .setScroll(TIMEOUT)
+ .addFields(JsonMessageConstants.UID, JsonMessageConstants.MAILBOX_ID, JsonMessageConstants.MESSAGE_ID)
+ .setQuery(queryConverter.from(users, query))
+ .setSize(computeRequiredSize(limit)),
+ (searchBuilder, sort) -> searchBuilder.addSort(SortConverter.convertSort(sort)),
+ (partialResult1, partialResult2) -> partialResult1);
+ }
+
+ private int computeRequiredSize(Optional<Long> limit) {
+ return limit.map(value -> Math.min(value.intValue(), size))
+ .orElse(size);
+ }
+
+ private Stream<MessageSearchIndex.SearchResult> transformResponseToUidStream(SearchResponse searchResponse) {
+ return Iterators.toStream(searchResponse.getHits().iterator())
+ .map(this::extractContentFromHit)
+ .filter(Optional::isPresent)
+ .map(Optional::get);
+ }
+
+ private Optional<MessageSearchIndex.SearchResult> extractContentFromHit(SearchHit hit) {
+ SearchHitField mailboxId = hit.field(JsonMessageConstants.MAILBOX_ID);
+ SearchHitField uid = hit.field(JsonMessageConstants.UID);
+ Optional<SearchHitField> id = retrieveMessageIdField(hit);
+ if (mailboxId != null && uid != null) {
+ Number uidAsNumber = uid.getValue();
+ return Optional.of(
+ new MessageSearchIndex.SearchResult(
+ id.map(field -> messageIdFactory.fromString(field.getValue())),
+ mailboxIdFactory.fromString(mailboxId.getValue()),
+ MessageUid.of(uidAsNumber.longValue())));
+ } else {
+ LOGGER.warn("Can not extract UID, MessageID and/or MailboxId for search result {}", hit.getId());
+ return Optional.empty();
+ }
+ }
+
+ private Optional<SearchHitField> retrieveMessageIdField(SearchHit hit) {
+ if (hit.fields().keySet().contains(JsonMessageConstants.MESSAGE_ID)) {
+ return Optional.ofNullable(hit.field(JsonMessageConstants.MESSAGE_ID));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/reporting-site/site.xml b/mailbox/elasticsearch-v6/src/reporting-site/site.xml
new file mode 100644
index 0000000..d919164
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/reporting-site/site.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ 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 name="${project.name}">
+
+ <body>
+
+ <menu ref="parent" />
+ <menu ref="reports" />
+
+ </body>
+
+</project>
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchIntegrationTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchIntegrationTest.java
new file mode 100644
index 0000000..81a4293
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchIntegrationTest.java
@@ -0,0 +1,223 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import org.apache.james.backends.es.DockerElasticSearchRule;
+import org.apache.james.backends.es.ElasticSearchConfiguration;
+import org.apache.james.backends.es.ElasticSearchIndexer;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MailboxSessionUtil;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.elasticsearch.v6.events.ElasticSearchListeningMessageSearchIndex;
+import org.apache.james.mailbox.elasticsearch.v6.json.MessageToElasticSearchJson;
+import org.apache.james.mailbox.elasticsearch.v6.query.CriterionConverter;
+import org.apache.james.mailbox.elasticsearch.v6.query.QueryConverter;
+import org.apache.james.mailbox.elasticsearch.v6.search.ElasticSearchSearcher;
+import org.apache.james.mailbox.inmemory.InMemoryId;
+import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.ComposedMessageId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.SearchQuery;
+import org.apache.james.mailbox.store.search.AbstractMessageSearchIndexTest;
+import org.apache.james.mailbox.tika.TikaConfiguration;
+import org.apache.james.mailbox.tika.TikaContainerSingletonRule;
+import org.apache.james.mailbox.tika.TikaHttpClientImpl;
+import org.apache.james.mailbox.tika.TikaTextExtractor;
+import org.apache.james.metrics.api.NoopMetricFactory;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.util.concurrent.NamedThreadFactory;
+import org.elasticsearch.client.Client;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+import com.google.common.base.Strings;
+
+public class ElasticSearchIntegrationTest extends AbstractMessageSearchIndexTest {
+
+ private static final int BATCH_SIZE = 1;
+ private static final int SEARCH_SIZE = 1;
+
+ @ClassRule
+ public static TikaContainerSingletonRule tika = TikaContainerSingletonRule.rule;
+
+ @Rule
+ public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule();
+ private TikaTextExtractor textExtractor;
+
+ @Override
+ public void setUp() throws Exception {
+ textExtractor = new TikaTextExtractor(new NoopMetricFactory(),
+ new TikaHttpClientImpl(TikaConfiguration.builder()
+ .host(tika.getIp())
+ .port(tika.getPort())
+ .timeoutInMillis(tika.getTimeoutInMillis())
+ .build()));
+ super.setUp();
+ }
+
+ @Override
+ protected void await() {
+ elasticSearch.awaitForElasticSearch();
+ }
+
+ @Override
+ protected void initializeMailboxManager() {
+ Client client = MailboxIndexCreationUtil.prepareDefaultClient(
+ elasticSearch.clientProvider().get(),
+ ElasticSearchConfiguration.builder()
+ .addHost(elasticSearch.getTcpHost())
+ .build());
+
+ InMemoryMessageId.Factory messageIdFactory = new InMemoryMessageId.Factory();
+ ThreadFactory threadFactory = NamedThreadFactory.withClassName(getClass());
+
+ InMemoryIntegrationResources resources = InMemoryIntegrationResources.builder()
+ .preProvisionnedFakeAuthenticator()
+ .fakeAuthorizator()
+ .inVmEventBus()
+ .defaultAnnotationLimits()
+ .defaultMessageParser()
+ .listeningSearchIndex(preInstanciationStage -> new ElasticSearchListeningMessageSearchIndex(
+ preInstanciationStage.getMapperFactory(),
+ new ElasticSearchIndexer(client,
+ Executors.newSingleThreadExecutor(threadFactory),
+ MailboxElasticSearchConstants.DEFAULT_MAILBOX_WRITE_ALIAS,
+ MailboxElasticSearchConstants.MESSAGE_TYPE,
+ BATCH_SIZE),
+ new ElasticSearchSearcher(client, new QueryConverter(new CriterionConverter()), SEARCH_SIZE,
+ new InMemoryId.Factory(), messageIdFactory,
+ MailboxElasticSearchConstants.DEFAULT_MAILBOX_READ_ALIAS,
+ MailboxElasticSearchConstants.MESSAGE_TYPE),
+ new MessageToElasticSearchJson(textExtractor, ZoneId.of("Europe/Paris"), IndexAttachments.YES),
+ preInstanciationStage.getSessionProvider()))
+ .noPreDeletionHooks()
+ .storeQuotaManager()
+ .build();
+
+ storeMailboxManager = resources.getMailboxManager();
+ messageIdManager = resources.getMessageIdManager();
+ messageSearchIndex = resources.getSearchIndex();
+ }
+
+ @Test
+ public void termsBetweenElasticSearchAndLuceneLimitDueTuNonAsciiCharsShouldBeTruncated() throws Exception {
+ MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
+ MailboxSession session = MailboxSessionUtil.create(USERNAME);
+ MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+
+ String recipient = "benwa@linagora.com";
+ ComposedMessageId composedMessageId = messageManager.appendMessage(MessageManager.AppendCommand.from(
+ Message.Builder.of()
+ .setTo(recipient)
+ .setBody(Strings.repeat("0à 2345678é", 3200), StandardCharsets.UTF_8)),
+ session);
+
+ elasticSearch.awaitForElasticSearch();
+
+ assertThat(messageManager.search(new SearchQuery(SearchQuery.address(SearchQuery.AddressType.To, recipient)), session))
+ .containsExactly(composedMessageId.getUid());
+ }
+
+ @Test
+ public void tooLongTermsShouldNotMakeIndexingFail() throws Exception {
+ MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
+ MailboxSession session = MailboxSessionUtil.create(USERNAME);
+ MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+
+ String recipient = "benwa@linagora.com";
+ ComposedMessageId composedMessageId = messageManager.appendMessage(MessageManager.AppendCommand.from(
+ Message.Builder.of()
+ .setTo(recipient)
+ .setBody(Strings.repeat("0123456789", 3300), StandardCharsets.UTF_8)),
+ session);
+
+ elasticSearch.awaitForElasticSearch();
+
+ assertThat(messageManager.search(new SearchQuery(SearchQuery.address(SearchQuery.AddressType.To, recipient)), session))
+ .containsExactly(composedMessageId.getUid());
+ }
+
+ @Test
+ public void fieldsExceedingLuceneLimitShouldNotBeIgnored() throws Exception {
+ MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
+ MailboxSession session = MailboxSessionUtil.create(USERNAME);
+ MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+
+ String recipient = "benwa@linagora.com";
+ ComposedMessageId composedMessageId = messageManager.appendMessage(MessageManager.AppendCommand.from(
+ Message.Builder.of()
+ .setTo(recipient)
+ .setBody(Strings.repeat("0123456789 ", 5000), StandardCharsets.UTF_8)),
+ session);
+
+ elasticSearch.awaitForElasticSearch();
+
+ assertThat(messageManager.search(new SearchQuery(SearchQuery.bodyContains("0123456789")), session))
+ .containsExactly(composedMessageId.getUid());
+ }
+
+ @Test
+ public void fieldsWithTooLongTermShouldStillBeIndexed() throws Exception {
+ MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
+ MailboxSession session = MailboxSessionUtil.create(USERNAME);
+ MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+
+ String recipient = "benwa@linagora.com";
+ ComposedMessageId composedMessageId = messageManager.appendMessage(MessageManager.AppendCommand.from(
+ Message.Builder.of()
+ .setTo(recipient)
+ .setBody(Strings.repeat("0123456789 ", 5000) + " matchMe", StandardCharsets.UTF_8)),
+ session);
+
+ elasticSearch.awaitForElasticSearch();
+
+ assertThat(messageManager.search(new SearchQuery(SearchQuery.bodyContains("matchMe")), session))
+ .containsExactly(composedMessageId.getUid());
+ }
+
+ @Test
+ public void reasonableLongTermShouldNotBeIgnored() throws Exception {
+ MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
+ MailboxSession session = MailboxSessionUtil.create(USERNAME);
+ MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session);
+
+ String recipient = "benwa@linagora.com";
+ String reasonableLongTerm = "dichlorodiphényltrichloroéthane";
+ ComposedMessageId composedMessageId = messageManager.appendMessage(MessageManager.AppendCommand.from(
+ Message.Builder.of()
+ .setTo(recipient)
+ .setBody(reasonableLongTerm, StandardCharsets.UTF_8)),
+ session);
+
+ elasticSearch.awaitForElasticSearch();
+
+ assertThat(messageManager.search(new SearchQuery(SearchQuery.bodyContains(reasonableLongTerm)), session))
+ .containsExactly(composedMessageId.getUid());
+ }
+}
\ No newline at end of file
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchMailboxConfigurationTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchMailboxConfigurationTest.java
new file mode 100644
index 0000000..64a1cd0
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/ElasticSearchMailboxConfigurationTest.java
@@ -0,0 +1,219 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.james.backends.es.IndexName;
+import org.apache.james.backends.es.ReadAliasName;
+import org.apache.james.backends.es.WriteAliasName;
+import org.junit.Test;
+
+public class ElasticSearchMailboxConfigurationTest {
+ @Test
+ public void getIndexMailboxNameShouldReturnOldConfiguredValue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.index.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexMailboxName())
+ .isEqualTo(new IndexName(name));
+ }
+
+ @Test
+ public void getIndexMailboxNameShouldReturnNewConfiguredValueWhenBoth() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.index.name", "other");
+ configuration.addProperty("elasticsearch.index.mailbox.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexMailboxName())
+ .isEqualTo(new IndexName(name));
+ }
+
+ @Test
+ public void getIndexMailboxNameShouldReturnConfiguredValue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.index.mailbox.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexMailboxName())
+ .isEqualTo(new IndexName(name));
+ }
+
+ @Test
+ public void getIndexMailboxNameShouldReturnDefaultValueWhenMissing() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexMailboxName())
+ .isEqualTo(MailboxElasticSearchConstants.DEFAULT_MAILBOX_INDEX);
+ }
+
+ @Test
+ public void getReadAliasMailboxNameShouldReturnOldConfiguredValue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.alias.read.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getReadAliasMailboxName())
+ .isEqualTo(new ReadAliasName(name));
+ }
+
+ @Test
+ public void getReadAliasMailboxNameShouldReturnConfiguredValue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.alias.read.mailbox.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getReadAliasMailboxName())
+ .isEqualTo(new ReadAliasName(name));
+ }
+
+ @Test
+ public void getReadAliasMailboxNameShouldReturnNewConfiguredValueWhenBoth() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.alias.read.mailbox.name", name);
+ configuration.addProperty("elasticsearch.alias.read.name", "other");
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getReadAliasMailboxName())
+ .isEqualTo(new ReadAliasName(name));
+ }
+
+ @Test
+ public void getReadAliasMailboxNameShouldReturnDefaultValueWhenMissing() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getReadAliasMailboxName())
+ .isEqualTo(MailboxElasticSearchConstants.DEFAULT_MAILBOX_READ_ALIAS);
+ }
+
+ @Test
+ public void getWriteAliasMailboxNameShouldReturnOldConfiguredValue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.alias.write.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getWriteAliasMailboxName())
+ .isEqualTo(new WriteAliasName(name));
+ }
+
+ @Test
+ public void getWriteAliasMailboxNameShouldReturnConfiguredValue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.alias.write.mailbox.name", name);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getWriteAliasMailboxName())
+ .isEqualTo(new WriteAliasName(name));
+ }
+
+ @Test
+ public void getWriteAliasMailboxNameShouldReturnNewConfiguredValueWhenBoth() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ String name = "name";
+ configuration.addProperty("elasticsearch.alias.write.mailbox.name", name);
+ configuration.addProperty("elasticsearch.alias.write.name", "other");
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getWriteAliasMailboxName())
+ .isEqualTo(new WriteAliasName(name));
+ }
+
+ @Test
+ public void getWriteAliasMailboxNameShouldReturnDefaultValueWhenMissing() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getWriteAliasMailboxName())
+ .isEqualTo(MailboxElasticSearchConstants.DEFAULT_MAILBOX_WRITE_ALIAS);
+ }
+
+ @Test
+ public void getIndexAttachmentShouldReturnConfiguredValueWhenTrue() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ configuration.addProperty("elasticsearch.indexAttachments", true);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexAttachment())
+ .isEqualTo(IndexAttachments.YES);
+ }
+
+ @Test
+ public void getIndexAttachmentShouldReturnConfiguredValueWhenFalse() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ configuration.addProperty("elasticsearch.indexAttachments", false);
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexAttachment())
+ .isEqualTo(IndexAttachments.NO);
+ }
+
+ @Test
+ public void getIndexAttachmentShouldReturnDefaultValueWhenMissing() {
+ PropertiesConfiguration configuration = new PropertiesConfiguration();
+ configuration.addProperty("elasticsearch.hosts", "127.0.0.1");
+
+ ElasticSearchMailboxConfiguration elasticSearchConfiguration = ElasticSearchMailboxConfiguration.fromProperties(configuration);
+
+ assertThat(elasticSearchConfiguration.getIndexAttachment())
+ .isEqualTo(IndexAttachments.YES);
+ }
+
+}
\ No newline at end of file
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/events/ElasticSearchListeningMessageSearchIndexTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/events/ElasticSearchListeningMessageSearchIndexTest.java
new file mode 100644
index 0000000..e17f6ca
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/events/ElasticSearchListeningMessageSearchIndexTest.java
@@ -0,0 +1,269 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.events;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.refEq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.mail.Flags;
+
+import org.apache.james.backends.es.ElasticSearchIndexer;
+import org.apache.james.backends.es.UpdatedRepresentation;
+import org.apache.james.core.User;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MailboxSessionUtil;
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.mailbox.elasticsearch.v6.json.MessageToElasticSearchJson;
+import org.apache.james.mailbox.elasticsearch.v6.search.ElasticSearchSearcher;
+import org.apache.james.mailbox.events.Group;
+import org.apache.james.mailbox.model.Mailbox;
+import org.apache.james.mailbox.model.TestId;
+import org.apache.james.mailbox.model.UpdatedFlags;
+import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.SessionProvider;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class ElasticSearchListeningMessageSearchIndexTest {
+ private static final long MODSEQ = 18L;
+ private static final MessageUid MESSAGE_UID = MessageUid.of(1);
+ private static final TestId MAILBOX_ID = TestId.of(12);
+ private static final String ELASTIC_SEARCH_ID = "12:1";
+ private static final String EXPECTED_JSON_CONTENT = "json content";
+ private static final String USERNAME = "username";
+
+ private ElasticSearchIndexer elasticSearchIndexer;
+ private MessageToElasticSearchJson messageToElasticSearchJson;
+ private ElasticSearchListeningMessageSearchIndex testee;
+ private MailboxSession session;
+ private List<User> users;
+ private Mailbox mailbox;
+
+ @Before
+ public void setup() {
+ MailboxSessionMapperFactory mapperFactory = mock(MailboxSessionMapperFactory.class);
+ messageToElasticSearchJson = mock(MessageToElasticSearchJson.class);
+ ElasticSearchSearcher elasticSearchSearcher = mock(ElasticSearchSearcher.class);
+ SessionProvider mockSessionProvider = mock(SessionProvider.class);
+
+ elasticSearchIndexer = mock(ElasticSearchIndexer.class);
+
+ testee = new ElasticSearchListeningMessageSearchIndex(mapperFactory, elasticSearchIndexer, elasticSearchSearcher,
+ messageToElasticSearchJson, mockSessionProvider);
+ session = MailboxSessionUtil.create(USERNAME);
+ users = ImmutableList.of(User.fromUsername(USERNAME));
+
+ mailbox = mock(Mailbox.class);
+ when(mailbox.getMailboxId()).thenReturn(MAILBOX_ID);
+ }
+
+ @Test
+ public void deserializeElasticSearchListeningMessageSearchIndexGroup() throws Exception {
+ assertThat(Group.deserialize("org.apache.james.mailbox.elasticsearch.v6.events.ElasticSearchListeningMessageSearchIndex$ElasticSearchListeningMessageSearchIndexGroup"))
+ .isEqualTo(new ElasticSearchListeningMessageSearchIndex.ElasticSearchListeningMessageSearchIndexGroup());
+ }
+
+ @Test
+ public void addShouldIndex() throws Exception {
+ //Given
+ MailboxMessage message = mockedMessage(MESSAGE_UID);
+
+ when(messageToElasticSearchJson.convertToJson(eq(message), eq(users)))
+ .thenReturn(EXPECTED_JSON_CONTENT);
+
+ //When
+ testee.add(session, mailbox, message);
+
+ //Then
+ verify(elasticSearchIndexer).index(eq(ELASTIC_SEARCH_ID), eq(EXPECTED_JSON_CONTENT));
+ }
+
+ @Test
+ public void addShouldIndexEmailBodyWhenNotIndexableAttachment() throws Exception {
+ //Given
+ MailboxMessage message = mockedMessage(MESSAGE_UID);
+
+ when(messageToElasticSearchJson.convertToJson(eq(message), eq(users)))
+ .thenThrow(JsonProcessingException.class);
+
+ when(messageToElasticSearchJson.convertToJsonWithoutAttachment(eq(message), eq(users)))
+ .thenReturn(EXPECTED_JSON_CONTENT);
+
+ //When
+ testee.add(session, mailbox, message);
+
+ //Then
+ verify(elasticSearchIndexer).index(eq(ELASTIC_SEARCH_ID), eq(EXPECTED_JSON_CONTENT));
+ }
+
+ private MailboxMessage mockedMessage(MessageUid uid) {
+ MailboxMessage message = mock(MailboxMessage.class);
+ when(message.getUid()).thenReturn(uid);
+ return message;
+ }
+
+ @Test
+ public void addShouldPropagateExceptionWhenExceptionOccurs() throws Exception {
+ //Given
+ MailboxMessage message = mockedMessage(MESSAGE_UID);
+
+ when(messageToElasticSearchJson.convertToJson(eq(message), eq(users)))
+ .thenThrow(JsonProcessingException.class);
+
+ // When
+ JsonGenerator jsonGenerator = null;
+ when(messageToElasticSearchJson.convertToJsonWithoutAttachment(eq(message), eq(users)))
+ .thenThrow(new JsonGenerationException("expected error", jsonGenerator));
+
+ //Then
+ assertThatThrownBy(() -> testee.add(session, mailbox, message)).isInstanceOf(JsonGenerationException.class);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void deleteShouldWork() {
+ //Given
+ BulkResponse expectedBulkResponse = mock(BulkResponse.class);
+ when(elasticSearchIndexer.delete(any(List.class)))
+ .thenReturn(Optional.of(expectedBulkResponse));
+
+ //When
+ testee.delete(session, mailbox, Lists.newArrayList(MESSAGE_UID));
+
+ //Then
+ verify(elasticSearchIndexer).delete(eq(Lists.newArrayList(ELASTIC_SEARCH_ID)));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void deleteShouldWorkWhenMultipleMessageIds() {
+ //Given
+ MessageUid messageId2 = MessageUid.of(2);
+ MessageUid messageId3 = MessageUid.of(3);
+ MessageUid messageId4 = MessageUid.of(4);
+ MessageUid messageId5 = MessageUid.of(5);
+
+ BulkResponse expectedBulkResponse = mock(BulkResponse.class);
+ when(elasticSearchIndexer.delete(any(List.class)))
+ .thenReturn(Optional.of(expectedBulkResponse));
+
+ //When
+ testee.delete(session, mailbox, Lists.newArrayList(MESSAGE_UID, messageId2, messageId3, messageId4, messageId5));
+
+ //Then
+ verify(elasticSearchIndexer).delete(eq(Lists.newArrayList(ELASTIC_SEARCH_ID, "12:2", "12:3", "12:4", "12:5")));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void deleteShouldPropagateExceptionWhenExceptionOccurs() {
+ //Given
+ when(elasticSearchIndexer.delete(any(List.class)))
+ .thenThrow(new ElasticsearchException(""));
+
+ // Then
+ assertThatThrownBy(() -> testee.delete(session, mailbox, Lists.newArrayList(MESSAGE_UID)))
+ .isInstanceOf(ElasticsearchException.class);
+ }
+
+ @Test
+ public void updateShouldWork() throws Exception {
+ //Given
+ Flags flags = new Flags();
+
+ UpdatedFlags updatedFlags = UpdatedFlags.builder()
+ .uid(MESSAGE_UID)
+ .modSeq(MODSEQ)
+ .oldFlags(flags)
+ .newFlags(flags)
+ .build();
+
+ when(messageToElasticSearchJson.getUpdatedJsonMessagePart(any(Flags.class), any(Long.class)))
+ .thenReturn("json updated content");
+
+ //When
+ testee.update(session, mailbox, Lists.newArrayList(updatedFlags));
+
+ //Then
+ ImmutableList<UpdatedRepresentation> expectedUpdatedRepresentations = ImmutableList.of(new UpdatedRepresentation(ELASTIC_SEARCH_ID, "json updated content"));
+ verify(elasticSearchIndexer).update(expectedUpdatedRepresentations);
+ }
+
+ @Test
+ public void updateShouldPropagateExceptionWhenExceptionOccurs() throws Exception {
+ //Given
+ Flags flags = new Flags();
+ UpdatedFlags updatedFlags = UpdatedFlags.builder()
+ .uid(MESSAGE_UID)
+ .modSeq(MODSEQ)
+ .oldFlags(flags)
+ .newFlags(flags)
+ .build();
+ when(messageToElasticSearchJson.getUpdatedJsonMessagePart(any(), anyLong())).thenReturn("update doc");
+
+ //When
+ when(elasticSearchIndexer.update(any())).thenThrow(new ElasticsearchException(""));
+
+ //Then
+ assertThatThrownBy(() -> testee.update(session, mailbox, Lists.newArrayList(updatedFlags))).isInstanceOf(ElasticsearchException.class);
+ }
+
+ @Test
+ public void deleteAllShouldWork() {
+ //Given
+ testee.deleteAll(session, mailbox);
+
+ //Then
+ QueryBuilder expectedQueryBuilder = QueryBuilders.termQuery("mailboxId", "12");
+ verify(elasticSearchIndexer).deleteAllMatchingQuery(refEq(expectedQueryBuilder));
+ }
+
+ @Test
+ public void deleteAllShouldNotPropagateExceptionWhenExceptionOccurs() {
+ //Given
+ doThrow(RuntimeException.class)
+ .when(elasticSearchIndexer).deleteAllMatchingQuery(any());
+
+ //Then
+ assertThatThrownBy(() -> testee.deleteAll(session, mailbox)).isInstanceOf(RuntimeException.class);
+ }
+
+}
\ No newline at end of file
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailersTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailersTest.java
new file mode 100644
index 0000000..c89a0ed
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/EMailersTest.java
@@ -0,0 +1,66 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.Test;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+public class EMailersTest {
+
+ @Test
+ public void fromShouldThrowWhenSetIsNull() {
+ assertThatThrownBy(() -> EMailers.from(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("'emailers' is mandatory");
+ }
+
+ @Test
+ public void serializeShouldReturnEmptyWhenEmptySet() {
+ EMailers eMailers = EMailers.from(ImmutableSet.of());
+
+ assertThat(eMailers.serialize()).isEmpty();
+ }
+
+ @Test
+ public void serializeShouldNotJoinWhenOneElement() {
+ EMailer emailer = new EMailer("name", "address");
+ EMailers eMailers = EMailers.from(ImmutableSet.of(emailer));
+
+ assertThat(eMailers.serialize()).isEqualTo(emailer.serialize());
+ }
+
+ @Test
+ public void serializeShouldJoinWhenMultipleElements() {
+ EMailer emailer = new EMailer("name", "address");
+ EMailer emailer2 = new EMailer("name2", "address2");
+ EMailer emailer3 = new EMailer("name3", "address3");
+
+ String expected = Joiner.on(" ").join(emailer.serialize(), emailer2.serialize(), emailer3.serialize());
+
+ EMailers eMailers = EMailers.from(ImmutableSet.of(emailer, emailer2, emailer3));
+
+ assertThat(eMailers.serialize()).isEqualTo(expected);
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/FieldImpl.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/FieldImpl.java
new file mode 100644
index 0000000..0525330
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/FieldImpl.java
@@ -0,0 +1,65 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import java.util.Objects;
+
+import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.util.ByteSequence;
+
+public class FieldImpl implements Field {
+ private final String name;
+ private final String body;
+
+ public FieldImpl(String name, String body) {
+ this.name = name;
+ this.body = body;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getBody() {
+ return body;
+ }
+
+ @Override
+ public ByteSequence getRaw() {
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, body);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof FieldImpl) {
+ FieldImpl otherField = (FieldImpl) o;
+ return Objects.equals(name, otherField.name)
+ && Objects.equals(body, otherField.body);
+ }
+ return false;
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/HeaderCollectionTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/HeaderCollectionTest.java
new file mode 100644
index 0000000..649c61c
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/HeaderCollectionTest.java
@@ -0,0 +1,334 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.time.format.DateTimeFormatter;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+
+class HeaderCollectionTest {
+
+ static class UTF8FromHeaderTestSource implements ArgumentsProvider {
+
+ @Override
+ public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
+ return Stream.of(
+ Arguments.of("=?UTF-8?B?RnLDqWTDqXJpYyBNQVJUSU4=?= <fm...@linagora.com>, Graham CROSMARIE <gc...@linagora.com>", "Frédéric MARTIN"),
+ Arguments.of("=?UTF-8?Q?=C3=9Csteli=C4=9Fhan_Ma=C5=9Frapa?= <us...@domain.tld>", "ÃœsteliÄŸhan MaÅŸrapa"),
+ Arguments.of("=?UTF-8?Q?Ke=C5=9Ffet_Turizm?= <ke...@domain.tld>", "KeÅŸfet Turizm"),
+ Arguments.of("=?UTF-8?Q?MODAL=C4=B0F?= <mo...@domain.tld>", "MODALÄ°F"));
+ }
+ }
+
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
+
+ @Test
+ void simpleValueAddressHeaderShouldBeAddedToTheAddressSet() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("To", "ben.tellier@linagora.com"))
+ .build();
+
+ assertThat(headerCollection.getToAddressSet())
+ .containsOnly(new EMailer("ben.tellier@linagora.com", "ben.tellier@linagora.com"));
+ }
+
+ @Test
+ void comaSeparatedAddressShouldBeBothAddedToTheAddressSet() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("To", "ben.tellier@linagora.com, btellier@minet.net"))
+ .build();
+
+ assertThat(headerCollection.getToAddressSet())
+ .containsOnly(
+ new EMailer("ben.tellier@linagora.com", "ben.tellier@linagora.com"),
+ new EMailer("btellier@minet.net", "btellier@minet.net"));
+ }
+
+ @Test
+ void addressesOfTwoFieldsHavingTheSameNameShouldBeMerged() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("To", "ben.tellier@linagora.com"))
+ .add(new FieldImpl("To", "ben.tellier@linagora.com, btellier@minet.net"))
+ .build();
+
+ assertThat(headerCollection.getToAddressSet())
+ .containsOnly(
+ new EMailer("ben.tellier@linagora.com", "ben.tellier@linagora.com"),
+ new EMailer("btellier@minet.net", "btellier@minet.net"));
+ }
+
+ @Test
+ void displayNamesShouldBeRetreived() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("To", "Christophe Hamerling <ch...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getToAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"));
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(UTF8FromHeaderTestSource.class)
+ void displayNamesShouldBeRetrievedWhenEncodedWord(String encodedFromHeader, String nameOfFromAddress) {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("From", encodedFromHeader))
+ .build();
+
+ assertThat(headerCollection.getFromAddressSet())
+ .extracting(EMailer::getName)
+ .contains(nameOfFromAddress);
+ }
+
+ @Test
+ void getHeadersShouldDecodeValues() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("From", "=?UTF-8?B?RnLDqWTDqXJpYyBNQVJUSU4=?= <fm...@linagora.com>, Graham CROSMARIE <gc...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getHeaders().get("from"))
+ .containsExactly("Frédéric MARTIN <fm...@linagora.com>, Graham CROSMARIE <gc...@linagora.com>");
+ }
+
+ @Test
+ void getHeadersShouldIgnoreHeadersWithDots() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("a.b.c", "value"))
+ .build();
+
+ assertThat(headerCollection.getHeaders().get("a.b.c"))
+ .isEmpty();
+ }
+
+ @Test
+ void addressWithTwoDisplayNamesOnTheSameFieldShouldBeRetrieved() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("From", "Christophe Hamerling <ch...@linagora.com>, Graham CROSMARIE <gr...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getFromAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"),
+ new EMailer("Graham CROSMARIE", "grah.crosmarie@linagora.com"));
+ }
+
+ @Test
+ void foldedFromHeaderShouldBeSupported() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("From", "Christophe Hamerling <ch...@linagora.com>,\r\n" +
+ " Graham CROSMARIE <gr...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getFromAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"),
+ new EMailer("Graham CROSMARIE", "grah.crosmarie@linagora.com"));
+ }
+
+ @Test
+ void foldedHeaderShouldBeSupported() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("From", "Christophe Hamerling <ch...@linagora.com>,\r\n" +
+ " Graham CROSMARIE <gr...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getHeaders().get("from"))
+ .containsOnly("Christophe Hamerling <ch...@linagora.com>, Graham CROSMARIE <gr...@linagora.com>");
+ }
+
+ @Test
+ void mixingAddressWithDisplayNamesWithOthersShouldBeAllowed() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("To", "Christophe Hamerling <ch...@linagora.com>, grah.crosmarie@linagora.com"))
+ .build();
+
+ assertThat(headerCollection.getToAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"),
+ new EMailer("grah.crosmarie@linagora.com", "grah.crosmarie@linagora.com"));
+ }
+
+ @Test
+ void displayNamesShouldBeRetreivedOnCc() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Cc", "Christophe Hamerling <ch...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getCcAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"));
+ }
+
+ @Test
+ void displayNamesShouldBeRetreivedOnReplyTo() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Reply-To", "Christophe Hamerling <ch...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getReplyToAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"));
+ }
+
+ @Test
+ void displayNamesShouldBeRetreivedOnBcc() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Bcc", "Christophe Hamerling <ch...@linagora.com>"))
+ .build();
+
+ assertThat(headerCollection.getBccAddressSet())
+ .containsOnly(new EMailer("Christophe Hamerling", "chri.hamerling@linagora.com"));
+ }
+
+ @Test
+ void headerContaingNoAddressShouldBeConsideredBothAsNameAndAddress() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Bcc", "Not an address"))
+ .build();
+
+ assertThat(headerCollection.getBccAddressSet())
+ .containsOnly(new EMailer("Not an address", "Not an address"));
+ }
+
+ @Test
+ void unclosedAddressSubpartShouldBeWellHandled() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Bcc", "Mickey <tricky@mouse.com"))
+ .build();
+
+ assertThat(headerCollection.getBccAddressSet())
+ .containsOnly(new EMailer("Mickey", "tricky@mouse.com"));
+ }
+
+ @Test
+ void notComaSeparatedAddressSubpartShouldBeWellHandled() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Bcc", "Mickey <tr...@mouse.com> Miny<he...@polo.com>"))
+ .build();
+
+ assertThat(headerCollection.getBccAddressSet())
+ .containsOnly(new EMailer("Mickey", "tricky@mouse.com"),
+ new EMailer("Miny", "hello@polo.com"));
+ }
+
+ @Test
+ void notSeparatedAddressSubpartShouldBeWellHandled() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Bcc", "Mickey <tr...@polo.com>"))
+ .build();
+
+ assertThat(headerCollection.getBccAddressSet())
+ .containsOnly(new EMailer("Mickey", "tricky@mouse.com"),
+ new EMailer("Miny", "hello@polo.com"));
+ }
+
+ @Test
+ void dateShouldBeRetreived() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Date", "Thu, 4 Jun 2015 06:08:41 +0200"))
+ .build();
+
+ assertThat(DATE_TIME_FORMATTER.format(headerCollection.getSentDate().get()))
+ .isEqualTo("2015/06/04 06:08:41");
+ }
+
+ @Test
+ void partialYearShouldBeCompleted() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Date", "Thu, 4 Jun 15 06:08:41 +0200"))
+ .build();
+
+ assertThat(DATE_TIME_FORMATTER.format(headerCollection.getSentDate().get()))
+ .isEqualTo("2015/06/04 06:08:41");
+ }
+
+ @Test
+ void nonStandardDatesShouldBeRetreived() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Date", "Thu, 4 Jun 2015 06:08:41 +0200 (UTC)"))
+ .build();
+
+ assertThat(DATE_TIME_FORMATTER.format(headerCollection.getSentDate().get()))
+ .isEqualTo("2015/06/04 06:08:41");
+ }
+
+ @Test
+ void dateShouldBeAbsentOnInvalidHeader() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Date", "Not a date"))
+ .build();
+
+ assertThat(headerCollection.getSentDate().isPresent())
+ .isFalse();
+ }
+
+ @Test
+ void subjectsShouldBeWellRetrieved() {
+ String subject = "A fantastic ElasticSearch module will be available soon for JAMES";
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Subject", subject))
+ .build();
+
+ assertThat(headerCollection.getSubjectSet()).containsOnly("A fantastic ElasticSearch module will be available soon for JAMES");
+ }
+
+ @Test
+ void getMessageIDShouldReturnMessageIdValue() {
+ String messageID = "<ab...@123>";
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Message-ID", messageID))
+ .build();
+
+ assertThat(headerCollection.getMessageID())
+ .contains(messageID);
+ }
+
+ @Test
+ void getMessageIDShouldReturnLatestEncounteredMessageIdValue() {
+ String messageID = "<ab...@123>";
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Message-ID", "<ot...@toto.com>"))
+ .add(new FieldImpl("Message-ID", messageID))
+ .build();
+
+ assertThat(headerCollection.getMessageID())
+ .contains(messageID);
+ }
+
+ @Test
+ void getMessageIDShouldReturnEmptyWhenNoMessageId() {
+ HeaderCollection headerCollection = HeaderCollection.builder()
+ .add(new FieldImpl("Other", "value"))
+ .build();
+
+ assertThat(headerCollection.getMessageID())
+ .isEmpty();
+ }
+
+ @Test
+ void nullFieldShouldThrow() {
+ assertThatThrownBy(() -> HeaderCollection.builder().add(null).build())
+ .isInstanceOf(NullPointerException.class);
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/IndexableMessageTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/IndexableMessageTest.java
new file mode 100644
index 0000000..c9e2e19
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/IndexableMessageTest.java
@@ -0,0 +1,578 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.ZoneId;
+import java.util.Optional;
+
+import javax.mail.Flags;
+
+import org.apache.james.core.User;
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.mailbox.elasticsearch.v6.IndexAttachments;
+import org.apache.james.mailbox.extractor.ParsedContent;
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.TestId;
+import org.apache.james.mailbox.store.extractor.DefaultTextExtractor;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleProperty;
+import org.apache.james.mailbox.tika.TikaConfiguration;
+import org.apache.james.mailbox.tika.TikaContainerSingletonRule;
+import org.apache.james.mailbox.tika.TikaHttpClientImpl;
+import org.apache.james.mailbox.tika.TikaTextExtractor;
+import org.apache.james.metrics.api.NoopMetricFactory;
+import org.assertj.core.api.iterable.Extractor;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class IndexableMessageTest {
+ private static final MessageUid MESSAGE_UID = MessageUid.of(154);
+
+ @ClassRule
+ public static TikaContainerSingletonRule tika = TikaContainerSingletonRule.rule;
+
+ private TikaTextExtractor textExtractor;
+
+ @Before
+ public void setUp() throws Exception {
+ textExtractor = new TikaTextExtractor(new NoopMetricFactory(), new TikaHttpClientImpl(TikaConfiguration.builder()
+ .host(tika.getIp())
+ .port(tika.getPort())
+ .timeoutInMillis(tika.getTimeoutInMillis())
+ .build()));
+ }
+
+ @Test
+ public void textShouldBeEmptyWhenNoMatchingHeaders() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEmpty();
+ }
+
+ @Test
+ public void textShouldContainsFromWhenFrom() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("From: First user <us...@james.org>\nFrom: Second user <us...@james.org>".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("Second user user2@james.org First user user@james.org");
+ }
+
+ @Test
+ public void textShouldContainsToWhenTo() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("To: First to <us...@james.org>\nTo: Second to <us...@james.org>".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("First to user@james.org Second to user2@james.org");
+ }
+
+ @Test
+ public void textShouldContainsCcWhenCc() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("Cc: First cc <us...@james.org>\nCc: Second cc <us...@james.org>".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("First cc user@james.org Second cc user2@james.org");
+ }
+
+ @Test
+ public void textShouldContainsBccWhenBcc() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("Bcc: First bcc <us...@james.org>\nBcc: Second bcc <us...@james.org>".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("Second bcc user2@james.org First bcc user@james.org");
+ }
+
+ @Test
+ public void textShouldContainsSubjectsWhenSubjects() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("Subject: subject1\nSubject: subject2".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("subject1 subject2");
+ }
+
+ @Test
+ public void textShouldContainsBodyWhenBody() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(new ByteArrayInputStream("\nMy body".getBytes()));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("My body");
+ }
+
+ @Test
+ public void textShouldContainsAllFieldsWhenAllSet() throws Exception {
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/mailWithHeaders.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ assertThat(indexableMessage.getText()).isEqualTo("Ad Min admin@opush.test " +
+ "a@test a@test B b@test " +
+ "c@test c@test " +
+ "dD d@test " +
+ "my subject " +
+ "Mail content\n" +
+ "\n" +
+ "-- \n" +
+ "Ad Min\n");
+ }
+
+ @Test
+ public void hasAttachmentsShouldReturnTrueWhenPropertyIsPresentAndTrue() throws IOException {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/mailWithHeaders.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+ when(mailboxMessage.getProperties()).thenReturn(ImmutableList.of(IndexableMessage.HAS_ATTACHMENT_PROPERTY));
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.YES)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getHasAttachment()).isTrue();
+ }
+
+ @Test
+ public void hasAttachmentsShouldReturnFalseWhenPropertyIsPresentButFalse() throws IOException {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/mailWithHeaders.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+ when(mailboxMessage.getProperties())
+ .thenReturn(ImmutableList.of(new SimpleProperty(PropertyBuilder.JAMES_INTERNALS, PropertyBuilder.HAS_ATTACHMENT, "false")));
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getHasAttachment()).isFalse();
+ }
+
+ @Test
+ public void hasAttachmentsShouldReturnFalseWhenPropertyIsAbsent() throws IOException {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/mailWithHeaders.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+ when(mailboxMessage.getProperties())
+ .thenReturn(ImmutableList.of());
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getHasAttachment()).isFalse();
+ }
+
+ @Test
+ public void attachmentsShouldNotBeenIndexedWhenAsked() throws Exception {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/mailWithHeaders.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.NO)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getAttachments()).isEmpty();
+ }
+
+ @Test
+ public void attachmentsShouldBeenIndexedWhenAsked() throws Exception {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/emailWith3Attachments.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(new DefaultTextExtractor())
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.YES)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getAttachments()).isNotEmpty();
+ }
+
+ @Test
+ public void otherAttachmentsShouldBeenIndexedWhenOneOfThemCannotBeParsed() throws Exception {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/emailWith3Attachments.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ TextExtractor textExtractor = mock(TextExtractor.class);
+ when(textExtractor.extractContent(any(), any()))
+ .thenReturn(new ParsedContent(Optional.of("first attachment content"), ImmutableMap.of()))
+ .thenThrow(new RuntimeException("second cannot be parsed"))
+ .thenReturn(new ParsedContent(Optional.of("third attachment content"), ImmutableMap.of()));
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(textExtractor)
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.YES)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getAttachments())
+ .extracting(new TextualBodyExtractor())
+ .contains("first attachment content", TextualBodyExtractor.NO_TEXTUAL_BODY, "third attachment content");
+ }
+
+ private static class TextualBodyExtractor implements Extractor<MimePart, String> {
+
+ public static final String NO_TEXTUAL_BODY = "The textual body is not present";
+
+ @Override
+ public String extract(MimePart input) {
+ return input.getTextualBody().orElse(NO_TEXTUAL_BODY);
+ }
+ }
+
+ @Test
+ public void messageShouldBeIndexedEvenIfTikaParserThrowsAnError() throws Exception {
+ //Given
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(InMemoryMessageId.of(42));
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/bodyMakeTikaToFail.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ // When
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(textExtractor)
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.YES)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getText()).contains("subject should be parsed");
+ }
+
+ @Test
+ public void shouldHandleCorrectlyMessageIdHavingSerializeMethodThatReturnNull() throws Exception {
+ MessageId invalidMessageIdThatReturnNull = mock(MessageId.class);
+ when(invalidMessageIdThatReturnNull.serialize())
+ .thenReturn(null);
+
+ // When
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(invalidMessageIdThatReturnNull);
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/bodyMakeTikaToFail.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(textExtractor)
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.YES)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getMessageId()).isNull();
+ }
+
+ @Test
+ public void shouldHandleCorrectlyNullMessageId() throws Exception {
+
+ // When
+ MailboxMessage mailboxMessage = mock(MailboxMessage.class);
+ TestId mailboxId = TestId.of(1);
+ when(mailboxMessage.getMailboxId())
+ .thenReturn(mailboxId);
+ when(mailboxMessage.getMessageId())
+ .thenReturn(null);
+ when(mailboxMessage.getFullContent())
+ .thenReturn(ClassLoader.getSystemResourceAsStream("eml/bodyMakeTikaToFail.eml"));
+ when(mailboxMessage.createFlags())
+ .thenReturn(new Flags());
+ when(mailboxMessage.getUid())
+ .thenReturn(MESSAGE_UID);
+
+ IndexableMessage indexableMessage = IndexableMessage.builder()
+ .message(mailboxMessage)
+ .users(ImmutableList.of(User.fromUsername("username")))
+ .extractor(textExtractor)
+ .zoneId(ZoneId.of("Europe/Paris"))
+ .indexAttachments(IndexAttachments.YES)
+ .build();
+
+ // Then
+ assertThat(indexableMessage.getMessageId()).isNull();
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageToElasticSearchJsonTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageToElasticSearchJsonTest.java
new file mode 100644
index 0000000..9c840be
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/MessageToElasticSearchJsonTest.java
@@ -0,0 +1,388 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER;
+import static net.javacrumbs.jsonunit.core.Option.IGNORING_VALUES;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
+import java.util.Date;
+
+import javax.mail.Flags;
+import javax.mail.util.SharedByteArrayInputStream;
+
+import org.apache.james.core.User;
+import org.apache.james.mailbox.FlagsBuilder;
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.mailbox.elasticsearch.v6.IndexAttachments;
+import org.apache.james.mailbox.extractor.TextExtractor;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.TestId;
+import org.apache.james.mailbox.model.TestMessageId;
+import org.apache.james.mailbox.store.extractor.DefaultTextExtractor;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
+import org.apache.james.mailbox.tika.TikaConfiguration;
+import org.apache.james.mailbox.tika.TikaContainerSingletonRule;
+import org.apache.james.mailbox.tika.TikaHttpClientImpl;
+import org.apache.james.mailbox.tika.TikaTextExtractor;
+import org.apache.james.metrics.api.NoopMetricFactory;
+import org.apache.james.util.ClassLoaderUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class MessageToElasticSearchJsonTest {
+ private static final int SIZE = 25;
+ private static final int BODY_START_OCTET = 100;
+ private static final TestId MAILBOX_ID = TestId.of(18L);
+ private static final MessageId MESSAGE_ID = TestMessageId.of(184L);
+ private static final long MOD_SEQ = 42L;
+ private static final MessageUid UID = MessageUid.of(25);
+ private static final String USERNAME = "username";
+ private static final User USER = User.fromUsername(USERNAME);
+
+ private TextExtractor textExtractor;
+
+ private Date date;
+ private PropertyBuilder propertyBuilder;
+
+ @ClassRule
+ public static TikaContainerSingletonRule tika = TikaContainerSingletonRule.rule;
+
+ @Before
+ public void setUp() throws Exception {
+ textExtractor = new TikaTextExtractor(new NoopMetricFactory(), new TikaHttpClientImpl(TikaConfiguration.builder()
+ .host(tika.getIp())
+ .port(tika.getPort())
+ .timeoutInMillis(tika.getTimeoutInMillis())
+ .build()));
+ // 2015/06/07 00:00:00 0200 (Paris time zone)
+ date = new Date(1433628000000L);
+ propertyBuilder = new PropertyBuilder();
+ propertyBuilder.setMediaType("plain");
+ propertyBuilder.setSubType("text");
+ propertyBuilder.setTextualLineCount(18L);
+ propertyBuilder.setContentDescription("An e-mail");
+ }
+
+ @Test
+ public void convertToJsonShouldThrowWhenNoUser() {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage spamMail = new SimpleMailboxMessage(MESSAGE_ID,
+ date,
+ SIZE,
+ BODY_START_OCTET,
+ new SharedByteArrayInputStream("message".getBytes(StandardCharsets.UTF_8)),
+ new Flags(),
+ propertyBuilder,
+ MAILBOX_ID);
+ ImmutableList<User> users = ImmutableList.of();
+
+ assertThatThrownBy(() -> messageToElasticSearchJson.convertToJson(spamMail, users))
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void spamEmailShouldBeWellConvertedToJson() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage spamMail = new SimpleMailboxMessage(MESSAGE_ID,
+ date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/spamMail.eml"),
+ new Flags(),
+ propertyBuilder,
+ MAILBOX_ID);
+ spamMail.setUid(UID);
+ spamMail.setModSeq(MOD_SEQ);
+ assertThatJson(messageToElasticSearchJson.convertToJson(spamMail, ImmutableList.of(USER)))
+ .when(IGNORING_ARRAY_ORDER)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/spamMail.json"));
+ }
+
+ @Test
+ public void htmlEmailShouldBeWellConvertedToJson() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage htmlMail = new SimpleMailboxMessage(MESSAGE_ID,
+ date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/htmlMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("social", "pocket-money").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ htmlMail.setModSeq(MOD_SEQ);
+ htmlMail.setUid(UID);
+ assertThatJson(messageToElasticSearchJson.convertToJson(htmlMail, ImmutableList.of(USER)))
+ .when(IGNORING_ARRAY_ORDER)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/htmlMail.json"));
+ }
+
+ @Test
+ public void pgpSignedEmailShouldBeWellConvertedToJson() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage pgpSignedMail = new SimpleMailboxMessage(MESSAGE_ID,
+ date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/pgpSignedMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ pgpSignedMail.setModSeq(MOD_SEQ);
+ pgpSignedMail.setUid(UID);
+ assertThatJson(messageToElasticSearchJson.convertToJson(pgpSignedMail, ImmutableList.of(USER)))
+ .when(IGNORING_ARRAY_ORDER)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/pgpSignedMail.json"));
+ }
+
+ @Test
+ public void simpleEmailShouldBeWellConvertedToJson() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage mail = new SimpleMailboxMessage(MESSAGE_ID,
+ date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/mail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ mail.setModSeq(MOD_SEQ);
+ mail.setUid(UID);
+ assertThatJson(messageToElasticSearchJson.convertToJson(mail,
+ ImmutableList.of(User.fromUsername("user1"), User.fromUsername("user2"))))
+ .when(IGNORING_ARRAY_ORDER).when(IGNORING_VALUES)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/mail.json"));
+ }
+
+ @Test
+ public void recursiveEmailShouldBeWellConvertedToJson() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage recursiveMail = new SimpleMailboxMessage(MESSAGE_ID,
+ date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/recursiveMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ recursiveMail.setModSeq(MOD_SEQ);
+ recursiveMail.setUid(UID);
+ assertThatJson(messageToElasticSearchJson.convertToJson(recursiveMail, ImmutableList.of(USER)))
+ .when(IGNORING_ARRAY_ORDER).when(IGNORING_VALUES)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/recursiveMail.json"));
+ }
+
+ @Test
+ public void emailWithNoInternalDateShouldUseNowDate() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage mailWithNoInternalDate = new SimpleMailboxMessage(MESSAGE_ID,
+ null,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/recursiveMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ mailWithNoInternalDate.setModSeq(MOD_SEQ);
+ mailWithNoInternalDate.setUid(UID);
+ assertThatJson(messageToElasticSearchJson.convertToJson(mailWithNoInternalDate, ImmutableList.of(USER)))
+ .when(IGNORING_ARRAY_ORDER)
+ .when(IGNORING_VALUES)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/recursiveMail.json"));
+ }
+
+ @Test
+ public void emailWithAttachmentsShouldConvertAttachmentsWhenIndexAttachmentsIsTrue() throws IOException {
+ // Given
+ MailboxMessage mailWithNoInternalDate = new SimpleMailboxMessage(MESSAGE_ID,
+ null,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/recursiveMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ mailWithNoInternalDate.setModSeq(MOD_SEQ);
+ mailWithNoInternalDate.setUid(UID);
+
+ // When
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.YES);
+ String convertToJson = messageToElasticSearchJson.convertToJson(mailWithNoInternalDate, ImmutableList.of(USER));
+
+ // Then
+ assertThatJson(convertToJson)
+ .when(IGNORING_ARRAY_ORDER)
+ .when(IGNORING_VALUES)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/recursiveMail.json"));
+ }
+
+ @Test
+ public void emailWithAttachmentsShouldNotConvertAttachmentsWhenIndexAttachmentsIsFalse() throws IOException {
+ // Given
+ MailboxMessage mailWithNoInternalDate = new SimpleMailboxMessage(MESSAGE_ID,
+ null,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/recursiveMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ mailWithNoInternalDate.setModSeq(MOD_SEQ);
+ mailWithNoInternalDate.setUid(UID);
+
+ // When
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.NO);
+ String convertToJson = messageToElasticSearchJson.convertToJson(mailWithNoInternalDate, ImmutableList.of(USER));
+
+ // Then
+ assertThatJson(convertToJson)
+ .when(IGNORING_ARRAY_ORDER)
+ .when(IGNORING_VALUES)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/recursiveMailWithoutAttachments.json"));
+ }
+
+ @Test
+ public void emailWithNoMailboxIdShouldThrow() {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"), IndexAttachments.YES);
+ MailboxMessage mailWithNoMailboxId = new SimpleMailboxMessage(MESSAGE_ID, date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/recursiveMail.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ null);
+ mailWithNoMailboxId.setModSeq(MOD_SEQ);
+ mailWithNoMailboxId.setUid(UID);
+
+ assertThatThrownBy(() ->
+ messageToElasticSearchJson.convertToJson(mailWithNoMailboxId, ImmutableList.of(USER)))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ public void getUpdatedJsonMessagePartShouldBehaveWellOnEmptyFlags() throws Exception {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.YES);
+ assertThatJson(messageToElasticSearchJson.getUpdatedJsonMessagePart(new Flags(), MOD_SEQ))
+ .isEqualTo("{\"modSeq\":42,\"isAnswered\":false,\"isDeleted\":false,\"isDraft\":false,\"isFlagged\":false,\"isRecent\":false,\"userFlags\":[],\"isUnread\":true}");
+ }
+
+ @Test
+ public void getUpdatedJsonMessagePartShouldBehaveWellOnNonEmptyFlags() throws Exception {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.YES);
+ assertThatJson(messageToElasticSearchJson.getUpdatedJsonMessagePart(new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.FLAGGED).add("user").build(), MOD_SEQ))
+ .isEqualTo("{\"modSeq\":42,\"isAnswered\":false,\"isDeleted\":true,\"isDraft\":false,\"isFlagged\":true,\"isRecent\":false,\"userFlags\":[\"user\"],\"isUnread\":true}");
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void getUpdatedJsonMessagePartShouldThrowIfFlagsIsNull() throws Exception {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.YES);
+ messageToElasticSearchJson.getUpdatedJsonMessagePart(null, MOD_SEQ);
+ }
+
+ @Test
+ public void spamEmailShouldBeWellConvertedToJsonWithApacheTika() throws IOException {
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ textExtractor,
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.YES);
+ MailboxMessage spamMail = new SimpleMailboxMessage(MESSAGE_ID, date,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/nonTextual.eml"),
+ new Flags(),
+ propertyBuilder,
+ MAILBOX_ID);
+ spamMail.setUid(UID);
+ spamMail.setModSeq(MOD_SEQ);
+
+ assertThatJson(messageToElasticSearchJson.convertToJson(spamMail, ImmutableList.of(USER)))
+ .when(IGNORING_ARRAY_ORDER)
+ .isEqualTo(
+ ClassLoaderUtils.getSystemResourceAsString("eml/nonTextual.json", StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void convertToJsonWithoutAttachmentShouldConvertEmailBoby() throws IOException {
+ // Given
+ MailboxMessage message = new SimpleMailboxMessage(MESSAGE_ID,
+ null,
+ SIZE,
+ BODY_START_OCTET,
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/emailWithNonIndexableAttachment.eml"),
+ new FlagsBuilder().add(Flags.Flag.DELETED, Flags.Flag.SEEN).add("debian", "security").build(),
+ propertyBuilder,
+ MAILBOX_ID);
+ message.setModSeq(MOD_SEQ);
+ message.setUid(UID);
+
+ // When
+ MessageToElasticSearchJson messageToElasticSearchJson = new MessageToElasticSearchJson(
+ new DefaultTextExtractor(),
+ ZoneId.of("Europe/Paris"),
+ IndexAttachments.NO);
+ String convertToJsonWithoutAttachment = messageToElasticSearchJson.convertToJsonWithoutAttachment(message, ImmutableList.of(USER));
+
+ // Then
+ assertThatJson(convertToJsonWithoutAttachment)
+ .when(IGNORING_ARRAY_ORDER)
+ .when(IGNORING_VALUES)
+ .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("eml/emailWithNonIndexableAttachmentWithoutAttachment.json"));
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartTest.java
new file mode 100644
index 0000000..89daa82
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/MimePartTest.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.mailbox.elasticsearch.v6.json;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.Test;
+
+public class MimePartTest {
+
+ @Test
+ public void buildShouldWorkWhenTextualContentFromParserIsEmpty() {
+ MimePart.builder()
+ .addBodyContent(new ByteArrayInputStream(new byte[] {}))
+ .addMediaType("text")
+ .addSubType("plain")
+ .build();
+ }
+
+ @Test
+ public void buildShouldWorkWhenTextualContentFromParserIsNonEmpty() {
+ String body = "text";
+ MimePart mimePart = MimePart.builder()
+ .addBodyContent(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)))
+ .addMediaType("text")
+ .addSubType("plain")
+ .build();
+
+ assertThat(mimePart.getTextualBody()).contains(body);
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/SubjectsTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/SubjectsTest.java
new file mode 100644
index 0000000..38f6581
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/json/SubjectsTest.java
@@ -0,0 +1,66 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.json;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.Test;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+public class SubjectsTest {
+
+ @Test
+ public void fromShouldThrowWhenSetIsNull() {
+ assertThatThrownBy(() -> Subjects.from(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("'subjects' is mandatory");
+ }
+
+ @Test
+ public void serializeShouldReturnEmptyWhenEmptySet() {
+ Subjects subjects = Subjects.from(ImmutableSet.of());
+
+ assertThat(subjects.serialize()).isEmpty();
+ }
+
+ @Test
+ public void serializeShouldNotJoinWhenOneElement() {
+ String expected = "subject";
+ Subjects subjects = Subjects.from(ImmutableSet.of(expected));
+
+ assertThat(subjects.serialize()).isEqualTo(expected);
+ }
+
+ @Test
+ public void serializeShouldJoinWhenMultipleElements() {
+ String subject = "subject";
+ String subject2 = "subject2";
+ String subject3 = "subject3";
+
+ String expected = Joiner.on(" ").join(subject, subject2, subject3);
+
+ Subjects subjects = Subjects.from(ImmutableSet.of(subject, subject2, subject3));
+
+ assertThat(subjects.serialize()).isEqualTo(expected);
+ }
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/query/DateResolutionFormaterTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/query/DateResolutionFormaterTest.java
new file mode 100644
index 0000000..db48dad
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/query/DateResolutionFormaterTest.java
@@ -0,0 +1,99 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.query;
+
+import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.text.ParseException;
+import java.time.ZonedDateTime;
+
+import org.apache.james.mailbox.model.SearchQuery;
+import org.junit.Test;
+
+
+public class DateResolutionFormaterTest {
+
+ private final String dateString = "2014-01-02T15:15:15Z";
+
+ @Test
+ public void calculateUpperDateShouldReturnDateUpToTheNextMinuteUsingMinuteUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeUpperDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Minute)
+ )
+ ).isEqualTo("2014-01-02T15:16:00Z");
+ }
+
+ @Test
+ public void calculateUpperDateShouldReturnDateUpToTheNextHourUsingHourUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeUpperDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Hour)
+ )
+ ).isEqualTo("2014-01-02T16:00:00Z");
+ }
+
+ @Test
+ public void calculateUpperDateShouldReturnDateUpToTheNextMonthUsingMonthUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeUpperDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Month)
+ )
+ ).isEqualTo("2014-02-01T00:00:00Z");
+ }
+
+ @Test
+ public void calculateUpperDateShouldReturnDateUpToTheNextYearUsingYearUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeUpperDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Year)
+ )
+ ).isEqualTo("2015-01-01T00:00:00Z");
+ }
+
+ @Test
+ public void calculateUpperDateShouldReturnDateUpToTheNextDayUsingDayUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeUpperDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Day)
+ )
+ ).isEqualTo("2014-01-03T00:00:00Z");
+ }
+
+ @Test
+ public void calculateLowerDateShouldReturnDateUpToThePreviousMinuteUsingMinuteUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeLowerDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Minute)
+ )
+ ).isEqualTo("2014-01-02T15:15:00Z");
+ }
+
+ @Test
+ public void calculateLowerDateShouldReturnDateUpToThePreviousDayUsingDayUnit() throws ParseException {
+ assertThat(
+ ISO_OFFSET_DATE_TIME.format(
+ DateResolutionFormater.computeLowerDate(ZonedDateTime.parse(dateString, ISO_OFFSET_DATE_TIME), SearchQuery.DateResolution.Day)
+ )
+ ).isEqualTo("2014-01-02T00:00:00Z");
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/query/SearchQueryTest.java b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/query/SearchQueryTest.java
new file mode 100644
index 0000000..bfa4245
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/java/org/apache/james/mailbox/elasticsearch/v6/query/SearchQueryTest.java
@@ -0,0 +1,77 @@
+/****************************************************************
+ * 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.mailbox.elasticsearch.v6.query;
+
+import java.util.Date;
+
+import org.apache.james.mailbox.model.SearchQuery;
+import org.apache.james.mailbox.model.SearchQuery.DateResolution;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class SearchQueryTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void sentDateOnShouldThrowOnNullDate() {
+ expectedException.expect(NullPointerException.class);
+
+ SearchQuery.sentDateOn(null, DateResolution.Day);
+ }
+
+ @Test
+ public void sentDateOnShouldThrowOnNullResolution() {
+ expectedException.expect(NullPointerException.class);
+
+ SearchQuery.sentDateOn(new Date(), null);
+ }
+
+ @Test
+ public void sentDateAfterShouldThrowOnNullDate() {
+ expectedException.expect(NullPointerException.class);
+
+ SearchQuery.sentDateAfter(null, DateResolution.Day);
+ }
+
+ @Test
+ public void sentDateAfterShouldThrowOnNullResolution() {
+ expectedException.expect(NullPointerException.class);
+
+ SearchQuery.sentDateAfter(new Date(), null);
+ }
+
+ @Test
+ public void sentDateBeforeShouldThrowOnNullDate() {
+ expectedException.expect(NullPointerException.class);
+
+ SearchQuery.sentDateBefore(null, DateResolution.Day);
+ }
+
+ @Test
+ public void sentDateBeforeShouldThrowOnNullResolution() {
+ expectedException.expect(NullPointerException.class);
+
+ SearchQuery.sentDateOn(new Date(), null);
+ }
+
+}
diff --git a/mailbox/elasticsearch-v6/src/test/resources/eml/bodyMakeTikaToFail.eml b/mailbox/elasticsearch-v6/src/test/resources/eml/bodyMakeTikaToFail.eml
new file mode 100644
index 0000000..e4e7ede
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/resources/eml/bodyMakeTikaToFail.eml
@@ -0,0 +1,1272 @@
+Date: Mon, 30 Jan 2017 11:51:35 +0100
+From: Raphael OUAZANA <ra...@linagora.com>
+To: OUAZANA Raphael <ra...@linagora.com>
+Subject: subject should be parsed
+Message-ID: <25...@linagora.com>
+X-Sender: raph.ouazana@linagora.com
+User-Agent: Roundcube Webmail/1.1.4
+
+Return-Path: <ip...@obm.lng.org>
+Received: from lenny.obm.lng.org (localhost [127.0.0.1])
+ by lenny.obm.lng.org (Cyrus v2.3.14-Debian-2.3.14-2) with LMTPA;
+ Wed, 21 Mar 2012 14:51:59 +0100
+X-Sieve: CMU Sieve 2.3
+Received: from [192.168.2.48] (unknown [192.168.2.48])
+ by lenny.obm.lng.org (Postfix) with ESMTP id E495D32929
+ for <us...@obm.lng.org>; Wed, 21 Mar 2012 14:51:58 +0100 (CET)
+Message-ID: <4F...@obm.lng.org>
+Date: Wed, 21 Mar 2012 14:52:14 +0100
+ From: iphone <ip...@obm.lng.org>
+User-Agent: Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US;
+rv:1.9.2.28) Gecko/20120306 Lightning/1.0b2 Thunderbird/3.1.20
+MIME-Version: 1.0
+To: usera <us...@obm.lng.org>
+Subject: Fwd: Re: email with sign
+Content-Type: multipart/mixed;
+ boundary="------------080809000000030101030405"
+
+This is a multi-part message in MIME format.
+--------------080809000000030101030405
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+
+new email text part
+
+--------------080809000000030101030405
+Content-Type: message/rfc822;
+ name="Re: email with sign.eml"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="Re: email with sign.eml"
+
+Return-Path: <us...@obm.lng.org>
+Received: from lenny.obm.lng.org (localhost [127.0.0.1])
+ by lenny.obm.lng.org (Cyrus v2.3.14-Debian-2.3.14-2) with LMTPA;
+ Wed, 21 Mar 2012 14:19:29 +0100
+X-Sieve: CMU Sieve 2.3
+Received: from [192.168.2.48] (unknown [192.168.2.48])
+ by lenny.obm.lng.org (Postfix) with ESMTP id 47EC832D51
+ for <ip...@obm.lng.org>; Wed, 21 Mar 2012 14:19:29 +0100 (CET)
+Message-ID: <4F...@obm.lng.org>
+Date: Wed, 21 Mar 2012 14:19:44 +0100
+ From: usera <us...@obm.lng.org>
+User-Agent: Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US;
+rv:1.9.2.28) Gecko/20120306 Thunderbird/3.1.20
+MIME-Version: 1.0
+To: iphone <ip...@obm.lng.org>
+Subject: Re: email with sign
+References: <4F...@obm.lng.org>
+In-Reply-To: <4F...@obm.lng.org>
+Content-Type: multipart/alternative;
+ boundary="------------050702060806040107070701"
+
+This is a multi-part message in MIME format.
+--------------050702060806040107070701
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+
+On 03/21/2012 01:59 PM, iphone wrote:
+> email with sign text part
+> --
+new email text part
+
+--------------050702060806040107070701
+Content-Type: multipart/related;
+ boundary="------------090109030206070103090500"
+
+
+--------------090109030206070103090500
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+ <head>
+ <meta content="text/html; charset=ISO-8859-1"
+ http-equiv="Content-Type">
+ </head>
+ <body text="#000000" bgcolor="#ffffff">
+ On 03/21/2012 01:59 PM, iphone wrote:
+ <blockquote cite="mid:4F69D0C6.501@obm.lng.org" type="cite">
+ <meta http-equiv="content-type" content="text/html;
+ charset=ISO-8859-1">
+ email with sign text part<br>
+ <div class="moz-signature">-- <br>
+ <img src="cid:part1.05020706.03070506@obm.lng.org"
+border="0"></div>
+ </blockquote>
+ new email text part<br>
+ </body>
+
+</html>
+
+--------------090109030206070103090500
+Content-Type: image/jpeg
+Content-Transfer-Encoding: base64
+Content-ID: <pa...@obm.lng.org>
+
+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
+RvvR7cp9+nyYupWotquN7t17lQ1W/iOqfiKi4Sl3iAJbUmI2CNhIVz3Yykxd66HItw0joyzq
+lWSbK0tavBjTt3sSJ8Z5BaZQtzgKWNjmWEKSoKO3KgdwJoC46n1W1brNapdnipvUm9SG49qa
+bfDbchS21OhRcwdqA22tZUAThPIE4FQrfSXEtUqRa9awFWW7tPstNxoXGuKJXGbeW0WS20Fq
+3CM+MFtJy2Rg5TmTvGjus6XsdsttyFunWBbLttltxG9jbjbSmubKNqNim1uIKE7QAo7duBiM
+hdH85zVNu1VfNQNT7zGuCJTy2IHAZUy3Elx22G0FxRQEmY45uUpZJJHIEbQPub0raPtUORIv
+dzTH6uuaXjFiyZKGWo0l1hTjiksjh5UypPjAArCkoU4AFK+k9KmkWHZEa43EMyY7zyXkxosl
+9tlpEuRGDriwyA2nfGcClKwhBB8dSSlaud9IXRzrONGv9o0il+ajVMGdEnyVR44ZaTImzZKA
+SuQlbezrriVKS27vHclBxi4o6KNto1zA8PZ7V22XA39T/wA148u4yN+N/j7fCO3Hi54Wcjdh
+IFsh6005L1IrT0ec6qeHHGk5iPJZccbBLjaHikNLWkA7kJUVDarIGDjLqDVmn7BPhQLtcBHk
+zXWmmGw0tZUp15DLYO0EJ3OOJSCrA7z3JURUtNdFMGxdIb+qY5sRS5NlzgrwCz4RLskrK0qm
+klZbBcXtCUpVghJUUjB+ekLRGor/AKvdl2i5QrexJYta1SZUMyktOW6c5KQ3ww80r9Ip1GVA
+nxWlDkVJUAL/AGy5Qrm285CfDoYfcjujaUlDiFFKkkEAjmP3ggjIINbdVvRNpm2+dqefNbDJ
+u95VLaZ3BWxtDDMdJ5EjxwxxMeTfzwcirJQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAKU
+pQClKUApSlAKUpQClKUApSlAKUpQClKUBjkuLajuutsOPrQgqS02UhSyB9UbiBk93MgecioX
+w9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/+
++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/++hfmKnqU
+BA+Hrr6lX/30L8xTw9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/++hfmKnqUBA+Hrr6l
+X/30L8xTw9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/++hfmKnqUBA+Hrr6lX/30L8xT
+w9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/++hfmKnqUBA+Hrr6lX/30L8xTw9dfUq/+
++hfmKnjWCdNiQYy5U2S1GYQMrcdWEpSPaTQER4euvqVf/fQvzFPD119Sr/76F+YqMHSl0dmW
+iKNY2fir+qOspwr9h7qtcSVGlsIkRX232VjKVtqCkkewigIfw9dfUq/++hfmKeHrr6lX/wB9
+C/MVPClAQPh66+pV/wDfQvzFPD119Sr/AO+hfmKnqUBA+Hrr6lX/AN9C/MU8PXX1Kv8A76F+
+YqepQED4euvqVf8A30L8xTw9dfUq/wDvoX5ip6lAQPh66+pV/wDfQvzFPD119Sr/AO+hfmKn
+qUBA+Hrr6lX/AN9C/MU8PXX1Kv8A76F+YqepQED4euvqVf8A30L8xTw9dfUq/wDvoX5ip6lA
+QPh66+pV/wDfQvzFPD119Sr/AO+hfmKnqUBA+Hrr6lX/AN9C/MU8PXX1Kv8A76F+YqepQED4
+euvqVf8A30L8xTw9dfUq/wDvoX5ip6lAQPh66+pV/wDfQvzFPD119Sr/AO+hfmKnqUApSlAK
+UpQETrDUll0jpyVqHUM3qVsibOO/wlubN60oT4qAVHKlJHIeWpaud/SR03etXdC9/wBPaehd
+ducvq/AY4qG9+yS0tXjLISMJSo8z5K5sx0W6tYvYvbNi4dxHSq5dxITLaCxZlnK1A7/qq8rf
+1zyymoIs7tdNS2S2aks+nZ03hXS9cfwexwlq43BQFueMAUpwkg+MRnyZrclXGPGuMOA43LU9
+M38JTcR1xpOwZPEcSkob5d28p3HkMnlXlU9D/SD1O0suaU4tziQtSM3O6dfjnwi9LjOojOc3
+N3MqSjKgCPLgDNXhnoknLc6Job2nmWYMCz3CPqhSHWsofkW5tglXjZcUVJKdyd2AkcwAKCzv
+ta10nxLXbJVzuD6Y8OIyt+Q6rubbQkqUo+wAE15t05pfWOoujBdziQIF3nXC8QrdKU61Ge32
+yCgx1usiSFNb1OB1YKgeSzgGrppTROp//ZZuWgNR2jrV9Zt06JHakPtPIkOZcXGWhRUQACWw
+kr2lJRnAwDQWdW01e7dqOxRL5aHXXoExvix3HGHGS4jPJW1xKVYPeCRzBBGQQaka87dEHRFf
+7F2tdk2SJZbhJ09bY1kmJW0rq84W5TMp1PDJKVh1R3LwCrKiCQok1i69EXSFI0Fe7fa9M+DH
+HtOW63vQevsHwncWpjTrs3IXtHiJX4yylRzjFBZ6wqJumpLLbNR2bT06bwrneuP4PY4S1cbg
+oC3PGAKU4SQfGIz5M1wDpB6IdSPq6T1ab06AZMi0StK8OW0ja+jZ1t1vcscNZwcqVtKvJnNW
+/wCkXonU+rb/AKdlaftnXWYdn1BGfVx229jkmAWmE4WoE7l8sjkO84HOgs7NVdTrXTrmo5+n
+mJMuTc7dIjRprEe3yHerrkIK2itSEFIQUpJKydqf6RTkV5/u3RDr5u0PQtO23waub0dwrfNU
+ia2kPXRuQ0p1tWF81FlK0Bf1MHG7FfSOivWCukBd3t2iRZrQrVunLkzFE2MeBGiMPpkHCXD9
+VS0+KOZz4uQKWLPUFK4B0G6M6RdP9Ka9Q6msjEOJcrXIizk28w2o7clMkuNOcNnaVJLWEhag
+tzco7sJxjv8AUkoUpSgFKUoBSlKA/Fd1eYPpgz4svWunNP3Vcx21iMt9URleEPO7sJ3jyivT
+6yAkknAHMmvHfTtf7Zq/pXhTbU+JEOKOrBwDkVJVhWPZmol0LR6lTsEnTWn71K7TRoEOUn6r
+Za38MEdyQAQOVXvo26aLNo2FeE2i0SZ9uDzfCaL/AAQFKB3KSCFYHLurjHS0kK1W+6eaiBk1
+paaSFWa5hWefC2Y/Wyf/AENc0k4pyXU0fPB696Men9jW2uLfphGlnIKpvFw+ZwcCNjS3Pq7B
+nOzHf5a7bXh36L3/AD66d/7V/dXa9xVfTzlONszmkmK0NRXq16eskq9Xqa1Ct8RHEffcztQM
+48nMkkgADmSQBW/VA+kHoubr7onu+nLYpsT3Ah6KF7QlbjawoJ3KB25wRkY7+ZwSDuVLZbL7
+brhZl3dtUqNDbSpS1zobsRSUpGSopeSlQTjnnGK09Faz01rOI/K03c0zm46kpdy0tpSdyQtB
+2rSk7VJIIVjBByCahYthl3Loan6TRbZVlfkWmRbmm5iYqFIK2lICsRDwgnKu5IHd3CqB0J6W
+1/oZ2VcZekg+7dnLJan43hNlBiRokMMPTcgqSsbhkNg7yD5Kggv8bpc6OZDVyeb1PHDVsjql
+SVrZdQnghzhFxBUkB1PE8TKNw3cu+rJpbUVm1PavCdjmdajB1bKiW1trbcQcKQtCwFIUD3hQ
+BriWqNCav1hqHWd1vmiC1EuWn2rf1AXxpTkmQ1KC21RnylXBb2JSopUhILnek81VfugTSl30
+rpy9m9pebl3i/S7rwn3kOvNodKQkOKb8QrIRuVs8XKjigLRqlTrki121D7jDc2Spt1bZwral
+ta8A+TOzGfbVanXHo8g6uY0nKvE1u8vlARHM+YRuWFFCVLCtiVKCFFKSQTg4BqxaoVsvOnle
+aY7/AHZ2uLat6PNUXHpxVfY0dCrRLvtmvDkzjoAYEFh5tbRQTvJWVpxgEczkihJ2rsvZ/wDp
+H4nJ+ZTsvZ/+kficn5lbnWj56daPnoSafZez/wDSPxOT8ysMzT1njtJc23FWXEIx4Ukj6ywn
+P1/JnNSXWj5617hIK2mk575DP9qmgIzUEDS1htTtzuki5Mxm8DxbhLWtaicBKEJWVLUTyCUg
+knuFZ7dZNO3K3sz7fKmvxn0Bxl5q6SFJUk9xHj4rBqy5m3NRbsqyKuqIbqluFhviSY6SgpLj
+SACVnBwUpIVtJxuPinPpiS4bXx3LO3aFSHXHzGSUlQ3qJ3ObRgOKzlQGcEnme+hBu6PkPv2p
+xEh1Ty40p+MHFfWWG3FJBPtIFV/pJu0GFcozF7uzlrsjUGROmvtuqbOG1NpA3J8b+n3J5kgA
+d+DNaFO62TVee5yz/wCMquefSO0rctaw39NWco6/LsshTCVr2ham5MVzbk8hu2YyeXPnTsSW
+TStn0Zqi1eFLHdr3Ki8VbKibnLbWhxBwpC0LUFJUD3hQBqW7CWX0q9/F5P46hugzTd509ZtR
+Sb5E6lKvmo5t4TELqHFRm3lJ2oUpBKSrCcnaSOffXQaEWVfsJZfSr38Xk/jp2EsvpV7+Lyfx
+1aKVIIHR5dZN2tbkh2Q3bpwYZcdVuWUKZadAUfLjiEZ8wGedT1QWmf8ATeqP96N/3ONU7QCl
+KUApSlAKxTYsabDfhTY7MmNIbU08y6gLQ4hQwpKknkQQSCD31lpQGGDFiwYbMOFGZixmUBDT
+LKAhDaRyCUpHIAeYVmqFZ1VYnbgYKJi+MJSomVR3Eo4ye9sLKdpV7M86mqEtNdRSlacu5wYt
+yhW59/ZKnb+ro2KO/YMq5gYGAfLihBuUpSgFKVpyrnBi3KHbX39kqbv6u3sUd+wblcwMDA8+
+KA3KUpQClKUApSlAKUpQHy82l1lbSxlK0lJ/Ya8nap6BNYaZv8mfpQM3q0uPqfRFdc2PNZOS
+AfLXrM8hWCXJRFiPSneTbLanF4HkAyf/ACqGrJTPC2r+jDpGvN8UtvSE1pS/1lpIH7wa6T0N
+/R7urcmNM1gpDMRpwOmGjvcIORuPmrsdi6U4s/8ATTrBc7bBUrDctxIW2R5CrbzT++r/ABpD
+UlhD8dxDrSxlK0nIIqqUX0LzUo9UVfT/AEa6GsF4YvFn05Ehz2N3CfQVbk7klJ7zjmFEfvq2
+0pVkkuhmKUpUgUpSgFKUoCM1DalXNhksv9XkxnOKw4U7gFYIII8oIJH76iPBGqvtOz/c3PmV
+ZZUhiKwp+S8hlpAypazgContdpf7ft3v01BJoeCNVfadn+5ufMp4I1V9p2f7m58yt/tdpf7f
+t3v007XaX+37d79NOByaHgjVX2nZ/ubnzK/FWbVCtublZztUFD+Ru94II/1nnAqQ7XaX+37d
+79NO12l/t+3e/TTgcmh4I1V9p2f7m58yngfVPludox7Ijmf7St/tdpf7ft3v00GrtMEgC/24
+k/8Az004HJuWC2ptVuTFDqnVlanHFn+ktRyo/vJrWvlmdmXCJdIExMO4RUrQ2txritqQvG5K
+k5SSOQPIg5AqWacQ62lxtaVoUMhQOQRWvdLlAtcYybjMYiMjvW6sJH8TUkEZ1XWH23YvhDv5
+mnVdYfbdi+EO/maw9u9Ges9p+8p/xp270Z6z2n7yn/GoJM3VdYfbdi+EO/madV1h9t2L4Q7+
+ZrD270Z6z2n7yn/GnbvRnrPafvKf8aAkrDbFW1mQXpJlSpTxfkvFASFr2hIwkdwCUpSBz5Ac
+yedSNYYcqPMjIkxHkPsuDKFoOQoew1mqSBSlKAUpSgFKUoDnUDS13buy5kpl1yONTOzkxeK0
+E8NQ8R8H62QceKT3D6ue/HYtK3tmfH62y6yptqYi4TWpCd8/i54e3nnKcggqAxiuk0qbNPNZ
+zSJYNRwbRdYVss8PYqKlEdcyNGD7qwsZCtilJWNuTlfMqxWjB0pfo9ygpl2NydAiS5r3DL7A
+SWnmkBCAkKAB3JOQAE8+XKrm/eLzO1Hc7RZUwGjbWmlOrloWrircBUEjaRtGB34P7K+7lq+3
+2yU9FmMyCqIlnrzrKQpqMXOSdxJBIPsB5VNsupz9CkXLSWsXNPWy3oYZceiwcpeCmuK2+Htw
+RxFeMAEchsIGe84qYYt0x7VN1ZisN7YrLs2Oy9tUESZLYAQvBKeRS4TgkePUjbNaFT02PMiL
+flJu8mDDjw0eO4hoBRUdygMgHmcjyYFfkfWenY6Fu222yVpfhKurqozCE7kBZStSsqGVgpOc
++bvNOSW5+hh6M7Pe7PNuJukQssy2mHEBBZDaHUpIcGxvATknIwOYHM574K3aR1G3LtZ6l1eX
+HFwTJuPHQeIt1Cg05yO7vIHdnlU7qTX7MWyzH7ZGcExuIzNj9aa/RvMrdSjcNqs/0vLg1uy9
+ZRhLahobkRpKbnHhPtPRws/pQVJIw4NoIH1uZH6ppyRc+tdTT6ONPTrRLU9Liy46jEQ08XJD
+JbdcB5qCG05Pl8datxzgg99XeoKzaot10vcizsBaJbDZdUCttaSkK2nm2tQBBxyODzHKp2oZ
+nNtu2KUpUFBSlKAUpSgPxXdVO1TdXLnMc05a3cAD+XyAeTaD/QB/WPl8wq3vpUplaUK2qKSA
+fMa83wJWq9MR5VpcbcfkSH3Q8XAcqOeSwr299Z5JNI6NPjUm36Fh6QtTQrPbfBUAJS2hO3AH
+1v21B9EPSLNtFwVa5TJfhSiertle3Y534BwcA+bz1UUWy53GY49dNwWFHcFdyazz4MxpERNn
+tb8p0yG1B8YS22AoEqyTz/dWUU1ydc9rW3qeibBrHwrdmIHg7g8Xd4/H3Ywknu2jzVbK5RoL
+/ldC/wD7P7NVdXraDbXJx54KEqQqK1Ze2dPWGRdn2XHksgAIQPrEkAAnyDJ7zUrUZqmyxtQW
+OTapR2oeHirCQShQOQoVcxjV8hm6OI0+7d7jGEYNMrfW2hSyQhIz/TQhQPI8iBWjozU6dRCS
+hUMxHo6WXCji7wUOthaDnA54PMY5ec1vR7VnTzlnmKjKacZUweqx+AgIUMYCdysd58v7qhdP
+6Rl2VIXEvhD63o3WF9VTh2Oy3sS1gk7SR3rBz5hU8F1tpmCdrp23quqbhY3Gl2+Ol8pbkpcP
+jOBCELKRhCjkKxlXinPsqd0re03yFIe4AYeiynIr7Yc3pC0HntVgZGCDnAqKVpKY9NnTpd96
+xKkQ+ptrVCbwlvib/HQcpWfJ3Dl7edSulLG1Ybe9GQ4lxb8hch1SGw2jeryJSCdqQAABk91H
+QlsrjqYNToQ7drAw6kLacmrK0KGQraw4pOR7FJB/dWjP1kzE1SLKYalNpkMRnX+Jja68lSkA
+JxzGE8zkYz3GtvVitl20+s8gJjnP9sd0D/zqDnabYlamF4MspQX2JDjOzO5xlKkoIVnkMK5j
+B7qhV3Iht/8AYvO6m6orrHtp1j21FlCV3Vr3F9xqOlTatqi80nOPIpxIP/AmtLrHtrDMe3oa
+TnvkM/2iaWSZdT3WdbYSBa7U7dLg+rhxmAott7sE7nXcENoAHNWCfIAokA57JcTdbWiQ/Akw
+nCVIejSUYU2sHCk+ZQz3KTlKhzBINQmpVXxcJD+n5UdE1hW9LEnkxJGCOGtQBUgc8hSeYIGQ
+oZSdixm4RrchF0npmzFErdcQ2EIBJztQkcwkdwyScDmSaWQbGhgEWqUygYbZuEpptI7koS8o
+JSPYAMVAdIV0jWnVdvnzmOsx4ltkvpZOMF3iMoSefcfHIz5iandBnNqlq/WuUsj2gvKqE11a
+Gb7rKBapDimm5NplICwM7VB1hSf2805x7KlFlV8k/pa4Rb3DkuKtzMZ+LKciPtDCwlxB54Vg
+ZHMeQVL9Vjejs/1BUdpeyiyw5DapHWX5UpyXId2bApxZ54Tk4HIeU1LUZEqvgxdVjejs/wBQ
+U6rG9HZ/qCstKEEBpJtuPcdRxWEJbYZuY4baRhKN0ZhasDyZUtR/aTU/UFpjnetTqHMG6Iwf
+2RI4P/EEVO0ApStDUVzbs1kl3NxHETHb3BG7G49wGfJkkCobSVslJt0jfpVYi6vYdft7bsYM
+JfiOSZSnHcdVCDtIPLn4wKfJ3furfRqeyLb3iaR+mQxtUysL3rGUDaU55juOMVRZYPuWeOS7
+ExSokaksnXjC68OOHHWyC2vAU2ncsbsY5Dn31+M6msjrkVCJvOXtDOWlgEqztBJHilWDgHGf
+JU+ZD1I2S9CXpUZLv1riTOqSn3GHdq1jiMOJSoIG5RSopwrA58ia+od7tkuRGjx5JW7Jj9Za
+Tw1Dc1nG7mOX76nfG6sbZdaNa46djyri/cGJ0+3yJDaWpCojiU8VKfq5yk4I7spwfbWtctHW
+qfLefedmBEkMiWyl0FEnhfU35BVkewjPlrUsutmpUeJJuMEQY8xh95lxLxd5M54gUNoIIAJ5
+ZyK29Qawtttta5cc9ceERuY2z4ze9lbiUBW4pIHNXd3+yqrPCt1mmzInR89i7al5chmXOYlK
+nPTkPoWje2t1IStKcpI2kAciCfbX4xoizR0rRHVKaQq1Lte0LGA0tRUpXMfXyonPd7KlmL1b
+5E5yHHceedacLThbjuKQhY70lYTtBHtNR8fUCUXCa3PlRGGIzSnilbLrTgRvwk4WAFDvyU95
+IA7smzyxXcj77NS46Cs86Ohl6TPSlNsato2rQDwm3EuJP1frZSMnux5KzK0XbFyxLdlTnZPX
+2ZynVrTuUtoEISfFxtAJ9vtqZtd0g3NLhhPFZaUEuJUhSFIJGRlKgCMj2VEK1fbl3qBAikPs
+yi+HJJJQhrhJ3EjKcLHIjIOBijyxSuwvMfB+ae0ZbLFcGZsGTN3ssrYCXFpUktqWV7T4ucBR
+yCDnzk1ZKjrTe7XdHFNwZXEWlAc2qbUglB7lDcBlPtHKpGpUlLlFJbr+8KUpUlRSlKAUpSgB
+7qgdRadZuiVPNFLMvHJShlJ/bU8e6ou/3uFZYwdlrJWs4baQMrcPmAqHSXJaLafBVLb0dtPT
+UzL862+lHdEZJ4aiPKokAq/ZyH7atY07Y8Y8FxgB/sVS9R6i1w1F8JW5i2ssI8bqziFOLWPM
+VAgA/sroNvf61BYkkbeK2leM92RnFZY8kJ2omk1kjyzXiWa1RJCZEaAw06jO1aU4IyMf+Rrf
+pStqMm2+opSlCBSlKAUpSgNW6W+Jc4pjTG96MhQwSCkjuII5g+2ofsfbfTbz8Se/FUxc58W2
+xTIlubEZCRgZKie4ADmSfNUb2mh/Z97+FP8A4KAw9j7b6befiT34qdj7b6befiT34qzdpof2
+fe/hT/4Kdpof2fe/hT/4Kjgnkw9j7b6befiT34qdj7b6befiT34qzdpof2fe/hT/AOCnaaH9
+n3v4U/8AgpwOTD2Ptvpt5+JPfip2Ptnpl4PsNxeI/wDqrN2mh/Z97+FP/godTwgMqgXpIHeT
+a3wB/wB2nA5JWBEjwIjcWK0lplsYSkVgu9pgXVDaZrKlKaVubcbcU242e7KVpIUnkT3GtiDK
+jzYjcqK6l1lwbkqSeRFal9vdvsrTa5ziwXVbW22m1OOLOM4SlIJPLzVJBp9lLZ6Xffjkz5tO
+yls9LvvxyZ82tTt1Z/Qr78Hkfgp26s/oV9+DyPwVBJt9lLZ6Xffjkz5tOyls9LvvxyZ82tTt
+1Z/Qr78Hkfgp26s/oV9+DyPwUBP2yBEtsRMSEyGmgSrGSSSTkkk8ySeZJ5mtmtS0XKHdoKJs
+F3iMqJGSMEEHBBB5gg+Q1t1JAqL1PaPDlvRBVI4LXHbcdGzdxEpOdneMZOOfP9lSlKiUVJUy
+U2naKjO0PHlyb06ue4kXJAQhIb/mPHDisHPMFYzjl3mvt3R7js1NyXc0mf1xuU4vq/6NXDTt
+QkI3ZGATzye+rXSs/Ix+hfzZ+pS3dCB2NFacuyytuS+9IcDOC+HsBafreL4oxnnUozpeI1qd
+284iuIcS1tZcipUWlNp2pLayfF8ncPIOdWClFhguweWb7lHb6P0pSvddErcWw+yp4xv0iuIc
+7irdzUOY58iDjA76mbBp5dtuirhImpkudTbiNpSxww2hHm8Y95wan6UjghF2kHlnJU2U62aG
+DEFiHNufWWo0WRHj7GOGUcbO9Ryo5ODgd1YZmg3pcUsvXpJItrdvSoRcYSh1KwrG/mfFx+/P
+sq70qPs+Oqonzp3dlckaY4moG7q1JYibHw6sRmFIce/2Vq37VA+Xxcmodro5aSh9C7qVh+Ot
+h1XVwFrBcDiVE7uagoAEnOQMcqtOqXHGdMXV5lxbbiITykLQcKSQgkEEdxrnka/3iyxXZinl
+urFmjSkNPyXJDbiluoSpxRUQUKwr6o5e01jlWKD+8jTG8klwy/WGzrt82fPkyxKlzlNl1aWu
+GkBCdqQE5Pk9tQkbQobEFhy6FyHCTKbbaDG1RQ+kggq3d43Hnj91fmr9VztP8No9XkykMiQ+
+2mOQjhlzZyUXMg8wPqqyRnAHdtm/XONqEQ7oyiDEelcCIvqynA+D9X9IF4So+Yp5VZvE3ta6
+fXn9yEsiVp9TY07ptVrnomPzutOMwkQWdrXDCWknIzzOVd3Pl+yrBXPej67zblf4JeedDTll
+ccLPHcWjeJSk7vHUok4GMknzd3KuhVpgcXC4oplUlL7wpSlbGQpSlAKUpQGOU6hiM4+4dqG0
+lSj5gOZrzdf9Zybtqd+4rzw0rKGR5EIB5AV6G1HCeuNgnwIzoaekR1ttrPclRBAJrzijQ2s7
+XZC7qOyttcFRRxorweCkjuWQOYBrk1cZSjS6HTpnGMrfU6Vo3UUe4xRHfUDkYINbd2uN408W
+o0SYUQ3SSyShKgCf6OSDXJbREuSX2+ouY3HO4HkB56tcqLdL1EDEtTzim1pI258VI78V50XK
+PQ7XFN2+heNJaivE7UEaLKmcRle/cnhoGcIJHMDPeKv9cx0RHko1TEccjuoSN+SUEAeIqunV
+6OjlKUHufc4dSkpKhUNrO9KsGnpFyQyl1xG1KEqVgFROBnnkj2Dn/wCdTNat3t8a621+3zEl
+TD6dqgDg+cEe0HBrpmm4tR6mMWk1fQ0o9zeb0u7eJSmXy3HXIwykJSQlJOOS1g93eFEVo6I1
+BLvRlszWmEPMNx3gWQQkpebCwMEnmO7Pl9lTTUEeDlwZUh6ahxBQtTwSFFJGCPFCR3eyoq26
+UhW9tCYs64tqS+06paXgFOJaTtQ0rAAKMcseXz1m1O010Lpwp31Im56ovlseuzMmHBedhxEy
+EJZ3jBU5tA8bBWAk7ipIABBFTejry5erfJddDJcjS3IylsghDm3BCkgkkAgjymvhrS0VD8mQ
+bjdFyHmOrpeVJ/SMt79+EKxnv8+fN3VI2a2R7XFWwwpxwuOqedccIK3FqOSo4AH8AKiEcilb
+fBM5QceFyR+o8eHNOg4x1x0kH2R3SP8AjUXcdWSY2q/BqGGDEblRorhOd5U8lSgoHOABtHLB
+7++t/WCwzcLFIWdrbcxYUo9w3MuJGf2qUB++o+TZ7fIvSbqsu8ULQ4UBQ2KWgEIURjOQFHy1
+bIputpWDir3Ft4g/WH8acQfrD+NRHWPbTrHtrSylEvxB+sP41q3N5SYyChZSS+yMpOORcSCP
+4VpdY9tYpTpWlpI5/p2ifYA4kk0sUZtTyr2iEhjT0eMuc+rYmRKOWIowSXFpCgpY5YCU4JJG
+SkZUM9imT5NtbXdYSYU1JKHW0OhxBIONyFDmUK7xkA4PMA8qgtSxZtxhINsuztsuDCuJHfCS
+43uwRhxvIDiCDzBIPlBSQCM9kYVbLaiM7PkzXQSt2RIXlTiycqPmSMnklIAA5AAClkUbmiP9
+HTgO4XOWAPMOMqq/0g3VVl1bBuSWkvKYtMpSEK7txdYSCf61TugTvs0h0c0Oz5LiFDuUlTqi
+CPYQQajNXwIlw1zaolw5RZVtlR927blZW0pIB/Wwgkf9Wonbi66lo1u56E3pO6v3WLM60hpM
+iHNdiOFoEJUUEeMASSMgjlk1M1o2S1x7TEWxHU64XHVPOuOkFbi1HJUcAD+AFb1IJqKvqJVf
+ApSlWKkFpjletTpHIC6IwP2xI5P/ABJNTtQGk1oeuWpJLKgtl26DhrScpVtjMIVg+XCkKH7Q
+an6AUpWkzdID18lWRt/dPiRmZT7WxXiNPKdS2rOMHKmHRgHI288ZGQN2lKUApSlAKUrSv10g
+WOxz73dH+rwLfGclSndilcNptJUtWEgk4SCcAE+agN2lQ2mNS27UXWPB8a9M9X27/CNmlwM7
+s429YaRv+qc7c45ZxkZmaA+Xm23mVsvNocbWkpWhYylQPIgg94rSbslmbjux27TAQy7jiNpj
+ICV4ORkYwcGt+lQ0n1JTaNaVAgS3A5KhRn1hJQFONJUdp7xzHd7Kxs2i0syEyWbXCbfSMJcQ
+wkKAxjkQM1u0ptXoLZqxLbboi0riQIsdaGy2lTTKUlKCrcUjA7sknHn51tUpRJLoQ3YpSlSB
+SlKAUpSgFfihuSQQCD3g1+0oCq3DQ9sdmLlwFGA65/OJQnKFe3b5D7RU1Z7REtbW1hG5ZGFO
+K+sakKVRY4J2lyXeSTVNilKVcoKUpQClKUApSlAYpcaPLjqjymUPNLGFIWMg1D9jtLfYMD3I
+qdpQEF2O0t9gQPcinY7S32BA9yKnaUBBdjtLfYED3Ip2O0t9gQPcip2lAQXY7S32BA9yKDR+
+lwQRYYAI7jwhU7SgPhlptlpLTSEoQkYSkDkKxXCDCuEZUafEjy2Fd7bzYWk/uPKtilAQXY3S
+HqtY/uDX4adjdIeq1j+4NfhqdpQEF2N0h6rWP7g1+GnY3SHqtY/uDX4anaUBjjMMRWER4zLb
+LKBhDbaQlKR5gB3VkpSgFc/uruoLH0p3a9w9GXq/QLhZLfFQ7bpEJPDdYfmqWlQkSGj9WQ2Q
+QCO/zVfJLyI8d19wOFDaCtQbbUtRAGeSUglR9gBJ8lQva21eiX/4DN+VQHLWNFapjdNUTWCN
+PrSlq9v9cehtW2O1JgvNPNtq3JAkuqQVtLcDqwMoUUIWduIPpJNsPTG7bX7bDuGoZeqLFKt0
+tM1kyYkJtyIXWeDv42wFt504Rw8L3FQUkCu3drbV6Jf/AIDN+VTtbavRL/8AAZvyqA4Cjou1
+ib7Efc0opLE+RGZvjUZNsiRSpu5wpKpCEMbXFshpiQlJdU4944G1OVEzUroovCXLezE082iK
+7cZSbihEhtIVDRqKE/DQRv5pRBbf2JH1E5RgKVtPZO1tq9Ev/wABm/Kp2ttXol/+AzflUBxm
+6dFWpW7Y/HsdsVb1SWruzK6vJZSp6MLzGdhRxv3IwYSH220rSUICylQAUQbHadD3eJ0Aa20t
+BtVzYl3WHcG7db58iCHEl2Nw0ICYqER2UqWCdiSQCoqKvGIHQ+1tq9Ev/wABm/Kp2ttXol/+
+AzflUBzG/aSdvGlX4jWgNdHhzo76ot71DHuRfCUvJy21JlyGVhO/xm1loHclQUVNpxT39G3i
+bPutgVoVp2+I0pAat7jcpoJsjy5t04Enx3VbCkbVEMqWUEcNGUGu/drbV6Jf/gM35VO1tq9E
+v/wGb8qgOVag6MLi9omQ2xZnRNmaunXG8Mw+prk3GCqVNVHR/KQphYAfZcDbvIYV9VZqKu3R
+rfUWmwpj6NuF1nRYBZYVcJ8F4RFdZdcQhak8FUYpStIDsMkpACdqw2gntXa21eiX/wCAzflU
+7W2r0S//AAGb8qgOS3Po/wBSr1nqKRYtOLtkq4t3MIvj8lglKn2XQytt5pSZCxxFI/QvNqQ2
+M7F/o0ZuXQ/p2RZbrepbOkex9plR4bTFp4zK8yG+NxpGGVqQN4W0nOdyuFlQBNWjtbavRL/8
+Bm/Kp2ttXol/+AzflUByCLZmZmt7pd9T6ZlOR7fJvBud5Q4xhLazIQ0y8Fuh0NCEphQCG1Ba
+ltEkcKuwdHfhf/J/pzw/xPDHgqL1/ifW6xwk8TPt3Zr87W2r0S//AAGb8qna21eiX/4DN+VQ
+E9SoHtbavRL/APAZvyqdrbV6Jf8A4DN+VQE9SoHtbavRL/8AAZvyqdrbV6Jf/gM35VAT1Kge
+1tq9Ev8A8Bm/Kp2ttXol/wDgM35VAT1Kge1tq9Ev/wABm/Kp2ttXol/+AzflUBPUqB7W2r0S
+/wDwGb8qna21eiX/AOAzflUBPUqB7W2r0S//AAGb8qna21eiX/4DN+VQE9SoHtbavRL/APAZ
+vyqdrbV6Jf8A4DN+VQE9SoHtbavRL/8AAZvyqdrbV6Jf/gM35VAT1Kge1tq9Ev8A8Bm/Kp2t
+tXol/wDgM35VAT1Kge1tq9Ev/wABm/Kp2ttXol/+AzflUBPUqB7W2r0S/wDwGb8qna21eiX/
+AOAzflUBPUqB7W2r0S//AAGb8qna21eiX/4DN+VQE9SoHtbavRL/APAZvyqdrbV6Jf8A4DN+
+VQE9SoHtbavRL/8AAZvyqdrbV6Jf/gM35VAT1Kge1tq9Ev8A8Bm/Kp2ttXol/wDgM35VAT1K
+ge1tq9Ev/wABm/Kp2ttXol/+AzflUBPUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUp
+SgFKUoBSlKAUpVA6d+ki39GehJN7klLkxwFqExuALrpHL9w7z7BUpWG6L+CCSAQSO+leHOgn
+6SNwtWq5bmsnpFxiXBQ4hSrx45yeaE/0hz7u/l5a9pabvto1HZ2LvY7gxOhPjKHWlZHtB8xH
+lB5iplFxKqVkjSlKqWFKUoBSlKAUpSgFKitQTpUXqcWClsyprxaaU59VGElZUcd+EpPLy91a
+/VNUfb9u+Fq+dQE7SoLqmqPt+3fC1fOp1TVH2/bvhavnUBO0qC6pqj7ft3wtXzq+HmdSMoC3
+NQ25KSpKQfBau9RAH+t85FAWClQRianAydQW0Af9Fq+dQxNUAZF9tqj5jbFDP7+LQE7So3Tl
+wcuVt4z7aW32nXGHkpOUhaFFKsHzZFaGpbnc0XiDY7OY7UyU04+XpCCtDbaCkE7QRk5Unlke
+XzcwLDSqv1HXnrFZPha/m06jrz1isnwtfzaAtFKq/UdeesVk+Fr+bTqOvPWKyfC1/NoC0UqI
+0xPmS2Zka4paE2BJ6u+prOxZ2IcSpOeYylxPLyHI599S9AKUpQClKUApSlAKUpQHGfptf+7F
+q7/sX99YrkLyZmlNfrgot9gsq5HShp2MrT0WM1LhwGVxXD1iMXWkhCnTn9IhDa0qbUARXsSl
+AeeOjDXmrJ/SKrQ+qdXu3O6Tmpobl6ak26VAh7ASlS0BgvMKSCkJ4ylhSxggjIFN+ihrnX2q
+dZ2XTF06QrpLYRp+Rd7pGfajOvtSkXRbXV1LU2XEJU1wyUqJUEr8UoGzb65pQHmnoV6QekzW
+Os2bbf8AU9tt7kxu4t3CzMt7p1qU2pSWnEI6nsZKTtx1h1xLgVkDI2VzjSGu+kFnT3RbeT0h
+aiuM6W9qBM+3SH2S2qewhaosJz9HvUXiptIbcUpQC08LhkJI9u0oDgn0dekHVuqtYIt1wvva
+O2OaXjXKfJ6o014Mua3NrkHLSUjknJ2rysbeZrvdKUApSlAKUpQClKUAr+f300dVytXdJT9u
+jOKXb7LmO2lJ5Fz+mr+OB+6v6ALBKFAHBI5V5Eu30XNZy1XW6r1LbX50ma9IbjrbUElKllQ8
+fyHnWmNpPkpO+x4tdW8w9hWQR3V1foL6X790fXhmfHluvRVPDrcIK5PtJGVZzy3YGArvqU1t
+0OavssgRr9piWxuISiQyOI0onuwtPIfsOKqGoLZZdFNyWG5TU67usFhLQIWmPuGFLUe7dgkA
+eTOa0lwrvgpf6nszog+lLZOkbpFtejIek7jAfuPG2yHZKFJRw2VunIAychBH769DV/NL6FP/
+ALzekv8Atv8Acn6/pbXMjVOxXJvpdx9RSPo/alRplclMtLTa3hHXhao4cSXQMJJI2ZyAU8s8
+8ZSrrNKkk5Vp9qK39HC6NaBetD04WWWI67E8y62qbwVbcKYaZQXN2zubQc45Vyj6JOqLTpdm
+5m5TnIdiu8jT1qteGXFtu3h23p622NqTtWXU+Oo4AUDuINerKUB4r1FA0pO1T0iM6LlIb08r
+TkV2XJkR5y2Y7qLqlUhE5tSXH3ZCtjhSogYawnG3x69AfRkuka7aGujsPTen7LGYvkuMy7Yo
+IiwrmhspSmY2gE8lgAZ3K+p3+bqlKAgdSq23vTqj5Jjv92drzH0huXs/S1ZW11nwh2hsBtWM
+7vBQjSev7P8A5e/G/wAmdufJXp7VkOY8IM2E1x3YT5dLQIBWlSFIIGfLhRP7qivCN09Wbx/B
+n5lQSWXrKfNTrKfNVa8I3T1ZvH8GfmU8I3T1ZvH8GfmUsUWXrKfNWrc3wuO2keWQz/apqE8I
+3T1ZvH8GfmV8OTrovZnTN4G1xC/qs89qgrH857KCjLrrwDOiQ7fqOS43bZUjhOMKO2PKJSdr
+TysckE/0SQFnCTuztOzoqVbuz7SbVcJU+Aha0R3pCiolAUQAlZAK0DuSs7twAO5XeYy4uSLl
+BfgXDR1ylxH0Ft5h9phbbiT3hSS5gj2Gszc65IQltvS92QhICUgJZCUjyDk5yFLFEpoY7rdO
+Pnukw/8AjKrkv0wReToq7iwdY692fkfzGd/C61F42Mc8cLiZ9ma7FpGDIgWgolpSl5592QtA
+OdpcWVYz7M1qaghT2tS26/woipwjR3o7sdtaUuFLhSdydxCSQUDkSORNSDn/ANE0LGi9TdWz
+4AOrbkdPbf5rwfxE8PheTh7t+Mcq7FUF4dufqbffew/n08O3P1NvvvYfz6EE7SoLw7c/U2++
+9h/Pp4dufqdffew/n0A0z/pvVH+9G/7nGqdqH0xEmNG5TpzIjv3GX1gsBYUWkhptpIJHInDY
+JxyySMnvqYoBSlKAUpSgFKUoBSlKAUqv651ppnRFuYuGp7mIEZ93gtK4LjpUvaVY2oSo9wPP
+GKnmnEOtIdbUFoWkKSoHkQe41ZwkkpNcMlxaV1wfVKUqpApStWTPYj3CJBcblF2Xv4am4ri2
+07Bk73EpKG+XdvI3HkMmpSvoKs2qUpUAUpSgFKUoBSlKAVilyY8RhT8p9phpIypbigkD95rL
+Xjn6bEvUczXUa2Q7is2xiMgrhpc2gqOTuI7jWuHE8stqIk6PT9u17oi7TF2+Fqe0yX08lNJk
+Jz//ALVC/wDZg6C1+N2I3Z558LTTn/xq8YRZrMFKEXPiNA96ggqUPaMV7S+iRqFy/dFu12Q8
++qFLcZQp0kq2cinv/aa6dTpI4o7k7+BCdktonoG6KNF6nial01pXqN1h7+A/4QlObN6FIV4q
+3Ck5SpQ5jy+eul0pXCWFKVo3+722w2eTeLxMbhwIqN7zzncgZx5OZOSAAOZJqUm3SJSt0jep
+Udbr1AnWld1bMmPEQFKWuZEdilKUjJUUupSoDHPOMVqaQ1dp7VsV+Tp+4pmNsKSl39EttSdy
+QpJ2rAOFJIIVjBHME1OyVN10J2y5ddCcpVKa6VdAutTnGr+lxMFsPPbIrx3Nl3g72wEfpU8T
+xMo3DPKrNY7tFvMRUqG1PabS4WyJkB+IvIAPJDyEqI5jmBjvGeRq0sU4K5RaJljlHqjfpUHq
+lTrki121D7jDc2Spt1bZwralta8A+TOzGfbVemy9Aw9VM6Xk3aa3dnigIY8ITCNywooSVhWx
+KlBCiEkgnBwDVIxlL/FWQot9C+0qB7L2f/pH4nJ+ZTsvZ/8ApH4nJ+ZUFSepUD2Xs/8A0j8T
+k/MrDM09Z47SXNtxVlxCMeFJI+ssJz9fyZzQFkpVS1BA0tYbU7c7pIuTMZvA8W4S1rWonASh
+CVlS1E8glIJJ7hWe3WTTtyt7M+3ypr8Z9AcZeaukhSVJPcR4+KAs1KhtHyH37U4iQ6p5caU/
+GDivrLDbikgn2kCoDpFucOLdYsW83R22WVuDImzHm3VNnDam0gbk+Njx/wCjzJAHlwSTfCJS
+t0i8Uqh6ZsejtSWzwlZbre5MbiLaUTdJbakLScKQpC1BSVA94IBqU7CWX0q9/F5P46lpxdNB
+qnTLRSqv2EsvpV7+Lyfx07CWX0q9/F5P46ggtFKgdHl1k3a1uSHZDdunBhlx1W5ZQplp0BR8
+uOIRnzAZ51PUApSlAKUpQClKUApSlAcm6Z9Eal1zq+1RYaIkezQ7VO3ypKA8hUh9HBCOGHEL
+3BBKgv6qSfKeVRnR5ofUruvtM3zWOn20tW3RzMBann2XQiczKJQcJUcq2ALCsYBI5hQ5dspX
+WtZkWPy1VdDoWpmobEeV+hDSGpbzpLSd305ENnDVovMaTd+sJHWluqcRHTtSSv8ARueNzSAM
+ZBJrdPR1qazdGWpUuCXYJb1kjxZEidd4TUWS+l5BWQGkJxuSFJDrzm88TaoH61ekbTbbdaLe
+1b7VAiwIbWeHHjMpabRklRwlIAGSSf2k1lmRo8yK5Flx2pEd1JQ406gKQtJ7wQeRFbz8Sm5t
+pKrv52ay1snJuuL+tnlR20rl6o1fHgWuJbYDV906XrM7PYbRKZSy7viJWV8JSiSDs3Y5YrY0
+DpHUt91A/cdOxeosQdS6naMkSEYhLfhtNMEHOVgLGMoCvq5r0g1pXTDVncszWnLOi2Oq3uQ0
+wmwwtXnKNu0nkPJW/bIEG2Qm4NthRoUVoYbYjtJbbR+xKQAKtLxF7Wor4fKvf1LPW8cL3VHJ
+uhHReo9PamTOm2jwHBRp9iBMY6y251+eleVy8NqUOYyMqwo7u6uxUpXBmzSzS3SOTLkeSW5i
+lKVkZilKUApSlAY5T7MWK7KkuJaZZQVuLUcBKQMkn91eIumvV1m1rrN2+2GV1mA6gJacxjcE
+8s4Pd3V7YusGPc7ZKt0tG+PKZUy6nzpUCCP4GvG2ovox6/03cX06PkQL3ZytRZZfe4L7ST/R
+yRg/tru0GWGLJukUkrOO3+Pxmsjxtpq89FnSLqnQelJDGn58ZhyVJKy0/HDm/YjJwT3HAP7a
+lI3Qb0uy3A0vSsaIM4Lj9wb2j2+LkmudyV2+IjMeUudLCChLoQW2mtwwopB5kkcsnFezjjDU
+5P8Axrd6+n5nRpME8mVbVaXX0PQnQR02681f0rWbTt6lwnIEvj8VLcVKFHYw4sYI7uaRXqev
+B/0VP+fvTf8A2r+6vV7wrg8bw48WdRxqlXb8WdfiWOOPKlFVx/IqjdO2kZmuOjC6WC3KQJyw
+h2MF7QFrQoKCckHbnBGRjv5nGQbzSvKx5Hjmpx6o4YTcJKS7FMjWSTcOiSbpdNvk2d5+2PwG
+25aYyVIK21JCsRf0QTlX9EDu7hVG6HdNa50Y5JnytLB9y6OWe2Px/CLKTFjxYnBdl5BUFjcM
+hsHcR5q7ZStY6mSjKNKpGiztJxrhnC9QaO1jqbUuqL1cNKuQ2pdoYipiovTRckyGJaXUFh8p
+VwWyhCSUlCMqJ5Z8eug9EFv1RbtOTWtUuSi45c5DsBmXKEl+PEURwm3HQpW9Q589yu8c6udK
+nJqZThsaVe/iJ53KO2iA1QrZedPK80x3+7O1yLVOhNRz+mVV6jsIVapV6tN2XL4yAGRDZebU
+0Uk7iVFacYBHM5IrtOobUq5sMll/q8mM5xWHCncArBBBHlBBI/fUR4I1V9p2f7m58ys8OeWF
+tx7qiMWV422u5MdaPnp1o+eofwRqr7Ts/wBzc+ZTwRqr7Ts/3Nz5lYmZMdaPnrXuEgraaTnv
+kM/2qaj/AARqr7Ts/wBzc+ZX4qzaoVtzcrOdqgofyN3vBBH+s84FAfOrLmbc1FuyrIq6ohuq
+W4WG+JJjpKCkuNIAJWcHBSkhW0nG4+Kc+mJLhtfHcs7doVIdcfMZJSVDeonc5tGA4rOVAZwS
+eZ76x+CNVfadn+5ufMp4H1T5bnaMeyI5n+0oDb0Kd1smq89zln/xlVQvpA6ZuGsEL07aijr0
+qyyCwFq2hSkSIzm3J5DOzHPlzrptgtqbVbkxQ6p1ZWpxxZ/pLUcqP7ya1r5ZnZlwiXSBMTDu
+EVK0Nrca4rakLxuSpOUkjkDyIOQKvjm8clNdVyWhNwkpLsVzoX09drDab9IvMbqcm9X+ZdUx
+S4lao6HVDahRQSknCcnBI51e6guq6w+27F8Id/M06rrD7bsXwh38zU5MjyScn3InJzk5MnaV
+BdV1h9t2L4Q7+Zp1XWH23YvhDv5mqFRpn/TeqP8Aejf9zjVO1HWG2KtrMgvSTKlSni/JeKAk
+LXtCRhI7gEpSkDnyA5k86kaAUpSgFKUoBSlKAUpSgFKUoBStLUFzYsthuF5lIdXHgRXJTqWg
+CspbSVEJBIGcDlkiq3pXpEs1+miIuHPtLq7Wi7tdfDSUuRFnAdCkLUAOYyFEEealii40qIb1
+RppyE1Ob1DaFxXXxHbfTNbLa3SMhsKzgqwD4vfyr9ian03LditRdQ2l9yWSIyGprai9jv2AH
+xseXFAS1Kj7VfLJdX32LXeLfPdjnDyI0lDim/wDrBJOP31IUApSlAKUpQClKUArHKfZisLfk
+OoaaQMqWs4AFfbi0toUtZCUpGST5BXh/6QXTdL1ZquTZLNLcj2OIstANqx1lQOCs+zzCrQg5
+MrKVHqOF0xdHc2/+BI2o47krdtBAPDJ827GKv1fzdtynFKbkR3ClxHMYr2x0e9LujrvpKHKu
+V5g2mYhAakRpUgJUlaRgkE94PeDUySXQRbfU6VSq/aNb6Qu9xat1r1Lapsx3PDYZkpUtWAVH
+AB54AJ/dVgqhYUpUPrLUlq0lp2Tfby8WokcDISMrWonASkeVRPk/9KAmKVDWvUcObpZWo3mX
+YUBLK5BU6607+iSNxWFMrWkjAPcc8q0dBa3tGs2ZS7azMjuRQypxmUhKV7HWw40sbVKG1STk
+c8+cCliiz0qgyOlWyxnrgxKsuoI0iHHTKDD8RLTj7SnwwlSEqWMZWRgL2EjnjFXGzTZM+Kp6
+VZ51qWFlIZlrZUtQwDuHCcWnHPHM55Hl3ZixRuKUEgqUQAPKTWPrMb0hr+uKhtVoTJm2a3vZ
+MaVLUl9AOAtKWVrAPsykZHlHKqtcr70eQNaN6Vf07EMpTzEdb6be0WWnnkrU02o/W3KCFEYB
+Hdkilk0dC6zG9Ia/rinWY3pDX9cVF9m9Ner1p+5t/wCFOzemvV60/c2/8KkglOsxvSGv64p1
+mN6Q1/XFRfZvTXq9afubf+Fa86wadYYStGnbOSXW0c4TfcpaUnyeY0BOdZjekNf1xTrMf0hr
++uKqmqGNKWKK24dIxrjJeXsYhwrc0t50jmogHAAA5kqIHcO8gHdtlo0jc7czOhWS1Ljvo3IV
+1JKFY8xBSFJUO4g4IIwcEUBYwQRkHIr5cWhtO5xaUJ86jgVC6IWs2h5lS1rTGmyI7ZUrJCEO
+qSkEnv5AVWelO62i23aK7qUKdska3SJj7ATuDi0raQgbe5WS5gA8skeahNF863E9KY94Kdbi
+elMe8FUnR1q6PtUW16bB0lBYVHlORJLEiE2lxl5s4UhW3IyOXcSOdTXYbRvqvaPuqP8AChBO
+dbielMe8FOtxPSmPeCoPsNo31XtH3VH+FOw2jfVe0fdUf4UBYUqStIUlQUD3EHIr9qv6NaRD
+evdrYymJBuAbjoKieGhUdlwpGfJucVgeQcvJVgoBSlKAUpSgFKUoBSlKAUpSgIjWtsfvWjb3
+Zoq2kSJ9vfitKdJCApxtSQVEAnGTzwDXL3Oh24nTUyzt3CMhc7TkO3uyXJLzzjMlhYUpLZWO
+UdeOaQRjAwnyV2elQ1ZKdHHZfRdqC4XoXqW7ZmHX9SW25yITLjimG2IrS21BBKBuWvdnBSBy
+76yN9Ft4bcDiHrUlY192iCgtYIh//C+p9f8A2fq/7VdepTahuZyno40BrCw68b1Lf7zFu7jt
+uegSnVS3luEF/itrSlacAYwjhgpSnmRkk56tSlEqDdilKVJApSlAKUpQFd6TBcD0d6hTakqV
+ONtfDAT37yg4r+dkHT0a+aeZfgqUi4x0bZDSklK0rHfkH21/TWoG+6P03eoL0SZaIgDpyXGm
+ghwK/WCgM5rTHPaUnGz+btplzIMww5ba2yjBV+z2GrzaI4uCLhPLAiwW4qlhIyRv5BAye85N
+d+vHQDLF0WIyok6Is+K44AlxKf8AaHn9ordvP0e5M6FHgxdUMQYjKR+hTBKsq8pJ3jP8Kvkl
+GrRWCldM5P8ARo/57dP/APaf7s7XtSuI9GHQQ/ozXNu1KvUzc1MPi5YEIoK97S0fW3nGN2e7
+yV26uaCpG8nbFVjpT0r200JctOpk9WdkpSppwqUEhaVBSdwHenIHLn58ZAqz0qxUriLLNuOg
+pmnbshuI7KhOw1KanOzMJWgp3Fx1KVqPM9+f2mqd0d6G1hpAuSmZNhdlzXrbFmpWp1SEwYsf
+glTZAB4ysZAI2juya6pSoomzk9w6PdUXi76gut3OmlOXG3tRRGa6wmNLdafDjbz4BStKghKU
+YC1d3fjxatXRVpidpSwTIU96MVSbi/MajRVKUxDbcIKWWyoAlKceYd55VbqUoWV/Va+HdtPu
+E4AmOc/2x3R/61zu/aBnXDpM7RNzoqbc9cYFyfSoq4yXYjbiEJSMYKVbwSSRjB5GutXS3xLn
+FMaY3vRkKGCQUkdxBHMH21D9j7b6befiT34qNWEzb62f1j/GnWz+sf41qdj7b6befiT34qdj
+7b6befiT34qDg2+tn9Y/xrBNkFaGk7icyGf7RNY+x9t9NvPxJ78VOx9t9NvPxJ78VBwampn9
+QmK29pyTD600vxo83IZfSeWCtIKkEd4IBzggjnkbtsdlsQGWp03rklKBxXg2GwtXlISPqjzD
+J5YySedfPY+2+m3n4k9+KnY+2emXg+w3F4j/AOqg4PrQZ3WqYr9a5S1D2gvKNVHpk0yvWFza
+043IRGcm2eSlp1YO0OJejuJBx5CUc/ZmujwIkeBEbixWktMtjCUisF3tMC6obTNZUpTStzbj
+bim3Gz3ZStJCk8ie40oWQPRjpmbpq2XU3J6O5Ou12k3SQmOpSm21OkeIkqAJACRzIH7KtlQX
+ZS2el3345M+bTspbPS778cmfNoQTtKguyls9LvvxyZ82nZS2el3345M+bUgaY53rU6hzBuiM
+H9kSOD/xBFTta1sgRLbETEhMhpoEqxkkkk5JJPMknmSeZrZoBSlKAUpSgFKUoDlGp+mJVg1Z
+qiBL04lVl0w/b2rjcEz/ANKlMxIKFpZ4eClKjhXj57iAeYFwPSBpAakGnjeE9fMrqWOA7wes
+bd3B423h8THPZu3eyqdqfodXf9W6nuErUiU2XU0i3u3G3JgfpVJhpAQ2l7iYCVKGVeJnuAI5
+k5bf0OQYGv5OpY8y2KZk3hV4cRIsbD8xLyuZQiUvJbb3jcAlIUD3KGTWCeVPp790etKOglBf
+ep12vrS62n33dOOnJZ7R0jaOvEifGtN2XPfgNl15qNDfcWpAcLZW0lKCXkhYKct7hkGq3rbp
+msdnsNsuNhhSb67cLyLOlngSGeBIHJaXRwVLQtPL9HsLisnak4ONBjoZuLHRvduj9nXcpqxS
+GVMwGUW9CVRd0kvqUte7c6TkoIyhO0nlnnWO1dB/g6JEgx9RR24cXWEfVLbLVrDYS4hO1bCc
+OYS2fF24HiAEeNnlDeVroTDH4fGTbm2k+Fzyv0X6fDq+pIaU6a7Bd5l8RcIcu1sQLqu2QgqL
+JdkzltpUpwhhLO5JSACUAqUkHxwjKd0hP6X9GwZwdkXq3+BjZUXcTGzIcc4a5IjpPDSyU7d6
+gknibweRQACqq5O6CIkmZIuC7zCkzFajuF7YRPtKZMVKZiEJWy4ypwcTbw0kLynmM4rW1R0A
+N3qE7Gb1OzDDunWrKrg2ZptAUmemYp4NtKQhIJSU7AB37ionOYvMl0L+X4ZKae5pPr14+X5/
+Q6ND6QNLTWrkuDLmy12t5DM1mPbJLj7SlgqQS0lsrKSASFAFJHlqKk9LuiWrhpuMzMmS2dRt
+uuwpjEJxUdLbYVvUteBtCSkhXeW/rLCE+NUFrDoXTqHUuobyrUYZRebla5yoi4AdaxCaU3wX
+AVgOoc3ZIIGMD61aMXoHSzYdMWVWqOJF0/4TYbzAwp+JPSUutqIc5OAKXtcHLmMoOOdnLL2X
+u/4M4YvD6TlN/h6fd/DtLj8DoukddaV1XJdi2G6dZfbZTILa47rKlNKOEuoDiU70EjAWnKfb
+VkrnfRv0ZuaVv7F7uF+8KyYdjZsMIIh9XS3EbXuG8b1b1kgZV4o5d1dErSDk197qcOpjhjkr
+C7Xv4L9hSlKuc4pSlAKUpQCtK+3a3WO1P3S6y2okRhJU464rAA/xrdrxh9MXXc/UOr3NH22S
+pNrtWOsJQrk88eeD/wBXl++qzmoK2dGl00tRkUIlu1n9Kvh3JTOkrE1KiNqwqRKWQVjzpSP/
+AFruvRLrJGu9ExNQpi9VU6VIW3nICgfIfNX82Yzi2HcLyPPmpsSpK7fwWZj7TSDlCkuFKUKP
+kOPIf/OuP7TKLdqz6F+DYMsVGL2vu+v1R/TeleBvooTJ6/pBaaZkS5K0/wAr3IW4SP8ANHvI
+TXvmujBm82O6qPJ8V8O/7fmWLdutX0ru18fQUpVK6btbudHvRxcdTsQ25khgobYacWEpK1qC
+QTzBIGckJyeXkGSNZSUVbODFillmscOrdIutKp1u1RNj9FMrWV0dhT1x7c9cMQkJbbUhDZXs
+BQ88knxSNyXFCoLoK6RLvrhV3h3yJBZmQI9umBcNK0tqamRg+hJClKO5OSknOD34FV8xWl6m
+v2TJsnNdI9f1r6o6dSuGam6VNdacuep7XdYemhJtcCNLaWy1IUhsvzEMpQQtSVSBw1hRW2Ep
+C/E766b0bX46isL043233hTctxhTkO2vQeCpGAppxp5xa0rBznJHIjl5TEcik6RbNosmKHmS
+6f6T+pOXOfFtsUyJbmxGQkYGSonuAA5knzVG9pof2fe/hT/4K/NR48OadBxjrjpIPsjukf8A
+Gub6l6XLpa+l7sszAgrtEe7Wy0SVrC+Op2c064hxCgraEp4YBBSScnmKmc1HqZ6fTZM7agui
+v3+p0ntND+z738Kf/BTtND+z738Kf/BUvxB+sP404g/WH8auYER2mh/Z97+FP/gp2mh/Z97+
+FP8A4Kl+IP1h/GtW5vKTGQULKSX2RlJxyLiQR/CgNLtND+z738Kf/BQ6nhAZVAvSQO8m1vgD
+/u1+6nlXtEJDGno8Zc59WxMiUcsRRgkuLSFBSxywEpwSSMlIyoZ7FMnyba2u6wkwpqSUOtod
+DiCQcbkKHMoV3jIBweYB5UBvQZUebEblRXUusuDclSTyIrUvt7t9labXOcWC6ra2202pxxZx
+nCUpBJ5eatLRH+jpwHcLnLAHmHGVVH6ddXOaGe7UtRW5bsCzyFNMuE7C4t+O2knHPAK8nHkz
+3VEpKKtmmLHLLNQj1bpfmW7t1Z/Qr78Hkfgp26s/oV9+DyPwVpdEWrbhqy03kXZiK3cLLe5V
+okqipUlp1bJHjpSoqKQQociT5edXWoi9ytDLjlim4S6oq/bqz+hX34PI/BTt1Z/Qr78Hkfgq
+0UqxmalouUO7QUTYLvEZUSMkYIIOCCDzBB8hrbqC0xyvWp0jkBdEYH7Ykcn/AIkmp2gFKUoB
+SlKAUpSgFKUoBXGPpBaqveltdaMmWma6htu2agluxC6sR5K2IHEaDqEqG8BQyM92Tgg12eo+
+7WOyXd1p27We33Bxlt1ppUmMh0todTsdSkqBwFp8VQH1hyORVMkXKNI6dJmhhy7pq1TVfimv
+qcUPStrh623BlabFGludHydXQn2YjigyeW5pSVOeMTz2q5BJIyF4IMZYumXpBXEisJttnua4
+FrtUq4ypC2ISZRltpc5LdkoS2QFBGUocCnAfFbBSkd7a07p9p1DrVitbbiIItyVJiNgpiDuj
+g45Nf7H1fZWq1ozR7K4S2tKWJtUD/Mym3tAxvGKv0fi+J4xJ5Y5kmsvLn/yO5a3SpNPEvfz/
+ALvqcqY6W9Wi9BT8KyKtf+UJekChDTofKCfEe3FZSFAd/i4V5Ntbdt6U9U22dMi65tLdpuSY
+U6XCtDdscPW0x0Kc2tTA8pCztSCf0ae/Hmz1Lszpz1ftP+kPCf8Ambf+eek9387/APM+t7a+
+LXpXS9quLtxtmm7PBmvAhyRHgtturB78qSkE5q2yfqZvVaVpry/fvrxfo0UDok6R79qbVEay
+3qPanBO03Hv7L1vQtIjh1e3q7m5atyxkHcNvceVdXqNslgsVjL5stlttsMhW57qcVDPEV51b
+QMnme+pKrwTSps5NTkx5Mm7HGkKUpVznFKUoBSlKAV436UegXpGPSDqC+WWNHulrmPLloPHC
+XefMo2nvPkH7q9kUqk4KapnRptVk0098D+Y93taw+7FksuxJTSilxp1BSpB8xBr80/pa8Xu4
+sWm3oLqn3UtIyeRUo4H7TXvfpS6ItK6+2yprKoN0QMInR0jeR5lg8lj9vP21m6NuirTGh0pd
+htGZOT3S30J3p/6uByrlWmkpVfB7svGcMse5x+/6HDOhDoT15pTpvtOpLjaGY1lhqfTxOutu
+LCDHcbQSAckklOcec16vpSunHiWNNI8bW67JrZxnkS4Vcei/sVE6x07bNWaYn6dvLS3IM5rh
+uhCtqhzBBB8hBAI9oqWpV2rVM5YycJKUXTRERbEns2/Yrtcpt7YkNLYecmhoOLbUnaUHhIQn
+GM88Z599VPTXRJZNOx2W7TfdSR3UT4kt19E1KHJLcVvhMxXdqAFMBGAUYyccya6HSocIvqax
+1OWCai6T6nPo3RRaGZ9zuKtR6reuM6Cbe3Ocuh6zDj8YvbGnQAv65zlZUcAJ7uVWHQmkbbo6
+2S4VvelyVzZzs+ZJlLSp2RIdIK1q2pSkE4HIADl3VYKUUIrlIT1OXItspcFd1gsM3CxSFna2
+3MWFKPcNzLiRn9qlAfvqpXTo905cddtawkKmCYh5iSthLiQw8+wlaWXVDbu3IS4oDCgO7IOK
+6VLjR5cdUeUyh5pYwpCxkGofsdpb7Bge5FJRUupXHmnitwdXwfvWPbTrHtr87HaW+wIHuRTs
+dpb7Age5FTRmfvWPbWKU6VpaSOf6don2AOJJNZOx2lvsCB7kU7HaW+wIHuRSgRepYs24wkG2
+XZ22XBhXEjvhJcb3YIw43kBxBB5gkHygpIBGeyMKtltRGdnyZroJW7IkLypxZOVHzJGTySkA
+AcgABW72O0t9gQPcig0fpcEEWGACO48IUoGPQJ32aQ6OaHZ8lxCh3KSp1RBHsIINV/pK09at
+UakhWG/JV4NuVrlRVlK9p3lbS07T+sOGVDv+rV+ZabZaS00hKEJGEpA5CsVwgwrhGVGnxI8t
+hXe282FpP7jyo1apkxm4SUoumiI0LpS26PtL9vtzsqQqVLdmypMpaVOyH3TlbiikJTk8u4Ac
+hyqfqC7G6Q9VrH9wa/DTsbpD1Wsf3Br8NEqVITnKcnKTtsnaVBdjdIeq1j+4Nfhp2N0h6rWP
+7g1+GpKnzpNaHrlqSSyoLZdug4a0nKVbYzCFYPlwpCh+0Gp+scZhiKwiPGZbZZQMIbbSEpSP
+MAO6slAKUpQClKUApSlAKUpQClKpfThNmW7ouvEy3y5ESS3wNjzDhQtOX2wcKHMZBI/fWebI
+sWOWR9k3+hfFDzJqC7ui6UqpdJV0kWsaa6unJl6gixV/pXUYSvdk/o1p3d31VZSfKk1XmukW
+4ottylXBESMLPDPhFTcZbpRLMlTKEJTxE5BCCcFX9JPMVjk1mPHNwl2/s1hppzipROnUrk9i
+6RNS3e72uztRLZHlSbhcYLy3WlEAx2kLSoJS4QM7yCNyhy5K8tav+UXUkmJo68tGGxGuMa4P
+zIaI5UXTFSslKVFWRu2jbjmk8zvHi1j/ANyw1av3X/6Rp9hy3XHu/wCGdipVD0BrG63q+M22
+5NQFiVZmrs25ESpIZC1beCvcpWVDvyMdx5VfK68OaOaO6Jz5cUsctshSlK1MxSlKAUpSgFcp
+6U+km8WXVDemNN2+O9LLaXH5D5Kktbu4BI7z+011avC30iL1d2/pD3eVZ7jIhvMBphLjS8dy
+R3+Qjn3GufUuflvY6Z3eH44ZMyWRWvQ6LqLpc6R9G3om5XCBeGAclgxg2AD5AU4PL25rvHRR
+rWLr7RsfUMWO5G3qU242sfVWnvwfKPbXlWEwu8xGpF8KrhJIBUp7uUf2DAqw3S8XqzWqA1Z7
+vPt0fxwWokhbSM5/VSQK8qGvnpsLll+9R6+s8PxZZRWJbbPWtK80dCmptR3DpNtEOfqC6y4z
+nG3svzHFoVhlwjKScHmAf3V6Xrv0GujrcbyRVU6/b+TxNZpXpZqDd8WKUqndM9xu1q6ObnOs
+rrjMpAQC62PGbQVAKUDkY5HvGSPN5R1ZsixY5TfZWYY4PJNQXcuNKq6TOtPRrMlMmUbg1bnn
+08d1Tq+KGyR9Z13ygcgtQqp9DOon1LuEa83pTrCkW3qrk2TuUuRIjBbjSVKOSSruQO7yCsXq
+lGcISXMvl3NVp3KEpp9DqlK4neZ09i836BadWTJsZdrZlMPuXdPDVmYlLq+NlKGCkbmwlHIg
+Z+tyroXRncmLlZ5wZjy47kS4vRH0v3B2YOIjAJQ64dxQRjHcM55d9Uw6xZcmyqLZdM8cN1k7
+eLk1bWELW24846sNtNNjK3FHyD/75VoeGbv6p3H7zG+ZX5qMhN906T3dcd/uztc41HqG9s9L
+RYZuEpDDN1tkRqMlwhpbDzTqnlFHco5A8YjIxWuo1CwJNrq6KYcLytpdlZ0jwxd/VO4/eY3z
+KeGLv6p3H7zG+ZUpxkfrU4yP1q3MCL8MXf1TuP3mN8ynhi7+qdx+8xvmVKcZH61al1dSqM2A
+efWGP7VNAa3hi7+qdx+8xvmUN5uwGTpS5ADvxIjn/gHKwarVcpbca22i+x7M9IUorkFtLj+1
+IzhpCxtJzjJOcDPLJBG9Y5rsi0x3ZcmE/IKcOuQ1EsrUDglOeYGR3ZOO7JxmgNu1T2LlBRLj
+lWxWQQoYUkg4II8hB8laWoL61aVsR0RJE6ZIzwY0cDeoDvOSQAB5yQO7zisGiDm3zyO7wpM/
+t11SOnG43C0yev2t1bUtuzSQhxH1kBUiMlSh5iEknPkqmXIseNzfZWaY4Oc1Bdy29pr16i3v
+30b5tO0169Rb376N82tTolmzZdqvLMqU/LahXuVEiPPOFxamEKG3KzzV3kZJPdVzqMOTzYKa
+7jJDy5OJV+0169Rb376N82naa9eot799G+ZVopWpmaFiujV2hqfbadYcbcLTzDycLaWO9Kh+
+wg+0EEcjW/UFpn/TeqP96N/3ONU7QClKUApSlAKUpQClKUArBcIUO4w1w7hEjy4zmN7L7YWh
+WCCMpPI4IB/dWelQ0mqZKdcoiYumNNRMdV09aGNrqHxw4TacOIzsXyH1k7lYPeMnHfWyq0Wl
+TcttVrhKRNVulJMdOHz51jHjH9ua3aVVY4JUkiXOT6sjY+n7DGnonx7JbWZjedj7cVCXE5Tt
+5KAyOXL9nKv1FhsaGIjCLNbkswllyK2IqAlhROSpAxhJzzyMc6kaU8uC7DfL1NO22q12wum2
+22HC4p3OdXYS3vPnO0DNblKVZJRVIhtt2xSlKkgUpSgFKUoCI1ozdpGkbsxYnQ1dHIbiYiyc
+bXCk7T/GvDMy3uu6n4t7S5BuTaUtymZWUqKkjG4E9+cV79qLvmnLDfEFF3tEKaCMfpmQo4/b
+WWXF5iqzs0mq+zu6s8jpnQmIqVLksoQkfrACouErUevdXRomkbfKnWm3MOmc8hB4a1qHipST
+yKgoD92a9Ux+iXo1ZfDyNG2krByNzO4A/sPKrdbrfBtsZMa3w48RhPJLbLYQkfuFcsdDFKSk
++qo68/ie+tq6Hnzob0Vqu0dJFquFyscuLEa43EdWBtTllaR5fOQP316MpSraHQw0WN44O7d8
++/gcWr1UtVNTkq4oV8PNNPsrZebQ60tJStC0gpUD3gg94r7pXacprW63wLbF6rboMaHHyTwm
+Gktoye84AArXasNjZZbYas1ubaakCS2hMVASh4dzgGOSx+t31I0quyPoW3S9SPasdlaEoNWe
+3tiZ/nO2Mgcf/r8vG/fWzAhQ7fFREgRGIkdH1WmGwhCf2AchWelFCK6IOTfVlf1i2+lVtuDT
+LjyIUlTjqW07lbVNrRkDy43Z/dUE5dbE5PbuDlvfXMbTtRIVani4lPmCuHkDmavtKOKfUhOi
+l9pYP6lw+Hv/AIKdpYP6lw+Hv/gq6UqaBS+0sH9S4fD3/wAFfD2oYLgbGyfhLraz/wDl7/cl
+YUf6Hsq70pQs5zqKRp3UFv6hdodwfZCw4kphyW1oUP6SVpSFJOCRkEZBI7iRW9EvtrixmokS
+LMZYaQG2mm7a8lKEgYCQNmAAOVXilKBCaJjvx7MtUhpTS5Ep6QEK70hxZUAfbzrQ1TGU1qi3
+XeRAdm25EOREkoaYLxAcKCCWwCVJ8TBAB7+7vq1UpQsrcG/6fgRURINsusWO3yQ0zYJaEJ/Y
+A1gVn7V2r0a+fA5nyqnaUSpUg3fLILtXavRr58DmfKp2rtfo18+BzPlVO0qSCD0m2+py73J6
+O7GRcJwfZbeTtWEJZaaBUPITwycHmARnBqcpSgFKUoBSlKAUpSgFKUoBSuV9Iotn+Ve2nWvA
+7J+BHuB1z/Nuu8QZ3Z8Xfw/q5557udV3pK15K0tbrZD0Eqe3FZtSZ8cSPGaeZD4QUbXmlPLU
+Ekn66NqBnnjByeVRuzGWZRu+x3alcIsnSJqKdqhtqHqNu4pd1o9bG7e3HZKV20DPGC0p3EIG
+PGB/bmsNs1HqK69HfR5qK83Rq4yrrqyIwtD8CMUNIS6+gqQOHlKztHjjBGBt2+MTHnJ9CPtE
+X0R32lcIgdIOr3NB6ivKLv1rUMWKtxyy+Cv9GKEpTeeKBg4bG7hryrluzt5VkumqLn2g0jIt
+N3jatdxfTGkrtCGnHlNRQpttCtufreKVtbUrHLnip85Dz40dzpXKeh7V1/vuokw5d18Mwl2N
+ibLe6uhvqU5S8Li5QkdwycKyoY766tV4yUlaNITU1aFKUqxcUpSgFKUoBVV6WdRtaW0DdLqZ
+jcWQGVIilR5qdIwkJHlPl/d5qs0p5EeM7IczsaQVqwMnAGa8KdLvSwrXmoXpTz640GIsohQH
+MjxfKvzbjV4JN8nHrtRLDibgrl2I6L0ha2s16XfIeq7ol8qyWnny6yr2FCuX8K9U/R96Yrd0
+l2xyHJDcS/Q05kxweTif/iI9n/lXhK5XJ66PliEjcs8sDyVK2CGdLyY97mFTjBdSzMbzgOMr
+OFpHtHeD5CBUzpvg4fD3nxwvM7b7H9L6V40+j1H6r092iLnPBdlt58+I7wr2XXPiyb03VHqY
+M3mxuqFKVQfpBtXh7okvSbKp8SAhCnAyrCi0FjiDkCT4ucgEcs8/IbydJs0nLbFsv1KodoQw
+jobnN6Tct7koW2QGlWtxpaDJ4RxhTTbaSvO3uQk58lUP6P8AfIFjbnGbKVHtdwdtECDhtakO
+XFyIOOgbQcKLg8YnAz3mqPJTS9Sjy00n3O8UrzVd4thlXzWDWm30otBs7Dj7zrUpTbTiZyVP
+JlIKVuuPHasgkDCMDG3xq610JTmZ+l5zkay2i2sNXSQw25a4oYjTUo2pEhCefJQAGcn6vfUR
+ybnREMu6VFm1BOlRepxYKWzKmvFppTn1UYSVlRx34Sk8vL3Vr9U1R9v274Wr51fmpVbb3p1R
+8kx3+7O1xjV7lyPT62tHG634XtJg4znqPBe61t/2N2N3kzjNWnPaXyT2JcHaOqao+37d8LV8
+6nVNUfb9u+Fq+dUj1lPmp1lPmqxcjuqao+37d8LV86vh5nUjKAtzUNuSkqSkHwWrvUQB/rfO
+RUp1lPmrVub4XHbSPLIZ/tU0JMBianAydQW0Af8ARavnUMTVAGRfbao+Y2xQz+/i1G668Azo
+kO36jkuN22VI4TjCjtjyiUna08rHJBP9EkBZwk7s7Ts6KlW7s+0m1XCVPgIWtEd6QoqJQFEA
+JWQCtA7krO7cADuV3kQSmnLg5crbxn20tvtOuMPJScpC0KKVYPmyK0NS3O5ovEGx2cx2pkpp
+x8vSEFaG20FIJ2gjJypPLI8vm5/Whjut04+e6TD/AOMque/SNFyMKR4J43WvAUn+azu4fWI3
+E7v9jdn2ZqJOo2RJ7U2XnqOvPWKyfC1/Np1HXnrFZPha/m1BdAW7s1e+B/ontBN8EY/m+qbh
+s2f7Gd2Mcq6LSL3KyIS3RTKv1HXnrFZPha/m06jrz1isnwtfzatFKsWIjTE+ZLZmRriloTYE
+nq76ms7FnYhxKk55jKXE8vIcjn31L1BaZ/03qj/ejf8Ac41TtAKUpQClKUApSlAKUpQClKUB
+CaT01A014W6i7Jc8K3N65v8AGUk7XXcbgnAGE+KMA5PtNTdKVCVdCEklSFKUqSRSlKAUpSgF
+KUoBSlKA/FAKSUqGQRgivFnTv0ICBqZ6TAK48OTIW6nBBCkqOSE+YgnGK9qVH6gg2a4W1ce+
+xYUqEea0S0JU3+0hXKpTozyY96+J4O07op5q8tWSwWp643WR4wbSPqju3LV3JT+2rrO+jZ0j
+3OSy/cXbYpCFBXV0S8I5HOPq91emdM3Ho2s0hy36eesNvcWfHRFShveR5yAM1cWXG3m0uNOJ
+cQoZCknINU3Rn/i/0KfZWv8AKzzt0QdD+tNM9J1t1JefB5isKfU8WpG5RK2XEjAx51CvRdKV
+GPGoKkXxYo4lURSlKuaClKUApSlAQerIcx4QZsJrjuwny6WgQCtKkKQQM+XCif3VFeEbp6s3
+j+DPzKuNKiiSneEbp6s3j+DPzKeEbp6s3j+DPzKuNKULKd4RunqzeP4M/Mr4cnXRezOmbwNr
+iF/VZ57VBWP5z2VdKUoWUa4uSLlBfgXDR1ylxH0Ft5h9phbbiT3hSS5gj2Gszc65IQltvS92
+QhICUgJZCUjyDk5yFXOlKFkRpGDIgWgolpSl5592QtAOdpcWVYz7M1qaghT2tS26/wAKIqcI
+0d6O7HbWlLhS4UncncQkkFA5EjkTVipUkEF4dufqbffew/n08O3P1NvvvYfz6naUBBeHbn6m
+333sP59PDtz9Tr772H8+p2lAQ+mIkxo3KdOZEd+4y+sFgLCi0kNNtJBI5E4bBOOWSRk99TFK
+UApSlAKUpQClKUApSlAKHupSgI5u92xyDFnIk5YluhlhXDVlaySAnGMjmD3jlg5rPd7jDtNv
+duFwe4MZrG9e0qxkgDkAT3kVVndPXRV0lx2cMwWusSYLu8eK+8gAchzG1RcOcf0hiq9K0lf3
+LFNjxrb1ZS7dHjrY46P5TIQ8lSnshWPqg8yQTmvCy+IazHCVYbdOuH1S9PRvpyuOlnZHBik1
+97g6VbbjDuPWepvcXqshUZ7xSNricbk8xzxkcxyrM4+w280y482h14kNIUoBSyBk4HlwOfKq
+BB0/fIepmrr4P4rbV5nv7A8gEtPIQlC+Z7sg5Hf7K0ouj7w3atNDwc2mZDbmNPr3NlTJWVFl
+ec8wlSt/LJHkGaleJauqeB2vx6XH4f8Ayff/ANW/wPT4r/z98/wv1OoUqmaFsdyt10Eh+H1C
+Om3Nx3m+KlXWJAVlT3ik+TynB51c69PSZ558e+cNr9H/AEv2OfLBQlSdilKV0mYpSlAKUpQC
+vP8A9KmJqG6XG2Q7VLzEbZUt6Ju27yT358v7DXfnFobbU44oJQkFSie4AeWvPupdZWTW12Xd
+LDJVIhNlTCXCnaFFBwSPZnuPlrg8SyvHgbR2aGG7MitdHVxsenLdIj6wZDLTrW0lTBcV3dwI
+Bqz9FfSZAtjNzgW2DJl29EgKjcV7YpII58sHA9lVDVUISoxBGeXdUXoqOmEuZxcoDim0I5f0
+iSP/AFHOvkpTy4YTy6d1k7f0z3MuOORVNWj0ZpbpDRfL7GtQtKmC/u/SF/djakq7to81XmuE
+dFX/AC9tv/8Ab/ZLru9ez/0x4hqNdpJZM8rak10S4pen4ni+IYYYcijBUq/kVguEyLb4TsyY
+8lmO0nctau4Cs9QOvrM9fdLS7fGKeOratoKxgqSQcZPdnuzy/hmvc1M8mPDKWNXJJ0vVnJjU
+ZTSk6RKxp0eRDVLSXW2Ugkl9lbRAHMnCwDj21hs14tt4aW7bpIeS2QF+IpJGRkHCgDgg5B7j
+Wm1Bdk6PftQjOwluRXI6UvBoEEpIBw14gHPyY/ZUDoy2X6yKdkO2oLVKVCirb6ygFptpnYt7
+lkEZ7kjmfZXHLV545cS2fda+86fHHy5pfn8DVYoOMnfK6ck6jVunlpkKRcQoMJ3rw0s5Tv2Z
+T4vjjdyynPOpSBMamsl1lEhKQraQ9HWyrP7FgHHPv7qpVxst6ulzus6TaVMoehttBlM5BU64
+28FJ4bhB2JKUjIKRzPn8arFo2PdY1tfRdlPblSnFx0PPcVxtknxEqVk5I5+U1TSavU5M2zJH
+7vPO1ru/Xparjr+ROXFjjG4vn8UfmqVOuSLXbUPuMNzZKm3VtnCtqW1rwD5M7MZ9tRr8LSbN
+2RanZk9MxeAG/CMrvIJAKt+ASAcAnJxW/qhWy86eV5pjv92dqr3Wy3GRrMzm0JMV2bElqd3g
+bOChaSnHfk5HcMV0a7Pmwxi8MdzbSfwVP/SvtZTDCEm9zrgtPZez/wDSPxOT8ynZez/9I/E5
+PzK3OtHz060fPXaZGn2Xs/8A0j8Tk/MrDM09Z47SXNtxVlxCMeFJI+ssJz9fyZzUl1o+ete4
+SCtppOe+Qz/apoCM1BA0tYbU7c7pIuTMZvA8W4S1rWonAShCVlS1E8glIJJ7hWe3WTTtyt7M
++3ypr8Z9AcZeaukhSVJPcR4+KwasuZtzUW7KsirqiG6pbhYb4kmOkoKS40gAlZwcFKSFbScb
+j4pz6YkuG18dyzt2hUh1x8xklJUN6idzm0YDis5UBnBJ5nvoQbuj5D79qcRIdU8uNKfjBxX1
+lhtxSQT7SBUNriS25qCBa5096FbDDflyVsultSuGpCQncnxgPHzy55AqT0Kd1smq89zln/xl
+VWuky1yLzqWLbYmOO7Z5RQCcAlL0dWM+3GKy1E5wwyljVySbS9XXCLwSc0pOkSNr0rpm5xes
+wbhe3WtxQT4VlJKVA4IIKwQR5iK2+wll9KvfxeT+OtnRFumW+HcHJzXBdnXF6WGtwUW0rIwk
+kEjPLyHy1P1XSznkxRlkVNjIlGTUXwVfsJZfSr38Xk/jp2EsvpV7+Lyfx1aKV0FCB0eXWTdr
+W5IdkN26cGGXHVbllCmWnQFHy44hGfMBnnU9UFpn/TeqP96N/wBzjVO0ApSlAKUpQClKUApS
+lAKUpQClKUApSlAKUpQClKUApSlAKUpQGOSy3IjOx3Rlt1BQoecEYNeXx0I620DdZCdHriXr
+TzrqnG4rqy2+xn+iDzCgK9SVH369WmxQVTbvPjwo6e9bqwkf/wC1lmwwzR2zXBriyzxSuB5/
+Y0vrqYrhuaSkMq87jyNv8c1ctH9FD4lsTNQraShpQcEVpWQVDnzPmr7tn0iOjK43xFpj3KYF
+rVtS+uIpLJP/AFq6y0tDraXW1pWhYCkqScgg9xFcOPwrTxlu5Z2ZNfqNtNVfwIm3aYsNumNz
+IVsZZfbzsWknIyCD5fMTUxSld2LBjwrbjior4KjglOUncnYpSlalRSlKAUpSgIzUNqVc2GSy
+/wBXkxnOKw4U7gFYIII8oIJH76iPBGqvtOz/AHNz5lWlSgkFSiAB5Sax9ZjekNf1xQFa8Eaq
++07P9zc+ZTwRqr7Ts/3Nz5lWXrMb0hr+uKdZjekNf1xUUTZWvBGqvtOz/c3PmV+Ks2qFbc3K
+znaoKH8jd7wQR/rPOBVm6zG9Ia/rinWY3pDX9cUoWVrwRqr7Ts/3Nz5lPA+qfLc7Rj2RHM/2
+lWXrMb0hr+uKdZj+kNf1xShZq2C2ptVuTFDqnVlanHFn+ktRyo/vJrWvlmdmXCJdIExMO4RU
+rQ2txritqQvG5Kk5SSOQPIg5AqYBBGQcivlxaG07nFpQnzqOBUkEJ1XWH23YvhDv5mnVdYfb
+di+EO/mal+txPSmPeCnW4npTHvBQER1XWH23YvhDv5mnVdYfbdi+EO/mal+txPSmPeCnW4np
+THvBQGpYbYq2syC9JMqVKeL8l4oCQte0JGEjuASlKQOfIDmTzqRr8SpK0hSVBQPcQciv2gFK
+UoBSlKAUpSgFKUoBSlKAVB631Ta9IWZNzuvHWl2Q3FjsR0b3ZDzhwhtCcjKifOQOR51OVTul
+rSErV9ltibdKYjXKz3eNd4RfBLS3WSSELxzCSFEZGccuRoWjV8m9atXQ34Fwm3m33HTDdvcC
+JCr0lthsAjkpLoWppafJlKzg8jitmTq3SkW0MXiTqaysW2QcMTHJ7SWXT5krKtp/capet9J6
+41lYogui9ORJtsvsW6wYbDrzjDqGSSWn3VIBVuyTlLYxgcjVfY6ILqxZDIUiG7qJV9nXdh+J
+eX4KbYZO0KbZWlhZWnCRnchOefnqC6jB9WdRt+qrTcNRtWWBIYll63eEW5DEyOtC2uJw+SA5
+xSM/09mzybt3Kp2uWaA0FrK2dI1t1bqm+wLu8zpQ2eU+2FJddkdcLwUE7QnYEEJ3ZBJGdvOu
+p1JSaSfApSlCopSlAKUpQGtdp8a12uVcprqWo0VlbzqyeSUpBJP8BX84umnpRvnSHrFy4TVP
+M2kKPg6NkhCW88lEfrEcya949O1luuouh7VNlsiVLuMq3uIYQk4K1d+0ftAI/fXhRWmpUmxM
+M3SzTrZMYSG1syo6m1ZSMEpyOdUndHVpau+5XrYtwPNvsuFLiFBSfYa9KW/6UZ07p+128aF6
+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
+--------------090109030206070103090500--
+
+--------------050702060806040107070701--
+
+--------------080809000000030101030405--
diff --git a/mailbox/elasticsearch-v6/src/test/resources/eml/emailWith3Attachments.eml b/mailbox/elasticsearch-v6/src/test/resources/eml/emailWith3Attachments.eml
new file mode 100644
index 0000000..7cacfcb
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/resources/eml/emailWith3Attachments.eml
@@ -0,0 +1,50 @@
+To: Laura ROYET <la...@linagora.com>
+From: Laura Royet <la...@linagora.com>
+Subject: test
+Message-ID: <cb...@linagora.com>
+Date: Wed, 11 Jan 2017 11:52:35 +0100
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101
+ Thunderbird/45.6.0
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------36566F1E9D791340FFB75FF8"
+
+This is a multi-part message in MIME format.
+--------------36566F1E9D791340FFB75FF8
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+
+
+--
+Laura Royet
+
+
+--------------36566F1E9D791340FFB75FF8
+Content-Type: application/vnd.oasis.opendocument.text;
+ name="attachment1.odt"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attachment1.odt"
+
+UEsDBBQAAAgAAGJVK0pexjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQu
+dC54bWxQSwUGAAAAABEAEQBwBAAAjyUAAAAA
+--------------36566F1E9D791340FFB75FF8
+Content-Type: application/vnd.oasis.opendocument.text;
+ name="attachment2-nonIndexableAttachment.html"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attachment2-nonIndexableAttachment.odt"
+
+PCFET0NUWVBFIGh0bWw+CjxodG1sIGNsYXNzPSJtb3ppbGxhIiBsYW5nPSJlbiI+PGhlYWQ+
+CI+PC9kaXY+PC9kaXY+PC9ib2R5PjwvaHRtbD4=
+--------------36566F1E9D791340FFB75FF8
+Content-Type: application/vnd.oasis.opendocument.text;
+ name="attachment3.odt"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="attachment3.odt"
+
+UEsDBBQAAAgAAG9VK0pexjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQu
+AAAAEgkAABNRVRBLUlORi9tYW5pZmVzdC54bWxQSwUGAAAAABEAEQBwBAAApyUAAAAA
+--------------36566F1E9D791340FFB75FF8--
diff --git a/mailbox/elasticsearch-v6/src/test/resources/eml/mailWithHeaders.eml b/mailbox/elasticsearch-v6/src/test/resources/eml/mailWithHeaders.eml
new file mode 100644
index 0000000..2aff55d
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/resources/eml/mailWithHeaders.eml
@@ -0,0 +1,14 @@
+Content-Type: text/plain; Charset=UTF-8
+Date: Fri, 17 Sep 2010 17:12:26 +0200
+Subject: my subject
+To: a@test, B <b...@test>
+Cc: c@test
+Bcc: dD <d...@test>
+MIME-Version: 1.0
+Message-Id: <20...@lenny>
+From: Ad Min <ad...@opush.test>
+
+Mail content
+
+--
+Ad Min
diff --git a/mailbox/elasticsearch-v6/src/test/resources/logback-test.xml b/mailbox/elasticsearch-v6/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..b0c305c
--- /dev/null
+++ b/mailbox/elasticsearch-v6/src/test/resources/logback-test.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
+ <resetJUL>true</resetJUL>
+ </contextListener>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx</pattern>
+ <immediateFlush>false</immediateFlush>
+ </encoder>
+ </appender>
+
+ <root level="ERROR">
+ <appender-ref ref="CONSOLE" />
+ </root>
+
+
+ <logger name="org.apache.james" level="WARN" >
+ <appender-ref ref="CONSOLE" />
+ </logger>
+
+</configuration>
diff --git a/mailbox/pom.xml b/mailbox/pom.xml
index 449bb35..7ae95b1 100644
--- a/mailbox/pom.xml
+++ b/mailbox/pom.xml
@@ -40,6 +40,7 @@
<module>caching</module>
<module>cassandra</module>
<module>elasticsearch</module>
+ <module>elasticsearch-v6</module>
<module>event/event-cassandra</module>
<module>event/event-memory</module>
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org