You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/11/30 12:12:26 UTC

[james-project] branch master updated (0374fd7 -> 4f8b75a)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 0374fd7  Fix docker JPA path in readme documentation
     new 6c4d392  JAMES-3459 Mailbox/changes MemoryMailboxChangeRepository implementation
     new e5e6cd4  JAMES-3458 Skip ACL verifications for owner
     new 9b7bbf6  JAMES-3458 Group Storage & Message quota reads
     new 1329641  JAMES-3458 Allow grouping max quota reads
     new 0320bd4  JAMES-3458 Limit Cassandra statements when retrieving all quota limits
     new ea51ec0  JAMES-2884 Document authentication on top of JMAP RFC-8621
     new 4f8b75a  JAMES-2884 Server JMAP documentation should point to the annotated JMAP specifications

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../servers/pages/distributed/configure/jmap.adoc  |  19 ++
 .../apache/james/mailbox/model/CurrentQuotas.java  |   4 +
 .../james/mailbox/quota/MaxQuotaManager.java       |  22 ++
 .../apache/james/mailbox/quota/QuotaManager.java   |  19 ++
 .../quota/CassandraPerDomainMaxQuotaDao.java       |  34 +--
 .../quota/CassandraPerUserMaxQuotaDao.java         |  33 +--
 .../quota/CassandraPerUserMaxQuotaManager.java     |  37 +++
 .../james/mailbox/cassandra/quota/Limits.java      |  32 ++-
 .../quota/CassandraPerUserMaxQuotaManagerTest.java |  15 ++
 .../quota/search/scanning/ClauseConverter.java     |   5 +-
 .../james/mailbox/store/StoreMessageIdManager.java |   2 +-
 .../james/mailbox/store/StoreRightManager.java     |   7 +
 .../store/quota/ListeningCurrentQuotaUpdater.java  |  17 +-
 .../james/mailbox/store/quota/NoQuotaManager.java  |   5 +
 .../james/mailbox/store/quota/QuotaChecker.java    |   9 +-
 .../mailbox/store/quota/StoreQuotaManager.java     |  17 ++
 .../AbstractMessageIdManagerSideEffectTest.java    |  17 +-
 .../james/mailbox/store/StoreRightManagerTest.java |   4 +-
 .../store/quota/GenericMaxQuotaManagerTest.java    |   4 +-
 .../mailbox/store/quota/QuotaCheckerTest.java      |  40 +--
 .../james/imap/processor/GetQuotaProcessor.java    |  16 +-
 .../imap/processor/GetQuotaRootProcessor.java      |   5 +-
 .../imap/processor/GetQuotaProcessorTest.java      |   9 +-
 .../imap/processor/GetQuotaRootProcessorTest.java  |   5 +-
 .../apache/james/jmap/draft/JmapGuiceProbe.java    |   2 +-
 .../vacation/CassandraNotificationRegistry.java    |   2 +-
 .../vacation/CassandraNotificationRegistryDAO.java |   2 +-
 .../cassandra/vacation/CassandraVacationDAO.java   |   2 +-
 .../vacation/CassandraVacationRepository.java      |   2 +-
 .../james/jmap/api/change/MailboxChange.java       | 124 ++++++++++
 .../MailboxChangeRepository.java}                  |  16 +-
 .../james/jmap/api/change/MailboxChanges.java      | 166 +++++++++++++
 .../api/exception/ChangeNotFoundException.java}    |  16 +-
 .../jmap/api/{vacation => model}/AccountId.java    |   2 +-
 .../jmap/api/vacation/NotificationRegistry.java    |   2 +
 .../jmap/api/vacation/VacationRepository.java      |   2 +
 .../change/MemoryMailboxChangeRepository.java      |  77 ++++++
 .../vacation/MemoryNotificationRegistry.java       |   2 +-
 .../memory/vacation/MemoryVacationRepository.java  |   2 +-
 .../change/MailboxChangeRepositoryContract.java    | 269 +++++++++++++++++++++
 .../james/jmap/api/vacation/AccountIdTest.java     |   1 +
 .../api/vacation/NotificationRegistryContract.java |   1 +
 .../api/vacation/VacationRepositoryContract.java   |   1 +
 .../MemoryMailboxChangeRepositoryTest.java}        |  16 +-
 .../james/transport/matchers/IsOverQuota.java      |   5 +-
 .../apache/james/jmap/VacationIntegrationTest.java |   2 +-
 .../james/jmap/VacationRelayIntegrationTest.java   |   2 +-
 .../integration/GetVacationResponseTest.java       |   2 +-
 .../integration/SetVacationResponseTest.java       |   2 +-
 .../draft/methods/GetVacationResponseMethod.java   |   2 +-
 .../draft/methods/SetVacationResponseMethod.java   |   2 +-
 .../draft/utils/quotas/DefaultQuotaLoader.java     |   6 +-
 .../quotas/QuotaLoaderWithDefaultPreloaded.java    |  11 +-
 .../apache/james/jmap/mailet/VacationMailet.java   |   2 +-
 .../methods/GetVacationResponseMethodTest.java     |   2 +-
 .../methods/SetVacationResponseMethodTest.java     |   2 +-
 .../james/jmap/mailet/VacationMailetTest.java      |   2 +-
 .../VacationResponseGetMethodContract.scala        |   3 +-
 .../doc/specs/spec/authentication.mdown            |  32 +++
 .../jmap/method/VacationResponseGetMethod.scala    |   3 +-
 .../jmap/method/VacationResponseSetMethod.scala    |   3 +-
 .../james/jmap/utils/quotas/QuotaReader.scala      |  11 +-
 .../james/webadmin/service/UserQuotaService.java   |   5 +-
 src/site/xdoc/server/config-jmap.xml               |  19 ++
 64 files changed, 1032 insertions(+), 170 deletions(-)
 copy mpt/core/src/main/java/org/apache/james/mpt/api/ImapHostSystem.java => mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/Limits.java (66%)
 create mode 100644 server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
 copy server/data/data-jmap/src/main/java/org/apache/james/jmap/api/{vacation/NotificationRegistry.java => change/MailboxChangeRepository.java} (75%)
 create mode 100644 server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChanges.java
 copy server/data/{data-api/src/main/java/org/apache/james/user/api/UsersRepositoryException.java => data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java} (76%)
 rename server/data/data-jmap/src/main/java/org/apache/james/jmap/api/{vacation => model}/AccountId.java (98%)
 create mode 100644 server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java
 create mode 100644 server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
 copy server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/{vacation/MemoryVacationRepositoryTest.java => change/MemoryMailboxChangeRepositoryTest.java} (72%)
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/authentication.mdown


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 03/07: JAMES-3458 Group Storage & Message quota reads

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 9b7bbf6d7792f82ec32df04922a3fbca7c9c468b
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 27 10:10:54 2020 +0700

    JAMES-3458 Group Storage & Message quota reads
    
    This unlocks reading tables once instead of two time.
    
    Also, we lookup the current quotas only once now.
---
 .../apache/james/mailbox/quota/QuotaManager.java   | 19 ++++++++++
 .../quota/search/scanning/ClauseConverter.java     |  5 +--
 .../james/mailbox/store/StoreMessageIdManager.java |  2 +-
 .../store/quota/ListeningCurrentQuotaUpdater.java  | 17 +++------
 .../james/mailbox/store/quota/NoQuotaManager.java  |  5 +++
 .../james/mailbox/store/quota/QuotaChecker.java    |  9 +++--
 .../mailbox/store/quota/StoreQuotaManager.java     | 18 ++++++++++
 .../mailbox/store/quota/QuotaCheckerTest.java      | 40 +++++++++++-----------
 .../james/imap/processor/GetQuotaProcessor.java    | 16 +++------
 .../imap/processor/GetQuotaRootProcessor.java      |  5 +--
 .../james/transport/matchers/IsOverQuota.java      |  5 +--
 .../draft/utils/quotas/DefaultQuotaLoader.java     |  6 ++--
 .../quotas/QuotaLoaderWithDefaultPreloaded.java    | 11 +++---
 .../james/jmap/utils/quotas/QuotaReader.scala      | 11 +++---
 .../james/webadmin/service/UserQuotaService.java   |  5 +--
 15 files changed, 107 insertions(+), 67 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/quota/QuotaManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/quota/QuotaManager.java
index 5d1262c..a08c0bc 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/quota/QuotaManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/quota/QuotaManager.java
@@ -32,6 +32,23 @@ import org.apache.james.mailbox.model.QuotaRoot;
  * Part of RFC 2087 implementation
  */
 public interface QuotaManager {
+    class Quotas {
+        private final Quota<QuotaCountLimit, QuotaCountUsage> messageQuota;
+        private final Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota;
+
+        public Quotas(Quota<QuotaCountLimit, QuotaCountUsage> messageQuota, Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota) {
+            this.messageQuota = messageQuota;
+            this.storageQuota = storageQuota;
+        }
+
+        public Quota<QuotaCountLimit, QuotaCountUsage> getMessageQuota() {
+            return messageQuota;
+        }
+
+        public Quota<QuotaSizeLimit, QuotaSizeUsage> getStorageQuota() {
+            return storageQuota;
+        }
+    }
 
     /**
      * Return the message count {@link Quota} for the given {@link QuotaRoot} (which in fact is
@@ -49,4 +66,6 @@ public interface QuotaManager {
      * @param quotaRoot Quota root argument from RFC 2087 ( correspond to the user owning this mailbox )
      */
     Quota<QuotaSizeLimit, QuotaSizeUsage> getStorageQuota(QuotaRoot quotaRoot) throws MailboxException;
+
+    Quotas getQuotas(QuotaRoot quotaRoot) throws MailboxException;
 }
