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