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 323762fcf1df8495dec6f60622050d98f3b501ac Author: Benoit Tellier <[email protected]> AuthorDate: Thu May 16 16:26:13 2019 +0700 JAMES-2765 Duplicate apache-james-mailbox-quota-search-elasticsearch Duplicated project will be migrated to ES-6 --- .../plugin/quota-search-elasticsearch-v6/pom.xml | 142 +++++++++++++++++++ .../ElasticSearchQuotaConfiguration.java | 152 +++++++++++++++++++++ .../elasticsearch/ElasticSearchQuotaSearcher.java | 91 ++++++++++++ .../search/elasticsearch/QuotaQueryConverter.java | 103 ++++++++++++++ .../QuotaRatioElasticSearchConstants.java | 37 +++++ .../elasticsearch/QuotaRatioMappingFactory.java | 67 +++++++++ .../QuotaSearchIndexCreationUtil.java | 55 ++++++++ .../events/ElasticSearchQuotaMailboxListener.java | 70 ++++++++++ .../elasticsearch/json/JsonMessageConstants.java | 28 ++++ .../elasticsearch/json/QuotaRatioAsJson.java | 120 ++++++++++++++++ .../json/QuotaRatioToElasticSearchJson.java | 49 +++++++ .../ElasticSearchQuotaConfigurationTest.java | 104 ++++++++++++++ ...lasticSearchQuotaSearchTestSystemExtension.java | 110 +++++++++++++++ .../ElasticSearchQuotaSearcherTest.java | 28 ++++ .../elasticsearch/QuotaQueryConverterTest.java | 83 +++++++++++ .../ElasticSearchQuotaMailboxListenerTest.java | 105 ++++++++++++++ .../elasticsearch/json/QuotaRatioAsJsonTest.java | 107 +++++++++++++++ .../json/QuotaRatioToElasticSearchJsonTest.java | 80 +++++++++++ .../src/test/resources/quotaRatio.json | 1 + .../src/test/resources/quotaRatioNoDomain.json | 1 + mailbox/pom.xml | 1 + 21 files changed, 1534 insertions(+) diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/pom.xml b/mailbox/plugin/quota-search-elasticsearch-v6/pom.xml new file mode 100644 index 0000000..c1eeca6 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/pom.xml @@ -0,0 +1,142 @@ +<?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> + <artifactId>apache-james-mailbox</artifactId> + <groupId>org.apache.james</groupId> + <version>3.4.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>apache-james-mailbox-quota-search-elasticsearch-v6</artifactId> + <name>Apache James :: Mailbox :: Plugin :: Quota Search :: ElasticSearch :: v6</name> + <description>Apache James Mailbox ElasticSearch v6 implementation for quota search</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-memory</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-memory</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-quota-search</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>apache-james-mailbox-quota-search</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-core</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-data-api</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-data-memory</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-testing</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-jdk8</artifactId> + </dependency> + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-migrationsupport</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>net.javacrumbs.json-unit</groupId> + <artifactId>json-unit-assertj</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + +</project> diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaConfiguration.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaConfiguration.java new file mode 100644 index 0000000..c14f339 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaConfiguration.java @@ -0,0 +1,152 @@ +/* + * 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.quota.search.elasticsearch; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.ConfigurationException; +import org.apache.james.backends.es.IndexName; +import org.apache.james.backends.es.ReadAliasName; +import org.apache.james.backends.es.WriteAliasName; + +public class ElasticSearchQuotaConfiguration { + + public static class Builder { + + private Optional<IndexName> indexQuotaRatioName; + private Optional<ReadAliasName> readAliasQuotaRatioName; + private Optional<WriteAliasName> writeAliasQuotaRatioName; + + public Builder() { + indexQuotaRatioName = Optional.empty(); + readAliasQuotaRatioName = Optional.empty(); + writeAliasQuotaRatioName = Optional.empty(); + } + + public Builder indexQuotaRatioName(IndexName indexQuotaRatioName) { + return indexQuotaRatioName(Optional.of(indexQuotaRatioName)); + } + + public Builder indexQuotaRatioName(Optional<IndexName> indexQuotaRatioName) { + this.indexQuotaRatioName = indexQuotaRatioName; + return this; + } + + public Builder readAliasQuotaRatioName(ReadAliasName readAliasQuotaRatioName) { + return readAliasQuotaRatioName(Optional.of(readAliasQuotaRatioName)); + } + + public Builder readAliasQuotaRatioName(Optional<ReadAliasName> readAliasQuotaRatioName) { + this.readAliasQuotaRatioName = readAliasQuotaRatioName; + return this; + } + + public Builder writeAliasQuotaRatioName(WriteAliasName writeAliasQuotaRatioName) { + return writeAliasQuotaRatioName(Optional.of(writeAliasQuotaRatioName)); + } + + public Builder writeAliasQuotaRatioName(Optional<WriteAliasName> writeAliasQuotaRatioName) { + this.writeAliasQuotaRatioName = writeAliasQuotaRatioName; + return this; + } + + + public ElasticSearchQuotaConfiguration build() { + return new ElasticSearchQuotaConfiguration( + indexQuotaRatioName.orElse(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_INDEX), + readAliasQuotaRatioName.orElse(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_READ_ALIAS), + writeAliasQuotaRatioName.orElse(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_WRITE_ALIAS)); + } + } + + public static Builder builder() { + return new Builder(); + } + + 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 ElasticSearchQuotaConfiguration DEFAULT_CONFIGURATION = builder().build(); + + public static ElasticSearchQuotaConfiguration fromProperties(Configuration configuration) throws ConfigurationException { + return builder() + .indexQuotaRatioName(computeQuotaSearchIndexName(configuration)) + .readAliasQuotaRatioName(computeQuotaSearchReadAlias(configuration)) + .writeAliasQuotaRatioName(computeQuotaSearchWriteAlias(configuration)) + .build(); + } + + public static Optional<IndexName> computeQuotaSearchIndexName(Configuration configuration) { + return Optional.ofNullable(configuration.getString(ELASTICSEARCH_INDEX_QUOTA_RATIO_NAME)) + .map(IndexName::new); + } + + public static Optional<WriteAliasName> computeQuotaSearchWriteAlias(Configuration configuration) { + return Optional.ofNullable(configuration.getString(ELASTICSEARCH_ALIAS_WRITE_QUOTA_RATIO_NAME)) + .map(WriteAliasName::new); + } + + public static Optional<ReadAliasName> computeQuotaSearchReadAlias(Configuration configuration) { + return Optional.ofNullable(configuration.getString(ELASTICSEARCH_ALIAS_READ_QUOTA_RATIO_NAME)) + .map(ReadAliasName::new); + } + + private final IndexName indexQuotaRatioName; + private final ReadAliasName readAliasQuotaRatioName; + private final WriteAliasName writeAliasQuotaRatioName; + + private ElasticSearchQuotaConfiguration(IndexName indexQuotaRatioName, ReadAliasName readAliasQuotaRatioName, WriteAliasName writeAliasQuotaRatioName) { + this.indexQuotaRatioName = indexQuotaRatioName; + this.readAliasQuotaRatioName = readAliasQuotaRatioName; + this.writeAliasQuotaRatioName = writeAliasQuotaRatioName; + } + + public IndexName getIndexQuotaRatioName() { + return indexQuotaRatioName; + } + + public ReadAliasName getReadAliasQuotaRatioName() { + return readAliasQuotaRatioName; + } + + public WriteAliasName getWriteAliasQuotaRatioName() { + return writeAliasQuotaRatioName; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof ElasticSearchQuotaConfiguration) { + ElasticSearchQuotaConfiguration that = (ElasticSearchQuotaConfiguration) o; + + return Objects.equals(this.indexQuotaRatioName, that.indexQuotaRatioName) + && Objects.equals(this.readAliasQuotaRatioName, that.readAliasQuotaRatioName) + && Objects.equals(this.writeAliasQuotaRatioName, that.writeAliasQuotaRatioName); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(indexQuotaRatioName, readAliasQuotaRatioName); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearcher.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearcher.java new file mode 100644 index 0000000..21d55a8 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearcher.java @@ -0,0 +1,91 @@ +/**************************************************************** + * 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.quota.search.elasticsearch; + +import static org.apache.james.quota.search.elasticsearch.QuotaRatioElasticSearchConstants.QUOTA_RATIO_TYPE; +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.USER; + +import java.util.Arrays; +import java.util.List; +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.search.ScrollIterable; +import org.apache.james.core.User; +import org.apache.james.quota.search.QuotaQuery; +import org.apache.james.quota.search.QuotaSearcher; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.search.sort.SortBuilders; +import org.elasticsearch.search.sort.SortOrder; + +import com.github.steveash.guavate.Guavate; + +public class ElasticSearchQuotaSearcher implements QuotaSearcher { + private static final TimeValue TIMEOUT = new TimeValue(60000); + + private final Client client; + private final AliasName readAlias; + private final QuotaQueryConverter quotaQueryConverter; + + public ElasticSearchQuotaSearcher(Client client, ReadAliasName readAlias) { + this.client = client; + this.readAlias = readAlias; + this.quotaQueryConverter = new QuotaQueryConverter(); + } + + @Override + public List<User> search(QuotaQuery query) { + Stream<User> results = new ScrollIterable(client, prepareSearch(query)) + .stream() + .flatMap(searchResponse -> Arrays.stream(searchResponse.getHits() + .getHits())) + .map(hit -> hit.field(USER)) + .map(field -> (String) field.getValue()) + .map(User::fromUsername) + .skip(query.getOffset().getValue()); + + return query.getLimit().getValue() + .map(results::limit) + .orElse(results) + .collect(Guavate.toImmutableList()); + } + + public SearchRequestBuilder prepareSearch(QuotaQuery query) { + SearchRequestBuilder searchRequestBuilder = client.prepareSearch(readAlias.getValue()) + .setTypes(QUOTA_RATIO_TYPE.getValue()) + .setScroll(TIMEOUT) + .addFields(USER) + .setQuery(quotaQueryConverter.from(query)); + + query.getLimit() + .getValue() + .ifPresent(searchRequestBuilder::setSize); + + searchRequestBuilder.addSort( + SortBuilders.fieldSort(USER) + .order(SortOrder.ASC)); + + return searchRequestBuilder; + } + +} \ No newline at end of file diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaQueryConverter.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaQueryConverter.java new file mode 100644 index 0000000..b02d8ac --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaQueryConverter.java @@ -0,0 +1,103 @@ +/**************************************************************** + * 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.quota.search.elasticsearch; + +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.DOMAIN; +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.QUOTA_RATIO; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.apache.james.quota.search.QuotaClause; +import org.apache.james.quota.search.QuotaClause.And; +import org.apache.james.quota.search.QuotaClause.HasDomain; +import org.apache.james.quota.search.QuotaClause.LessThan; +import org.apache.james.quota.search.QuotaClause.MoreThan; +import org.apache.james.quota.search.QuotaQuery; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.RangeQueryBuilder; +import org.elasticsearch.index.query.TermQueryBuilder; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; + +public class QuotaQueryConverter { + private final Map<Class<? extends QuotaClause>, Function<QuotaClause, QueryBuilder>> clauseConverter; + + public QuotaQueryConverter() { + Builder<Class<? extends QuotaClause>, Function<QuotaClause, QueryBuilder>> builder = ImmutableMap.builder(); + + builder.put(HasDomain.class, this::convertHasDomain); + builder.put(And.class, this::disableNestedAnd); + builder.put(MoreThan.class, this::convertMoreThan); + builder.put(LessThan.class, this::convertLessThan); + + clauseConverter = builder.build(); + } + + public QueryBuilder from(QuotaQuery query) { + List<QuotaClause> clauses = query.getClause().getClauses(); + if (clauses.isEmpty()) { + return matchAllQuery(); + } + if (clauses.size() == 1) { + return singleClauseAsESQuery(clauses.get(0)); + } + + return clausesAsAndESQuery(clauses); + } + + private BoolQueryBuilder clausesAsAndESQuery(List<QuotaClause> clauses) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + clauses.stream() + .map(this::singleClauseAsESQuery) + .forEach(boolQueryBuilder::must); + return boolQueryBuilder; + } + + private QueryBuilder disableNestedAnd(QuotaClause clause) { + throw new IllegalArgumentException("Nested \"And\" clauses are not supported"); + } + + private TermQueryBuilder convertHasDomain(QuotaClause clause) { + HasDomain hasDomain = (HasDomain) clause; + return termQuery(DOMAIN, hasDomain.getDomain().asString()); + } + + private RangeQueryBuilder convertMoreThan(QuotaClause clause) { + MoreThan moreThan = (MoreThan) clause; + return rangeQuery(QUOTA_RATIO).gte(moreThan.getQuotaBoundary().getRatio()); + } + + private RangeQueryBuilder convertLessThan(QuotaClause clause) { + LessThan lessThan = (LessThan) clause; + return rangeQuery(QUOTA_RATIO).lte(lessThan.getQuotaBoundary().getRatio()); + } + + private QueryBuilder singleClauseAsESQuery(QuotaClause clause) { + return clauseConverter.get(clause.getClass()).apply(clause); + } + +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaRatioElasticSearchConstants.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaRatioElasticSearchConstants.java new file mode 100644 index 0000000..e3ceadd --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaRatioElasticSearchConstants.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.quota.search.elasticsearch; + +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 QuotaRatioElasticSearchConstants { + + interface InjectionNames { + String QUOTA_RATIO = "quotaRatio"; + } + + WriteAliasName DEFAULT_QUOTA_RATIO_WRITE_ALIAS = new WriteAliasName("quota_ratio_write_alias"); + ReadAliasName DEFAULT_QUOTA_RATIO_READ_ALIAS = new ReadAliasName("quota_ratio_read_alias"); + IndexName DEFAULT_QUOTA_RATIO_INDEX = new IndexName("quota_ratio_v1"); + TypeName QUOTA_RATIO_TYPE = new TypeName("quota_ratio"); +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaRatioMappingFactory.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaRatioMappingFactory.java new file mode 100644 index 0000000..128f1d1 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaRatioMappingFactory.java @@ -0,0 +1,67 @@ +/**************************************************************** + * 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.quota.search.elasticsearch; + +import static org.apache.james.backends.es.NodeMappingFactory.DOUBLE; +import static org.apache.james.backends.es.NodeMappingFactory.INDEX; +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.STRING; +import static org.apache.james.backends.es.NodeMappingFactory.TYPE; +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.DOMAIN; +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.QUOTA_RATIO; +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.USER; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +public class QuotaRatioMappingFactory { + + public static XContentBuilder getMappingContent() { + try { + return jsonBuilder() + .startObject() + + .startObject(QuotaRatioElasticSearchConstants.QUOTA_RATIO_TYPE.getValue()) + .startObject(PROPERTIES) + + .startObject(USER) + .field(TYPE, STRING) + .field(INDEX, NOT_ANALYZED) + .endObject() + + .startObject(DOMAIN) + .field(TYPE, STRING) + .field(INDEX, NOT_ANALYZED) + .endObject() + + .startObject(QUOTA_RATIO) + .field(TYPE, DOUBLE) + .endObject() + .endObject() + .endObject() + .endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaSearchIndexCreationUtil.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaSearchIndexCreationUtil.java new file mode 100644 index 0000000..f546230 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/QuotaSearchIndexCreationUtil.java @@ -0,0 +1,55 @@ +/**************************************************************** + * 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.quota.search.elasticsearch; + +import org.apache.james.backends.es.AliasName; +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.elasticsearch.client.Client; + +public class QuotaSearchIndexCreationUtil { + + public static Client prepareClient(Client client, + AliasName readAlias, + AliasName writeAlias, + IndexName indexName, + ElasticSearchConfiguration configuration) { + + return NodeMappingFactory.applyMapping( + new IndexCreationFactory(configuration) + .useIndex(indexName) + .addAlias(readAlias) + .addAlias(writeAlias) + .createIndexAndAliases(client), + indexName, + QuotaRatioElasticSearchConstants.QUOTA_RATIO_TYPE, + QuotaRatioMappingFactory.getMappingContent()); + } + + public static Client prepareDefaultClient(Client client, ElasticSearchConfiguration configuration) { + return prepareClient(client, + QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_READ_ALIAS, + QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_WRITE_ALIAS, + QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_INDEX, + configuration); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/events/ElasticSearchQuotaMailboxListener.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/events/ElasticSearchQuotaMailboxListener.java new file mode 100644 index 0000000..2b828f2 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/events/ElasticSearchQuotaMailboxListener.java @@ -0,0 +1,70 @@ +/**************************************************************** + * 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.quota.search.elasticsearch.events; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.james.backends.es.ElasticSearchIndexer; +import org.apache.james.core.User; +import org.apache.james.mailbox.events.Event; +import org.apache.james.mailbox.events.Group; +import org.apache.james.mailbox.events.MailboxListener; +import org.apache.james.quota.search.elasticsearch.QuotaRatioElasticSearchConstants; +import org.apache.james.quota.search.elasticsearch.json.QuotaRatioToElasticSearchJson; + +import com.fasterxml.jackson.core.JsonProcessingException; + +public class ElasticSearchQuotaMailboxListener implements MailboxListener.GroupMailboxListener { + public static class ElasticSearchQuotaMailboxListenerGroup extends Group { + } + + private static final Group GROUP = new ElasticSearchQuotaMailboxListenerGroup(); + + private final ElasticSearchIndexer indexer; + private final QuotaRatioToElasticSearchJson quotaRatioToElasticSearchJson; + + @Inject + public ElasticSearchQuotaMailboxListener( + @Named(QuotaRatioElasticSearchConstants.InjectionNames.QUOTA_RATIO) ElasticSearchIndexer indexer, + QuotaRatioToElasticSearchJson quotaRatioToElasticSearchJson) { + this.indexer = indexer; + this.quotaRatioToElasticSearchJson = quotaRatioToElasticSearchJson; + } + + @Override + public Group getDefaultGroup() { + return GROUP; + } + + @Override + public boolean isHandling(Event event) { + return event instanceof QuotaUsageUpdatedEvent; + } + + @Override + public void event(Event event) throws JsonProcessingException { + handleEvent(event.getUser(), (QuotaUsageUpdatedEvent) event); + } + + private void handleEvent(User user, QuotaUsageUpdatedEvent event) throws JsonProcessingException { + indexer.index(user.asString(), + quotaRatioToElasticSearchJson.convertToJson(user.asString(), event)); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/JsonMessageConstants.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/JsonMessageConstants.java new file mode 100644 index 0000000..8f930ff --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/JsonMessageConstants.java @@ -0,0 +1,28 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.quota.search.elasticsearch.json; + +public interface JsonMessageConstants { + + String USER = "user"; + String DOMAIN = "domain"; + String QUOTA_RATIO = "quotaRatio"; + +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioAsJson.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioAsJson.java new file mode 100644 index 0000000..4084727 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioAsJson.java @@ -0,0 +1,120 @@ +/**************************************************************** + * 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.quota.search.elasticsearch.json; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.james.mailbox.model.QuotaRatio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +public class QuotaRatioAsJson { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String user; + private Optional<String> domain; + private QuotaRatio quotaRatio; + + private Builder() { + domain = Optional.empty(); + } + + public Builder user(String user) { + this.user = user; + return this; + } + + public Builder domain(Optional<String> domain) { + this.domain = domain; + return this; + } + + public Builder quotaRatio(QuotaRatio quotaRatio) { + this.quotaRatio = quotaRatio; + return this; + } + + public QuotaRatioAsJson build() { + Preconditions.checkState(!Strings.isNullOrEmpty(user), "'user' is mandatory"); + Preconditions.checkNotNull(quotaRatio, "'quotaRatio' is mandatory"); + + return new QuotaRatioAsJson(user, domain, quotaRatio); + } + } + + private final String user; + private final Optional<String> domain; + private final QuotaRatio quotaRatio; + + private QuotaRatioAsJson(String user, Optional<String> domain, QuotaRatio quotaRatio) { + this.user = user; + this.domain = domain; + this.quotaRatio = quotaRatio; + } + + @JsonProperty(JsonMessageConstants.USER) + public String getUser() { + return user; + } + + @JsonProperty(JsonMessageConstants.DOMAIN) + public Optional<String> getDomain() { + return domain; + } + + @JsonProperty(JsonMessageConstants.QUOTA_RATIO) + public double getMaxQuotaRatio() { + return quotaRatio.max(); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof QuotaRatioAsJson) { + QuotaRatioAsJson that = (QuotaRatioAsJson) o; + + return Objects.equals(this.quotaRatio, that.quotaRatio) + && Objects.equals(this.user, that.user) + && Objects.equals(this.domain, that.domain); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(user, domain, quotaRatio); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("quotaRatio", quotaRatio) + .add("user", user) + .add("domain", domain) + .toString(); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioToElasticSearchJson.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioToElasticSearchJson.java new file mode 100644 index 0000000..12f9629 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/main/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioToElasticSearchJson.java @@ -0,0 +1,49 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.quota.search.elasticsearch.json; + +import javax.inject.Inject; + +import org.apache.james.core.Domain; +import org.apache.james.mailbox.events.MailboxListener.QuotaUsageUpdatedEvent; +import org.apache.james.mailbox.model.QuotaRatio; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +public class QuotaRatioToElasticSearchJson { + + private final ObjectMapper mapper; + + @Inject + public QuotaRatioToElasticSearchJson() { + this.mapper = new ObjectMapper(); + this.mapper.registerModule(new Jdk8Module()); + } + + public String convertToJson(String user, QuotaUsageUpdatedEvent event) throws JsonProcessingException { + return mapper.writeValueAsString(QuotaRatioAsJson.builder() + .user(user) + .domain(event.getQuotaRoot().getDomain().map(Domain::asString)) + .quotaRatio(QuotaRatio.from(event.getSizeQuota(), event.getCountQuota())) + .build()); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaConfigurationTest.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaConfigurationTest.java new file mode 100644 index 0000000..a57a10e --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaConfigurationTest.java @@ -0,0 +1,104 @@ +/* + * 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.quota.search.elasticsearch; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.commons.configuration.ConfigurationException; +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 ElasticSearchQuotaConfigurationTest { + + @Test + public void getReadAliasQuotaRatioNameShouldReturnConfiguredValue() throws ConfigurationException { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + String name = "name"; + configuration.addProperty("elasticsearch.alias.read.quota.ratio.name", name); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchQuotaConfiguration elasticSearchConfiguration = ElasticSearchQuotaConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getReadAliasQuotaRatioName()) + .isEqualTo(new ReadAliasName(name)); + } + + @Test + public void getReadAliasQuotaRatioNameShouldReturnDefaultValueWhenMissing() throws ConfigurationException { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchQuotaConfiguration elasticSearchConfiguration = ElasticSearchQuotaConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getReadAliasQuotaRatioName()) + .isEqualTo(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_READ_ALIAS); + } + + @Test + public void getWriteAliasQuotaRatioNameShouldReturnConfiguredValue() throws ConfigurationException { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + String name = "name"; + configuration.addProperty("elasticsearch.alias.write.quota.ratio.name", name); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchQuotaConfiguration elasticSearchConfiguration = ElasticSearchQuotaConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getWriteAliasQuotaRatioName()) + .isEqualTo(new WriteAliasName(name)); + } + + @Test + public void getWriteAliasQuotaRatioNameShouldReturnDefaultValueWhenMissing() throws ConfigurationException { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchQuotaConfiguration elasticSearchConfiguration = ElasticSearchQuotaConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getWriteAliasQuotaRatioName()) + .isEqualTo(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_WRITE_ALIAS); + } + + @Test + public void getIndexQuotaRatioNameShouldReturnConfiguredValue() throws ConfigurationException { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + String name = "name"; + configuration.addProperty("elasticsearch.index.quota.ratio.name", name); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchQuotaConfiguration elasticSearchConfiguration = ElasticSearchQuotaConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getIndexQuotaRatioName()) + .isEqualTo(new IndexName(name)); + } + + @Test + public void getIndexQuotaRatioNameShouldReturnDefaultValueWhenMissing() throws ConfigurationException { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchQuotaConfiguration elasticSearchConfiguration = ElasticSearchQuotaConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getIndexQuotaRatioName()) + .isEqualTo(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_INDEX); + } +} \ No newline at end of file diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearchTestSystemExtension.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearchTestSystemExtension.java new file mode 100644 index 0000000..5e63c46 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearchTestSystemExtension.java @@ -0,0 +1,110 @@ +/**************************************************************** + * 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.quota.search.elasticsearch; + +import static org.mockito.Mockito.mock; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import org.apache.james.backends.es.DockerElasticSearch; +import org.apache.james.backends.es.DockerElasticSearchSingleton; +import org.apache.james.backends.es.ElasticSearchConfiguration; +import org.apache.james.backends.es.ElasticSearchIndexer; +import org.apache.james.dnsservice.api.DNSService; +import org.apache.james.domainlist.memory.MemoryDomainList; +import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources; +import org.apache.james.mailbox.store.quota.QuotaComponents; +import org.apache.james.quota.search.QuotaSearchTestSystem; +import org.apache.james.quota.search.elasticsearch.events.ElasticSearchQuotaMailboxListener; +import org.apache.james.quota.search.elasticsearch.json.QuotaRatioToElasticSearchJson; +import org.apache.james.user.memory.MemoryUsersRepository; +import org.apache.james.util.concurrent.NamedThreadFactory; +import org.elasticsearch.client.Client; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +public class ElasticSearchQuotaSearchTestSystemExtension implements ParameterResolver, BeforeEachCallback, AfterEachCallback { + + private final DockerElasticSearch elasticSearch = DockerElasticSearchSingleton.INSTANCE; + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return (parameterContext.getParameter().getType() == QuotaSearchTestSystem.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + try { + Client client = QuotaSearchIndexCreationUtil.prepareDefaultClient( + elasticSearch.clientProvider().get(), + ElasticSearchConfiguration.builder() + .addHost(elasticSearch.getTcpHost()) + .build()); + + InMemoryIntegrationResources resources = InMemoryIntegrationResources.defaultResources(); + + MemoryUsersRepository usersRepository = MemoryUsersRepository.withVirtualHosting(); + + DNSService dnsService = mock(DNSService.class); + MemoryDomainList domainList = new MemoryDomainList(dnsService); + usersRepository.setDomainList(domainList); + + ThreadFactory threadFactory = NamedThreadFactory.withClassName(getClass()); + ElasticSearchQuotaMailboxListener listener = new ElasticSearchQuotaMailboxListener( + new ElasticSearchIndexer(client, Executors.newSingleThreadExecutor(threadFactory), + QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_WRITE_ALIAS, + QuotaRatioElasticSearchConstants.QUOTA_RATIO_TYPE), + new QuotaRatioToElasticSearchJson()); + + resources.getMailboxManager().getEventBus().register(listener); + + QuotaComponents quotaComponents = resources.getMailboxManager().getQuotaComponents(); + + return new QuotaSearchTestSystem( + quotaComponents.getMaxQuotaManager(), + resources.getMailboxManager(), + quotaComponents.getQuotaManager(), + resources.getDefaultUserQuotaRootResolver(), + new ElasticSearchQuotaSearcher(client, + QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_READ_ALIAS), + usersRepository, + domainList, + resources.getCurrentQuotaManager(), + () -> elasticSearch.awaitForElasticSearch()); + } catch (Exception e) { + throw new ParameterResolutionException("Error while resolving parameter", e); + } + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + elasticSearch.start(); + } + + @Override + public void afterEach(ExtensionContext context) { + elasticSearch.cleanUpData(); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearcherTest.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearcherTest.java new file mode 100644 index 0000000..ad26532 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/ElasticSearchQuotaSearcherTest.java @@ -0,0 +1,28 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.quota.search.elasticsearch; + +import org.apache.james.quota.search.QuotaSearcherContract; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(ElasticSearchQuotaSearchTestSystemExtension.class) +public class ElasticSearchQuotaSearcherTest implements QuotaSearcherContract { + +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/QuotaQueryConverterTest.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/QuotaQueryConverterTest.java new file mode 100644 index 0000000..86daa88 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/QuotaQueryConverterTest.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.quota.search.elasticsearch; + +import static org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants.QUOTA_RATIO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; + +import org.apache.james.core.Domain; +import org.apache.james.quota.search.QuotaBoundary; +import org.apache.james.quota.search.QuotaQuery; +import org.apache.james.quota.search.elasticsearch.json.JsonMessageConstants; +import org.elasticsearch.index.query.QueryBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class QuotaQueryConverterTest { + private QuotaQueryConverter testee; + + @BeforeEach + void setup() { + testee = new QuotaQueryConverter(); + } + + @Test + void fromShouldReturnMatchAllWhenEmptyClauses() { + QuotaQuery query = QuotaQuery.builder().build(); + QueryBuilder expected = matchAllQuery(); + + QueryBuilder actual = testee.from(query); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + void fromShouldReturnDomainMatchWhenOnlyDomain() { + QuotaQuery query = QuotaQuery.builder().hasDomain(Domain.of("my.tld")).build(); + QueryBuilder expected = termQuery(JsonMessageConstants.DOMAIN, "my.tld"); + + QueryBuilder actual = testee.from(query); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + void fromShouldReturnQuotaRatioMatchWhenLessThan() { + QuotaQuery query = QuotaQuery.builder().lessThan(new QuotaBoundary(0.1)).build(); + QueryBuilder expected = rangeQuery(QUOTA_RATIO).lte(0.1); + + QueryBuilder actual = testee.from(query); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + void fromShouldReturnQuotaRatioMatchWhenMoreThan() { + QuotaQuery query = QuotaQuery.builder().moreThan(new QuotaBoundary(0.1)).build(); + QueryBuilder expected = rangeQuery(QUOTA_RATIO).gte(0.1); + + QueryBuilder actual = testee.from(query); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/events/ElasticSearchQuotaMailboxListenerTest.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/events/ElasticSearchQuotaMailboxListenerTest.java new file mode 100644 index 0000000..d3f7811 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/events/ElasticSearchQuotaMailboxListenerTest.java @@ -0,0 +1,105 @@ +/**************************************************************** + * 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.quota.search.elasticsearch.events; + +import static org.apache.james.quota.search.QuotaSearchFixture.TestConstants.BOB_USER; +import static org.apache.james.quota.search.QuotaSearchFixture.TestConstants.NOW; +import static org.apache.james.quota.search.QuotaSearchFixture.TestConstants.QUOTAROOT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.mockito.Mockito.mock; + +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.events.Event; +import org.apache.james.mailbox.events.Group; +import org.apache.james.mailbox.quota.QuotaFixture.Counts; +import org.apache.james.mailbox.quota.QuotaFixture.Sizes; +import org.apache.james.mailbox.store.event.EventFactory; +import org.apache.james.quota.search.elasticsearch.QuotaRatioElasticSearchConstants; +import org.apache.james.quota.search.elasticsearch.QuotaSearchIndexCreationUtil; +import org.apache.james.quota.search.elasticsearch.json.QuotaRatioToElasticSearchJson; +import org.apache.james.util.concurrent.NamedThreadFactory; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.Client; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +public class ElasticSearchQuotaMailboxListenerTest { + private static Event.EventId EVENT_ID = Event.EventId.of("6e0dd59d-660e-4d9b-b22f-0354479f47b4"); + + private static final int BATCH_SIZE = 1; + private static final Event DUMB_EVENT = mock(Event.class); + + @Rule + public DockerElasticSearchRule elasticSearch = new DockerElasticSearchRule(); + private ElasticSearchQuotaMailboxListener quotaMailboxListener; + private Client client; + + @Before + public void setUp() { + client = QuotaSearchIndexCreationUtil.prepareDefaultClient( + elasticSearch.clientProvider().get(), + ElasticSearchConfiguration.builder() + .addHost(elasticSearch.getTcpHost()) + .build()); + + ThreadFactory threadFactory = NamedThreadFactory.withClassName(getClass()); + quotaMailboxListener = new ElasticSearchQuotaMailboxListener( + new ElasticSearchIndexer(client, + Executors.newSingleThreadExecutor(threadFactory), + QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_WRITE_ALIAS, + QuotaRatioElasticSearchConstants.QUOTA_RATIO_TYPE, + BATCH_SIZE), + new QuotaRatioToElasticSearchJson()); + } + + @Test + public void deserializeElasticSearchQuotaMailboxListenerGroup() throws Exception { + assertThat(Group.deserialize("org.apache.james.quota.search.elasticsearch.events.ElasticSearchQuotaMailboxListener$ElasticSearchQuotaMailboxListenerGroup")) + .isEqualTo(new ElasticSearchQuotaMailboxListener.ElasticSearchQuotaMailboxListenerGroup()); + } + + @Test + public void eventShouldIndexEventWhenQuotaEvent() throws Exception { + quotaMailboxListener.event(EventFactory.quotaUpdated() + .eventId(EVENT_ID) + .user(BOB_USER) + .quotaRoot(QUOTAROOT) + .quotaCount(Counts._52_PERCENT) + .quotaSize(Sizes._55_PERCENT) + .instant(NOW) + .build()); + + elasticSearch.awaitForElasticSearch(); + + SearchResponse searchResponse = client.prepareSearch(QuotaRatioElasticSearchConstants.DEFAULT_QUOTA_RATIO_READ_ALIAS.getValue()) + .setTypes(QuotaRatioElasticSearchConstants.QUOTA_RATIO_TYPE.getValue()) + .setQuery(matchAllQuery()) + .execute() + .get(); + assertThat(searchResponse.getHits().totalHits()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioAsJsonTest.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioAsJsonTest.java new file mode 100644 index 0000000..b2c6411 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioAsJsonTest.java @@ -0,0 +1,107 @@ +/**************************************************************** + * 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.quota.search.elasticsearch.json; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Optional; + +import org.apache.james.core.quota.QuotaCount; +import org.apache.james.core.quota.QuotaSize; +import org.apache.james.mailbox.model.Quota; +import org.apache.james.mailbox.model.QuotaRatio; +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +public class QuotaRatioAsJsonTest { + + private static final Quota<QuotaSize> QUOTA_SIZE = Quota.<QuotaSize>builder() + .used(QuotaSize.size(15)) + .computedLimit(QuotaSize.size(60)) + .build(); + private static final Quota<QuotaCount> QUOTA_COUNT = Quota.<QuotaCount>builder() + .used(QuotaCount.count(1)) + .computedLimit(QuotaCount.count(2)) + .build(); + + @Test + public void shouldMatchBeanContract() { + EqualsVerifier.forClass(QuotaRatioAsJson.class) + .verify(); + } + + @Test + public void buildShouldThrownWhenUserIsNull() { + assertThatThrownBy(() -> QuotaRatioAsJson.builder() + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void buildShouldThrownWhenUserIsEmpty() { + assertThatThrownBy(() -> QuotaRatioAsJson.builder() + .user("") + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void buildShouldThrownWhenQuotaRatioIsNull() { + assertThatThrownBy(() -> QuotaRatioAsJson.builder() + .user("user") + .build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + public void getDomainShouldReturnEmptyWhenNone() { + QuotaRatioAsJson quotaRatioAsJson = QuotaRatioAsJson.builder() + .user("user") + .quotaRatio(QuotaRatio.from(QUOTA_SIZE, QUOTA_COUNT)) + .build(); + + assertThat(quotaRatioAsJson.getDomain()).isEmpty(); + } + + @Test + public void getDomainShouldReturnTheDomainWhenGiven() { + String domain = "domain"; + QuotaRatioAsJson quotaRatioAsJson = QuotaRatioAsJson.builder() + .user("user") + .domain(Optional.of(domain)) + .quotaRatio(QuotaRatio.from(QUOTA_SIZE, QUOTA_COUNT)) + .build(); + + assertThat(quotaRatioAsJson.getDomain()).contains(domain); + } + + @Test + public void getMaxQuotaRatioShouldReturnTheMaxQuotaRatio() { + String domain = "domain"; + QuotaRatioAsJson quotaRatioAsJson = QuotaRatioAsJson.builder() + .user("user") + .domain(Optional.of(domain)) + .quotaRatio(QuotaRatio.from(QUOTA_SIZE, QUOTA_COUNT)) + .build(); + + assertThat(quotaRatioAsJson.getMaxQuotaRatio()).isEqualTo(0.5); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioToElasticSearchJsonTest.java b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioToElasticSearchJsonTest.java new file mode 100644 index 0000000..4f42c92 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/java/org/apache/james/quota/search/elasticsearch/json/QuotaRatioToElasticSearchJsonTest.java @@ -0,0 +1,80 @@ +/**************************************************************** + * 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.quota.search.elasticsearch.json; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER; + +import java.io.IOException; +import java.time.Instant; +import java.util.Optional; + +import org.apache.james.core.Domain; +import org.apache.james.core.User; +import org.apache.james.mailbox.events.Event; +import org.apache.james.mailbox.events.MailboxListener.QuotaUsageUpdatedEvent; +import org.apache.james.mailbox.model.QuotaRoot; +import org.apache.james.mailbox.quota.QuotaFixture; +import org.apache.james.mailbox.store.event.EventFactory; +import org.apache.james.util.ClassLoaderUtils; +import org.junit.jupiter.api.Test; + +class QuotaRatioToElasticSearchJsonTest { + private static Event.EventId EVENT_ID = Event.EventId.of("6e0dd59d-660e-4d9b-b22f-0354479f47b4"); + + @Test + void quotaRatioShouldBeWellConvertedToJson() throws IOException { + String user = "[email protected]"; + QuotaUsageUpdatedEvent event = EventFactory.quotaUpdated() + .eventId(EVENT_ID) + .user(User.fromUsername(user)) + .quotaRoot(QuotaRoot.quotaRoot(user, Optional.of(Domain.of("domain.org")))) + .quotaCount(QuotaFixture.Counts._52_PERCENT) + .quotaSize(QuotaFixture.Sizes._55_PERCENT) + .instant(Instant.now()) + .build(); + QuotaRatioToElasticSearchJson quotaRatioToElasticSearchJson = new QuotaRatioToElasticSearchJson(); + String convertToJson = quotaRatioToElasticSearchJson.convertToJson(user, event); + + assertThatJson(convertToJson) + .when(IGNORING_ARRAY_ORDER) + .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("quotaRatio.json")); + } + + @Test + void quotaRatioShouldBeWellConvertedToJsonWhenNoDomain() throws IOException { + String user = "user"; + QuotaUsageUpdatedEvent event = EventFactory.quotaUpdated() + .eventId(EVENT_ID) + .user(User.fromUsername(user)) + .quotaRoot(QuotaRoot.quotaRoot(user, Optional.empty())) + .quotaCount(QuotaFixture.Counts._52_PERCENT) + .quotaSize(QuotaFixture.Sizes._55_PERCENT) + .instant(Instant.now()) + .build(); + + + QuotaRatioToElasticSearchJson quotaRatioToElasticSearchJson = new QuotaRatioToElasticSearchJson(); + String convertToJson = quotaRatioToElasticSearchJson.convertToJson(user, event); + + assertThatJson(convertToJson) + .when(IGNORING_ARRAY_ORDER) + .isEqualTo(ClassLoaderUtils.getSystemResourceAsString("quotaRatioNoDomain.json")); + } +} diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/resources/quotaRatio.json b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/resources/quotaRatio.json new file mode 100644 index 0000000..8b39489 --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/resources/quotaRatio.json @@ -0,0 +1 @@ +{"user":"[email protected]","domain":"domain.org","quotaRatio":0.55} \ No newline at end of file diff --git a/mailbox/plugin/quota-search-elasticsearch-v6/src/test/resources/quotaRatioNoDomain.json b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/resources/quotaRatioNoDomain.json new file mode 100644 index 0000000..b3e2f2e --- /dev/null +++ b/mailbox/plugin/quota-search-elasticsearch-v6/src/test/resources/quotaRatioNoDomain.json @@ -0,0 +1 @@ +{"user":"user","domain":null,"quotaRatio":0.55} \ No newline at end of file diff --git a/mailbox/pom.xml b/mailbox/pom.xml index 7e3dcd2..449bb35 100644 --- a/mailbox/pom.xml +++ b/mailbox/pom.xml @@ -57,6 +57,7 @@ <module>plugin/quota-mailing-memory</module> <module>plugin/quota-search</module> <module>plugin/quota-search-elasticsearch</module> + <module>plugin/quota-search-elasticsearch-v6</module> <module>plugin/quota-search-scanning</module> <module>plugin/spamassassin</module> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