diff --git a/mailbox/plugin/quota-search-scanning/src/main/java/org/apache/james/quota/search/scanning/ClauseConverter.java b/mailbox/plugin/quota-search-scanning/src/main/java/org/apache/james/quota/search/scanning/ClauseConverter.java
index b7728c0..7cd1c61 100644
--- a/mailbox/plugin/quota-search-scanning/src/main/java/org/apache/james/quota/search/scanning/ClauseConverter.java
+++ b/mailbox/plugin/quota-search-scanning/src/main/java/org/apache/james/quota/search/scanning/ClauseConverter.java
@@ -95,8 +95,9 @@ public class ClauseConverter {
     private double retrieveUserRatio(Username username) {
         try {
             QuotaRoot quotaRoot = quotaRootResolver.forUser(username);
-            Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota = quotaManager.getStorageQuota(quotaRoot);
-            Quota<QuotaCountLimit, QuotaCountUsage> messageQuota = quotaManager.getMessageQuota(quotaRoot);
+            QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
+            Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota = quotas.getStorageQuota();
+            Quota<QuotaCountLimit, QuotaCountUsage> messageQuota = quotas.getMessageQuota();
 
             return QuotaRatio.from(storageQuota, messageQuota).max();
         } catch (MailboxException e) {
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMessageIdManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMessageIdManager.java
index d98bf70..43578c4 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMessageIdManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMessageIdManager.java
@@ -390,7 +390,7 @@ public class StoreMessageIdManager implements MessageIdManager {
             Integer additionalCopyCount = entry.getValue();
             if (additionalCopyCount > 0) {
                 long additionalOccupiedSpace = additionalCopyCount * mailboxMessage.getFullContentOctets();
-                new QuotaChecker(quotaManager.getMessageQuota(entry.getKey()), quotaManager.getStorageQuota(entry.getKey()), entry.getKey())
+                new QuotaChecker(quotaManager.getQuotas(entry.getKey()), entry.getKey())
                     .tryAddition(additionalCopyCount, additionalOccupiedSpace);
             }
         }
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/ListeningCurrentQuotaUpdater.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/ListeningCurrentQuotaUpdater.java
index c5d96da..d7cbb35 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/ListeningCurrentQuotaUpdater.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/ListeningCurrentQuotaUpdater.java
@@ -23,16 +23,13 @@ import java.time.Instant;
 import javax.inject.Inject;
 
 import org.apache.james.core.Username;
-import org.apache.james.core.quota.QuotaCountLimit;
 import org.apache.james.core.quota.QuotaCountUsage;
-import org.apache.james.core.quota.QuotaSizeLimit;
 import org.apache.james.core.quota.QuotaSizeUsage;
 import org.apache.james.mailbox.events.Event;
 import org.apache.james.mailbox.events.EventBus;
 import org.apache.james.mailbox.events.Group;
 import org.apache.james.mailbox.events.MailboxListener;
 import org.apache.james.mailbox.events.RegistrationKey;
-import org.apache.james.mailbox.model.Quota;
 import org.apache.james.mailbox.model.QuotaOperation;
 import org.apache.james.mailbox.model.QuotaRoot;
 import org.apache.james.mailbox.quota.CurrentQuotaManager;
@@ -45,7 +42,6 @@ import com.google.common.collect.ImmutableSet;
 
 import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Schedulers;
-import reactor.util.function.Tuple2;
 
 public class ListeningCurrentQuotaUpdater implements MailboxListener.ReactiveGroupMailboxListener, QuotaUpdater {
     public static class ListeningCurrentQuotaUpdaterGroup extends Group {
@@ -110,21 +106,16 @@ public class ListeningCurrentQuotaUpdater implements MailboxListener.ReactiveGro
     }
 
     private Mono<Void> dispatchNewQuota(QuotaRoot quotaRoot, Username username) {
-        Mono<Quota<QuotaCountLimit, QuotaCountUsage>> messageQuota = Mono.fromCallable(() -> quotaManager.getMessageQuota(quotaRoot));
-        Mono<Quota<QuotaSizeLimit, QuotaSizeUsage>> storageQuota = Mono.fromCallable(() -> quotaManager.getStorageQuota(quotaRoot));
+        Mono<QuotaManager.Quotas> quotasMono = Mono.fromCallable(() -> quotaManager.getQuotas(quotaRoot));
 
-        Mono<Tuple2<Quota<QuotaCountLimit, QuotaCountUsage>, Quota<QuotaSizeLimit, QuotaSizeUsage>>> quotasMono =
-            messageQuota.zipWith(storageQuota)
-                .subscribeOn(Schedulers.elastic());
-
-        return quotasMono
+        return quotasMono.subscribeOn(Schedulers.elastic())
             .flatMap(quotas -> eventBus.dispatch(
                 EventFactory.quotaUpdated()
                     .randomEventId()
                     .user(username)
                     .quotaRoot(quotaRoot)
-                    .quotaCount(quotas.getT1())
-                    .quotaSize(quotas.getT2())
+                    .quotaCount(quotas.getMessageQuota())
+                    .quotaSize(quotas.getStorageQuota())
                     .instant(Instant.now())
                     .build(),
                 NO_REGISTRATION_KEYS));
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/NoQuotaManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/NoQuotaManager.java
index 15ae69a..e40b084 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/NoQuotaManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/NoQuotaManager.java
@@ -47,4 +47,9 @@ public class NoQuotaManager implements QuotaManager {
             .computedLimit(QuotaSizeLimit.unlimited())
             .build();
     }
+
+    @Override
+    public Quotas getQuotas(QuotaRoot quotaRoot) {
+        return new Quotas(getMessageQuota(quotaRoot), getStorageQuota(quotaRoot));
+    }
 }
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/QuotaChecker.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/QuotaChecker.java
index 87e0816..c09bad0 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/QuotaChecker.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/QuotaChecker.java
@@ -39,8 +39,9 @@ public class QuotaChecker {
 
     public QuotaChecker(QuotaManager quotaManager, QuotaRootResolver quotaRootResolver, Mailbox mailbox) throws MailboxException {
         this.quotaRoot = quotaRootResolver.getQuotaRoot(mailbox.generateAssociatedPath());
-        this.messageQuota = quotaManager.getMessageQuota(quotaRoot);
-        this.sizeQuota = quotaManager.getStorageQuota(quotaRoot);
+        QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
+        this.messageQuota = quotas.getMessageQuota();
+        this.sizeQuota = quotas.getStorageQuota();
     }
 
     public QuotaChecker(Quota<QuotaCountLimit, QuotaCountUsage> messageQuota, Quota<QuotaSizeLimit, QuotaSizeUsage> sizeQuota, QuotaRoot quotaRoot) {
@@ -49,6 +50,10 @@ public class QuotaChecker {
         this.quotaRoot = quotaRoot;
     }
 
+    public QuotaChecker(QuotaManager.Quotas quotas, QuotaRoot quotaRoot) {
+        this(quotas.getMessageQuota(), quotas.getStorageQuota(), quotaRoot);
+    }
+
     public void tryAddition(long count, long size) throws OverQuotaException {
         tryCountAddition(count);
         trySizeAddition(size);
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java
index a731e91..6e4d42d 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java
@@ -28,6 +28,7 @@ import org.apache.james.core.quota.QuotaCountUsage;
 import org.apache.james.core.quota.QuotaSizeLimit;
 import org.apache.james.core.quota.QuotaSizeUsage;
 import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.CurrentQuotas;
 import org.apache.james.mailbox.model.Quota;
 import org.apache.james.mailbox.model.Quota.Scope;
 import org.apache.james.mailbox.model.QuotaRoot;
@@ -73,4 +74,21 @@ public class StoreQuotaManager implements QuotaManager {
             .build();
     }
 
+    @Override
+    public Quotas getQuotas(QuotaRoot quotaRoot) throws MailboxException {
+        Map<Scope, QuotaSizeLimit> maxStorageDetails = maxQuotaManager.listMaxStorageDetails(quotaRoot);
+        Map<Scope, QuotaCountLimit> maxMessageDetails = maxQuotaManager.listMaxMessagesDetails(quotaRoot);
+        CurrentQuotas currentQuotas = Mono.from(currentQuotaManager.getCurrentQuotas(quotaRoot)).block();
+        return new Quotas(
+            Quota.<QuotaCountLimit, QuotaCountUsage>builder()
+                .used(currentQuotas.count())
+                .computedLimit(maxQuotaManager.getMaxMessage(maxMessageDetails).orElse(QuotaCountLimit.unlimited()))
+                .limitsByScope(maxMessageDetails)
+                .build(),
+            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder()
+                .used(currentQuotas.size())
+                .computedLimit(maxQuotaManager.getMaxStorage(maxStorageDetails).orElse(QuotaSizeLimit.unlimited()))
+                .limitsByScope(maxStorageDetails)
+                .build());
+    }
 }
\ No newline at end of file
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/QuotaCheckerTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/QuotaCheckerTest.java
index 9c09ab2..958e544 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/QuotaCheckerTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/QuotaCheckerTest.java
@@ -63,10 +63,10 @@ class QuotaCheckerTest {
     @Test
     void quotaCheckerShouldNotThrowOnRegularQuotas() throws MailboxException {
         when(mockedQuotaRootResolver.getQuotaRoot(MAILBOX_PATH)).thenReturn(QUOTA_ROOT);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build());
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build());
+        when(mockedQuotaManager.getQuotas(QUOTA_ROOT))
+            .thenReturn(new QuotaManager.Quotas(
+                Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build(),
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build()));
         QuotaChecker quotaChecker = new QuotaChecker(mockedQuotaManager, mockedQuotaRootResolver, MAILBOX);
 
         quotaChecker.tryAddition(0, 0);
@@ -75,10 +75,10 @@ class QuotaCheckerTest {
     @Test
     void quotaCheckerShouldNotThrowOnRegularModifiedQuotas() throws MailboxException {
         when(mockedQuotaRootResolver.getQuotaRoot(MAILBOX_PATH)).thenReturn(QUOTA_ROOT);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build());
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build());
+        when(mockedQuotaManager.getQuotas(QUOTA_ROOT))
+            .thenReturn(new QuotaManager.Quotas(
+                Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build(),
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build()));
         QuotaChecker quotaChecker = new QuotaChecker(mockedQuotaManager, mockedQuotaRootResolver, MAILBOX);
 
         quotaChecker.tryAddition(89, 899);
@@ -87,10 +87,10 @@ class QuotaCheckerTest {
     @Test
     void quotaCheckerShouldNotThrowOnReachedMaximumQuotas() throws MailboxException {
         when(mockedQuotaRootResolver.getQuotaRoot(MAILBOX_PATH)).thenReturn(QUOTA_ROOT);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build());
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build());
+        when(mockedQuotaManager.getQuotas(QUOTA_ROOT))
+            .thenReturn(new QuotaManager.Quotas(
+                Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build(),
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build()));
         QuotaChecker quotaChecker = new QuotaChecker(mockedQuotaManager, mockedQuotaRootResolver, MAILBOX);
 
         quotaChecker.tryAddition(90, 900);
@@ -99,10 +99,10 @@ class QuotaCheckerTest {
     @Test
     void quotaCheckerShouldThrowOnExceededMessages() throws MailboxException {
         when(mockedQuotaRootResolver.getQuotaRoot(MAILBOX_PATH)).thenReturn(QUOTA_ROOT);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build());
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build());
+        when(mockedQuotaManager.getQuotas(QUOTA_ROOT))
+            .thenReturn(new QuotaManager.Quotas(
+                Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build(),
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build()));
         QuotaChecker quotaChecker = new QuotaChecker(mockedQuotaManager, mockedQuotaRootResolver, MAILBOX);
 
         assertThatThrownBy(() -> quotaChecker.tryAddition(91, 899))
@@ -112,10 +112,10 @@ class QuotaCheckerTest {
     @Test
     void quotaCheckerShouldThrowOnExceededStorage() throws MailboxException {
         when(mockedQuotaRootResolver.getQuotaRoot(MAILBOX_PATH)).thenReturn(QUOTA_ROOT);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build());
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build());
+        when(mockedQuotaManager.getQuotas(QUOTA_ROOT))
+            .thenReturn(new QuotaManager.Quotas(
+                Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(10)).computedLimit(QuotaCountLimit.count(100)).build(),
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(100)).computedLimit(QuotaSizeLimit.size(1000)).build()));
         QuotaChecker quotaChecker = new QuotaChecker(mockedQuotaManager, mockedQuotaRootResolver, MAILBOX);
 
         assertThatThrownBy(() -> quotaChecker.tryAddition(89, 901))
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaProcessor.java
index 273aff8..51cdee5 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaProcessor.java
@@ -22,10 +22,6 @@ package org.apache.james.imap.processor;
 import java.io.Closeable;
 import java.util.List;
 
-import org.apache.james.core.quota.QuotaCountLimit;
-import org.apache.james.core.quota.QuotaCountUsage;
-import org.apache.james.core.quota.QuotaSizeLimit;
-import org.apache.james.core.quota.QuotaSizeUsage;
 import org.apache.james.imap.api.ImapConstants;
 import org.apache.james.imap.api.display.HumanReadableText;
 import org.apache.james.imap.api.message.Capability;
@@ -39,7 +35,6 @@ import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.Mailbox;
 import org.apache.james.mailbox.model.MailboxACL;
-import org.apache.james.mailbox.model.Quota;
 import org.apache.james.mailbox.model.QuotaRoot;
 import org.apache.james.mailbox.quota.QuotaManager;
 import org.apache.james.mailbox.quota.QuotaRootResolver;
@@ -79,13 +74,12 @@ public class GetQuotaProcessor extends AbstractMailboxProcessor<GetQuotaRequest>
         try {
             QuotaRoot quotaRoot = quotaRootResolver.fromString(request.getQuotaRoot());
             if (hasRight(quotaRoot, session)) {
-                Quota<QuotaCountLimit, QuotaCountUsage> messageQuota = quotaManager.getMessageQuota(quotaRoot);
-                Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota = quotaManager.getStorageQuota(quotaRoot);
-                if (messageQuota.getLimit().isLimited()) {
-                    responder.respond(new QuotaResponse(ImapConstants.MESSAGE_QUOTA_RESOURCE, quotaRoot.getValue(), messageQuota));
+                QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
+                if (quotas.getMessageQuota().getLimit().isLimited()) {
+                    responder.respond(new QuotaResponse(ImapConstants.MESSAGE_QUOTA_RESOURCE, quotaRoot.getValue(), quotas.getMessageQuota()));
                 }
-                if (storageQuota.getLimit().isLimited()) {
-                    responder.respond(new QuotaResponse(ImapConstants.STORAGE_QUOTA_RESOURCE, quotaRoot.getValue(), storageQuota));
+                if (quotas.getStorageQuota().getLimit().isLimited()) {
+                    responder.respond(new QuotaResponse(ImapConstants.STORAGE_QUOTA_RESOURCE, quotaRoot.getValue(), quotas.getStorageQuota()));
                 }
                 okComplete(request, responder);
             } else {
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaRootProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaRootProcessor.java
index d794b03..75c8738 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaRootProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/GetQuotaRootProcessor.java
@@ -82,8 +82,9 @@ public class GetQuotaRootProcessor extends AbstractMailboxProcessor<GetQuotaRoot
         try {
             if (mailboxManager.hasRight(mailboxPath, MailboxACL.Right.Read, mailboxSession)) {
                 QuotaRoot quotaRoot = quotaRootResolver.getQuotaRoot(mailboxPath);
-                Quota<QuotaCountLimit, QuotaCountUsage> messageQuota = quotaManager.getMessageQuota(quotaRoot);
-                Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota = quotaManager.getStorageQuota(quotaRoot);
+                QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
+                Quota<QuotaCountLimit, QuotaCountUsage> messageQuota = quotas.getMessageQuota();
+                Quota<QuotaSizeLimit, QuotaSizeUsage> storageQuota = quotas.getStorageQuota();
                 responder.respond(new QuotaRootResponse(request.getMailboxName(), quotaRoot.getValue()));
                 if (messageQuota.getLimit().isLimited()) {
                     responder.respond(new QuotaResponse(ImapConstants.MESSAGE_QUOTA_RESOURCE, quotaRoot.getValue(), messageQuota));
diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/IsOverQuota.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/IsOverQuota.java
index 921e227..5699533 100644
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/IsOverQuota.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/matchers/IsOverQuota.java
@@ -75,9 +75,10 @@ public class IsOverQuota extends GenericMatcher {
                 Username userName = usersRepository.getUsername(mailAddress);
                 MailboxPath mailboxPath = MailboxPath.inbox(userName);
                 QuotaRoot quotaRoot = quotaRootResolver.getQuotaRoot(mailboxPath);
+                QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
 
-                if (quotaManager.getMessageQuota(quotaRoot).isOverQuotaWithAdditionalValue(SINGLE_EMAIL) ||
-                    quotaManager.getStorageQuota(quotaRoot).isOverQuotaWithAdditionalValue(mail.getMessageSize())) {
+                if (quotas.getMessageQuota().isOverQuotaWithAdditionalValue(SINGLE_EMAIL) ||
+                    quotas.getStorageQuota().isOverQuotaWithAdditionalValue(mail.getMessageSize())) {
                     result.add(mailAddress);
                 }
             }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/DefaultQuotaLoader.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/DefaultQuotaLoader.java
index ebb4afa..25a851f 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/DefaultQuotaLoader.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/DefaultQuotaLoader.java
@@ -41,12 +41,12 @@ public class DefaultQuotaLoader extends QuotaLoader {
     public Quotas getQuotas(MailboxPath mailboxPath) throws MailboxException {
         QuotaRoot quotaRoot = quotaRootResolver.getQuotaRoot(mailboxPath);
         Quotas.QuotaId quotaId = Quotas.QuotaId.fromQuotaRoot(quotaRoot);
-
+        QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
         return Quotas.from(
             quotaId,
             Quotas.Quota.from(
-                quotaToValue(quotaManager.getStorageQuota(quotaRoot)),
-                quotaToValue(quotaManager.getMessageQuota(quotaRoot))));
+                quotaToValue(quotas.getStorageQuota()),
+                quotaToValue(quotas.getMessageQuota())));
     }
 
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/QuotaLoaderWithDefaultPreloaded.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/QuotaLoaderWithDefaultPreloaded.java
index 498ec33..326f0d2 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/QuotaLoaderWithDefaultPreloaded.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/quotas/QuotaLoaderWithDefaultPreloaded.java
@@ -52,11 +52,12 @@ public class QuotaLoaderWithDefaultPreloaded extends QuotaLoader {
         if (containsQuotaId(preloadedUserDefaultQuotas, quotaId)) {
             return preloadedUserDefaultQuotas.get();
         }
+        QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
         return Quotas.from(
             quotaId,
             Quotas.Quota.from(
-                quotaToValue(quotaManager.getStorageQuota(quotaRoot)),
-                quotaToValue(quotaManager.getMessageQuota(quotaRoot))));
+                quotaToValue(quotas.getStorageQuota()),
+                quotaToValue(quotas.getMessageQuota())));
     }
 
     private boolean containsQuotaId(Optional<Quotas> preloadedUserDefaultQuotas, Quotas.QuotaId quotaId) {
@@ -69,12 +70,12 @@ public class QuotaLoaderWithDefaultPreloaded extends QuotaLoader {
     private Quotas getUserDefaultQuotas() throws MailboxException {
         QuotaRoot quotaRoot = quotaRootResolver.getQuotaRoot(MailboxPath.inbox(session));
         Quotas.QuotaId quotaId = Quotas.QuotaId.fromQuotaRoot(quotaRoot);
+        QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
         return Quotas.from(
             quotaId,
             Quotas.Quota.from(
-                quotaToValue(quotaManager.getStorageQuota(quotaRoot)),
-                quotaToValue(quotaManager.getMessageQuota(quotaRoot))));
-
+                quotaToValue(quotas.getStorageQuota()),
+                quotaToValue(quotas.getMessageQuota())));
     }
 
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/quotas/QuotaReader.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/quotas/QuotaReader.scala
index 3c8a7a6..de77245 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/quotas/QuotaReader.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/quotas/QuotaReader.scala
@@ -31,12 +31,15 @@ import reactor.core.scala.publisher.SMono
 
 class QuotaReader @Inject() (quotaManager: QuotaManager) {
   @throws[MailboxException]
-  def retrieveQuotas(quotaRoot: QuotaRoot): SMono[Quotas] =
+  def retrieveQuotas(quotaRoot: QuotaRoot): SMono[Quotas] = {
+    val quotaId = QuotaId.fromQuotaRoot(quotaRoot)
+    val quotas = quotaManager.getQuotas(quotaRoot.toModel)
     SMono.just(Quotas.from(
-      QuotaId.fromQuotaRoot(quotaRoot),
+      quotaId,
       Quota.from(Map(
-        Quotas.Storage -> quotaToValue(quotaManager.getStorageQuota(quotaRoot.toModel)),
-        Quotas.Message -> quotaToValue(quotaManager.getMessageQuota(quotaRoot.toModel))))))
+        Quotas.Storage -> quotaToValue(quotas.getStorageQuota),
+        Quotas.Message -> quotaToValue(quotas.getMessageQuota)))))
+  }
 
   private def quotaToValue[T <: QuotaLimitValue[T], U <: QuotaUsageValue[U, T]](quota: ModelQuota[T, U]): Value =
     Value(
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java
index cb42f06..0f6b466 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java
@@ -81,9 +81,10 @@ public class UserQuotaService {
 
     public QuotaDetailsDTO getQuota(Username username) throws MailboxException {
         QuotaRoot quotaRoot = userQuotaRootResolver.forUser(username);
+        QuotaManager.Quotas quotas = quotaManager.getQuotas(quotaRoot);
         QuotaDetailsDTO.Builder quotaDetails = QuotaDetailsDTO.builder()
-            .occupation(quotaManager.getStorageQuota(quotaRoot),
-                quotaManager.getMessageQuota(quotaRoot));
+            .occupation(quotas.getStorageQuota(),
+                quotas.getMessageQuota());
 
         mergeMaps(
                 maxQuotaManager.listMaxMessagesDetails(quotaRoot),


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 04/07: JAMES-3458 Allow grouping max quota reads

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 1329641fea7387c908de09dfcbb1622607ec4993
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 27 10:19:39 2020 +0700

    JAMES-3458 Allow grouping max quota reads
---
 .../james/mailbox/quota/MaxQuotaManager.java       | 22 ++++++++++++++++++++++
 .../mailbox/store/quota/StoreQuotaManager.java     | 11 +++++------
 .../AbstractMessageIdManagerSideEffectTest.java    | 17 ++++++++---------
 .../imap/processor/GetQuotaProcessorTest.java      |  9 +++++----
 .../imap/processor/GetQuotaRootProcessorTest.java  |  5 +++--
 5 files changed, 43 insertions(+), 21 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/quota/MaxQuotaManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/quota/MaxQuotaManager.java
index adf979e..b105b42 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/quota/MaxQuotaManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/quota/MaxQuotaManager.java
@@ -152,6 +152,10 @@ public interface MaxQuotaManager {
 
     Map<Quota.Scope, QuotaSizeLimit> listMaxStorageDetails(QuotaRoot quotaRoot);
 
+    default QuotaDetails quotaDetails(QuotaRoot quotaRoot) {
+        return new QuotaDetails(listMaxMessagesDetails(quotaRoot), listMaxStorageDetails(quotaRoot));
+    }
+
     Optional<QuotaCountLimit> getDomainMaxMessage(Domain domain);
 
     void setDomainMaxMessage(Domain domain, QuotaCountLimit count) throws MailboxException;
@@ -181,4 +185,22 @@ public interface MaxQuotaManager {
             .flatMap(Optional::stream)
             .findFirst();
     }
+
+    class QuotaDetails {
+        private final Map<Quota.Scope, QuotaCountLimit> maxMessageDetails;
+        private final Map<Quota.Scope, QuotaSizeLimit> maxStorageDetails;
+
+        public QuotaDetails(Map<Scope, QuotaCountLimit> maxMessageDetails, Map<Scope, QuotaSizeLimit> maxStorageDetails) {
+            this.maxMessageDetails = maxMessageDetails;
+            this.maxStorageDetails = maxStorageDetails;
+        }
+
+        public Map<Scope, QuotaCountLimit> getMaxMessageDetails() {
+            return maxMessageDetails;
+        }
+
+        public Map<Scope, QuotaSizeLimit> getMaxStorageDetails() {
+            return maxStorageDetails;
+        }
+    }
 }
\ No newline at end of file
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java
index 6e4d42d..17d80e7 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/StoreQuotaManager.java
@@ -76,19 +76,18 @@ public class StoreQuotaManager implements QuotaManager {
 
     @Override
     public Quotas getQuotas(QuotaRoot quotaRoot) throws MailboxException {
-        Map<Scope, QuotaSizeLimit> maxStorageDetails = maxQuotaManager.listMaxStorageDetails(quotaRoot);
-        Map<Scope, QuotaCountLimit> maxMessageDetails = maxQuotaManager.listMaxMessagesDetails(quotaRoot);
+        MaxQuotaManager.QuotaDetails quotaDetails = maxQuotaManager.quotaDetails(quotaRoot);
         CurrentQuotas currentQuotas = Mono.from(currentQuotaManager.getCurrentQuotas(quotaRoot)).block();
         return new Quotas(
             Quota.<QuotaCountLimit, QuotaCountUsage>builder()
                 .used(currentQuotas.count())
-                .computedLimit(maxQuotaManager.getMaxMessage(maxMessageDetails).orElse(QuotaCountLimit.unlimited()))
-                .limitsByScope(maxMessageDetails)
+                .computedLimit(maxQuotaManager.getMaxMessage(quotaDetails.getMaxMessageDetails()).orElse(QuotaCountLimit.unlimited()))
+                .limitsByScope(quotaDetails.getMaxMessageDetails())
                 .build(),
             Quota.<QuotaSizeLimit, QuotaSizeUsage>builder()
                 .used(currentQuotas.size())
-                .computedLimit(maxQuotaManager.getMaxStorage(maxStorageDetails).orElse(QuotaSizeLimit.unlimited()))
-                .limitsByScope(maxStorageDetails)
+                .computedLimit(maxQuotaManager.getMaxStorage(quotaDetails.getMaxStorageDetails()).orElse(QuotaSizeLimit.unlimited()))
+                .limitsByScope(quotaDetails.getMaxStorageDetails())
                 .build());
     }
 }
\ No newline at end of file
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/AbstractMessageIdManagerSideEffectTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/AbstractMessageIdManagerSideEffectTest.java
index e351d9a..751704b 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/AbstractMessageIdManagerSideEffectTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/AbstractMessageIdManagerSideEffectTest.java
@@ -357,11 +357,10 @@ public abstract class AbstractMessageIdManagerSideEffectTest {
     void setInMailboxesShouldThrowExceptionWhenOverQuota() throws Exception {
         MessageId messageId = testingData.persist(mailbox1.getMailboxId(), messageUid1, FLAGS, session);
 
-        when(quotaManager.getStorageQuota(any(QuotaRoot.class))).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(2)).computedLimit(QuotaSizeLimit.unlimited()).build());
-        when(quotaManager.getMessageQuota(any(QuotaRoot.class))).thenReturn(OVER_QUOTA);
-        when(quotaManager.getStorageQuota(any(QuotaRoot.class))).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(2)).computedLimit(QuotaSizeLimit.unlimited()).build());
+        when(quotaManager.getQuotas(any(QuotaRoot.class)))
+            .thenReturn(new QuotaManager.Quotas(
+                OVER_QUOTA,
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(2)).computedLimit(QuotaSizeLimit.unlimited()).build()));
 
         assertThatThrownBy(() -> messageIdManager.setInMailboxes(messageId,
                 ImmutableList.of(mailbox1.getMailboxId(), mailbox2.getMailboxId()),
@@ -526,9 +525,9 @@ public abstract class AbstractMessageIdManagerSideEffectTest {
     }
 
     protected void givenUnlimitedQuota() throws MailboxException {
-        when(quotaManager.getMessageQuota(any(QuotaRoot.class))).thenReturn(
-            Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(2)).computedLimit(QuotaCountLimit.unlimited()).build());
-        when(quotaManager.getStorageQuota(any(QuotaRoot.class))).thenReturn(
-            Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(2)).computedLimit(QuotaSizeLimit.unlimited()).build());
+        when(quotaManager.getQuotas(any(QuotaRoot.class)))
+            .thenReturn(new QuotaManager.Quotas(
+                Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(2)).computedLimit(QuotaCountLimit.unlimited()).build(),
+                Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(2)).computedLimit(QuotaSizeLimit.unlimited()).build()));
     }
 }
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaProcessorTest.java b/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaProcessorTest.java
index 3bf1c3d..c6c11ae 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaProcessorTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaProcessorTest.java
@@ -21,6 +21,7 @@ package org.apache.james.imap.processor;
 
 import static org.apache.james.imap.ImapFixture.TAG;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -105,8 +106,8 @@ public class GetQuotaProcessorTest {
             .thenReturn(Flux.just(mailbox));
         when(mockedMailboxManager.hasRight(MAILBOX_PATH, MailboxACL.Right.Read, mailboxSession))
             .thenReturn(true);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(MESSAGE_QUOTA);
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(STORAGE_QUOTA);
+        when(mockedQuotaManager.getQuotas(any(QuotaRoot.class)))
+            .thenReturn(new QuotaManager.Quotas(MESSAGE_QUOTA, STORAGE_QUOTA));
 
         QuotaResponse storageQuotaResponse = new QuotaResponse("STORAGE", "plop", STORAGE_QUOTA);
         QuotaResponse messageQuotaResponse = new QuotaResponse("MESSAGE", "plop", MESSAGE_QUOTA);
@@ -132,8 +133,8 @@ public class GetQuotaProcessorTest {
             .thenReturn(Flux.just((mailbox)));
         when(mockedMailboxManager.hasRight(MAILBOX_PATH, MailboxACL.Right.Read, mailboxSession))
             .thenReturn(true);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenThrow(new MailboxException());
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(STORAGE_QUOTA);
+        when(mockedQuotaManager.getQuotas(any(QuotaRoot.class)))
+            .thenThrow(new MailboxException());
 
         testee.doProcess(getQuotaRequest, mockedResponder, imapSession);
 
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaRootProcessorTest.java b/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaRootProcessorTest.java
index 440e5b5..28d95eb 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaRootProcessorTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/processor/GetQuotaRootProcessorTest.java
@@ -22,6 +22,7 @@ package org.apache.james.imap.processor;
 import static org.apache.james.imap.ImapFixture.TAG;
 import static org.apache.james.imap.api.message.response.StatusResponse.Type.OK;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.only;
 import static org.mockito.Mockito.times;
@@ -98,8 +99,8 @@ public class GetQuotaRootProcessorTest {
         imapSession.setMailboxSession(mailboxSession);
         when(mockedQuotaRootResolver.getQuotaRoot(MAILBOX_PATH)).thenReturn(QUOTA_ROOT);
         when(mockedMailboxManager.hasRight(MAILBOX_PATH, MailboxACL.Right.Read, mailboxSession)).thenReturn(true);
-        when(mockedQuotaManager.getMessageQuota(QUOTA_ROOT)).thenReturn(MESSAGE_QUOTA);
-        when(mockedQuotaManager.getStorageQuota(QUOTA_ROOT)).thenReturn(STORAGE_QUOTA);
+        when(mockedQuotaManager.getQuotas(any(QuotaRoot.class)))
+            .thenReturn(new QuotaManager.Quotas(MESSAGE_QUOTA, STORAGE_QUOTA));
 
         final QuotaResponse storageQuotaResponse = new QuotaResponse("STORAGE", "plop", STORAGE_QUOTA);
         final QuotaResponse messageQuotaResponse = new QuotaResponse("MESSAGE", "plop", MESSAGE_QUOTA);


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 02/07: JAMES-3458 Skip ACL verifications for owner

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit e5e6cd4c0e2ca4caddb8b3fecf91dc8a91baa73f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 27 10:09:31 2020 +0700

    JAMES-3458 Skip ACL verifications for owner
---
 .../java/org/apache/james/mailbox/store/StoreRightManager.java     | 7 +++++++
 .../java/org/apache/james/mailbox/store/StoreRightManagerTest.java | 4 ++--
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
index 2845ed8..3c15ace 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
@@ -95,6 +95,13 @@ public class StoreRightManager implements RightManager {
 
     @Override
     public Rfc4314Rights myRights(MailboxPath mailboxPath, MailboxSession session) throws MailboxException {
+        if (mailboxPath.belongsTo(session)) {
+            if (mailboxSessionMapperFactory.getMailboxMapper(session).pathExists(mailboxPath).block()) {
+                return MailboxACL.FULL_RIGHTS;
+            } else {
+                throw new MailboxNotFoundException(mailboxPath);
+            }
+        }
         MailboxMapper mapper = mailboxSessionMapperFactory.getMailboxMapper(session);
         Mailbox mailbox = blockOptional(mapper.findMailboxByPath(mailboxPath))
             .orElseThrow(() -> new MailboxNotFoundException(mailboxPath));
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
index 465da65..a315927 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
@@ -86,8 +86,8 @@ class StoreRightManagerTest {
     @Test
     void hasRightShouldThrowMailboxNotFoundExceptionWhenMailboxDoesNotExist() {
         MailboxPath mailboxPath = MailboxPath.forUser(MailboxFixture.ALICE, "unexisting mailbox");
-        when(mockedMailboxMapper.findMailboxByPath(mailboxPath))
-            .thenReturn(Mono.error(new MailboxNotFoundException("")));
+        when(mockedMailboxMapper.pathExists(mailboxPath))
+            .thenReturn(Mono.just(false));
 
         assertThatThrownBy(() ->
             storeRightManager.hasRight(mailboxPath, Right.Read, aliceSession))


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 06/07: JAMES-2884 Document authentication on top of JMAP RFC-8621

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit ea51ec081fb5fba1d4f8aa9e2fadfce0f001576c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 30 09:02:00 2020 +0700

    JAMES-2884 Document authentication on top of JMAP RFC-8621
---
 .../doc/specs/spec/authentication.mdown            | 32 ++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/authentication.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/authentication.mdown
new file mode 100644
index 0000000..1f14a3d
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/authentication.mdown
@@ -0,0 +1,32 @@
+# Authentication
+
+JMAP RFC-8620 deliberately does not address authentication concerns, and only assumes authenticated requests are handled.
+
+Discovery of available authentication mechanism is not part of the JMAP specification.
+
+This document summarizes available authentication mechanism on top of JMAP RFC-8621 implementation as part of the James
+project.
+
+## Basic authentication
+
+James JMAP RFC-8621 supports [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
+
+Please note that while convenient for testing purpose, this authentication mechanism should not be used for production
+workflow: the credentials are transmitted again and over again, should be retained in memory, authentication is
+challenged for each request...
+
+## JWT authentication
+
+We rely on a third party software to supply a signed JWT token, valid according to the James JWT public key.
+If valid, the request is blindly trusted.
+
+[Read more](https://github.com/apache/james-project/blob/master/docs/modules/servers/pages/distributed/configure/jmap.adoc#generating-a-jwt-key-pair).
+
+## Implementing new authentication mechanisms
+
+Administrator might need to adapt authentication to their needs.
+
+To implement custom authentication mechanisms, you need to implement `org.apache.james.jmap.http.AuthenticationStrategy`
+and register it in `RFC8621MethodsModule::provideAuthenticator`.
+
+Note that the Apache James project would happily welcome contributions regarding support of other authentication flows.


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 05/07: JAMES-3458 Limit Cassandra statements when retrieving all quota limits

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 0320bd4a82d2b95ab70323a181345f3acfb26e96
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 27 10:54:51 2020 +0700

    JAMES-3458 Limit Cassandra statements when retrieving all quota limits
    
    Fire 6 queries instead of 4: reads for count/size hits the same
    primary key for both user limits and domain limits, reading them in double is thus useless
    
    This optimisation do not benefit global quota that are hosted on different primary keys
---
 .../apache/james/mailbox/model/CurrentQuotas.java  |  4 ++
 .../quota/CassandraPerDomainMaxQuotaDao.java       | 34 ++++++++--------
 .../quota/CassandraPerUserMaxQuotaDao.java         | 33 +++++++++-------
 .../quota/CassandraPerUserMaxQuotaManager.java     | 37 ++++++++++++++++++
 .../james/mailbox/cassandra/quota/Limits.java}     | 45 ++++++++++++----------
 .../quota/CassandraPerUserMaxQuotaManagerTest.java | 15 ++++++++
 .../store/quota/GenericMaxQuotaManagerTest.java    |  4 +-
 7 files changed, 120 insertions(+), 52 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/CurrentQuotas.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/CurrentQuotas.java
index f6f0821..47d00af 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/CurrentQuotas.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/CurrentQuotas.java
@@ -25,6 +25,7 @@ import org.apache.james.core.quota.QuotaCountUsage;
 import org.apache.james.core.quota.QuotaSizeUsage;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
 
 public class CurrentQuotas {
     private final QuotaCountUsage count;
@@ -39,6 +40,9 @@ public class CurrentQuotas {
     }
 
     public CurrentQuotas(QuotaCountUsage count, QuotaSizeUsage size) {
+        Preconditions.checkNotNull(count);
+        Preconditions.checkNotNull(size);
+
         this.count = count;
         this.size = size;
     }
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerDomainMaxQuotaDao.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerDomainMaxQuotaDao.java
index bc88058..c35ba1b 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerDomainMaxQuotaDao.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerDomainMaxQuotaDao.java
@@ -45,12 +45,10 @@ import com.datastax.driver.core.querybuilder.Select;
 import reactor.core.publisher.Mono;
 
 public class CassandraPerDomainMaxQuotaDao {
-
     private final CassandraAsyncExecutor queryExecutor;
     private final PreparedStatement setMaxStorageStatement;
     private final PreparedStatement setMaxMessageStatement;
-    private final PreparedStatement getMaxStorageStatement;
-    private final PreparedStatement getMaxMessageStatement;
+    private final PreparedStatement getMaxStatement;
     private final PreparedStatement removeMaxStorageStatement;
     private final PreparedStatement removeMaxMessageStatement;
 
@@ -59,8 +57,7 @@ public class CassandraPerDomainMaxQuotaDao {
         this.queryExecutor = new CassandraAsyncExecutor(session);
         this.setMaxStorageStatement = session.prepare(setMaxStorageStatement());
         this.setMaxMessageStatement = session.prepare(setMaxMessageStatement());
-        this.getMaxStorageStatement = session.prepare(getMaxStorageStatement());
-        this.getMaxMessageStatement = session.prepare(getMaxMessageStatement());
+        this.getMaxStatement = session.prepare(getMaxStatement());
         this.removeMaxStorageStatement = session.prepare(removeMaxStorageStatement());
         this.removeMaxMessageStatement = session.prepare(removeMaxMessageStatement());
     }
@@ -77,14 +74,8 @@ public class CassandraPerDomainMaxQuotaDao {
             .where(eq(CassandraDomainMaxQuota.DOMAIN, bindMarker()));
     }
 
-    private Select.Where getMaxMessageStatement() {
-        return select(CassandraDomainMaxQuota.MESSAGE_COUNT)
-            .from(CassandraDomainMaxQuota.TABLE_NAME)
-            .where(eq(CassandraDomainMaxQuota.DOMAIN, bindMarker()));
-    }
-
-    private Select.Where getMaxStorageStatement() {
-        return select(CassandraDomainMaxQuota.STORAGE)
+    private Select.Where getMaxStatement() {
+        return select()
             .from(CassandraDomainMaxQuota.TABLE_NAME)
             .where(eq(CassandraDomainMaxQuota.DOMAIN, bindMarker()));
     }
@@ -110,7 +101,7 @@ public class CassandraPerDomainMaxQuotaDao {
     }
 
     Mono<QuotaSizeLimit> getMaxStorage(Domain domain) {
-        return queryExecutor.executeSingleRow(getMaxStorageStatement.bind(domain.asString()))
+        return queryExecutor.executeSingleRow(getMaxStatement.bind(domain.asString()))
             .map(row -> Optional.ofNullable(row.get(CassandraDomainMaxQuota.STORAGE, Long.class)))
             .handle(publishIfPresent())
             .map(QuotaCodec::longToQuotaSize)
@@ -118,13 +109,26 @@ public class CassandraPerDomainMaxQuotaDao {
     }
 
     Mono<QuotaCountLimit> getMaxMessage(Domain domain) {
-        return queryExecutor.executeSingleRow(getMaxMessageStatement.bind(domain.asString()))
+        return queryExecutor.executeSingleRow(getMaxStatement.bind(domain.asString()))
             .map(row -> Optional.ofNullable(row.get(CassandraDomainMaxQuota.MESSAGE_COUNT, Long.class)))
             .handle(publishIfPresent())
             .map(QuotaCodec::longToQuotaCount)
             .handle(publishIfPresent());
     }
 
+    Mono<Limits> getLimits(Domain domain) {
+        return queryExecutor.executeSingleRow(getMaxStatement.bind(domain.asString()))
+            .map(row -> {
+                Optional<Long> sizeLimit = Optional.ofNullable(row.get(CassandraDomainMaxQuota.STORAGE, Long.class));
+                Optional<Long> countLimit = Optional.ofNullable(row.get(CassandraDomainMaxQuota.MESSAGE_COUNT, Long.class));
+
+                return new Limits(
+                    sizeLimit.flatMap(QuotaCodec::longToQuotaSize),
+                    countLimit.flatMap(QuotaCodec::longToQuotaCount));
+            })
+            .switchIfEmpty(Mono.just(Limits.empty()));
+    }
+
     Mono<Void> removeMaxMessage(Domain domain) {
         return queryExecutor.executeVoid(removeMaxMessageStatement.bind(domain.asString()));
     }
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaDao.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaDao.java
index 9085623..0d7b8ff 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaDao.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaDao.java
@@ -49,8 +49,7 @@ public class CassandraPerUserMaxQuotaDao {
     private final CassandraAsyncExecutor queryExecutor;
     private final PreparedStatement setMaxStorageStatement;
     private final PreparedStatement setMaxMessageStatement;
-    private final PreparedStatement getMaxStorageStatement;
-    private final PreparedStatement getMaxMessageStatement;
+    private final PreparedStatement getMaxStatement;
     private final PreparedStatement removeMaxStorageStatement;
     private final PreparedStatement removeMaxMessageStatement;
 
@@ -59,8 +58,7 @@ public class CassandraPerUserMaxQuotaDao {
         this.queryExecutor = new CassandraAsyncExecutor(session);
         this.setMaxStorageStatement = session.prepare(setMaxStorageStatement());
         this.setMaxMessageStatement = session.prepare(setMaxMessageStatement());
-        this.getMaxStorageStatement = session.prepare(getMaxStorageStatement());
-        this.getMaxMessageStatement = session.prepare(getMaxMessageStatement());
+        this.getMaxStatement = session.prepare(getMaxStatement());
         this.removeMaxStorageStatement = session.prepare(removeMaxStorageStatement());
         this.removeMaxMessageStatement = session.prepare(removeMaxMessageStatement());
     }
@@ -77,14 +75,8 @@ public class CassandraPerUserMaxQuotaDao {
             .where(eq(CassandraMaxQuota.QUOTA_ROOT, bindMarker()));
     }
 
-    private Select.Where getMaxMessageStatement() {
-        return select(CassandraMaxQuota.MESSAGE_COUNT)
-            .from(CassandraMaxQuota.TABLE_NAME)
-            .where(eq(CassandraMaxQuota.QUOTA_ROOT, bindMarker()));
-    }
-
-    private Select.Where getMaxStorageStatement() {
-        return select(CassandraMaxQuota.STORAGE)
+    private Select.Where getMaxStatement() {
+        return select()
             .from(CassandraMaxQuota.TABLE_NAME)
             .where(eq(CassandraMaxQuota.QUOTA_ROOT, bindMarker()));
     }
@@ -110,7 +102,7 @@ public class CassandraPerUserMaxQuotaDao {
     }
 
     Mono<QuotaSizeLimit> getMaxStorage(QuotaRoot quotaRoot) {
-        return queryExecutor.executeSingleRow(getMaxStorageStatement.bind(quotaRoot.getValue()))
+        return queryExecutor.executeSingleRow(getMaxStatement.bind(quotaRoot.getValue()))
             .map(row -> Optional.ofNullable(row.get(CassandraMaxQuota.STORAGE, Long.class)))
             .handle(publishIfPresent())
             .map(QuotaCodec::longToQuotaSize)
@@ -118,13 +110,26 @@ public class CassandraPerUserMaxQuotaDao {
     }
 
     Mono<QuotaCountLimit> getMaxMessage(QuotaRoot quotaRoot) {
-        return queryExecutor.executeSingleRow(getMaxMessageStatement.bind(quotaRoot.getValue()))
+        return queryExecutor.executeSingleRow(getMaxStatement.bind(quotaRoot.getValue()))
             .map(row -> Optional.ofNullable(row.get(CassandraMaxQuota.MESSAGE_COUNT, Long.class)))
             .handle(publishIfPresent())
             .map(QuotaCodec::longToQuotaCount)
             .handle(publishIfPresent());
     }
 
+    Mono<Limits> getLimits(QuotaRoot quotaRoot) {
+        return queryExecutor.executeSingleRow(getMaxStatement.bind(quotaRoot.getValue()))
+            .map(row -> {
+                Optional<Long> sizeLimit = Optional.ofNullable(row.get(CassandraMaxQuota.STORAGE, Long.class));
+                Optional<Long> countLimit = Optional.ofNullable(row.get(CassandraMaxQuota.MESSAGE_COUNT, Long.class));
+
+                return new Limits(
+                    sizeLimit.flatMap(QuotaCodec::longToQuotaSize),
+                    countLimit.flatMap(QuotaCodec::longToQuotaCount));
+            })
+            .switchIfEmpty(Mono.just(Limits.empty()));
+    }
+
     Mono<Void> removeMaxMessage(QuotaRoot quotaRoot) {
         return queryExecutor.executeVoid(removeMaxMessageStatement.bind(quotaRoot.getValue()));
     }
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManager.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManager.java
index 12d90c1..5fe0095 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManager.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManager.java
@@ -21,6 +21,8 @@ package org.apache.james.mailbox.cassandra.quota;
 
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
 
 import javax.inject.Inject;
 
@@ -163,4 +165,39 @@ public class CassandraPerUserMaxQuotaManager implements MaxQuotaManager {
                 Pair::getValue))
             .block();
     }
+
+    @Override
+    public QuotaDetails quotaDetails(QuotaRoot quotaRoot) {
+        return Mono.zip(
+                perUserQuota.getLimits(quotaRoot),
+                Mono.justOrEmpty(quotaRoot.getDomain()).flatMap(perDomainQuota::getLimits).switchIfEmpty(Mono.just(Limits.empty())),
+                globalQuota.getGlobalMaxStorage().map(Optional::of).switchIfEmpty(Mono.just(Optional.empty())),
+                globalQuota.getGlobalMaxMessage().map(Optional::of).switchIfEmpty(Mono.just(Optional.empty())))
+            .map(tuple -> new QuotaDetails(
+                countDetails(tuple.getT1(), tuple.getT2(), tuple.getT4()),
+                sizeDetails(tuple.getT1(), tuple.getT2(), tuple.getT3())))
+            .block();
+    }
+
+    private Map<Quota.Scope, QuotaSizeLimit> sizeDetails(Limits userLimits, Limits domainLimits, Optional<QuotaSizeLimit> globalLimits) {
+        return Stream.of(
+                userLimits.getSizeLimit().stream().map(limit -> Pair.of(Quota.Scope.User, limit)),
+                domainLimits.getSizeLimit().stream().map(limit -> Pair.of(Quota.Scope.Domain, limit)),
+                globalLimits.stream().map(limit -> Pair.of(Quota.Scope.Global, limit)))
+            .flatMap(Function.identity())
+            .collect(Guavate.toImmutableMap(
+                Pair::getKey,
+                Pair::getValue));
+    }
+
+    private Map<Quota.Scope, QuotaCountLimit> countDetails(Limits userLimits, Limits domainLimits, Optional<QuotaCountLimit> globalLimits) {
+        return Stream.of(
+                userLimits.getCountLimit().stream().map(limit -> Pair.of(Quota.Scope.User, limit)),
+                domainLimits.getCountLimit().stream().map(limit -> Pair.of(Quota.Scope.Domain, limit)),
+                globalLimits.stream().map(limit -> Pair.of(Quota.Scope.Global, limit)))
+            .flatMap(Function.identity())
+            .collect(Guavate.toImmutableMap(
+                Pair::getKey,
+                Pair::getValue));
+    }
 }
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/Limits.java
similarity index 54%
copy from mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java
copy to mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/Limits.java
index b1117a0..3ef7aec 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/Limits.java
@@ -19,26 +19,29 @@
 
 package org.apache.james.mailbox.cassandra.quota;
 
-import org.apache.james.backends.cassandra.CassandraClusterExtension;
-import org.apache.james.backends.cassandra.components.CassandraModule;
-import org.apache.james.blob.cassandra.CassandraBlobModule;
-import org.apache.james.mailbox.cassandra.mail.utils.GuiceUtils;
-import org.apache.james.mailbox.cassandra.modules.CassandraQuotaModule;
-import org.apache.james.mailbox.quota.MaxQuotaManager;
-import org.apache.james.mailbox.store.quota.GenericMaxQuotaManagerTest;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-class CassandraPerUserMaxQuotaManagerTest extends GenericMaxQuotaManagerTest {
-
-    @RegisterExtension
-    static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraModule.aggregateModules(
-        CassandraBlobModule.MODULE,
-        CassandraQuotaModule.MODULE));
-
-    @Override
-    protected MaxQuotaManager provideMaxQuotaManager() {
-        return GuiceUtils.testInjector(cassandraCluster.getCassandraCluster())
-            .getInstance(CassandraPerUserMaxQuotaManager.class);
+import java.util.Optional;
+
+import org.apache.james.core.quota.QuotaCountLimit;
+import org.apache.james.core.quota.QuotaSizeLimit;
+
+public class Limits {
+    public static Limits empty() {
+        return new Limits(Optional.empty(), Optional.empty());
+    }
+
+    private final Optional<QuotaSizeLimit> sizeLimit;
+    private final Optional<QuotaCountLimit> countLimit;
+
+    public Limits(Optional<QuotaSizeLimit> sizeLimit, Optional<QuotaCountLimit> countLimit) {
+        this.sizeLimit = sizeLimit;
+        this.countLimit = countLimit;
+    }
+
+    public Optional<QuotaSizeLimit> getSizeLimit() {
+        return sizeLimit;
     }
 
-}
+    public Optional<QuotaCountLimit> getCountLimit() {
+        return countLimit;
+    }
+}
\ No newline at end of file
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java
index b1117a0..9159211 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserMaxQuotaManagerTest.java
@@ -19,13 +19,18 @@
 
 package org.apache.james.mailbox.cassandra.quota;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.james.backends.cassandra.CassandraCluster;
 import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.backends.cassandra.StatementRecorder;
 import org.apache.james.backends.cassandra.components.CassandraModule;
 import org.apache.james.blob.cassandra.CassandraBlobModule;
 import org.apache.james.mailbox.cassandra.mail.utils.GuiceUtils;
 import org.apache.james.mailbox.cassandra.modules.CassandraQuotaModule;
 import org.apache.james.mailbox.quota.MaxQuotaManager;
 import org.apache.james.mailbox.store.quota.GenericMaxQuotaManagerTest;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 class CassandraPerUserMaxQuotaManagerTest extends GenericMaxQuotaManagerTest {
@@ -41,4 +46,14 @@ class CassandraPerUserMaxQuotaManagerTest extends GenericMaxQuotaManagerTest {
             .getInstance(CassandraPerUserMaxQuotaManager.class);
     }
 
+    @Test
+    void quotaDetailsShouldGroupStatements(CassandraCluster cassandra) {
+        StatementRecorder statementRecorder = new StatementRecorder();
+        cassandra.getConf().recordStatements(statementRecorder);
+
+        maxQuotaManager.quotaDetails(QUOTA_ROOT);
+
+        assertThat(statementRecorder.listExecutedStatements()).hasSize(4);
+        // 1 statement for user limits, 1 for domain limits, 2 for global limits (as count and size don't share the same primary key)
+    }
 }
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/GenericMaxQuotaManagerTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/GenericMaxQuotaManagerTest.java
index dbc8e49..c3f467d 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/GenericMaxQuotaManagerTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/quota/GenericMaxQuotaManagerTest.java
@@ -36,8 +36,8 @@ public abstract class GenericMaxQuotaManagerTest {
 
     private static final Domain DOMAIN = Domain.of("domain");
     private static final Domain DOMAIN_CASE_VARIATION = Domain.of("doMain");
-    private static final QuotaRoot QUOTA_ROOT = QuotaRoot.quotaRoot("benwa@domain", Optional.of(DOMAIN));
-    private MaxQuotaManager maxQuotaManager;
+    protected static final QuotaRoot QUOTA_ROOT = QuotaRoot.quotaRoot("benwa@domain", Optional.of(DOMAIN));
+    protected MaxQuotaManager maxQuotaManager;
 
     protected abstract MaxQuotaManager provideMaxQuotaManager();
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 01/07: JAMES-3459 Mailbox/changes MemoryMailboxChangeRepository implementation

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 6c4d39222915334f7c48861c62ad92380d5daec7
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Nov 25 17:54:51 2020 +0700

    JAMES-3459 Mailbox/changes MemoryMailboxChangeRepository implementation
---
 .../apache/james/jmap/draft/JmapGuiceProbe.java    |   2 +-
 .../vacation/CassandraNotificationRegistry.java    |   2 +-
 .../vacation/CassandraNotificationRegistryDAO.java |   2 +-
 .../cassandra/vacation/CassandraVacationDAO.java   |   2 +-
 .../vacation/CassandraVacationRepository.java      |   2 +-
 .../james/jmap/api/change/MailboxChange.java       | 124 ++++++++++
 .../MailboxChangeRepository.java}                  |  16 +-
 .../james/jmap/api/change/MailboxChanges.java      | 166 +++++++++++++
 .../ChangeNotFoundException.java}                  |  22 +-
 .../jmap/api/{vacation => model}/AccountId.java    |   2 +-
 .../jmap/api/vacation/NotificationRegistry.java    |   2 +
 .../jmap/api/vacation/VacationRepository.java      |   2 +
 .../change/MemoryMailboxChangeRepository.java      |  77 ++++++
 .../vacation/MemoryNotificationRegistry.java       |   2 +-
 .../memory/vacation/MemoryVacationRepository.java  |   2 +-
 .../change/MailboxChangeRepositoryContract.java    | 269 +++++++++++++++++++++
 .../james/jmap/api/vacation/AccountIdTest.java     |   1 +
 .../api/vacation/NotificationRegistryContract.java |   1 +
 .../api/vacation/VacationRepositoryContract.java   |   1 +
 .../change/MemoryMailboxChangeRepositoryTest.java} |  22 +-
 .../apache/james/jmap/VacationIntegrationTest.java |   2 +-
 .../james/jmap/VacationRelayIntegrationTest.java   |   2 +-
 .../integration/GetVacationResponseTest.java       |   2 +-
 .../integration/SetVacationResponseTest.java       |   2 +-
 .../draft/methods/GetVacationResponseMethod.java   |   2 +-
 .../draft/methods/SetVacationResponseMethod.java   |   2 +-
 .../apache/james/jmap/mailet/VacationMailet.java   |   2 +-
 .../methods/GetVacationResponseMethodTest.java     |   2 +-
 .../methods/SetVacationResponseMethodTest.java     |   2 +-
 .../james/jmap/mailet/VacationMailetTest.java      |   2 +-
 .../VacationResponseGetMethodContract.scala        |   3 +-
 .../jmap/method/VacationResponseGetMethod.scala    |   3 +-
 .../jmap/method/VacationResponseSetMethod.scala    |   3 +-
 33 files changed, 700 insertions(+), 48 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java
index 77ef5bc..6347e36 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java
@@ -25,8 +25,8 @@ import javax.inject.Inject;
 
 import org.apache.james.core.Username;
 import org.apache.james.jmap.JMAPServer;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.projections.MessageFastViewProjection;
-import org.apache.james.jmap.api.vacation.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.api.vacation.VacationRepository;
diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistry.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistry.java
index 4fa556f..4f564f8 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistry.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistry.java
@@ -25,7 +25,7 @@ import java.util.Optional;
 
 import javax.inject.Inject;
 
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.NotificationRegistry;
 import org.apache.james.jmap.api.vacation.RecipientId;
 import org.apache.james.util.date.ZonedDateTimeProvider;
diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistryDAO.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistryDAO.java
index bf297ce..508b12f 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistryDAO.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistryDAO.java
@@ -31,7 +31,7 @@ import java.util.Optional;
 import javax.inject.Inject;
 
 import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.RecipientId;
 import org.apache.james.jmap.cassandra.vacation.tables.CassandraNotificationTable;
 
diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationDAO.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationDAO.java
index 36d0562..5a3e472 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationDAO.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationDAO.java
@@ -34,7 +34,7 @@ import javax.inject.Inject;
 import org.apache.james.backends.cassandra.init.CassandraTypesProvider;
 import org.apache.james.backends.cassandra.init.CassandraZonedDateTimeModule;
 import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.cassandra.vacation.tables.CassandraVacationTable;
diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationRepository.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationRepository.java
index 06fc937..a3a24bb 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationRepository.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraVacationRepository.java
@@ -21,7 +21,7 @@ package org.apache.james.jmap.cassandra.vacation;
 
 import javax.inject.Inject;
 
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.api.vacation.VacationRepository;
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
new file mode 100644
index 0000000..3bb849e
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
@@ -0,0 +1,124 @@
+/****************************************************************
+ * 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.jmap.api.change;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.model.MailboxId;
+
+public class MailboxChange {
+
+    public static class State {
+
+        public static State of(UUID value) {
+            return new State(value);
+        }
+
+        private final UUID value;
+
+        private State(UUID value) {
+            this.value = value;
+        }
+
+        public UUID getValue() {
+            return value;
+        }
+
+        @Override
+        public final boolean equals(Object o) {
+            if (o instanceof State) {
+                State state = (State) o;
+
+                return Objects.equals(this.value, state.value);
+            }
+            return false;
+        }
+
+        @Override
+        public final int hashCode() {
+            return Objects.hash(value);
+        }
+    }
+
+    public static class Limit {
+
+        public static Limit of(int value) {
+            return new Limit(value);
+        }
+
+        private final int value;
+
+        private Limit(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+    }
+
+    public static MailboxChange of(AccountId accountId, State state,  ZonedDateTime date, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
+        return new MailboxChange(accountId, state, date, created, updated, destroyed);
+    }
+
+    private final AccountId accountId;
+    private final State state;
+    private final ZonedDateTime date;
+    private final List<MailboxId> created;
+    private final List<MailboxId> updated;
+    private final List<MailboxId> destroyed;
+
+    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
+        this.accountId = accountId;
+        this.state = state;
+        this.date = date;
+        this.created = created;
+        this.updated = updated;
+        this.destroyed = destroyed;
+    }
+
+    public AccountId getAccountId() {
+        return accountId;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public ZonedDateTime getDate() {
+        return date;
+    }
+
+    public List<MailboxId> getCreated() {
+        return created;
+    }
+
+    public List<MailboxId> getUpdated() {
+        return updated;
+    }
+
+    public List<MailboxId> getDestroyed() {
+        return destroyed;
+    }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
similarity index 75%
copy from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
copy to server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
index 1526ca4..fb965be 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
@@ -17,19 +17,19 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.api.vacation;
+package org.apache.james.jmap.api.change;
 
-import java.time.ZonedDateTime;
 import java.util.Optional;
 
-import reactor.core.publisher.Mono;
-
-public interface NotificationRegistry {
+import org.apache.james.jmap.api.change.MailboxChange.Limit;
+import org.apache.james.jmap.api.change.MailboxChange.State;
+import org.apache.james.jmap.api.model.AccountId;
 
-    Mono<Void> register(AccountId accountId, RecipientId recipientId, Optional<ZonedDateTime> expiryDate);
+import reactor.core.publisher.Mono;
 
-    Mono<Boolean> isRegistered(AccountId accountId, RecipientId recipientId);
+public interface MailboxChangeRepository {
 
-    Mono<Void> flush(AccountId accountId);
+    Mono<Void> save(MailboxChange change);
 
+    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxIdsToReturn);
 }
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChanges.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChanges.java
new file mode 100644
index 0000000..0dc1bc9
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChanges.java
@@ -0,0 +1,166 @@
+/****************************************************************
+ * 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.jmap.api.change;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.james.mailbox.model.MailboxId;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+public class MailboxChanges {
+
+    public static class MailboxChangesBuilder {
+
+        public static class MailboxChangeCollector implements Collector<MailboxChange, MailboxChangesBuilder, MailboxChanges> {
+            private final MailboxChange.Limit limit;
+
+            public MailboxChangeCollector(MailboxChange.Limit limit) {
+                this.limit = limit;
+            }
+
+            @Override
+            public Supplier<MailboxChangesBuilder> supplier() {
+                return () -> new MailboxChangesBuilder(limit);
+            }
+
+            public BiConsumer<MailboxChangesBuilder, MailboxChange> accumulator() {
+                return MailboxChangesBuilder::add;
+            }
+
+            @Override
+            public BinaryOperator<MailboxChangesBuilder> combiner() {
+                throw new NotImplementedException("Not supported");
+            }
+
+            @Override
+            public Function<MailboxChangesBuilder, MailboxChanges> finisher() {
+                return MailboxChangesBuilder::build;
+            }
+
+            @Override
+            public Set<Characteristics> characteristics() {
+                return Sets.immutableEnumSet(Characteristics.UNORDERED);
+            }
+        }
+
+        private MailboxChange.State state;
+        private boolean hasMoreChanges;
+        private boolean canAddMoreItem;
+        private MailboxChange.Limit limit;
+        private Set<MailboxId> created;
+        private Set<MailboxId> updated;
+        private Set<MailboxId> destroyed;
+
+        public MailboxChangesBuilder(MailboxChange.Limit limit) {
+            this.limit = limit;
+            this.hasMoreChanges = false;
+            this.canAddMoreItem = true;
+            this.created = new HashSet<>();
+            this.updated = new HashSet<>();
+            this.destroyed = new HashSet<>();
+        }
+
+        public MailboxChanges.MailboxChangesBuilder add(MailboxChange change) {
+            if (!canAddMoreItem) {
+                return this;
+            }
+
+            Set<MailboxId> createdTemp = new HashSet<>(created);
+            Set<MailboxId> updatedTemp = new HashSet<>(updated);
+            Set<MailboxId> destroyedTemp = new HashSet<>(destroyed);
+            createdTemp.addAll(change.getCreated());
+            updatedTemp.addAll(change.getUpdated());
+            destroyedTemp.addAll(change.getDestroyed());
+
+            if (createdTemp.size() + updatedTemp.size() + destroyedTemp.size() > limit.getValue()) {
+                hasMoreChanges = true;
+                canAddMoreItem = false;
+                return this;
+            }
+
+            state = change.getState();
+            this.created.addAll(change.getCreated());
+            this.updated.addAll(change.getUpdated());
+            this.destroyed.addAll(change.getDestroyed());
+
+            return this;
+        }
+
+        public MailboxChanges build() {
+            return new MailboxChanges(state, hasMoreChanges, created, updated, destroyed);
+        }
+    }
+
+    private MailboxChange.State newState;
+    private final boolean hasMoreChanges;
+    private final Set<MailboxId> created;
+    private final Set<MailboxId> updated;
+    private final Set<MailboxId> destroyed;
+
+    private MailboxChanges(MailboxChange.State newState, boolean hasMoreChanges, Set<MailboxId> created, Set<MailboxId> updated, Set<MailboxId> destroyed) {
+        this.newState = newState;
+        this.hasMoreChanges = hasMoreChanges;
+        this.created = created;
+        this.updated = updated;
+        this.destroyed = destroyed;
+    }
+
+    public MailboxChangesBuilder build(MailboxChange.Limit limit) {
+        return new MailboxChangesBuilder(limit);
+    }
+
+    public MailboxChange.State getNewState() {
+        return newState;
+    }
+
+    public boolean hasMoreChanges() {
+        return hasMoreChanges;
+    }
+
+    public Set<MailboxId> getCreated() {
+        return created;
+    }
+
+    public Set<MailboxId> getUpdated() {
+        return updated;
+    }
+
+    public Set<MailboxId> getDestroyed() {
+        return destroyed;
+    }
+
+    public List<MailboxId> getAllChanges() {
+        return ImmutableList.<MailboxId>builder()
+            .addAll(created)
+            .addAll(updated)
+            .addAll(destroyed)
+            .build();
+    }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
similarity index 74%
copy from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
copy to server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
index 1526ca4..9fcc4a0 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
@@ -17,19 +17,19 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.api.vacation;
+package org.apache.james.jmap.api.exception;
 
-import java.time.ZonedDateTime;
-import java.util.Optional;
+import org.apache.james.jmap.api.change.MailboxChange;
 
-import reactor.core.publisher.Mono;
+public class ChangeNotFoundException extends RuntimeException {
+    private final MailboxChange.State state;
 
-public interface NotificationRegistry {
-
-    Mono<Void> register(AccountId accountId, RecipientId recipientId, Optional<ZonedDateTime> expiryDate);
-
-    Mono<Boolean> isRegistered(AccountId accountId, RecipientId recipientId);
-
-    Mono<Void> flush(AccountId accountId);
+    public ChangeNotFoundException(MailboxChange.State state, String msg) {
+        super(msg);
+        this.state = state;
+    }
 
+    public MailboxChange.State getState() {
+        return state;
+    }
 }
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/AccountId.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/AccountId.java
similarity index 98%
rename from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/AccountId.java
rename to server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/AccountId.java
index 59b181a..4f6417c 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/AccountId.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/AccountId.java
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.api.vacation;
+package org.apache.james.jmap.api.model;
 
 import java.util.Objects;
 
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
index 1526ca4..b6f5a58 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/NotificationRegistry.java
@@ -22,6 +22,8 @@ package org.apache.james.jmap.api.vacation;
 import java.time.ZonedDateTime;
 import java.util.Optional;
 
+import org.apache.james.jmap.api.model.AccountId;
+
 import reactor.core.publisher.Mono;
 
 public interface NotificationRegistry {
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java
index fe58e49..9077da7 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.jmap.api.vacation;
 
+import org.apache.james.jmap.api.model.AccountId;
+
 import reactor.core.publisher.Mono;
 
 public interface VacationRepository {
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java
new file mode 100644
index 0000000..e920904
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepository.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.jmap.memory.change;
+
+import java.util.Comparator;
+import java.util.Optional;
+
+import org.apache.james.jmap.api.change.MailboxChange;
+import org.apache.james.jmap.api.change.MailboxChange.Limit;
+import org.apache.james.jmap.api.change.MailboxChange.State;
+import org.apache.james.jmap.api.change.MailboxChangeRepository;
+import org.apache.james.jmap.api.change.MailboxChanges;
+import org.apache.james.jmap.api.change.MailboxChanges.MailboxChangesBuilder.MailboxChangeCollector;
+import org.apache.james.jmap.api.exception.ChangeNotFoundException;
+import org.apache.james.jmap.api.model.AccountId;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class MemoryMailboxChangeRepository implements MailboxChangeRepository {
+
+    public static final Limit DEFAULT_NUMBER_OF_CHANGES = Limit.of(5);
+    private final Multimap<AccountId, MailboxChange> mailboxChangeMap;
+
+    public MemoryMailboxChangeRepository() {
+        this.mailboxChangeMap = ArrayListMultimap.create();
+    }
+
+    @Override
+    public Mono<Void> save(MailboxChange change) {
+        Preconditions.checkNotNull(change.getAccountId());
+        Preconditions.checkNotNull(change.getState());
+
+        return Mono.just(mailboxChangeMap.put(change.getAccountId(), change)).then();
+    }
+
+    @Override
+    public Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges) {
+        Preconditions.checkNotNull(accountId);
+        Preconditions.checkNotNull(state);
+        maxChanges.ifPresent(limit -> Preconditions.checkArgument(limit.getValue() > 0, "maxChanges must be a positive integer"));
+
+        return findByState(accountId, state)
+            .flatMapMany(currentState -> Flux.fromIterable(mailboxChangeMap.get(accountId))
+                .filter(change -> change.getDate().isAfter(currentState.getDate()))
+                .sort(Comparator.comparing(MailboxChange::getDate)))
+            .collect(new MailboxChangeCollector(maxChanges.orElse(DEFAULT_NUMBER_OF_CHANGES)));
+    }
+
+    private Mono<MailboxChange> findByState(AccountId accountId, State state) {
+        return Flux.fromIterable(mailboxChangeMap.get(accountId))
+            .filter(change -> change.getState().equals(state))
+            .switchIfEmpty(Mono.error(new ChangeNotFoundException(state, String.format("State '%s' could not be found", state.getValue()))))
+            .single();
+    }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryNotificationRegistry.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryNotificationRegistry.java
index 315680e..914bbb2 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryNotificationRegistry.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryNotificationRegistry.java
@@ -24,7 +24,7 @@ import java.util.Optional;
 
 import javax.inject.Inject;
 
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.NotificationRegistry;
 import org.apache.james.jmap.api.vacation.RecipientId;
 import org.apache.james.util.date.ZonedDateTimeProvider;
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryVacationRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryVacationRepository.java
index 82e3b14..3948d05 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryVacationRepository.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/vacation/MemoryVacationRepository.java
@@ -22,7 +22,7 @@ package org.apache.james.jmap.memory.vacation;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.api.vacation.VacationRepository;
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
new file mode 100644
index 0000000..66f27e3
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeRepositoryContract.java
@@ -0,0 +1,269 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.api.change;
+
+import static org.apache.james.mailbox.fixture.MailboxFixture.BOB;
+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 java.time.ZonedDateTime;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.apache.james.jmap.api.change.MailboxChange.Limit;
+import org.apache.james.jmap.api.change.MailboxChange.State;
+import org.apache.james.jmap.api.exception.ChangeNotFoundException;
+import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.model.TestId;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public interface MailboxChangeRepositoryContract {
+    AccountId ACCOUNT_ID = AccountId.fromUsername(BOB);
+    ZonedDateTime DATE = ZonedDateTime.now();
+    State STATE_0 = State.of(UUID.randomUUID());
+
+    MailboxChangeRepository mailboxChangeRepository();
+
+    @Test
+    default void saveChangeShouldSuccess() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange change = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+
+        assertThatCode(() -> repository.save(change))
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    default void saveChangeShouldFailWhenNoAccountId() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange change = MailboxChange.of(null, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+
+        assertThatThrownBy(() -> repository.save(change).block())
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    default void saveChangeShouldFailWhenNoState() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange change = MailboxChange.of(ACCOUNT_ID, null, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+
+        assertThatThrownBy(() -> repository.save(change).block())
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    default void getChangesShouldSuccess() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(1), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(1)), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .hasSameElementsAs(change.getUpdated());
+    }
+
+    @Test
+    default void getChangesShouldReturnEmptyWhenNoNewerState() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        repository.save(oldState);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .isEmpty();
+    }
+
+    @Test
+    default void getChangesShouldLimitChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+        repository.save(change3);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getCreated())
+            .containsExactlyInAnyOrder(TestId.of(2), TestId.of(3), TestId.of(4));
+    }
+
+    @Test
+    default void getChangesShouldLimitChangesWhenMaxChangesOmitted() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(7)), ImmutableList.of(), ImmutableList.of());
+
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .hasSameElementsAs(change1.getCreated());
+    }
+
+    @Test
+    default void getChangesShouldNotReturnMoreThanMaxChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4), TestId.of(5)), ImmutableList.of(), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getAllChanges())
+            .hasSameElementsAs(change1.getCreated());
+    }
+
+    @Test
+    default void getChangesShouldReturnEmptyWhenNumberOfChangesExceedMaxChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change1);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().getAllChanges())
+            .isEmpty();
+    }
+
+    @Test
+    default void getChangesShouldReturnNewState() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+            .isEqualTo(change2.getState());
+    }
+
+    @Test
+    default void hasMoreChangesShouldBeTrueWhenMoreChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().hasMoreChanges())
+            .isTrue();
+    }
+
+    @Test
+    default void hasMoreChangesShouldBeFalseWhenNoMoreChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(4))).block().hasMoreChanges())
+            .isFalse();
+    }
+
+    @Test
+    default void changesShouldBeStoredInTheirRespectiveType() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(6), TestId.of(7)), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(TestId.of(4)));
+        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(8)), ImmutableList.of(TestId.of(6), TestId.of(7)), ImmutableList.of(TestId.of(5)));
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+        repository.save(change3);
+
+        MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mailboxChanges.getCreated()).containsExactlyInAnyOrder(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6), TestId.of(7), TestId.of(8));
+            softly.assertThat(mailboxChanges.getUpdated()).containsExactlyInAnyOrder(TestId.of(2), TestId.of(3), TestId.of(6), TestId.of(7));
+            softly.assertThat(mailboxChanges.getDestroyed()).containsExactlyInAnyOrder(TestId.of(4), TestId.of(5));
+        });
+    }
+
+    @Test
+    default void getChangesShouldIgnoreDuplicatedValues() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(), ImmutableList.of(TestId.of(1), TestId.of(2)), ImmutableList.of());
+        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(3)), ImmutableList.of(TestId.of(1), TestId.of(2)), ImmutableList.of());
+
+        repository.save(oldState);
+        repository.save(change1);
+        repository.save(change2);
+
+        MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block();
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(mailboxChanges.getUpdated()).containsExactly(TestId.of(1), TestId.of(2));
+            softly.assertThat(mailboxChanges.getCreated()).containsExactly(TestId.of(3));
+        });
+    }
+
+    @Test
+    default void getChangesShouldFailWhenInvalidMaxChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange currentState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
+        repository.save(currentState);
+        repository.save(change);
+
+        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(-1))))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    default void getChangesShouldFailWhenSinceStateNotFound() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block())
+            .isInstanceOf(ChangeNotFoundException.class);
+    }
+}
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/AccountIdTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/AccountIdTest.java
index ac4aa25..1870a49 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/AccountIdTest.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/AccountIdTest.java
@@ -22,6 +22,7 @@ package org.apache.james.jmap.api.vacation;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import org.apache.james.jmap.api.model.AccountId;
 import org.junit.jupiter.api.Test;
 
 class AccountIdTest {
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/NotificationRegistryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/NotificationRegistryContract.java
index afd2c64..0d9d986 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/NotificationRegistryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/NotificationRegistryContract.java
@@ -28,6 +28,7 @@ import java.time.ZonedDateTime;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.util.date.ZonedDateTimeProvider;
 import org.junit.jupiter.api.Test;
 
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/VacationRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/VacationRepositoryContract.java
index 436e13a..23d5770 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/VacationRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/vacation/VacationRepositoryContract.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import java.time.ZonedDateTime;
 import java.util.Optional;
 
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.util.ValuePatch;
 import org.junit.jupiter.api.Test;
 
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
similarity index 66%
copy from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java
copy to server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
index fe58e49..9079e65 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/vacation/VacationRepository.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
@@ -17,16 +17,22 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.api.vacation;
+package org.apache.james.jmap.memory.change;
 
-import reactor.core.publisher.Mono;
+import org.apache.james.jmap.api.change.MailboxChangeRepository;
+import org.apache.james.jmap.api.change.MailboxChangeRepositoryContract;
+import org.junit.jupiter.api.BeforeEach;
 
-public interface VacationRepository {
+public class MemoryMailboxChangeRepositoryTest implements MailboxChangeRepositoryContract {
+    MailboxChangeRepository mailboxChangeRepository;
 
-    Vacation DEFAULT_VACATION = Vacation.builder().enabled(false).build();
-
-    Mono<Void> modifyVacation(AccountId accountId, VacationPatch vacationPatch);
-
-    Mono<Vacation> retrieveVacation(AccountId accountId);
+    @BeforeEach
+    void setup() {
+        mailboxChangeRepository = new MemoryMailboxChangeRepository();
+    }
 
+    @Override
+    public MailboxChangeRepository mailboxChangeRepository() {
+        return mailboxChangeRepository;
+    }
 }
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationIntegrationTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationIntegrationTest.java
index ff7c010..9f72d73 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationIntegrationTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationIntegrationTest.java
@@ -40,7 +40,7 @@ import java.util.concurrent.TimeUnit;
 
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.core.Username;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.draft.JmapGuiceProbe;
 import org.apache.james.junit.categories.BasicFeature;
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationRelayIntegrationTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationRelayIntegrationTest.java
index 38d7344..77ad8a4 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationRelayIntegrationTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/VacationRelayIntegrationTest.java
@@ -30,7 +30,7 @@ import java.util.concurrent.TimeUnit;
 import org.apache.commons.net.smtp.SMTPClient;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.dnsservice.api.InMemoryDNSService;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.draft.JmapGuiceProbe;
 import org.apache.james.junit.categories.BasicFeature;
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetVacationResponseTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetVacationResponseTest.java
index 3139ef0..dd150be 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetVacationResponseTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/GetVacationResponseTest.java
@@ -38,7 +38,7 @@ import java.time.ZonedDateTime;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.jmap.AccessToken;
 import org.apache.james.jmap.FixedDateZonedDateTimeProvider;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.draft.JmapGuiceProbe;
 import org.apache.james.junit.categories.BasicFeature;
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetVacationResponseTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetVacationResponseTest.java
index bf69793..0180c11 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetVacationResponseTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetVacationResponseTest.java
@@ -36,7 +36,7 @@ import java.util.Optional;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.core.Username;
 import org.apache.james.jmap.AccessToken;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationPatch;
 import org.apache.james.jmap.draft.JmapGuiceProbe;
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethod.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethod.java
index e47af51..df9aae3 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethod.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethod.java
@@ -23,7 +23,7 @@ import static org.apache.james.jmap.http.LoggingHelper.jmapAction;
 
 import javax.inject.Inject;
 
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationRepository;
 import org.apache.james.jmap.draft.model.GetVacationRequest;
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethod.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethod.java
index e8f2839..3b41a6c 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethod.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethod.java
@@ -24,7 +24,7 @@ import static org.apache.james.util.ReactorUtils.context;
 
 import javax.inject.Inject;
 
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.NotificationRegistry;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationRepository;
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java
index aea13e6..f5fd41f 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/VacationMailet.java
@@ -27,7 +27,7 @@ import javax.mail.MessagingException;
 
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.james.core.MailAddress;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.NotificationRegistry;
 import org.apache.james.jmap.api.vacation.RecipientId;
 import org.apache.james.jmap.api.vacation.Vacation;
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethodTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethodTest.java
index 22ad61d..35c05ac 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethodTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/GetVacationResponseMethodTest.java
@@ -28,7 +28,7 @@ import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.apache.james.core.Username;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationRepository;
 import org.apache.james.jmap.draft.model.GetMailboxesRequest;
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethodTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethodTest.java
index f8c9c20..04386b6 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethodTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetVacationResponseMethodTest.java
@@ -31,7 +31,7 @@ import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.apache.james.core.Username;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.NotificationRegistry;
 import org.apache.james.jmap.api.vacation.Vacation;
 import org.apache.james.jmap.api.vacation.VacationRepository;
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java
index 4d36816..971628f 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/mailet/VacationMailetTest.java
@@ -33,7 +33,7 @@ import java.util.Optional;
 import javax.mail.MessagingException;
 
 import org.apache.james.core.MailAddress;
-import org.apache.james.jmap.api.vacation.AccountId;
+import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.vacation.NotificationRegistry;
 import org.apache.james.jmap.api.vacation.RecipientId;
 import org.apache.james.jmap.api.vacation.Vacation;
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationResponseGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationResponseGetMethodContract.scala
index 1ca2caf..0e39b11 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationResponseGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationResponseGetMethodContract.scala
@@ -27,7 +27,8 @@ import io.restassured.http.ContentType.JSON
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
-import org.apache.james.jmap.api.vacation.{AccountId, VacationPatch}
+import org.apache.james.jmap.api.model.AccountId
+import org.apache.james.jmap.api.vacation.VacationPatch
 import org.apache.james.jmap.draft.JmapGuiceProbe
 import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala
index 926f5e0..b06821b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseGetMethod.scala
@@ -21,7 +21,8 @@ package org.apache.james.jmap.method
 
 import eu.timepit.refined.auto._
 import javax.inject.Inject
-import org.apache.james.jmap.api.vacation.{VacationRepository, AccountId => JavaAccountId}
+import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
+import org.apache.james.jmap.api.vacation.VacationRepository
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodCallId, MethodName}
 import org.apache.james.jmap.core.State.INSTANCE
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
index 56bd506..8cb940a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/VacationResponseSetMethod.scala
@@ -21,7 +21,8 @@ package org.apache.james.jmap.method
 
 import eu.timepit.refined.auto._
 import javax.inject.Inject
-import org.apache.james.jmap.api.vacation.{AccountId, VacationPatch, VacationRepository}
+import org.apache.james.jmap.api.model.AccountId
+import org.apache.james.jmap.api.vacation.{VacationPatch, VacationRepository}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.SetError.SetErrorDescription


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 07/07: JAMES-2884 Server JMAP documentation should point to the annotated JMAP specifications

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 4f8b75a724aa7142ccb01ac8cd5559e54e50609a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 30 09:17:28 2020 +0700

    JAMES-2884 Server JMAP documentation should point to the annotated JMAP specifications
---
 .../servers/pages/distributed/configure/jmap.adoc     | 19 +++++++++++++++++++
 src/site/xdoc/server/config-jmap.xml                  | 19 +++++++++++++++++++
 2 files changed, 38 insertions(+)

diff --git a/docs/modules/servers/pages/distributed/configure/jmap.adoc b/docs/modules/servers/pages/distributed/configure/jmap.adoc
index 6650de7..8c29504 100644
--- a/docs/modules/servers/pages/distributed/configure/jmap.adoc
+++ b/docs/modules/servers/pages/distributed/configure/jmap.adoc
@@ -77,3 +77,22 @@ This token can then be passed as `Bearer` of the `Authorization` header :
     curl -H "Authorization: Bearer $token" -XPOST http://127.0.0.1:80/jmap -d '...'
 
 The public key can be referenced as `jwt.publickeypem.url` of the `jmap.properties` configuration file.
+
+== Annotated specification
+
+The [annotated documentation](https://github.com/apache/james-project/tree/master/server/protocols/jmap-rfc-8621/doc/specs/spec)
+presents the limits of the JMAP RFC-8621 implementation part of the Apache James project.
+
+Some methods / types are not yet implemented, some implementations are naive, and the PUSH is not supported yet.
+
+Users are invited to read these limitations before using actively the JMAP RFC-8621 implementation, and should ensure their
+client applications only uses supported operations.
+
+Contributions enhancing support are furthermore welcomed.
+
+The list of tested JMAP clients are:
+
+ - [OpenPaaS](https://open-paas.org/) is actively using the draft version of the JMAP implementation. Migration to
+ RFC-8621 is planned.
+ - Experiments had been run on top of [LTT.RS](https://github.com/iNPUTmice/lttrs-android). Version in the Accept
+ headers needs to be explicitly set to `rfc-8621`. [Read more](https://github.com/linagora/james-project/pull/4089).
\ No newline at end of file
diff --git a/src/site/xdoc/server/config-jmap.xml b/src/site/xdoc/server/config-jmap.xml
index 77c5326..b006481 100644
--- a/src/site/xdoc/server/config-jmap.xml
+++ b/src/site/xdoc/server/config-jmap.xml
@@ -104,6 +104,25 @@
                     <li><b>JMAP-RFC-8621</b>: <em>Accept: application/json; jmapVersion=rfc-8621</em></li>
                 </ul>
             </subsection>
+
+            <subsection name="Annotated specification">
+                <p>The <a href="https://github.com/apache/james-project/tree/master/server/protocols/jmap-rfc-8621/doc/specs/spec">annotated documentation</a>
+                presents the limits of the JMAP RFC-8621 implementation part of the Apache James project.</p>
+
+                <p>Some methods / types are not yet implemented, some implementations are naive, and the PUSH is not supported yet.</p>
+
+                <p>Users are invited to read these limitations before using actively the JMAP RFC-8621 implementation, and should ensure their
+                client applications only uses supported operations.</p>
+
+                <p>Contributions enhancing support are furthermore welcomed.</p>
+
+                <ul>The list of tested JMAP clients are:
+                    <li><a href="https://open-paas.org/">OpenPaaS</a> is actively using the draft version of the JMAP implementation. Migration to
+                    RFC-8621 is planned.</li>
+                    <li>Experiments had been run on top of <a href="https://github.com/iNPUTmice/lttrs-android">LTT.RS</a>. Version in the Accept
+                        headers needs to be explicitly set to `rfc-8621`. <a href="https://github.com/linagora/james-project/pull/4089">Read more</a>.</li>
+                </ul>
+            </subsection>
         </section>
 
     </body>


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org