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 2018/05/08 08:42:40 UTC
[1/3] james-project git commit: MAILBOX-331 Implement Quota Threshold
notifier as an EventSourcing project
Repository: james-project
Updated Branches:
refs/heads/master b03574767 -> 0e437d259
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
new file mode 100644
index 0000000..e652e0b
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNoticeTest.java
@@ -0,0 +1,202 @@
+/****************************************************************
+ * 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.quota.mailing.subscribers;
+
+import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.AlreadyReachedDuringGracePriod;
+import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.NOW;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._80;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Optional;
+
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaCount;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.model.HistoryEvolution;
+import org.apache.james.mailbox.quota.model.QuotaThresholdChange;
+import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Counts;
+import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Sizes;
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class QuotaThresholdNoticeTest {
+
+ @Test
+ void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(QuotaThresholdNotice.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ void buildShouldReturnEmptyWhenNoThresholds() {
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._82_PERCENT)
+ .build())
+ .isEmpty();
+ }
+
+ @Test
+ void buildShouldReturnEmptyWhenNoChanges() {
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._82_PERCENT)
+ .sizeThreshold(HistoryEvolution.noChanges())
+ .build())
+ .isEmpty();
+ }
+
+ @Test
+ void buildShouldReturnEmptyWhenBelow() {
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._82_PERCENT)
+ .sizeThreshold(HistoryEvolution.lowerThresholdReached(new QuotaThresholdChange(_80, NOW)))
+ .build())
+ .isEmpty();
+ }
+
+ @Test
+ void buildShouldReturnEmptyWhenAboveButRecentChanges() {
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._82_PERCENT)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(new QuotaThresholdChange(_80, NOW), AlreadyReachedDuringGracePriod))
+ .build())
+ .isEmpty();
+ }
+
+ @Test
+ void buildShouldReturnPresentWhenAbove() {
+ Quota<QuotaSize> sizeQuota = Sizes._82_PERCENT;
+ Quota<QuotaCount> countQuota = Counts._82_PERCENT;
+ QuotaThresholdChange sizeThresholdChange = new QuotaThresholdChange(_80, NOW);
+
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(sizeQuota)
+ .countQuota(countQuota)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(sizeThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .build())
+ .isNotEmpty()
+ .contains(new QuotaThresholdNotice(Optional.empty(), Optional.of(sizeThresholdChange.getQuotaThreshold()), sizeQuota, countQuota));
+ }
+
+ @Test
+ void buildShouldFilterOutNotInterestingFields() {
+ Quota<QuotaSize> sizeQuota = Sizes._82_PERCENT;
+ Quota<QuotaCount> countQuota = Counts._82_PERCENT;
+ QuotaThresholdChange sizeThresholdChange = new QuotaThresholdChange(_80, NOW);
+ QuotaThresholdChange countThresholdChange = new QuotaThresholdChange(_80, NOW);
+
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(sizeQuota)
+ .countQuota(countQuota)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(sizeThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .countThreshold(HistoryEvolution.lowerThresholdReached(countThresholdChange))
+ .build())
+ .isNotEmpty()
+ .contains(new QuotaThresholdNotice(Optional.empty(), Optional.of(sizeThresholdChange.getQuotaThreshold()), sizeQuota, countQuota));
+ }
+
+ @Test
+ void buildShouldKeepAllInterestingFields() {
+ Quota<QuotaSize> sizeQuota = Sizes._82_PERCENT;
+ Quota<QuotaCount> countQuota = Counts._82_PERCENT;
+ QuotaThresholdChange sizeThresholdChange = new QuotaThresholdChange(_80, NOW);
+ QuotaThresholdChange countThresholdChange = new QuotaThresholdChange(_80, NOW);
+
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(sizeQuota)
+ .countQuota(countQuota)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(sizeThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .countThreshold(HistoryEvolution.higherThresholdReached(countThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .build())
+ .isNotEmpty()
+ .contains(new QuotaThresholdNotice(Optional.of(countThresholdChange.getQuotaThreshold()), Optional.of(sizeThresholdChange.getQuotaThreshold()), sizeQuota, countQuota));
+ }
+
+ @Test
+ void generateReportShouldGenerateAHumanReadableMessage() {
+ QuotaThresholdChange sizeThresholdChange = new QuotaThresholdChange(_80, NOW);
+ QuotaThresholdChange countThresholdChange = new QuotaThresholdChange(_80, NOW);
+
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._92_PERCENT)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(sizeThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .countThreshold(HistoryEvolution.higherThresholdReached(countThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .build()
+ .get()
+ .generateReport())
+ .isEqualTo("You receive this email because you recently exceeded a threshold related to the quotas of your email account.\n" +
+ "\n" +
+ "You currently occupy more than 80 % of the total size allocated to you.\n" +
+ "You currently occupy 82 bytes on a total of 100 bytes allocated to you.\n" +
+ "\n" +
+ "You currently occupy more than 80 % of the total message count allocated to you.\n" +
+ "You currently have 92 messages on a total of 100 allowed for you.\n" +
+ "\n" +
+ "You need to be aware that actions leading to exceeded quotas will be denied. This will result in a degraded service.\n" +
+ "To mitigate this issue you might reach your administrator in order to increase your configured quota. You might also delete some non important emails.");
+ }
+
+ @Test
+ void generateReportShouldOmitCountPartWhenNone() {
+ QuotaThresholdChange sizeThresholdChange = new QuotaThresholdChange(_80, NOW);
+
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._72_PERCENT)
+ .sizeThreshold(HistoryEvolution.higherThresholdReached(sizeThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .build()
+ .get()
+ .generateReport())
+ .isEqualTo("You receive this email because you recently exceeded a threshold related to the quotas of your email account.\n" +
+ "\n" +
+ "You currently occupy more than 80 % of the total size allocated to you.\n" +
+ "You currently occupy 82 bytes on a total of 100 bytes allocated to you.\n" +
+ "\n" +
+ "You need to be aware that actions leading to exceeded quotas will be denied. This will result in a degraded service.\n" +
+ "To mitigate this issue you might reach your administrator in order to increase your configured quota. You might also delete some non important emails.");
+ }
+
+ @Test
+ void generateReportShouldOmitSizePartWhenNone() {
+ QuotaThresholdChange countThresholdChange = new QuotaThresholdChange(_80, NOW);
+
+ assertThat(QuotaThresholdNotice.builder()
+ .sizeQuota(Sizes._82_PERCENT)
+ .countQuota(Counts._92_PERCENT)
+ .countThreshold(HistoryEvolution.higherThresholdReached(countThresholdChange, NotAlreadyReachedDuringGracePeriod))
+ .build()
+ .get()
+ .generateReport())
+ .isEqualTo("You receive this email because you recently exceeded a threshold related to the quotas of your email account.\n" +
+ "\n" +
+ "You currently occupy more than 80 % of the total message count allocated to you.\n" +
+ "You currently have 92 messages on a total of 100 allowed for you.\n" +
+ "\n" +
+ "You need to be aware that actions leading to exceeded quotas will be denied. This will result in a degraded service.\n" +
+ "To mitigate this issue you might reach your administrator in order to increase your configured quota. You might also delete some non important emails.");
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/HistoryEvolutionTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/HistoryEvolutionTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/HistoryEvolutionTest.java
new file mode 100644
index 0000000..566991c
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/HistoryEvolutionTest.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.mailbox.quota.model;
+
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.NOW;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._75;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class HistoryEvolutionTest {
+
+ private static final QuotaThresholdChange SAMPLE_THRESHOLD = new QuotaThresholdChange(_75, NOW);
+
+ @Test
+ void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(HistoryEvolution.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ void isModifiedShouldReturnFalseWhenNoChange() {
+ assertThat(
+ HistoryEvolution.noChanges()
+ .isChange())
+ .isFalse();
+ }
+
+ @Test
+ void isModifiedShouldReturnTrueWhenLowerThresholdReached() {
+ assertThat(
+ HistoryEvolution.lowerThresholdReached(SAMPLE_THRESHOLD)
+ .isChange())
+ .isTrue();
+ }
+
+ @Test
+ void isModifiedShouldReturnTrueWhenHigherThresholdAlreadyReachedWithinGracePeriod() {
+ assertThat(
+ HistoryEvolution.higherThresholdReached(SAMPLE_THRESHOLD, HistoryEvolution.HighestThresholdRecentness.AlreadyReachedDuringGracePriod)
+ .isChange())
+ .isTrue();
+ }
+
+ @Test
+ void isModifiedShouldReturnTrueWhenHigherThresholdReachedNotAlreadyReachedWithinGracePeriod() {
+ assertThat(
+ HistoryEvolution.higherThresholdReached(SAMPLE_THRESHOLD, HistoryEvolution.HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod)
+ .isChange())
+ .isTrue();
+ }
+
+ @Test
+ void currentThresholdNotRecentlyReachedShouldReturnFalseWhenNoChange() {
+ assertThat(
+ HistoryEvolution.noChanges()
+ .currentThresholdNotRecentlyReached())
+ .isFalse();
+ }
+
+ @Test
+ void currentThresholdNotRecentlyReachedShouldReturnFalseWhenLowerThresholdReached() {
+ assertThat(
+ HistoryEvolution.lowerThresholdReached(SAMPLE_THRESHOLD)
+ .currentThresholdNotRecentlyReached())
+ .isFalse();
+ }
+
+ @Test
+ void currentThresholdNotRecentlyReachedShouldReturnFalseWhenHigherThresholdReachedAlreadyReachedWithinGracePeriod() {
+ assertThat(
+ HistoryEvolution.higherThresholdReached(SAMPLE_THRESHOLD, HistoryEvolution.HighestThresholdRecentness.AlreadyReachedDuringGracePriod)
+ .currentThresholdNotRecentlyReached())
+ .isFalse();
+ }
+
+ @Test
+ void currentThresholdNotRecentlyReachedShouldReturnTrueWhenHigherThresholdReachedNotAlreadyReachedWithinGracePeriod() {
+ assertThat(
+ HistoryEvolution.higherThresholdReached(SAMPLE_THRESHOLD, HistoryEvolution.HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod)
+ .currentThresholdNotRecentlyReached())
+ .isTrue();
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdChangeTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdChangeTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdChangeTest.java
new file mode 100644
index 0000000..5fb150e
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdChangeTest.java
@@ -0,0 +1,59 @@
+/****************************************************************
+ * 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.quota.model;
+
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.ONE_HOUR_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.THREE_HOURS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWO_HOURS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._75;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class QuotaThresholdChangeTest {
+ @Test
+ void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(QuotaThresholdChange.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ void isAfterShouldReturnTrueWhenRecent() {
+ QuotaThresholdChange change = new QuotaThresholdChange(_75, TWO_HOURS_AGO);
+ assertThat(change.isAfter(THREE_HOURS_AGO)).isTrue();
+ }
+
+ @Test
+ void isAfterShouldReturnFalseWhenOld() {
+ QuotaThresholdChange change = new QuotaThresholdChange(_75, TWO_HOURS_AGO);
+
+ assertThat(change.isAfter(ONE_HOUR_AGO)).isFalse();
+ }
+
+ @Test
+ void isAfterShouldReturnFalseWhenSameInstant() {
+ QuotaThresholdChange change = new QuotaThresholdChange(_75, TWO_HOURS_AGO);
+
+ assertThat(change.isAfter(TWO_HOURS_AGO)).isFalse();
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdFixture.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdFixture.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdFixture.java
new file mode 100644
index 0000000..1770420
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdFixture.java
@@ -0,0 +1,149 @@
+/****************************************************************
+ * 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.quota.model;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+
+import org.apache.james.mailbox.mock.MockMailboxSession;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mailbox.quota.QuotaCount;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration;
+import org.apache.mailet.base.MailAddressFixture;
+import org.apache.mailet.base.test.FakeMailContext;
+
+import com.google.common.collect.ImmutableList;
+
+public interface QuotaThresholdFixture {
+ QuotaThreshold _50 = new QuotaThreshold(0.50);
+ QuotaThreshold _75 = new QuotaThreshold(0.75);
+ QuotaThreshold _759 = new QuotaThreshold(0.759);
+ QuotaThreshold _80 = new QuotaThreshold(0.8);
+ QuotaThreshold _90 = new QuotaThreshold(0.9);
+ QuotaThreshold _95 = new QuotaThreshold(0.95);
+ QuotaThreshold _99 = new QuotaThreshold(0.99);
+
+ interface Quotas {
+ interface Counts {
+ Quota<QuotaCount> _32_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(32))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+
+ Quota<QuotaCount> _40_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(40))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+
+ Quota<QuotaCount> _52_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(52))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+
+ Quota<QuotaCount> _72_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(72))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+
+ Quota<QuotaCount> _82_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(82))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+
+ Quota<QuotaCount> _85_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(85))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+
+ Quota<QuotaCount> _92_PERCENT = Quota.<QuotaCount>builder()
+ .used(QuotaCount.count(92))
+ .computedLimit(QuotaCount.count(100))
+ .build();
+ }
+ interface Sizes {
+ Quota<QuotaSize> _30_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(30))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+ Quota<QuotaSize> _42_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(42))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+
+ Quota<QuotaSize> _55_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(55))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+
+ Quota<QuotaSize> _60_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(60))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+
+ Quota<QuotaSize> _75_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(75))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+
+ Quota<QuotaSize> _82_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(82))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+
+ Quota<QuotaSize> _92_PERCENT = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(92))
+ .computedLimit(QuotaSize.size(100))
+ .build();
+
+ Quota<QuotaSize> _992_PERTHOUSAND = Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(992))
+ .computedLimit(QuotaSize.size(1000))
+ .build();
+ }
+ }
+
+ interface TestConstants {
+ Duration GRACE_PERIOD = Duration.ofDays(1);
+ QuotaThresholds SINGLE_THRESHOLD = new QuotaThresholds(ImmutableList.of(_50));
+ QuotaMailingListenerConfiguration DEFAULT_CONFIGURATION = new QuotaMailingListenerConfiguration(SINGLE_THRESHOLD, GRACE_PERIOD);
+ String BOB = "bob@domain";
+ MockMailboxSession BOB_SESSION = new MockMailboxSession(BOB);
+ Instant NOW = Instant.now();
+ QuotaRoot QUOTAROOT = QuotaRoot.quotaRoot("any", Optional.empty());
+ Instant ONE_HOUR_AGO = NOW.minus(Duration.ofHours(1));
+ Instant TWO_HOURS_AGO = NOW.minus(Duration.ofHours(2));
+ Instant THREE_HOURS_AGO = NOW.minus(Duration.ofHours(3));
+ Instant SIX_HOURS_AGO = NOW.minus(Duration.ofHours(6));
+ Instant TWELVE_HOURS_AGO = NOW.minus(Duration.ofHours(12));
+ Instant TWO_DAYS_AGO = NOW.minus(Duration.ofDays(2));
+ Instant SIX_DAYS_AGO = NOW.minus(Duration.ofDays(6));
+ Instant TWELVE_DAYS_AGO = NOW.minus(Duration.ofDays(12));
+ }
+
+ static FakeMailContext mailetContext() {
+ return FakeMailContext.builder()
+ .postmaster(MailAddressFixture.POSTMASTER_AT_JAMES)
+ .build();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistoryTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistoryTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistoryTest.java
new file mode 100644
index 0000000..9ea774d
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistoryTest.java
@@ -0,0 +1,87 @@
+/****************************************************************
+ * 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.quota.model;
+
+import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.AlreadyReachedDuringGracePriod;
+import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.GRACE_PERIOD;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.NOW;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._50;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._75;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class QuotaThresholdHistoryTest {
+
+ @Test
+ public void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(QuotaThresholdHistory.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ public void compareWithCurrentThresholdShouldReturnAboveWhenStrictlyAboveDuringDuration() {
+ assertThat(
+ new QuotaThresholdHistory(
+ new QuotaThresholdChange(_50, NOW.minus(Duration.ofDays(24))),
+ new QuotaThresholdChange(_75, NOW.minus(Duration.ofDays(12))),
+ new QuotaThresholdChange(_50, NOW.minus(Duration.ofDays(6))))
+ .compareWithCurrentThreshold(new QuotaThresholdChange(_75, NOW), GRACE_PERIOD))
+ .isEqualTo(HistoryEvolution.higherThresholdReached(new QuotaThresholdChange(_75, NOW), NotAlreadyReachedDuringGracePeriod));
+ }
+
+ @Test
+ public void compareWithCurrentThresholdShouldReturnBelowWhenLowerThanLastChange() {
+ assertThat(
+ new QuotaThresholdHistory(
+ new QuotaThresholdChange(_50, NOW.minus(Duration.ofDays(24))),
+ new QuotaThresholdChange(_75, NOW.minus(Duration.ofDays(12))))
+ .compareWithCurrentThreshold(new QuotaThresholdChange(_50, NOW), GRACE_PERIOD))
+ .isEqualTo(HistoryEvolution.lowerThresholdReached(new QuotaThresholdChange(_50, NOW)));
+ }
+
+ @Test
+ public void compareWithCurrentThresholdShouldReturnNoChangeWhenEqualsLastChange() {
+ assertThat(
+ new QuotaThresholdHistory(
+ new QuotaThresholdChange(_50, NOW.minus(Duration.ofDays(24))),
+ new QuotaThresholdChange(_75, NOW.minus(Duration.ofDays(12))))
+ .compareWithCurrentThreshold(new QuotaThresholdChange(_75, NOW), GRACE_PERIOD))
+ .isEqualTo(HistoryEvolution.noChanges());
+ }
+
+ @Test
+ public void compareWithCurrentThresholdShouldReturnAboveWithRecentChangesWhenThresholdExceededDuringDuration() {
+ assertThat(
+ new QuotaThresholdHistory(
+ new QuotaThresholdChange(_50, NOW.minus(Duration.ofDays(24))),
+ new QuotaThresholdChange(_75, NOW.minus(Duration.ofHours(12))),
+ new QuotaThresholdChange(_50, NOW.minus(Duration.ofHours(6))))
+ .compareWithCurrentThreshold(new QuotaThresholdChange(_75, NOW), GRACE_PERIOD))
+ .isEqualTo(HistoryEvolution.higherThresholdReached(new QuotaThresholdChange(_75, NOW), AlreadyReachedDuringGracePriod));
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdTest.java
new file mode 100644
index 0000000..f46eb38
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdTest.java
@@ -0,0 +1,141 @@
+/****************************************************************
+ * 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.quota.model;
+
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._75;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._759;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._90;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Sizes;
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class QuotaThresholdTest {
+
+ @Test
+ public void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(QuotaThreshold.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ public void constructorShouldThrowBelowLowerValue() {
+ assertThatThrownBy(() -> new QuotaThreshold(-0.00001))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+
+ @Test
+ public void constructorShouldThrowAboveUpperValue() {
+ assertThatThrownBy(() -> new QuotaThreshold(1.00001))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void constructorShouldNotThrowOnLowerValue() {
+ assertThatCode(() -> new QuotaThreshold(0.))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void constructorShouldNotThrowOnUpperValue() {
+ assertThatCode(() -> new QuotaThreshold(1.))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void isExceededShouldReturnFalseWhenBelowThreshold() {
+ assertThat(_75.isExceeded(Sizes._60_PERCENT))
+ .isFalse();
+ }
+
+ @Test
+ public void isExceededShouldReturnTrueWhenAboveThreshold() {
+ assertThat(_75.isExceeded(Sizes._82_PERCENT))
+ .isTrue();
+ }
+
+ @Test
+ public void isExceededShouldReturnFalseWhenOnThreshold() {
+ assertThat(_75.isExceeded(Sizes._75_PERCENT))
+ .isFalse();
+ }
+
+ @Test
+ public void isExceededShouldReturnFalseWhenUnlimited() {
+ Quota<QuotaSize> quota = Quota.<QuotaSize>builder()
+ .computedLimit(QuotaSize.unlimited())
+ .used(QuotaSize.size(80))
+ .build();
+
+ assertThat(_75.isExceeded(quota))
+ .isFalse();
+ }
+
+ @Test
+ public void nonZeroShouldFilterZero() {
+ assertThat(QuotaThreshold.ZERO.nonZero())
+ .isEmpty();
+ }
+
+ @Test
+ public void nonZeroShouldNotFilterNonZeroValues() {
+ assertThat(_75.nonZero())
+ .contains(_75);
+ }
+
+ @Test
+ public void getQuotaOccupationRatioAsPercentShouldReturnIntRepresentationOfThreshold() {
+ assertThat(_75.getQuotaOccupationRatioAsPercent())
+ .isEqualTo(75);
+ }
+
+ @Test
+ public void getQuotaOccupationRatioAsPercentShouldTruncateValues() {
+ assertThat(_759.getQuotaOccupationRatioAsPercent())
+ .isEqualTo(75);
+ }
+
+ @Test
+ public void compareToShouldReturnNegativeWhenLowerThanComparedValue() {
+ assertThat(_75.compareTo(_90))
+ .isLessThan(0);
+ }
+
+ @Test
+ public void compareToShouldReturnPositiveWhenHigherThanComparedValue() {
+ assertThat(_90.compareTo(_75))
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void compareToShouldReturnZeroWhenEquals() {
+ assertThat(_75.compareTo(_75))
+ .isEqualTo(0);
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdsTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdsTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdsTest.java
new file mode 100644
index 0000000..09245eb
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/model/QuotaThresholdsTest.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.quota.model;
+
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._50;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._80;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._95;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._99;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Sizes;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class QuotaThresholdsTest {
+
+ @Test
+ public void highestExceededThresholdShouldReturnZeroWhenBelowAllThresholds() {
+ assertThat(
+ new QuotaThresholds(ImmutableList.of(_50, _80, _95, _99))
+ .highestExceededThreshold(Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(40))
+ .computedLimit(QuotaSize.size(100))
+ .build()))
+ .isEqualTo(QuotaThreshold.ZERO);
+ }
+
+ @Test
+ public void highestExceededThresholdShouldReturnHighestExceededThreshold() {
+ assertThat(
+ new QuotaThresholds(ImmutableList.of(_50, _80, _95, _99))
+ .highestExceededThreshold(Sizes._92_PERCENT))
+ .isEqualTo(_80);
+ }
+
+ @Test
+ public void highestExceededThresholdShouldReturnHighestThresholdWhenAboveAllThresholds() {
+ assertThat(
+ new QuotaThresholds(ImmutableList.of(_50, _80, _95, _99))
+ .highestExceededThreshold(Sizes._992_PERTHOUSAND))
+ .isEqualTo(_99);
+ }
+
+ @Test
+ public void highestExceededThresholdShouldReturnZeroWhenNoThresholds() {
+ assertThat(
+ new QuotaThresholds(ImmutableList.of())
+ .highestExceededThreshold(Sizes._992_PERTHOUSAND))
+ .isEqualTo(QuotaThreshold.ZERO);
+ }
+
+ @Test
+ public void highestExceededThresholdShouldReturnZeroWhenUnlimitedQuota() {
+ assertThat(
+ new QuotaThresholds(ImmutableList.of(_50, _80, _95, _99))
+ .highestExceededThreshold(Quota.<QuotaSize>builder()
+ .used(QuotaSize.size(992))
+ .computedLimit(QuotaSize.unlimited())
+ .build()))
+ .isEqualTo(QuotaThreshold.ZERO);
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/pom.xml
----------------------------------------------------------------------
diff --git a/mailbox/pom.xml b/mailbox/pom.xml
index a8204d2..560d59b 100644
--- a/mailbox/pom.xml
+++ b/mailbox/pom.xml
@@ -53,6 +53,8 @@
<module>tool</module>
<module>zoo-seq-provider</module>
+ <module>plugin/quota-mailing</module>
+ <module>plugin/quota-mailing-memory</module>
<module>plugin/spamassassin</module>
</modules>
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/MailboxEventDispatcher.java
----------------------------------------------------------------------
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/MailboxEventDispatcher.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/MailboxEventDispatcher.java
index 37779cb..80dcc18 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/MailboxEventDispatcher.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/MailboxEventDispatcher.java
@@ -19,12 +19,14 @@
package org.apache.james.mailbox.store.event;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import javax.inject.Inject;
+import org.apache.james.mailbox.Event;
import org.apache.james.mailbox.MailboxListener;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageUid;
@@ -167,6 +169,10 @@ public class MailboxEventDispatcher {
}
public void quota(MailboxSession session, QuotaRoot quotaRoot, Quota<QuotaCount> countQuota, Quota<QuotaSize> sizeQuota) {
- listener.event(new MailboxListener.QuotaUsageUpdatedEvent(session, quotaRoot, countQuota, sizeQuota));
+ listener.event(new MailboxListener.QuotaUsageUpdatedEvent(session, quotaRoot, countQuota, sizeQuota, Instant.now()));
+ }
+
+ public void event(Event event) {
+ listener.event(event);
}
}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailet/base/src/test/java/org/apache/mailet/base/MailAddressFixture.java
----------------------------------------------------------------------
diff --git a/mailet/base/src/test/java/org/apache/mailet/base/MailAddressFixture.java b/mailet/base/src/test/java/org/apache/mailet/base/MailAddressFixture.java
index fed98ba..24ebc74 100644
--- a/mailet/base/src/test/java/org/apache/mailet/base/MailAddressFixture.java
+++ b/mailet/base/src/test/java/org/apache/mailet/base/MailAddressFixture.java
@@ -38,6 +38,7 @@ public class MailAddressFixture {
public static final MailAddress ANY_AT_LOCAL = createMailAddress("any@" + JAMES_LOCAL);
public static final MailAddress OTHER_AT_LOCAL = createMailAddress("other@" + JAMES_LOCAL);
public static final MailAddress ANY_AT_JAMES = createMailAddress("any@" + JAMES_APACHE_ORG);
+ public static final MailAddress POSTMASTER_AT_JAMES = createMailAddress("postmaster@" + JAMES_APACHE_ORG);
public static final MailAddress OTHER_AT_JAMES = createMailAddress("other@" + JAMES_APACHE_ORG);
public static final MailAddress ANY_AT_JAMES2 = createMailAddress("any@" + JAMES2_APACHE_ORG);
public static final MailAddress OTHER_AT_JAMES2 = createMailAddress("other@" + JAMES2_APACHE_ORG);
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailet/base/src/test/java/org/apache/mailet/base/test/FakeMailContext.java
----------------------------------------------------------------------
diff --git a/mailet/base/src/test/java/org/apache/mailet/base/test/FakeMailContext.java b/mailet/base/src/test/java/org/apache/mailet/base/test/FakeMailContext.java
index 687b996..eff41c9 100644
--- a/mailet/base/src/test/java/org/apache/mailet/base/test/FakeMailContext.java
+++ b/mailet/base/src/test/java/org/apache/mailet/base/test/FakeMailContext.java
@@ -20,7 +20,6 @@
package org.apache.mailet.base.test;
import java.io.Serializable;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
@@ -28,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import javax.mail.MessagingException;
@@ -365,15 +365,15 @@ public class FakeMailContext implements MailetContext {
}
private final HashMap<String, Object> attributes;
- private final List<SentMail> sentMails;
- private final List<BouncedMail> bouncedMails;
+ private final Collection<SentMail> sentMails;
+ private final Collection<BouncedMail> bouncedMails;
private final Optional<Logger> logger;
private final MailAddress postmaster;
private FakeMailContext(Optional<Logger> logger, MailAddress postmaster) {
attributes = new HashMap<>();
- sentMails = new ArrayList<>();
- bouncedMails = new ArrayList<>();
+ sentMails = new ConcurrentLinkedQueue<>();
+ bouncedMails = new ConcurrentLinkedQueue<>();
this.logger = logger;
this.postmaster = postmaster;
}
@@ -590,11 +590,15 @@ public class FakeMailContext implements MailetContext {
}
public List<SentMail> getSentMails() {
- return sentMails;
+ return ImmutableList.copyOf(sentMails);
+ }
+
+ public void resetSentMails() {
+ sentMails.clear();
}
public List<BouncedMail> getBouncedMails() {
- return bouncedMails;
+ return ImmutableList.copyOf(bouncedMails);
}
@Override
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 9c296f1..9ba6851 100644
--- a/pom.xml
+++ b/pom.xml
@@ -824,6 +824,17 @@
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-quota-mailing</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-quota-mailing</artifactId>
+ <type>test-jar</type>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
<artifactId>apache-james-mailbox-spring</artifactId>
<version>${project.version}</version>
</dependency>
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org
[2/3] james-project git commit: MAILBOX-331 Implement Quota Threshold
notifier as an EventSourcing project
Posted by bt...@apache.org.
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
new file mode 100644
index 0000000..34b166b
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdNotice.java
@@ -0,0 +1,199 @@
+/****************************************************************
+ * 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.quota.mailing.subscribers;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaCount;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.model.HistoryEvolution;
+import org.apache.james.mailbox.quota.model.QuotaThreshold;
+import org.apache.james.mailbox.quota.model.QuotaThresholdChange;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+public class QuotaThresholdNotice {
+
+ public static class Builder {
+ private Optional<QuotaThreshold> countThreshold;
+ private Optional<QuotaThreshold> sizeThreshold;
+ private Quota<QuotaSize> sizeQuota;
+ private Quota<QuotaCount> countQuota;
+
+ public Builder() {
+ countThreshold = Optional.empty();
+ sizeThreshold = Optional.empty();
+ }
+
+ public Builder sizeQuota(Quota<QuotaSize> sizeQuota) {
+ this.sizeQuota = sizeQuota;
+ return this;
+ }
+
+ public Builder countQuota(Quota<QuotaCount> countQuota) {
+ this.countQuota = countQuota;
+ return this;
+ }
+
+ public Builder countThreshold(HistoryEvolution countHistoryEvolution) {
+ this.countThreshold = Optional.of(countHistoryEvolution)
+ .filter(this::needsNotification)
+ .flatMap(HistoryEvolution::getThresholdChange)
+ .map(QuotaThresholdChange::getQuotaThreshold);
+ return this;
+ }
+
+ public Builder sizeThreshold(HistoryEvolution sizeHistoryEvolution) {
+ this.sizeThreshold = Optional.of(sizeHistoryEvolution)
+ .filter(this::needsNotification)
+ .flatMap(HistoryEvolution::getThresholdChange)
+ .map(QuotaThresholdChange::getQuotaThreshold);
+ return this;
+ }
+
+ boolean needsNotification(HistoryEvolution evolution) {
+ return evolution.getThresholdHistoryChange() == HistoryEvolution.HistoryChangeType.HigherThresholdReached
+ && evolution.currentThresholdNotRecentlyReached();
+ }
+
+ public Optional<QuotaThresholdNotice> build() {
+ Preconditions.checkNotNull(sizeQuota);
+ Preconditions.checkNotNull(countQuota);
+
+ if (sizeThreshold.isPresent() || countThreshold.isPresent()) {
+ return Optional.of(
+ new QuotaThresholdNotice(countThreshold, sizeThreshold, sizeQuota, countQuota));
+ }
+ return Optional.empty();
+ }
+ }
+
+ public static class MessageBuilder {
+ public static final String PREAMBLE = "You receive this email because you recently exceeded a threshold related " +
+ "to the quotas of your email account.\n\n";
+ public static final String CONCLUSION = "You need to be aware that actions leading to exceeded quotas will be denied. " +
+ "This will result in a degraded service.\n" +
+ "To mitigate this issue you might reach your administrator in order to increase your configured quota. " +
+ "You might also delete some non important emails.";
+
+ private final StringBuilder stringBuilder;
+
+
+ public MessageBuilder() {
+ this.stringBuilder = new StringBuilder();
+ }
+
+ public MessageBuilder appendSizeReport(QuotaThreshold threshold, Quota<QuotaSize> sizeQuota) {
+ stringBuilder.append(String.format("You currently occupy more than %d %% of the total size allocated to you.\n" +
+ "You currently occupy %s on a total of %s allocated to you.\n\n",
+ threshold.getQuotaOccupationRatioAsPercent(),
+ FileUtils.byteCountToDisplaySize(sizeQuota.getUsed().asLong()),
+ FileUtils.byteCountToDisplaySize(sizeQuota.getLimit().asLong())));
+ return this;
+ }
+
+ public MessageBuilder appendCountReport(QuotaThreshold threshold, Quota<QuotaCount> countQuota) {
+ stringBuilder.append(String.format("You currently occupy more than %d %% of the total message count allocated to you.\n" +
+ "You currently have %d messages on a total of %d allowed for you.\n\n",
+ threshold.getQuotaOccupationRatioAsPercent(),
+ countQuota.getUsed().asLong(),
+ countQuota.getLimit().asLong()));
+ return this;
+ }
+
+ public MessageBuilder appendSizeReport(Optional<QuotaThreshold> threshold, Quota<QuotaSize> sizeQuota) {
+ if (threshold.isPresent()) {
+ return appendSizeReport(threshold.get(), sizeQuota);
+ }
+ return this;
+ }
+
+ public MessageBuilder appendCountReport(Optional<QuotaThreshold> threshold, Quota<QuotaCount> countQuota) {
+ if (threshold.isPresent()) {
+ return appendCountReport(threshold.get(), countQuota);
+ }
+ return this;
+ }
+
+ public MessageBuilder appendPreamble() {
+ stringBuilder.append(PREAMBLE);
+ return this;
+ }
+
+ public MessageBuilder appendConclusion() {
+ stringBuilder.append(CONCLUSION);
+ return this;
+ }
+
+ public String build() {
+ return stringBuilder.toString();
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final Optional<QuotaThreshold> countThreshold;
+ private final Optional<QuotaThreshold> sizeThreshold;
+ private final Quota<QuotaSize> sizeQuota;
+ private final Quota<QuotaCount> countQuota;
+
+ @VisibleForTesting
+ QuotaThresholdNotice(Optional<QuotaThreshold> countThreshold, Optional<QuotaThreshold> sizeThreshold,
+ Quota<QuotaSize> sizeQuota, Quota<QuotaCount> countQuota) {
+ this.countThreshold = countThreshold;
+ this.sizeThreshold = sizeThreshold;
+ this.sizeQuota = sizeQuota;
+ this.countQuota = countQuota;
+ }
+
+ public String generateReport() {
+ return new MessageBuilder()
+ .appendPreamble()
+ .appendSizeReport(sizeThreshold, sizeQuota)
+ .appendCountReport(countThreshold, countQuota)
+ .appendConclusion()
+ .build();
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaThresholdNotice) {
+ QuotaThresholdNotice that = (QuotaThresholdNotice) o;
+
+ return Objects.equals(this.countThreshold, that.countThreshold)
+ && Objects.equals(this.sizeThreshold, that.sizeThreshold)
+ && Objects.equals(this.sizeQuota, that.sizeQuota)
+ && Objects.equals(this.countQuota, that.countQuota);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(countThreshold, sizeThreshold, sizeQuota, countQuota);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java
new file mode 100644
index 0000000..e7cdf14
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/HistoryEvolution.java
@@ -0,0 +1,111 @@
+/****************************************************************
+ * 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.quota.model;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class HistoryEvolution {
+
+ public static HistoryEvolution noChanges() {
+ return new HistoryEvolution(HistoryChangeType.NoChange,
+ Optional.empty(),
+ Optional.empty()
+ );
+ }
+
+ public static HistoryEvolution lowerThresholdReached(QuotaThresholdChange currentThreshold) {
+ return new HistoryEvolution(HistoryChangeType.LowerThresholdReached,
+ Optional.empty(),
+ Optional.of(currentThreshold));
+ }
+
+ public static HistoryEvolution higherThresholdReached(QuotaThresholdChange currentThreshold, HighestThresholdRecentness recentness) {
+ return new HistoryEvolution(HistoryChangeType.HigherThresholdReached,
+ Optional.of(recentness),
+ Optional.of(currentThreshold));
+ }
+
+ public enum HistoryChangeType {
+ HigherThresholdReached,
+ NoChange,
+ LowerThresholdReached
+ }
+
+ public enum HighestThresholdRecentness {
+ AlreadyReachedDuringGracePriod,
+ NotAlreadyReachedDuringGracePeriod
+ }
+
+ private final HistoryChangeType thresholdHistoryChange;
+ private final Optional<HighestThresholdRecentness> recentness;
+ private final Optional<QuotaThresholdChange> thresholdChange;
+
+ private HistoryEvolution(HistoryChangeType thresholdHistoryChange, Optional<HighestThresholdRecentness> recentness, Optional<QuotaThresholdChange> thresholdChange) {
+ this.thresholdHistoryChange = thresholdHistoryChange;
+ this.recentness = recentness;
+ this.thresholdChange = thresholdChange;
+ }
+
+ public boolean isChange() {
+ return thresholdHistoryChange != HistoryChangeType.NoChange;
+ }
+
+ public boolean currentThresholdNotRecentlyReached() {
+ return recentness
+ .map(value -> value == HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod)
+ .orElse(false);
+ }
+
+ public Optional<QuotaThresholdChange> getThresholdChange() {
+ return thresholdChange;
+ }
+
+ public HistoryChangeType getThresholdHistoryChange() {
+ return thresholdHistoryChange;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof HistoryEvolution) {
+ HistoryEvolution that = (HistoryEvolution) o;
+
+ return Objects.equals(this.thresholdHistoryChange, that.thresholdHistoryChange)
+ && Objects.equals(this.recentness, that.recentness)
+ && Objects.equals(this.thresholdChange, that.thresholdChange);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(thresholdHistoryChange, recentness, thresholdChange);
+ }
+
+
+ @Override
+ public String toString() {
+ return "HistoryEvolution{" +
+ "thresholdHistoryChange=" + thresholdHistoryChange +
+ ", recentness=" + recentness +
+ ", thresholdChange=" + thresholdChange +
+ '}';
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.java
new file mode 100644
index 0000000..dea2676
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThreshold.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.quota.model;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.james.mailbox.model.Quota;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+public class QuotaThreshold implements Comparable<QuotaThreshold> {
+
+ public static final QuotaThreshold ZERO = new QuotaThreshold(0.);
+
+ private final double quotaOccupationRatio;
+
+ public QuotaThreshold(double quotaOccupationRatio) {
+ Preconditions.checkArgument(quotaOccupationRatio >= 0., "Threshold should be contained in [0., 1.] range");
+ Preconditions.checkArgument(quotaOccupationRatio <= 1., "Threshold should be contained in [0., 1.] range");
+ this.quotaOccupationRatio = quotaOccupationRatio;
+ }
+
+ public double getQuotaOccupationRatio() {
+ return quotaOccupationRatio;
+ }
+
+ public int getQuotaOccupationRatioAsPercent() {
+ return Double.valueOf(quotaOccupationRatio * 100).intValue();
+ }
+
+ public boolean isExceeded(Quota<?> quota) {
+ if (quota.getLimit().isUnlimited()) {
+ return false;
+ }
+ double used = toDouble(quota.getUsed().asLong());
+ double limit = toDouble(quota.getLimit().asLong());
+
+ double ratio = (used / limit);
+
+ return ratio > quotaOccupationRatio;
+ }
+
+ public Optional<QuotaThreshold> nonZero() {
+ if (this.equals(ZERO)) {
+ return Optional.empty();
+ }
+ return Optional.of(this);
+ }
+
+ @Override
+ public int compareTo(QuotaThreshold o) {
+ return Double.compare(this.quotaOccupationRatio, o.quotaOccupationRatio);
+ }
+
+ private double toDouble(long aLong) {
+ return Long.valueOf(aLong).doubleValue();
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaThreshold) {
+ QuotaThreshold that = (QuotaThreshold) o;
+
+ return Objects.equals(this.quotaOccupationRatio, that.quotaOccupationRatio);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(quotaOccupationRatio);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("quotaOccupationInPercent", quotaOccupationRatio)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java
new file mode 100644
index 0000000..874d1ca
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdChange.java
@@ -0,0 +1,71 @@
+/****************************************************************
+ * 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.quota.model;
+
+import java.time.Instant;
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+public class QuotaThresholdChange {
+ private final QuotaThreshold quotaThreshold;
+ private final Instant instant;
+
+ public QuotaThresholdChange(QuotaThreshold quotaThreshold, Instant instant) {
+ this.quotaThreshold = quotaThreshold;
+ this.instant = instant;
+ }
+
+ public boolean isAfter(Instant instant) {
+ return this.instant.isAfter(instant);
+ }
+
+ public QuotaThreshold getQuotaThreshold() {
+ return quotaThreshold;
+ }
+
+ public Instant getInstant() {
+ return instant;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaThresholdChange) {
+ QuotaThresholdChange that = (QuotaThresholdChange) o;
+
+ return Objects.equals(this.quotaThreshold, that.quotaThreshold)
+ && Objects.equals(this.instant, that.instant);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(quotaThreshold, instant);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("quotaThreshold", quotaThreshold)
+ .add("instant", instant)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java
new file mode 100644
index 0000000..b80a39f
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholdHistory.java
@@ -0,0 +1,109 @@
+/****************************************************************
+ * 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.quota.model;
+
+import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.AlreadyReachedDuringGracePriod;
+import static org.apache.james.mailbox.quota.model.HistoryEvolution.HighestThresholdRecentness.NotAlreadyReachedDuringGracePeriod;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class QuotaThresholdHistory {
+
+ private final ImmutableList<QuotaThresholdChange> changes;
+
+ public QuotaThresholdHistory() {
+ this(ImmutableList.of());
+ }
+
+ public QuotaThresholdHistory(QuotaThresholdChange... changes) {
+ this(Arrays.asList(changes));
+ }
+
+ public QuotaThresholdHistory(List<QuotaThresholdChange> changes) {
+ this.changes = changes.stream()
+ .sorted(Comparator.comparing(QuotaThresholdChange::getInstant))
+ .collect(Guavate.toImmutableList());
+ }
+
+ public HistoryEvolution compareWithCurrentThreshold(QuotaThresholdChange thresholdChange, Duration gracePeriod) {
+ Optional<QuotaThreshold> lastThreshold = Optional.ofNullable(Iterables.getLast(changes, null))
+ .map(QuotaThresholdChange::getQuotaThreshold);
+
+ return compareWithCurrentThreshold(thresholdChange, gracePeriod, lastThreshold.orElse(QuotaThreshold.ZERO));
+ }
+
+ private HistoryEvolution compareWithCurrentThreshold(QuotaThresholdChange thresholdChange, Duration gracePeriod, QuotaThreshold lastThreshold) {
+ QuotaThreshold quotaThreshold = thresholdChange.getQuotaThreshold();
+ int comparisonResult = quotaThreshold.compareTo(lastThreshold);
+
+ if (comparisonResult < 0) {
+ return HistoryEvolution.lowerThresholdReached(thresholdChange);
+ }
+ if (comparisonResult == 0) {
+ return HistoryEvolution.noChanges();
+ }
+ return recentlyExceededQuotaThreshold(thresholdChange, gracePeriod)
+ .map(any -> HistoryEvolution.higherThresholdReached(thresholdChange, AlreadyReachedDuringGracePriod))
+ .orElse(HistoryEvolution.higherThresholdReached(thresholdChange, NotAlreadyReachedDuringGracePeriod));
+ }
+
+ private Optional<QuotaThresholdChange> recentlyExceededQuotaThreshold(QuotaThresholdChange thresholdChange, Duration gracePeriod) {
+ return changes.stream()
+ .filter(change -> change.isAfter(thresholdChange.getInstant().minus(gracePeriod)))
+ .filter(change -> change.getQuotaThreshold().compareTo(thresholdChange.getQuotaThreshold()) >= 0)
+ .findFirst();
+ }
+
+ public QuotaThresholdHistory combineWith(QuotaThresholdChange change) {
+ return new QuotaThresholdHistory(
+ ImmutableList.<QuotaThresholdChange>builder()
+ .addAll(changes)
+ .add(change)
+ .build());
+ }
+
+ public ImmutableList<QuotaThresholdChange> getChanges() {
+ return changes;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaThresholdHistory) {
+ QuotaThresholdHistory that = (QuotaThresholdHistory) o;
+
+ return Objects.equals(this.changes, that.changes);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(changes);
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java
new file mode 100644
index 0000000..7a7cff4
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/model/QuotaThresholds.java
@@ -0,0 +1,58 @@
+/****************************************************************
+ * 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.quota.model;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.james.mailbox.model.Quota;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+
+public class QuotaThresholds {
+ private final ImmutableList<QuotaThreshold> quotaThresholds;
+
+ public QuotaThresholds(QuotaThreshold... thresholds) {
+ this(Arrays.asList(thresholds));
+ }
+
+ public QuotaThresholds(List<QuotaThreshold> quotaThresholds) {
+ this.quotaThresholds = quotaThresholds.stream()
+ .sorted(Comparator.comparing(QuotaThreshold::getQuotaOccupationRatio).reversed())
+ .collect(Guavate.toImmutableList());
+ }
+
+ public QuotaThreshold highestExceededThreshold(Quota quota) {
+ return quotaThresholds.stream()
+ .filter(quotaLevel -> quotaLevel.isExceeded(quota))
+ .findFirst()
+ .orElse(QuotaThreshold.ZERO);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("quotaThresholds", quotaThresholds)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java
new file mode 100644
index 0000000..85fccd1
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java
@@ -0,0 +1,46 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+
+public class DataCollectorSubscriber implements Subscriber {
+
+ private final List<String> data;
+
+ public DataCollectorSubscriber() {
+ data = new ArrayList<>();
+ }
+
+ @Override
+ public void handle(Event event) {
+ if (event instanceof TestEvent) {
+ data.add(((TestEvent) event).getData());
+ }
+ }
+
+
+ public List<String> getData() {
+ return ImmutableList.copyOf(data);
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java
new file mode 100644
index 0000000..ffdc5f2
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventIdTest.java
@@ -0,0 +1,85 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class EventIdTest {
+
+ @Test
+ public void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(EventStore.History.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ public void firstShouldReturnAConstant() {
+ assertThat(EventId.first())
+ .isEqualTo(EventId.first());
+ }
+
+ @Test
+ public void previousShouldReturnEmptyWhenBeforeFirst() {
+ assertThat(EventId.first().previous())
+ .isEmpty();
+ }
+
+ @Test
+ public void compareToShouldReturnNegativeWhenComparedToNext() {
+ assertThat(EventId.first())
+ .isLessThan(EventId.first().next());
+ }
+
+ @Test
+ public void compareToShouldReturnNegativeWhenComparedToPrevious() {
+ assertThat(EventId.first().next())
+ .isGreaterThan(EventId.first());
+ }
+
+ @Test
+ public void nextShouldAlwaysHaveTheSameIncrement() {
+ assertThat(EventId.first().next())
+ .isEqualTo(EventId.first().next());
+ }
+
+ @Test
+ public void previousShouldRevertNext() {
+ assertThat(EventId.first().next().previous())
+ .contains(EventId.first());
+ }
+
+ @Test
+ public void compareToShouldReturnNegativeWhenComparedToNextWithPreviousCall() {
+ assertThat(EventId.first().next().previous().get())
+ .isLessThan(EventId.first().next());
+ }
+
+ @Test
+ public void compareToShouldReturnNegativeWhenComparedToPreviousWithPreviousCall() {
+ assertThat(EventId.first().next())
+ .isGreaterThan(EventId.first().next().previous().get());
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java
new file mode 100644
index 0000000..b2b26a3
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java
@@ -0,0 +1,251 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyList;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+
+interface EventSourcingSystemTest {
+
+ String PAYLOAD_1 = "payload1";
+ String PAYLOAD_2 = "payload2";
+ TestAggregateId AGGREGATE_ID = TestAggregateId.testId(42);
+
+ class MyCommand implements CommandDispatcher.Command {
+ private final String payload;
+
+ public MyCommand(String payload) {
+ this.payload = payload;
+ }
+
+ public String getPayload() {
+ return payload;
+ }
+ }
+
+ @Test
+ default void dispatchShouldApplyCommandHandlerThenCallSubscribers(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(simpleDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore);
+
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1));
+
+ assertThat(subscriber.getData()).containsExactly(PAYLOAD_1);
+ }
+
+ @Test
+ default void throwingSubscribersShouldNotAbortSubscriberChain(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(simpleDispatcher(eventStore)),
+ ImmutableSet.of(
+ events -> {
+ throw new RuntimeException();
+ },
+ subscriber),
+ eventStore);
+
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1));
+
+ assertThat(subscriber.getData()).containsExactly(PAYLOAD_1);
+ }
+
+ @Test
+ default void throwingStoreShouldNotLeadToPusblishing() {
+ EventStore eventStore = mock(EventStore.class);
+ doThrow(new RuntimeException()).when(eventStore).appendAll(anyList());
+ when(eventStore.getEventsOfAggregate(any())).thenReturn(EventStore.History.empty());
+
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(simpleDispatcher(eventStore)),
+ ImmutableSet.of(
+ events -> {
+ throw new RuntimeException();
+ },
+ subscriber),
+ eventStore);
+
+ assertThatThrownBy(() -> eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)))
+ .isInstanceOf(RuntimeException.class);
+
+ assertThat(subscriber.getData()).isEmpty();
+ }
+
+ @Test
+ default void dispatchShouldApplyCommandHandlerThenStoreGeneratedEvents(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(simpleDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore);
+
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1));
+
+ TestEvent expectedEvent = new TestEvent(EventId.first(), AGGREGATE_ID, PAYLOAD_1);
+ assertThat(eventStore.getEventsOfAggregate(AGGREGATE_ID).getEvents())
+ .containsOnly(expectedEvent);
+ }
+
+ @Test
+ default void dispatchShouldCallSubscriberForSubsequentCommands(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(simpleDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore);
+
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1));
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_2));
+
+ assertThat(subscriber.getData()).containsExactly(PAYLOAD_1, PAYLOAD_2);
+ }
+
+ @Test
+ default void dispatchShouldStoreEventsForSubsequentCommands(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(simpleDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore);
+
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1));
+ eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_2));
+
+ TestEvent expectedEvent1 = new TestEvent(EventId.first(), AGGREGATE_ID, PAYLOAD_1);
+ TestEvent expectedEvent2 = new TestEvent(expectedEvent1.eventId().next(), AGGREGATE_ID, PAYLOAD_2);
+ assertThat(eventStore.getEventsOfAggregate(AGGREGATE_ID).getEvents())
+ .containsOnly(expectedEvent1, expectedEvent2);
+ }
+
+ @Test
+ default void dispatcherShouldBeAbleToReturnSeveralEvents(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(wordCuttingDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore);
+
+ eventSourcingSystem.dispatch(new MyCommand("This is a test"));
+
+ assertThat(subscriber.getData()).containsExactly("This", "is", "a", "test");
+ }
+
+ @Test
+ default void unknownCommandsShouldBeIgnored(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(wordCuttingDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore);
+
+ assertThatThrownBy(() -> eventSourcingSystem.dispatch(new CommandDispatcher.Command() {}))
+ .isInstanceOf(CommandDispatcher.UnknownCommandException.class);
+ }
+
+ @Test
+ default void constructorShouldThrowWhenSeveralHandlersForTheSameCommand(EventStore eventStore) {
+ DataCollectorSubscriber subscriber = new DataCollectorSubscriber();
+
+ assertThatThrownBy(() ->
+ new EventSourcingSystem(
+ ImmutableSet.of(wordCuttingDispatcher(eventStore),
+ simpleDispatcher(eventStore)),
+ ImmutableSet.of(subscriber),
+ eventStore))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ default CommandDispatcher.CommandHandler<MyCommand> simpleDispatcher(EventStore eventStore) {
+ return new CommandDispatcher.CommandHandler<MyCommand>() {
+ @Override
+ public Class<MyCommand> handledClass() {
+ return MyCommand.class;
+ }
+
+ @Override
+ public List<? extends Event> handle(MyCommand myCommand) {
+ EventStore.History history = eventStore.getEventsOfAggregate(AGGREGATE_ID);
+
+ return ImmutableList.of(new TestEvent(
+ history.getNextEventId(),
+ AGGREGATE_ID,
+ myCommand.getPayload()));
+ }
+ };
+ }
+
+ default CommandDispatcher.CommandHandler<MyCommand> wordCuttingDispatcher(EventStore eventStore) {
+ return new CommandDispatcher.CommandHandler<MyCommand>() {
+ @Override
+ public Class<MyCommand> handledClass() {
+ return MyCommand.class;
+ }
+
+ @Override
+ public List<? extends Event> handle(MyCommand myCommand) {
+ EventStore.History history = eventStore.getEventsOfAggregate(AGGREGATE_ID);
+
+ EventIdIncrementer eventIdIncrementer = new EventIdIncrementer(history.getNextEventId());
+
+ return Splitter.on(" ")
+ .splitToList(myCommand.getPayload())
+ .stream()
+ .map(word -> new TestEvent(
+ eventIdIncrementer.next(),
+ AGGREGATE_ID,
+ word))
+ .collect(Guavate.toImmutableList());
+ }
+ };
+ }
+
+ class EventIdIncrementer {
+ private EventId currentEventId;
+
+ public EventIdIncrementer(EventId base) {
+ this.currentEventId = base;
+ }
+
+ public EventId next() {
+ currentEventId = currentEventId.next();
+ return currentEventId;
+ }
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java
new file mode 100644
index 0000000..c847f81
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/EventStoreTest.java
@@ -0,0 +1,75 @@
+package org.apache.james.eventsourcing;
+
+import static org.apache.james.eventsourcing.TestAggregateId.testId;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+interface EventStoreTest {
+
+ TestAggregateId AGGREGATE_1 = testId(1);
+ TestAggregateId AGGREGATE_2 = testId(2);
+
+ @Test
+ default void historyShouldMatchBeanContract() {
+ EqualsVerifier.forClass(EventStore.History.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ default void getEventsOfAggregateShouldThrowOnNullAggregateId(EventStore testee) {
+ assertThatThrownBy(() -> testee.getEventsOfAggregate(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ default void appendShouldThrowWhenEventFromSeveralAggregates(EventStore testee) {
+ TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first");
+ TestEvent event2 = new TestEvent(event1.eventId().next(), AGGREGATE_2, "second");
+ assertThatThrownBy(() -> testee.appendAll(event1, event2)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ default void appendShouldDoNothingOnEmptyEventList(EventStore testee) {
+ assertThatCode(testee::appendAll).doesNotThrowAnyException();
+ }
+
+ @Test
+ default void appendShouldThrowWhenTryingToRewriteHistory(EventStore testee) {
+ TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first");
+ testee.append(event1);
+ TestEvent event2 = new TestEvent(EventId.first(), AGGREGATE_1, "second");
+ assertThatThrownBy(() -> testee.append(event2)).isInstanceOf(EventStore.EventStoreFailedException.class);
+ }
+
+ @Test
+ default void getEventsOfAggregateShouldReturnEmptyHistoryWhenUnknown(EventStore testee) {
+ assertThat(testee.getEventsOfAggregate(AGGREGATE_1)).isEqualTo(EventStore.History.empty());
+ }
+
+ @Test
+ default void getEventsOfAggregateShouldReturnAppendedEvent(EventStore testee) {
+ TestEvent event = new TestEvent(EventId.first(), AGGREGATE_1, "first");
+ testee.append(event);
+ assertThat(testee.getEventsOfAggregate(AGGREGATE_1))
+ .isEqualTo(EventStore.History.of(ImmutableList.of(event)));
+ }
+
+ @Test
+ default void getEventsOfAggregateShouldReturnAppendedEvents(EventStore testee) {
+ TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first");
+ TestEvent event2 = new TestEvent(event1.eventId().next(), AGGREGATE_1, "second");
+ testee.append(event1);
+ testee.append(event2);
+ assertThat(testee.getEventsOfAggregate(AGGREGATE_1))
+ .isEqualTo(EventStore.History.of(ImmutableList.of(event1, event2)));
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java
new file mode 100644
index 0000000..9419087
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/HistoryTest.java
@@ -0,0 +1,87 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class HistoryTest {
+
+ @Test
+ public void shouldMatchBeanContract() {
+ EqualsVerifier.forClass(EventStore.History.class)
+ .verify();
+ }
+
+ @Test
+ public void emptyShouldGenerateAnEmptyHistory() {
+ assertThat(EventStore.History.empty())
+ .isEqualTo(EventStore.History.of());
+ }
+
+ @Test
+ public void getVersionShouldReturnEmptyWhenEmpty() {
+ assertThat(EventStore.History.empty()
+ .getVersion())
+ .isEmpty();
+ }
+
+ @Test
+ public void getVersionShouldReturnSingleEventIdWhenSingleEvent() {
+ assertThat(EventStore.History
+ .of(new TestEvent(EventId.first(),
+ TestAggregateId.testId(42),
+ "any"))
+ .getVersion())
+ .contains(EventId.first());
+ }
+
+ @Test
+ public void getVersionShouldReturnHighestEventId() {
+ TestEvent event1 = new TestEvent(EventId.first(),
+ TestAggregateId.testId(42),
+ "any");
+ TestEvent event2 = new TestEvent(event1.eventId().next(),
+ TestAggregateId.testId(42),
+ "any");
+
+ assertThat(EventStore.History.of(event1, event2)
+ .getVersion())
+ .contains(event2.eventId());
+ }
+
+ @Test
+ public void duplicateHistoryShouldThrow() {
+ TestEvent event1 = new TestEvent(EventId.first(),
+ TestAggregateId.testId(42),
+ "any");
+ TestEvent event2 = new TestEvent(EventId.first(),
+ TestAggregateId.testId(42),
+ "any");
+
+ assertThatThrownBy(() -> EventStore.History.of(event1, event2))
+ .isInstanceOf(EventStore.EventStoreFailedException.class);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java
new file mode 100644
index 0000000..1d7c8af
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java
@@ -0,0 +1,64 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+public class TestAggregateId implements AggregateId {
+
+ public static TestAggregateId testId(int id) {
+ return new TestAggregateId(id);
+ }
+
+ private final int id;
+
+ private TestAggregateId(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public String asAggregateKey() {
+ return "TestAggregateId-" + id;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof TestAggregateId) {
+ TestAggregateId that = (TestAggregateId) o;
+
+ return Objects.equals(this.id, that.id);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("id", id)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java
new file mode 100644
index 0000000..c46f804
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/eventsourcing/TestEvent.java
@@ -0,0 +1,82 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+public class TestEvent implements Event {
+ private final EventId id;
+ private final TestAggregateId aggregateId;
+ private final String data;
+
+ public TestEvent(EventId id, TestAggregateId aggregateId, String data) {
+ this.id = id;
+ this.aggregateId = aggregateId;
+ this.data = data;
+ }
+
+ @Override
+ public EventId eventId() {
+ return id;
+ }
+
+ @Override
+ public TestAggregateId getAggregateId() {
+ return aggregateId;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ @Override
+ public int compareTo(Event o) {
+ return Comparator.<EventId>naturalOrder().compare(id, o.eventId());
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof TestEvent) {
+ TestEvent testEvent = (TestEvent) o;
+
+ return Objects.equals(this.id, testEvent.id)
+ && Objects.equals(this.aggregateId, testEvent.aggregateId)
+ && Objects.equals(this.data, testEvent.data);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, aggregateId, data);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("id", id)
+ .add("aggregateId", aggregateId)
+ .add("data", data)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java
new file mode 100644
index 0000000..45aef96
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholdsTest.java
@@ -0,0 +1,47 @@
+/****************************************************************
+ * 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.quota.mailing.aggregates;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.james.core.User;
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class UserQuotaThresholdsTest {
+
+ public static final User BOB = User.fromUsername("bob@domain");
+
+ @Test
+ public void aggregateShouldMatchBeanContract() {
+ EqualsVerifier.forClass(UserQuotaThresholds.Id.class)
+ .allFieldsShouldBeUsed()
+ .verify();
+ }
+
+ @Test
+ public void asAggregationKeyShouldConvertAggregateToAStringRepresentation() {
+ assertThat(UserQuotaThresholds.Id.from(BOB)
+ .asAggregateKey())
+ .isEqualTo("QuotaThreasholdEvents-bob@domain");
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java
new file mode 100644
index 0000000..9a287c2
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingTest.java
@@ -0,0 +1,33 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.quota.mailing.commands;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class DetectThresholdCrossingTest {
+
+ @Test
+ public void aggregateShouldMatchBeanContract() {
+ EqualsVerifier.forClass(DetectThresholdCrossing.class)
+ .verify();
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java
new file mode 100644
index 0000000..cfae4fc
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdListenersTestSystem.java
@@ -0,0 +1,58 @@
+/****************************************************************
+ * 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.quota.mailing.listeners;
+
+import org.apache.james.eventsourcing.EventSourcingSystem;
+import org.apache.james.eventsourcing.EventStore;
+import org.apache.james.mailbox.Event;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.mock.MockMailboxSession;
+import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration;
+import org.apache.james.mailbox.quota.mailing.commands.DetectThresholdCrossingHandler;
+import org.apache.james.mailbox.quota.mailing.subscribers.QuotaThresholdMailer;
+import org.apache.james.mailbox.store.event.DefaultDelegatingMailboxListener;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.mailet.MailetContext;
+
+import com.google.common.collect.ImmutableSet;
+
+public class QuotaThresholdListenersTestSystem {
+
+ private final DefaultDelegatingMailboxListener delegatingListener;
+
+ public QuotaThresholdListenersTestSystem(MailetContext mailetContext, EventStore eventStore, QuotaMailingListenerConfiguration configuration) throws MailboxException {
+ delegatingListener = new DefaultDelegatingMailboxListener();
+
+ EventSourcingSystem eventSourcingSystem = new EventSourcingSystem(
+ ImmutableSet.of(new DetectThresholdCrossingHandler(eventStore, configuration)),
+ ImmutableSet.of(new QuotaThresholdMailer(mailetContext, MemoryUsersRepository.withVirtualHosting())),
+ eventStore);
+
+ QuotaThresholdCrossingListener thresholdCrossingListener =
+ new QuotaThresholdCrossingListener(eventSourcingSystem);
+
+ MockMailboxSession mailboxSession = new MockMailboxSession("system");
+ delegatingListener.addGlobalListener(thresholdCrossingListener, mailboxSession);
+ }
+
+ public void event(Event event) {
+ delegatingListener.event(event);
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
new file mode 100644
index 0000000..54854cc
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/test/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdMailingIntegrationTest.java
@@ -0,0 +1,217 @@
+/****************************************************************
+ * 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.quota.mailing.listeners;
+
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.BOB_SESSION;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.DEFAULT_CONFIGURATION;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.GRACE_PERIOD;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.NOW;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.ONE_HOUR_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.QUOTAROOT;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.SIX_DAYS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.SIX_HOURS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWELVE_DAYS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWELVE_HOURS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.TestConstants.TWO_DAYS_AGO;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._50;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture._80;
+import static org.apache.james.mailbox.quota.model.QuotaThresholdFixture.mailetContext;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.james.eventsourcing.EventStore;
+import org.apache.james.mailbox.MailboxListener.QuotaUsageUpdatedEvent;
+import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration;
+import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Counts;
+import org.apache.james.mailbox.quota.model.QuotaThresholdFixture.Quotas.Sizes;
+import org.apache.james.mailbox.quota.model.QuotaThresholds;
+import org.apache.james.util.concurrency.ConcurrentTestRunner;
+import org.apache.mailet.base.test.FakeMailContext;
+import org.junit.jupiter.api.Test;
+
+public interface QuotaThresholdMailingIntegrationTest {
+
+ @Test
+ default void shouldNotSendMailWhenUnderAllThresholds(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).isEmpty();
+ }
+
+ @Test
+ default void shouldNotSendMailWhenNoThresholdUpdate(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, ONE_HOUR_AGO));
+ mailetContext.resetSentMails();
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).isEmpty();
+ }
+
+ @Test
+ default void shouldNotSendMailWhenThresholdOverPassedRecently(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, TWELVE_HOURS_AGO));
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_HOURS_AGO));
+ mailetContext.resetSentMails();
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).isEmpty();
+ }
+
+ @Test
+ default void shouldSendMailWhenThresholdOverPassed(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).hasSize(1);
+ }
+
+ @Test
+ default void shouldNotSendDuplicates(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, ONE_HOUR_AGO));
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).hasSize(1);
+ }
+
+ @Test
+ default void shouldNotifySeparatelyCountAndSize(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, ONE_HOUR_AGO));
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._60_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).hasSize(2);
+ }
+
+ @Test
+ default void shouldGroupSizeAndCountNotificationsWhenTriggeredByASingleEvent(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._55_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).hasSize(1);
+ }
+
+ @Test
+ default void shouldSendMailWhenThresholdOverPassedOverGracePeriod(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, TWELVE_DAYS_AGO));
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_DAYS_AGO));
+ mailetContext.resetSentMails();
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).hasSize(1);
+ }
+
+ @Test
+ default void shouldNotSendMailWhenNoThresholdUpdateForCount(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._32_PERCENT, Sizes._55_PERCENT, TWO_DAYS_AGO));
+ mailetContext.resetSentMails();
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._60_PERCENT, TWO_DAYS_AGO));
+
+ assertThat(mailetContext.getSentMails()).isEmpty();
+ }
+
+ @Test
+ default void shouldNotSendMailWhenThresholdOverPassedRecentlyForCount(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, TWELVE_HOURS_AGO));
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_HOURS_AGO));
+ mailetContext.resetSentMails();
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).isEmpty();
+ }
+
+ @Test
+ default void shouldSendMailWhenThresholdOverPassedForCount(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, TWELVE_HOURS_AGO));
+
+ assertThat(mailetContext.getSentMails()).hasSize(1);
+ }
+
+ @Test
+ default void shouldSendMailWhenThresholdOverPassedOverGracePeriodForCount(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, DEFAULT_CONFIGURATION);
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, TWELVE_DAYS_AGO));
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._30_PERCENT, SIX_DAYS_AGO));
+ mailetContext.resetSentMails();
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails()).hasSize(1);
+ }
+
+ @Test
+ default void shouldSendOneNoticePerThreshold(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store,
+ new QuotaMailingListenerConfiguration(new QuotaThresholds(_50, _80), GRACE_PERIOD));
+
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._52_PERCENT, Sizes._30_PERCENT, NOW));
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._85_PERCENT, Sizes._42_PERCENT, NOW));
+
+ assertThat(mailetContext.getSentMails())
+ .hasSize(2);
+ }
+
+ @Test
+ default void shouldSendOneMailUponConcurrentEvents(EventStore store) throws Exception {
+ FakeMailContext mailetContext = mailetContext();
+ QuotaThresholdListenersTestSystem testee = new QuotaThresholdListenersTestSystem(mailetContext, store, new QuotaMailingListenerConfiguration(new QuotaThresholds(_50, _80), GRACE_PERIOD));
+
+ new ConcurrentTestRunner(10, 1, (threadNb, step) ->
+ testee.event(new QuotaUsageUpdatedEvent(BOB_SESSION, QUOTAROOT, Counts._40_PERCENT, Sizes._55_PERCENT, NOW)))
+ .run()
+ .awaitTermination(1, TimeUnit.MINUTES);
+
+ assertThat(mailetContext.getSentMails())
+ .hasSize(1);
+ }
+
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org
[3/3] james-project git commit: MAILBOX-331 Implement Quota Threshold
notifier as an EventSourcing project
Posted by bt...@apache.org.
MAILBOX-331 Implement Quota Threshold notifier as an EventSourcing project
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/0e437d25
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/0e437d25
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/0e437d25
Branch: refs/heads/master
Commit: 0e437d2593d3b666de51af493283d1d792761d35
Parents: b035747
Author: benwa <bt...@linagora.com>
Authored: Thu Apr 26 12:32:37 2018 +0700
Committer: benwa <bt...@linagora.com>
Committed: Tue May 8 15:42:03 2018 +0700
----------------------------------------------------------------------
.../apache/james/mailbox/MailboxListener.java | 14 +-
.../james/mailbox/MailboxManagerTest.java | 14 +-
mailbox/memory/pom.xml | 1 +
mailbox/plugin/quota-mailing-memory/pom.xml | 96 +++++++
.../james/eventsource/InMemoryEventStore.java | 104 ++++++++
.../InMemoryEventSourcingSystemTest.java | 27 ++
.../InMemoryEventStoreExtension.java | 39 +++
.../eventsourcing/InMemoryEventStoreTest.java | 27 ++
...oryQuotaMailingListenersIntegrationTest.java | 29 +++
mailbox/plugin/quota-mailing/pom.xml | 103 ++++++++
.../apache/james/eventsourcing/AggregateId.java | 24 ++
.../james/eventsourcing/CommandDispatcher.java | 115 +++++++++
.../org/apache/james/eventsourcing/Event.java | 32 +++
.../apache/james/eventsourcing/EventBus.java | 61 +++++
.../org/apache/james/eventsourcing/EventId.java | 78 ++++++
.../eventsourcing/EventSourcingSystem.java | 36 +++
.../apache/james/eventsourcing/EventStore.java | 114 +++++++++
.../apache/james/eventsourcing/Subscriber.java | 24 ++
.../QuotaMailingListenerConfiguration.java | 74 ++++++
.../mailing/aggregates/UserQuotaThresholds.java | 163 ++++++++++++
.../commands/DetectThresholdCrossing.java | 77 ++++++
.../DetectThresholdCrossingHandler.java | 59 +++++
.../events/QuotaThresholdChangedEvent.java | 75 ++++++
.../QuotaThresholdCrossingListener.java | 71 ++++++
.../subscribers/QuotaThresholdMailer.java | 79 ++++++
.../subscribers/QuotaThresholdNotice.java | 199 +++++++++++++++
.../mailbox/quota/model/HistoryEvolution.java | 111 ++++++++
.../mailbox/quota/model/QuotaThreshold.java | 99 ++++++++
.../quota/model/QuotaThresholdChange.java | 71 ++++++
.../quota/model/QuotaThresholdHistory.java | 109 ++++++++
.../mailbox/quota/model/QuotaThresholds.java | 58 +++++
.../eventsourcing/DataCollectorSubscriber.java | 46 ++++
.../apache/james/eventsourcing/EventIdTest.java | 85 +++++++
.../eventsourcing/EventSourcingSystemTest.java | 251 +++++++++++++++++++
.../james/eventsourcing/EventStoreTest.java | 75 ++++++
.../apache/james/eventsourcing/HistoryTest.java | 87 +++++++
.../james/eventsourcing/TestAggregateId.java | 64 +++++
.../apache/james/eventsourcing/TestEvent.java | 82 ++++++
.../aggregates/UserQuotaThresholdsTest.java | 47 ++++
.../commands/DetectThresholdCrossingTest.java | 33 +++
.../QuotaThresholdListenersTestSystem.java | 58 +++++
.../QuotaThresholdMailingIntegrationTest.java | 217 ++++++++++++++++
.../subscribers/QuotaThresholdNoticeTest.java | 202 +++++++++++++++
.../quota/model/HistoryEvolutionTest.java | 104 ++++++++
.../quota/model/QuotaThresholdChangeTest.java | 59 +++++
.../quota/model/QuotaThresholdFixture.java | 149 +++++++++++
.../quota/model/QuotaThresholdHistoryTest.java | 87 +++++++
.../mailbox/quota/model/QuotaThresholdTest.java | 141 +++++++++++
.../quota/model/QuotaThresholdsTest.java | 83 ++++++
mailbox/pom.xml | 2 +
.../store/event/MailboxEventDispatcher.java | 8 +-
.../apache/mailet/base/MailAddressFixture.java | 1 +
.../mailet/base/test/FakeMailContext.java | 18 +-
pom.xml | 11 +
54 files changed, 4071 insertions(+), 22 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxListener.java
----------------------------------------------------------------------
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxListener.java b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxListener.java
index de16426..664a107 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxListener.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxListener.java
@@ -20,6 +20,7 @@
package org.apache.james.mailbox;
import java.io.Serializable;
+import java.time.Instant;
import java.util.List;
import java.util.Objects;
@@ -71,12 +72,14 @@ public interface MailboxListener {
private final QuotaRoot quotaRoot;
private final Quota<QuotaCount> countQuota;
private final Quota<QuotaSize> sizeQuota;
+ private final Instant instant;
- public QuotaUsageUpdatedEvent(MailboxSession session, QuotaRoot quotaRoot, Quota<QuotaCount> countQuota, Quota<QuotaSize> sizeQuota) {
+ public QuotaUsageUpdatedEvent(MailboxSession session, QuotaRoot quotaRoot, Quota<QuotaCount> countQuota, Quota<QuotaSize> sizeQuota, Instant instant) {
this.session = session;
this.quotaRoot = quotaRoot;
this.countQuota = countQuota;
this.sizeQuota = sizeQuota;
+ this.instant = instant;
}
@Override
@@ -97,6 +100,10 @@ public interface MailboxListener {
return quotaRoot;
}
+ public Instant getInstant() {
+ return instant;
+ }
+
@Override
public final boolean equals(Object o) {
if (o instanceof QuotaUsageUpdatedEvent) {
@@ -105,14 +112,15 @@ public interface MailboxListener {
return Objects.equals(this.session, that.session)
&& Objects.equals(this.quotaRoot, that.quotaRoot)
&& Objects.equals(this.countQuota, that.countQuota)
- && Objects.equals(this.sizeQuota, that.sizeQuota);
+ && Objects.equals(this.sizeQuota, that.sizeQuota)
+ && Objects.equals(this.instant, that.instant);
}
return false;
}
@Override
public final int hashCode() {
- return Objects.hash(session, quotaRoot, countQuota, sizeQuota);
+ return Objects.hash(session, quotaRoot, countQuota, sizeQuota, instant);
}
}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
----------------------------------------------------------------------
diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
index 93573f1..cda734b 100644
--- a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
+++ b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.List;
import java.util.Optional;
@@ -928,17 +929,8 @@ public abstract class MailboxManagerTest {
.build(message), session);
assertThat(listener.getEvents())
- .contains(new MailboxListener.QuotaUsageUpdatedEvent(
- session,
- QuotaRoot.quotaRoot("#private&" + USER_1, Optional.empty()),
- Quota.<QuotaCount>builder()
- .used(QuotaCount.count(1))
- .computedLimit(QuotaCount.unlimited())
- .build(),
- Quota.<QuotaSize>builder()
- .used(QuotaSize.size(85))
- .computedLimit(QuotaSize.unlimited())
- .build()));
+ .filteredOn(event -> event instanceof MailboxListener.QuotaUsageUpdatedEvent)
+ .isNotEmpty();
}
@Test
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/memory/pom.xml
----------------------------------------------------------------------
diff --git a/mailbox/memory/pom.xml b/mailbox/memory/pom.xml
index 3994681..21b245c 100644
--- a/mailbox/memory/pom.xml
+++ b/mailbox/memory/pom.xml
@@ -64,6 +64,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing-memory/pom.xml
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing-memory/pom.xml b/mailbox/plugin/quota-mailing-memory/pom.xml
new file mode 100644
index 0000000..612ed6a
--- /dev/null
+++ b/mailbox/plugin/quota-mailing-memory/pom.xml
@@ -0,0 +1,96 @@
+<?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.1.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>quota-mailing-memory</artifactId>
+ <name>Apache James :: Mailbox :: Plugin :: Quota Mailing :: Memory</name>
+ <description>Apache James Mailbox memory implementation of Quota mailing listener</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-quota-mailing</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-quota-mailing</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-store</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mailet-base</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>james-server-data-memory</artifactId>
+ <scope>test</scope>
+ </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.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing-memory/src/main/java/org/apache/james/eventsource/InMemoryEventStore.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing-memory/src/main/java/org/apache/james/eventsource/InMemoryEventStore.java b/mailbox/plugin/quota-mailing-memory/src/main/java/org/apache/james/eventsource/InMemoryEventStore.java
new file mode 100644
index 0000000..91cdbb9
--- /dev/null
+++ b/mailbox/plugin/quota-mailing-memory/src/main/java/org/apache/james/eventsource/InMemoryEventStore.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.eventsource;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.james.eventsourcing.AggregateId;
+import org.apache.james.eventsourcing.Event;
+import org.apache.james.eventsourcing.EventStore;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+public class InMemoryEventStore implements EventStore {
+
+ private final ConcurrentHashMap<AggregateId, History> store;
+
+ public InMemoryEventStore() {
+ this.store = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public void appendAll(List<Event> events) {
+ if (events.isEmpty()) {
+ return;
+ }
+ AggregateId aggregateId = getAggregateId(events);
+
+ if (!store.containsKey(aggregateId)) {
+ appendToEmptyHistory(aggregateId, events);
+ } else {
+ appendToExistingHistory(aggregateId, events);
+ }
+ }
+
+ private AggregateId getAggregateId(List<? extends Event> events) {
+ Preconditions.checkArgument(!events.isEmpty());
+ Preconditions.checkArgument(belongsToSameAggregate(events));
+ return events.stream()
+ .map(Event::getAggregateId)
+ .findFirst()
+ .get();
+ }
+
+ private boolean belongsToSameAggregate(List<? extends Event> events) {
+ return events.stream()
+ .map(Event::getAggregateId)
+ .distinct()
+ .limit(2)
+ .count() <= 1;
+ }
+
+ private void appendToEmptyHistory(AggregateId aggregateId, List<Event> events) {
+ History newHistory = History.of(events);
+
+ History previousHistory = store.putIfAbsent(aggregateId, newHistory);
+ if (previousHistory != null) {
+ throw new EventStore.EventStoreFailedException();
+ }
+ }
+
+ private void appendToExistingHistory(AggregateId aggregateId, List<? extends Event> events) {
+ History currentHistory = store.get(aggregateId);
+ List<Event> updatedEvents = updatedEvents(currentHistory, events);
+ History updatedHistory = History.of(updatedEvents);
+
+ boolean isReplaced = store.replace(aggregateId, currentHistory, updatedHistory);
+ if (!isReplaced) {
+ throw new EventStore.EventStoreFailedException();
+ }
+ }
+
+ private List<Event> updatedEvents(History currentHistory, List<? extends Event> newEvents) {
+ return ImmutableList.<Event>builder()
+ .addAll(currentHistory.getEvents())
+ .addAll(newEvents)
+ .build();
+ }
+
+ @Override
+ public History getEventsOfAggregate(AggregateId aggregateId) {
+ return Optional.ofNullable(store.get(aggregateId))
+ .orElse(History.empty());
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventSourcingSystemTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventSourcingSystemTest.java b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventSourcingSystemTest.java
new file mode 100644
index 0000000..c2f9935
--- /dev/null
+++ b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventSourcingSystemTest.java
@@ -0,0 +1,27 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(InMemoryEventStoreExtension.class)
+public class InMemoryEventSourcingSystemTest implements EventSourcingSystemTest {
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreExtension.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreExtension.java b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreExtension.java
new file mode 100644
index 0000000..c4b345e
--- /dev/null
+++ b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreExtension.java
@@ -0,0 +1,39 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.eventsourcing;
+
+import org.apache.james.eventsource.InMemoryEventStore;
+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 InMemoryEventStoreExtension implements ParameterResolver {
+
+ @Override
+ public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+ return (parameterContext.getParameter().getType() == EventStore.class);
+ }
+
+ @Override
+ public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+ return new InMemoryEventStore();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreTest.java b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreTest.java
new file mode 100644
index 0000000..eddc2d0
--- /dev/null
+++ b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/eventsourcing/InMemoryEventStoreTest.java
@@ -0,0 +1,27 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(InMemoryEventStoreExtension.class)
+public class InMemoryEventStoreTest implements EventStoreTest {
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/mailbox/quota/memory/listeners/InMemoryQuotaMailingListenersIntegrationTest.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/mailbox/quota/memory/listeners/InMemoryQuotaMailingListenersIntegrationTest.java b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/mailbox/quota/memory/listeners/InMemoryQuotaMailingListenersIntegrationTest.java
new file mode 100644
index 0000000..4b17682
--- /dev/null
+++ b/mailbox/plugin/quota-mailing-memory/src/test/java/org/apache/james/mailbox/quota/memory/listeners/InMemoryQuotaMailingListenersIntegrationTest.java
@@ -0,0 +1,29 @@
+/****************************************************************
+ * 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.quota.memory.listeners;
+
+import org.apache.james.eventsourcing.InMemoryEventStoreExtension;
+import org.apache.james.mailbox.quota.mailing.listeners.QuotaThresholdMailingIntegrationTest;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(InMemoryEventStoreExtension.class)
+public class InMemoryQuotaMailingListenersIntegrationTest implements QuotaThresholdMailingIntegrationTest {
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/pom.xml
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/pom.xml b/mailbox/plugin/quota-mailing/pom.xml
new file mode 100644
index 0000000..480bae9
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/pom.xml
@@ -0,0 +1,103 @@
+<?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.1.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>apache-james-mailbox-quota-mailing</artifactId>
+ <name>Apache James :: Mailbox :: Plugin :: Quota Mailing</name>
+ <description>Apache James Mailbox Quota mailing listener</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-james-mailbox-store</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apache-mailet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mailet-base</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>james-server-data-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>james-server-data-memory</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</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.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+
+</project>
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/AggregateId.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/AggregateId.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/AggregateId.java
new file mode 100644
index 0000000..18c6224
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/AggregateId.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.eventsourcing;
+
+public interface AggregateId {
+ String asAggregateKey();
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java
new file mode 100644
index 0000000..ef5bfd4
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java
@@ -0,0 +1,115 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.eventsourcing;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+import javax.inject.Inject;
+
+import com.github.steveash.guavate.Guavate;
+
+public class CommandDispatcher {
+
+ private static final int MAX_RETRY = 10;
+
+ public interface Command {
+ }
+
+ public class UnknownCommandException extends RuntimeException {
+ private final Command command;
+
+ public UnknownCommandException(Command command) {
+ super(String.format("Unknown command %s", command));
+ this.command = command;
+ }
+
+ public Command getCommand() {
+ return command;
+ }
+ }
+
+ public class TooManyRetries extends RuntimeException {
+ private final Command command;
+ private final int retries;
+
+
+ public TooManyRetries(Command command, int retries) {
+ super(String.format("Too much retries for command %s. Store failure after %d retries", command, retries));
+ this.command = command;
+ this.retries = retries;
+ }
+
+
+ public Command getCommand() {
+ return command;
+ }
+
+ public int getRetries() {
+ return retries;
+ }
+ }
+
+ public interface CommandHandler<C extends Command> {
+ Class<C> handledClass();
+
+ List<? extends Event> handle(C c);
+ }
+
+ private final EventBus eventBus;
+ private final Map<Class, CommandHandler> handlers;
+
+ @Inject
+ public CommandDispatcher(EventBus eventBus, Collection<CommandHandler> handlers) {
+ this.eventBus = eventBus;
+ this.handlers = handlers.stream()
+ .collect(Guavate.toImmutableMap(CommandHandler::handledClass, handler -> handler));
+ }
+
+ public void dispatch(Command c) {
+ trySeveralTimes(() -> tryDispatch(c))
+ .orElseThrow(() -> new TooManyRetries(c, MAX_RETRY));
+ }
+
+ public Optional<Integer> trySeveralTimes(Supplier<Boolean> singleTry) {
+ return IntStream.range(0, MAX_RETRY)
+ .boxed()
+ .filter(any -> singleTry.get())
+ .findFirst();
+ }
+
+ private boolean tryDispatch(Command c) {
+ try {
+ List<Event> events =
+ Optional.ofNullable(handlers.get(c.getClass()))
+ .map(f -> f.handle(c))
+ .orElseThrow(() -> new UnknownCommandException(c));
+
+ eventBus.publish(events);
+ return true;
+ } catch (EventStore.EventStoreFailedException e) {
+ return false;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Event.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Event.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Event.java
new file mode 100644
index 0000000..7b3bc00
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Event.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.eventsourcing;
+
+public interface Event extends Comparable<Event> {
+
+ EventId eventId();
+
+ AggregateId getAggregateId();
+
+ @Override
+ default int compareTo(Event o) {
+ return eventId().compareTo(o.eventId());
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventBus.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventBus.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventBus.java
new file mode 100644
index 0000000..067d432
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventBus.java
@@ -0,0 +1,61 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+
+public class EventBus {
+
+ public static final Logger LOGGER = LoggerFactory.getLogger(EventBus.class);
+ private final EventStore eventStore;
+ private final Set<Subscriber> subscribers;
+
+ @Inject
+ public EventBus(EventStore eventStore, Set<Subscriber> subscribers) {
+ this.eventStore = eventStore;
+ this.subscribers = ImmutableSet.copyOf(subscribers);
+ }
+
+ public void publish(List<Event> events) throws EventStore.EventStoreFailedException {
+ eventStore.appendAll(events);
+ events.stream()
+ .flatMap(event -> subscribers.stream().map(subscriber -> Pair.of(event, subscriber)))
+ .forEach(this::handle);
+ }
+
+ public void handle(Pair<Event, Subscriber> pair) {
+ Subscriber subscriber = pair.getRight();
+ Event event = pair.getLeft();
+ try {
+ subscriber.handle(event);
+ } catch (Exception e) {
+ LOGGER.error("Error while calling {} for {}", subscriber, event, e);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventId.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventId.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventId.java
new file mode 100644
index 0000000..1d636f6
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventId.java
@@ -0,0 +1,78 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+public class EventId implements Comparable<EventId> {
+
+ public static EventId first() {
+ return new EventId(0);
+ }
+
+ private final long value;
+
+ private EventId(long value) {
+ Preconditions.checkArgument(value >= 0, "EventId can not be negative");
+ this.value = value;
+ }
+
+ public EventId next() {
+ return new EventId(value + 1);
+ }
+
+ public Optional<EventId> previous() {
+ if (value > 0) {
+ return Optional.of(new EventId(value - 1));
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public int compareTo(EventId o) {
+ return Long.compare(value, o.value);
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof EventId) {
+ EventId eventId = (EventId) o;
+
+ return Objects.equals(this.value, eventId.value);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(value);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("value", value)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java
new file mode 100644
index 0000000..e627224
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java
@@ -0,0 +1,36 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.Set;
+
+public class EventSourcingSystem {
+ private final EventBus eventBus;
+ private final CommandDispatcher commandDispatcher;
+
+ public EventSourcingSystem(Set<CommandDispatcher.CommandHandler> handlers, Set<Subscriber> subscribers, EventStore eventStore) {
+ this.eventBus = new EventBus(eventStore, subscribers);
+ this.commandDispatcher = new CommandDispatcher(eventBus, handlers);
+ }
+
+ public void dispatch(CommandDispatcher.Command c) {
+ commandDispatcher.dispatch(c);
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventStore.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventStore.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventStore.java
new file mode 100644
index 0000000..1ba8028
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/EventStore.java
@@ -0,0 +1,114 @@
+/****************************************************************
+ * 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.eventsourcing;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.collect.ImmutableList;
+
+public interface EventStore {
+
+ class EventStoreFailedException extends RuntimeException {
+
+ }
+
+ class History {
+ public static History empty() {
+ return new History(ImmutableList.of());
+ }
+
+ public static History of(List<Event> events) {
+ return new History(ImmutableList.copyOf(events));
+ }
+
+ public static History of(Event... events) {
+ return of(ImmutableList.copyOf(events));
+ }
+
+ private final List<Event> events;
+
+ private History(List<Event> events) {
+ if (hasEventIdDuplicates(events)) {
+ throw new EventStoreFailedException();
+ }
+ this.events = events;
+ }
+
+ public boolean hasEventIdDuplicates(List<Event> events) {
+ Set<EventId> eventIds = events.stream()
+ .map(Event::eventId)
+ .collect(Guavate.toImmutableSet());
+
+ return eventIds.size() != events.size();
+ }
+
+ public Optional<EventId> getVersion() {
+ return events.stream()
+ .map(Event::eventId)
+ .max(Comparator.naturalOrder());
+ }
+
+ public List<Event> getEvents() {
+ return events;
+ }
+
+ public EventId getNextEventId() {
+ return getVersion()
+ .map(EventId::next)
+ .orElse(EventId.first());
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof History) {
+ History history = (History) o;
+
+ return Objects.equals(this.events, history.events);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(events);
+ }
+ }
+
+ default void append(Event event) {
+ appendAll(ImmutableList.of(event));
+ }
+
+ default void appendAll(Event... events) {
+ appendAll(ImmutableList.copyOf(events));
+ }
+
+ /**
+ * This method should check that no input event has an id already stored and throw otherwise
+ * It should also check that all events belong to the same aggregate
+ */
+ void appendAll(List<Event> events);
+
+ History getEventsOfAggregate(AggregateId aggregateId);
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Subscriber.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Subscriber.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Subscriber.java
new file mode 100644
index 0000000..42a804d
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/eventsourcing/Subscriber.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.eventsourcing;
+
+public interface Subscriber {
+ void handle(Event event);
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
new file mode 100644
index 0000000..25d504c
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/QuotaMailingListenerConfiguration.java
@@ -0,0 +1,74 @@
+/****************************************************************
+ * 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.quota.mailing;
+
+import java.time.Duration;
+import java.util.Objects;
+
+import org.apache.james.mailbox.quota.model.QuotaThresholds;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+
+public class QuotaMailingListenerConfiguration {
+ public static QuotaMailingListenerConfiguration DEFAULT = new QuotaMailingListenerConfiguration(
+ new QuotaThresholds(ImmutableList.of()),
+ Duration.ofDays(1));
+
+ private final QuotaThresholds thresholds;
+ private final Duration gracePeriod;
+
+ public QuotaMailingListenerConfiguration(QuotaThresholds thresholds, Duration gracePeriod) {
+ this.thresholds = thresholds;
+ this.gracePeriod = gracePeriod;
+ }
+
+ public QuotaThresholds getThresholds() {
+ return thresholds;
+ }
+
+ public Duration getGracePeriod() {
+ return gracePeriod;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaMailingListenerConfiguration) {
+ QuotaMailingListenerConfiguration that = (QuotaMailingListenerConfiguration) o;
+
+ return Objects.equals(this.thresholds, that.thresholds)
+ && Objects.equals(this.gracePeriod, that.gracePeriod);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(thresholds, gracePeriod);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("thresholds", thresholds)
+ .add("gracePeriod", gracePeriod)
+ .toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholds.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholds.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholds.java
new file mode 100644
index 0000000..8290baf
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/aggregates/UserQuotaThresholds.java
@@ -0,0 +1,163 @@
+/****************************************************************
+ * 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.quota.mailing.aggregates;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.james.core.User;
+import org.apache.james.eventsourcing.AggregateId;
+import org.apache.james.eventsourcing.EventStore;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaCount;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration;
+import org.apache.james.mailbox.quota.mailing.commands.DetectThresholdCrossing;
+import org.apache.james.mailbox.quota.mailing.events.QuotaThresholdChangedEvent;
+import org.apache.james.mailbox.quota.model.HistoryEvolution;
+import org.apache.james.mailbox.quota.model.QuotaThresholdChange;
+import org.apache.james.mailbox.quota.model.QuotaThresholdHistory;
+import org.apache.james.mailbox.quota.model.QuotaThresholds;
+import org.apache.james.util.OptionalUtils;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.collect.ImmutableList;
+
+public class UserQuotaThresholds {
+
+ public static class Id implements AggregateId {
+
+ public static Id from(User user) {
+ return new Id(user);
+ }
+
+ private final User user;
+
+ private Id(User user) {
+ this.user = user;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ @Override
+ public String asAggregateKey() {
+ return "QuotaThreasholdEvents-" + user.asString();
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof Id) {
+ Id id = (Id) o;
+
+ return Objects.equals(this.user, id.user);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(user);
+ }
+ }
+
+ public static UserQuotaThresholds fromEvents(Id aggregateId, EventStore.History history) {
+ return new UserQuotaThresholds(aggregateId, history);
+ }
+
+ private final Id aggregateId;
+ private final EventStore.History history;
+ private final List<QuotaThresholdChangedEvent> events;
+
+ private UserQuotaThresholds(Id aggregateId, EventStore.History history) {
+ this.aggregateId = aggregateId;
+ this.history = history;
+ this.events = history.getEvents().stream()
+ .map(QuotaThresholdChangedEvent.class::cast)
+ .collect(Collectors.toList());
+ }
+
+ public List<QuotaThresholdChangedEvent> detectThresholdCrossing(QuotaMailingListenerConfiguration configuration,
+ DetectThresholdCrossing command) {
+
+ List<QuotaThresholdChangedEvent> events = generateEvents(
+ configuration.getThresholds(),
+ configuration.getGracePeriod(),
+ command.getCountQuota(),
+ command.getSizeQuota(),
+ command.getInstant());
+ events.forEach(this::apply);
+ return events;
+ }
+
+ private List<QuotaThresholdChangedEvent> generateEvents(QuotaThresholds configuration, Duration gracePeriod, Quota<QuotaCount> countQuota, Quota<QuotaSize> sizeQuota, Instant now) {
+ QuotaThresholdChange countThresholdChange = new QuotaThresholdChange(configuration.highestExceededThreshold(countQuota), now);
+ QuotaThresholdChange sizeThresholdChange = new QuotaThresholdChange(configuration.highestExceededThreshold(sizeQuota), now);
+
+ HistoryEvolution countHistoryEvolution = computeCountHistory()
+ .compareWithCurrentThreshold(countThresholdChange, gracePeriod);
+ HistoryEvolution sizeHistoryEvolution = computeSizeHistory()
+ .compareWithCurrentThreshold(sizeThresholdChange, gracePeriod);
+
+ return generateEvents(countHistoryEvolution, sizeHistoryEvolution, countQuota, sizeQuota);
+ }
+
+ private QuotaThresholdHistory computeSizeHistory() {
+ return new QuotaThresholdHistory(
+ events.stream()
+ .map(QuotaThresholdChangedEvent::getSizeHistoryEvolution)
+ .map(HistoryEvolution::getThresholdChange)
+ .flatMap(OptionalUtils::toStream)
+ .collect(Guavate.toImmutableList()));
+ }
+
+ private QuotaThresholdHistory computeCountHistory() {
+ return new QuotaThresholdHistory(
+ events.stream()
+ .map(QuotaThresholdChangedEvent::getCountHistoryEvolution)
+ .map(HistoryEvolution::getThresholdChange)
+ .flatMap(OptionalUtils::toStream)
+ .collect(Guavate.toImmutableList()));
+ }
+
+ private List<QuotaThresholdChangedEvent> generateEvents(HistoryEvolution countHistoryEvolution, HistoryEvolution sizeHistoryEvolution, Quota<QuotaCount> countQuota, Quota<QuotaSize> sizeQuota) {
+ if (countHistoryEvolution.isChange() || sizeHistoryEvolution.isChange()) {
+ return ImmutableList.of(
+ new QuotaThresholdChangedEvent(
+ history.getNextEventId(),
+ sizeHistoryEvolution,
+ countHistoryEvolution,
+ sizeQuota,
+ countQuota,
+ aggregateId));
+ }
+
+ return ImmutableList.of();
+ }
+
+ private void apply(QuotaThresholdChangedEvent event) {
+ events.add(event);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossing.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossing.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossing.java
new file mode 100644
index 0000000..0de0023
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossing.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.quota.mailing.commands;
+
+import java.time.Instant;
+import java.util.Objects;
+
+import org.apache.james.core.User;
+import org.apache.james.eventsourcing.CommandDispatcher;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaCount;
+import org.apache.james.mailbox.quota.QuotaSize;
+
+public class DetectThresholdCrossing implements CommandDispatcher.Command {
+
+ private final User user;
+ private final Quota<QuotaCount> countQuota;
+ private final Quota<QuotaSize> sizeQuota;
+ private final Instant instant;
+
+ public DetectThresholdCrossing(User user, Quota<QuotaCount> countQuota, Quota<QuotaSize> sizeQuota, Instant instant) {
+ this.user = user;
+ this.countQuota = countQuota;
+ this.sizeQuota = sizeQuota;
+ this.instant = instant;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public Quota<QuotaCount> getCountQuota() {
+ return countQuota;
+ }
+
+ public Quota<QuotaSize> getSizeQuota() {
+ return sizeQuota;
+ }
+
+ public Instant getInstant() {
+ return instant;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof DetectThresholdCrossing) {
+ DetectThresholdCrossing that = (DetectThresholdCrossing) o;
+
+ return Objects.equals(this.user, that.user)
+ && Objects.equals(this.countQuota, that.countQuota)
+ && Objects.equals(this.sizeQuota, that.sizeQuota);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(user, countQuota, sizeQuota);
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingHandler.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingHandler.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingHandler.java
new file mode 100644
index 0000000..39b2013
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/commands/DetectThresholdCrossingHandler.java
@@ -0,0 +1,59 @@
+/****************************************************************
+ * 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.quota.mailing.commands;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.james.eventsourcing.CommandDispatcher;
+import org.apache.james.eventsourcing.Event;
+import org.apache.james.eventsourcing.EventStore;
+import org.apache.james.mailbox.quota.mailing.QuotaMailingListenerConfiguration;
+import org.apache.james.mailbox.quota.mailing.aggregates.UserQuotaThresholds;
+
+public class DetectThresholdCrossingHandler implements CommandDispatcher.CommandHandler<DetectThresholdCrossing> {
+
+ private final EventStore eventStore;
+ private final QuotaMailingListenerConfiguration quotaMailingListenerConfiguration;
+
+ @Inject
+ public DetectThresholdCrossingHandler(EventStore eventStore, QuotaMailingListenerConfiguration quotaMailingListenerConfiguration) {
+ this.eventStore = eventStore;
+ this.quotaMailingListenerConfiguration = quotaMailingListenerConfiguration;
+ }
+
+ @Override
+ public List<? extends Event> handle(DetectThresholdCrossing command) {
+ return loadAggregate(command)
+ .detectThresholdCrossing(quotaMailingListenerConfiguration, command);
+ }
+
+ private UserQuotaThresholds loadAggregate(DetectThresholdCrossing command) {
+ UserQuotaThresholds.Id aggregateId = UserQuotaThresholds.Id.from(command.getUser());
+ EventStore.History history = eventStore.getEventsOfAggregate(aggregateId);
+ return UserQuotaThresholds.fromEvents(aggregateId, history);
+ }
+
+ @Override
+ public Class<DetectThresholdCrossing> handledClass() {
+ return DetectThresholdCrossing.class;
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/events/QuotaThresholdChangedEvent.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/events/QuotaThresholdChangedEvent.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/events/QuotaThresholdChangedEvent.java
new file mode 100644
index 0000000..3c21a3f
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/events/QuotaThresholdChangedEvent.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.quota.mailing.events;
+
+import org.apache.james.eventsourcing.Event;
+import org.apache.james.eventsourcing.EventId;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.quota.QuotaCount;
+import org.apache.james.mailbox.quota.QuotaSize;
+import org.apache.james.mailbox.quota.mailing.aggregates.UserQuotaThresholds;
+import org.apache.james.mailbox.quota.model.HistoryEvolution;
+
+public class QuotaThresholdChangedEvent implements Event {
+
+ private final EventId eventId;
+ private final HistoryEvolution sizeHistoryEvolution;
+ private final HistoryEvolution countHistoryEvolution;
+ private final Quota<QuotaSize> sizeQuota;
+ private final Quota<QuotaCount> countQuota;
+ private final UserQuotaThresholds.Id aggregateId;
+
+ public QuotaThresholdChangedEvent(EventId eventId, HistoryEvolution sizeHistoryEvolution, HistoryEvolution countHistoryEvolution, Quota<QuotaSize> sizeQuota, Quota<QuotaCount> countQuota, UserQuotaThresholds.Id aggregateId) {
+ this.eventId = eventId;
+ this.sizeHistoryEvolution = sizeHistoryEvolution;
+ this.countHistoryEvolution = countHistoryEvolution;
+ this.sizeQuota = sizeQuota;
+ this.countQuota = countQuota;
+ this.aggregateId = aggregateId;
+ }
+
+ public HistoryEvolution getSizeHistoryEvolution() {
+ return sizeHistoryEvolution;
+ }
+
+ public HistoryEvolution getCountHistoryEvolution() {
+ return countHistoryEvolution;
+ }
+
+ public Quota<QuotaSize> getSizeQuota() {
+ return sizeQuota;
+ }
+
+ public Quota<QuotaCount> getCountQuota() {
+ return countQuota;
+ }
+
+ @Override
+ public EventId eventId() {
+ return eventId;
+ }
+
+ @Override
+ public UserQuotaThresholds.Id getAggregateId() {
+ return aggregateId;
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdCrossingListener.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdCrossingListener.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdCrossingListener.java
new file mode 100644
index 0000000..a4b1094
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/listeners/QuotaThresholdCrossingListener.java
@@ -0,0 +1,71 @@
+/****************************************************************
+ * 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.quota.mailing.listeners;
+
+import org.apache.james.core.User;
+import org.apache.james.eventsourcing.EventSourcingSystem;
+import org.apache.james.mailbox.Event;
+import org.apache.james.mailbox.MailboxListener;
+import org.apache.james.mailbox.quota.mailing.commands.DetectThresholdCrossing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class QuotaThresholdCrossingListener implements MailboxListener {
+ private static final Logger LOGGER = LoggerFactory.getLogger(QuotaThresholdCrossingListener.class);
+
+ private final EventSourcingSystem eventSourcingSystem;
+
+ public QuotaThresholdCrossingListener(EventSourcingSystem eventSourcingSystem) {
+ this.eventSourcingSystem = eventSourcingSystem;
+ }
+
+ @Override
+ public ListenerType getType() {
+ return ListenerType.ONCE;
+ }
+
+ @Override
+ public ExecutionMode getExecutionMode() {
+ return ExecutionMode.SYNCHRONOUS;
+ }
+
+ @Override
+ public void event(Event event) {
+ try {
+ if (event instanceof QuotaUsageUpdatedEvent) {
+ handleEvent(getUser(event), (QuotaUsageUpdatedEvent) event);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Can not re-emmit quota threshold events", e);
+ }
+ }
+
+ private void handleEvent(User user, QuotaUsageUpdatedEvent event) {
+ eventSourcingSystem.dispatch(
+ new DetectThresholdCrossing(user, event.getCountQuota(), event.getSizeQuota(), event.getInstant()));
+ }
+
+ private User getUser(Event event) {
+ return User.fromUsername(
+ event.getSession()
+ .getUser()
+ .getUserName());
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/0e437d25/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdMailer.java
----------------------------------------------------------------------
diff --git a/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdMailer.java b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdMailer.java
new file mode 100644
index 0000000..2b5cc94
--- /dev/null
+++ b/mailbox/plugin/quota-mailing/src/main/java/org/apache/james/mailbox/quota/mailing/subscribers/QuotaThresholdMailer.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.quota.mailing.subscribers;
+
+import java.util.Optional;
+
+import javax.mail.MessagingException;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.User;
+import org.apache.james.core.builder.MimeMessageBuilder;
+import org.apache.james.eventsourcing.Event;
+import org.apache.james.eventsourcing.Subscriber;
+import org.apache.james.mailbox.quota.mailing.events.QuotaThresholdChangedEvent;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.mailet.MailetContext;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableList;
+
+public class QuotaThresholdMailer implements Subscriber {
+ private final MailetContext mailetContext;
+ private final UsersRepository usersRepository;
+
+ public QuotaThresholdMailer(MailetContext mailetContext, UsersRepository usersRepository) {
+ this.mailetContext = mailetContext;
+ this.usersRepository = usersRepository;
+ }
+
+ @Override
+ public void handle(Event event) {
+ if (event instanceof QuotaThresholdChangedEvent) {
+ handleEvent((QuotaThresholdChangedEvent) event);
+ }
+ }
+
+ private void handleEvent(QuotaThresholdChangedEvent event) {
+ Optional<QuotaThresholdNotice> maybeNotice = QuotaThresholdNotice.builder()
+ .countQuota(event.getCountQuota())
+ .sizeQuota(event.getSizeQuota())
+ .countThreshold(event.getCountHistoryEvolution())
+ .sizeThreshold(event.getSizeHistoryEvolution())
+ .build();
+
+ maybeNotice.ifPresent(Throwing.consumer(notice -> sendNotice(notice, event.getAggregateId().getUser())));
+ }
+
+ private void sendNotice(QuotaThresholdNotice notice, User user) throws UsersRepositoryException, MessagingException {
+ MailAddress sender = mailetContext.getPostmaster();
+ MailAddress recipient = usersRepository.getMailAddressFor(user);
+
+ mailetContext.sendMail(sender, ImmutableList.of(recipient),
+ MimeMessageBuilder.mimeMessageBuilder()
+ .addFrom(sender.asString())
+ .addToRecipient(recipient.asString())
+ .setSubject("Warning: Your email usage just exceeded a configured threshold")
+ .setText(notice.generateReport())
+ .build());
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org