You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/12/28 07:43:32 UTC

[james-project] branch master updated (7691aae -> 3785ad2)

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

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


    from 7691aae  JAMES-3481 MailboxDeletion event should carry mailboxACL
     new 1e66405  JAMES-3117 PeriodicalHealthChecks should not fail upon slow checks
     new 626b740  JAMES-3469 Pojoify */changes State & Limit
     new 39cef3a  JAMES-3469 MemoryMailboxChangeRepository should be Thread safe
     new bf6aa0c  JAMES-3469 Add EmailChange POJO
     new d3bfd8e  JAMES-3469 EmailChangeRepository interface
     new c071bae  JAMES-3469 EmailChangeRepository contract test
     new 4309efd  JAMES-3469 EmailChangeRepository memory implementation
     new 2ffb39b  JAMES-3469 Avoid returning duplicated changes
     new ae03324  JAMES-3469 Purge changes for deleted records
     new 27f9200  JAMES-3469 EmailChangeRepositoryContract: tests for delegation
     new 75293e5  JAMES-3469 Add null checks within MailboxChange POJO builder
     new ed215c4  JAMES-3469 Mailbox/changes should as well not return creates+updates when created or deleted within the same API call
     new 8d2ff80  MAILBOX-392 Allow mailbox names to contain `#`
     new 57534ca  JAMES-3431 Mock SMTP server docker image should use a fixed version
     new 27dedd5  JAMES-3465 Mailbox/changes updatedProperties handling
     new 3785ad2  JAMES-3465 Mailbox/changes response should not filter null updatedProperties

The 16 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:
 .../pages/distributed/operate/webadmin.adoc        |    3 +-
 .../apache/james/mailbox/model/MailboxPath.java    |    7 +-
 .../james/mailbox/model/MailboxPathTest.java       |   11 +-
 .../apache/james/imap/scripts/ListSpecialChar.test |    1 +
 .../imapmailbox/inmemory/InMemoryListingTest.java  |    1 +
 .../org/apache/james/PeriodicalHealthChecks.java   |    6 +-
 .../apache/james/PeriodicalHealthChecksTest.java   |   16 +
 .../james/modules/mailbox/MemoryMailboxModule.java |    2 +-
 .../apache/james/jmap/draft/JmapGuiceProbe.java    |    5 +
 .../change/CassandraMailboxChangeRepository.java   |   15 +-
 .../apache/james/jmap/api/change/EmailChange.java  |  195 ++++
 ...eRepository.java => EmailChangeRepository.java} |   11 +-
 .../{MailboxChanges.java => EmailChanges.java}     |   99 +-
 .../org/apache/james/jmap/api/change/Limit.java    |   20 +-
 .../james/jmap/api/change/MailboxChange.java       |  139 +--
 .../jmap/api/change/MailboxChangeRepository.java   |    2 -
 .../james/jmap/api/change/MailboxChanges.java      |   66 +-
 .../org/apache/james/jmap/api/change/State.java}   |   38 +-
 .../api/exception/ChangeNotFoundException.java     |    8 +-
 .../memory/change/MemoryEmailChangeRepository.java |  121 ++
 .../change/MemoryMailboxChangeRepository.java      |   12 +-
 .../api/change/EmailChangeRepositoryContract.java  | 1223 ++++++++++++++++++++
 .../james/jmap/api/change/EmailChangeTest.java     |   64 +-
 .../apache/james/jmap/api/change/LimitTest.java    |   21 +-
 .../change/MailboxChangeRepositoryContract.java    |  241 ++--
 .../james/jmap/api/change/MailboxChangeTest.java}  |   42 +-
 .../apache/james/jmap/api/change/StateTest.java}   |   20 +-
 .../MemoryEmailChangeRepositoryTest.java}          |   17 +-
 .../change/MemoryMailboxChangeRepositoryTest.java  |    8 +-
 .../contract/MailboxChangesMethodContract.scala    |  458 +++++++-
 .../contract/MailboxGetMethodContract.scala        |    3 +-
 .../contract/MailboxSetMethodContract.scala        |    5 +-
 .../memory/MemoryMailboxChangesMethodTest.java     |    6 +-
 .../scala/org/apache/james/jmap/core/Session.scala |    4 +-
 .../apache/james/jmap/json/MailboxSerializer.scala |   13 +-
 .../org/apache/james/jmap/mail/MailboxGet.scala    |    2 +-
 .../james/jmap/method/MailboxChangesMethod.scala   |   19 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |   25 +-
 .../org/apache/james/cli/MailboxManageTest.java    |    2 +-
 .../webadmin/routes/UserMailboxesRoutesTest.java   |   43 +-
 .../java/org/apache/james/util/docker/Images.java  |    2 +-
 src/site/markdown/server/manage-webadmin.md        |    2 +-
 42 files changed, 2595 insertions(+), 403 deletions(-)
 create mode 100644 server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChange.java
 copy server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/{MailboxChangeRepository.java => EmailChangeRepository.java} (77%)
 copy server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/{MailboxChanges.java => EmailChanges.java} (55%)
 copy mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Combinator.java => server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/Limit.java (77%)
 copy server/{task/task-api/src/main/java/org/apache/james/task/TaskId.java => data/data-jmap/src/main/java/org/apache/james/jmap/api/change/State.java} (70%)
 create mode 100644 server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepository.java
 create mode 100644 server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
 copy mdn/src/test/java/org/apache/james/mdn/modifier/DispositionModifierTest.java => server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeTest.java (54%)
 copy backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/ScenarioTest.java => server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/LimitTest.java (77%)
 copy server/{blob/blob-api/src/test/java/org/apache/james/blob/api/BucketNameTest.java => data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeTest.java} (56%)
 copy server/data/{data-library/src/test/java/org/apache/james/dlp/eventsourcing/commands/ClearCommandTest.java => data-jmap/src/test/java/org/apache/james/jmap/api/change/StateTest.java} (76%)
 copy server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/{vacation/MemoryVacationRepositoryTest.java => change/MemoryEmailChangeRepositoryTest.java} (70%)


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


[james-project] 06/16: JAMES-3469 EmailChangeRepository contract test

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

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

commit c071bae3fff3d54e4173d3284866135315909cb0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 11:06:03 2020 +0700

    JAMES-3469 EmailChangeRepository contract test
---
 .../api/change/EmailChangeRepositoryContract.java  | 587 +++++++++++++++++++++
 1 file changed, 587 insertions(+)

diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
new file mode 100644
index 0000000..e917729
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
@@ -0,0 +1,587 @@
+/****************************************************************
+ * 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.State;
+import org.apache.james.jmap.api.exception.ChangeNotFoundException;
+import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.model.TestMessageId;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+
+public interface EmailChangeRepositoryContract {
+    AccountId ACCOUNT_ID = AccountId.fromUsername(BOB);
+    ZonedDateTime DATE = ZonedDateTime.now();
+    State STATE_0 = State.of(UUID.randomUUID());
+
+    EmailChangeRepository emailChangeRepository();
+
+    @Test
+    default void saveChangeShouldSuccess() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange change = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+
+        assertThatCode(() -> repository.save(change).block())
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    default void getLatestStateShouldReturnInitialWhenEmpty() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        assertThat(repository.getLatestState(ACCOUNT_ID).block())
+            .isEqualTo(State.INITIAL);
+    }
+
+    @Test
+    default void getLatestStateShouldReturnLastPersistedState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getLatestState(ACCOUNT_ID).block())
+            .isEqualTo(change3.getState());
+    }
+
+    @Test
+    default void getChangesShouldSuccess() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(1))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .hasSameElementsAs(change.getUpdated());
+    }
+
+    @Test
+    default void getChangesShouldReturnEmptyWhenNoNewerState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        repository.save(oldState).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .isEmpty();
+    }
+
+    @Test
+    default void getChangesShouldReturnCurrentStateWhenNoNewerState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        repository.save(oldState).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+            .isEqualTo(oldState.getState());
+    }
+
+    @Test
+    default void getChangesShouldLimitChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+        EmailChange change4 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.plusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+        repository.save(change4).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getCreated())
+            .containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4));
+    }
+
+    @Test
+    default void getChangesShouldReturnAllFromInitial() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, State.INITIAL, Optional.of(Limit.of(3))).block().getCreated())
+            .containsExactlyInAnyOrder(TestMessageId.of(1), TestMessageId.of(2), TestMessageId.of(3));
+    }
+
+    @Test
+    default void getChangesFromInitialShouldReturnNewState() {
+        EmailChangeRepository repository = emailChangeRepository();
+        State state2 = State.of(UUID.randomUUID());
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(state2)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, State.INITIAL, Optional.of(Limit.of(3))).block().getNewState())
+            .isEqualTo(state2);
+    }
+
+    @Test
+    default void getChangesShouldLimitChangesWhenMaxChangesOmitted() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(6))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(7))
+            .build();
+
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .hasSameElementsAs(change1.getCreated());
+    }
+
+    @Test
+    default void getChangesShouldNotReturnMoreThanMaxChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getAllChanges())
+            .hasSameElementsAs(change1.getCreated());
+    }
+
+    @Test
+    default void getChangesShouldReturnEmptyWhenNumberOfChangesExceedMaxChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().getAllChanges())
+            .isEmpty();
+    }
+
+    @Test
+    default void getChangesShouldReturnNewState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+            .isEqualTo(change2.getState());
+    }
+
+    @Test
+    default void hasMoreChangesShouldBeTrueWhenMoreChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().hasMoreChanges())
+            .isTrue();
+    }
+
+    @Test
+    default void hasMoreChangesShouldBeFalseWhenNoMoreChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(4))).block().hasMoreChanges())
+            .isFalse();
+    }
+
+    @Test
+    default void changesShouldBeStoredInTheirRespectiveType() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(6), TestMessageId.of(7))
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .destroyed(TestMessageId.of(4))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(8))
+            .updated(TestMessageId.of(6), TestMessageId.of(7))
+            .destroyed(TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        EmailChanges emailChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(emailChanges.getCreated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(8));
+            softly.assertThat(emailChanges.getUpdated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(6), TestMessageId.of(7));
+            softly.assertThat(emailChanges.getDestroyed()).containsExactlyInAnyOrder(TestMessageId.of(4), TestMessageId.of(5));
+        });
+    }
+
+    @Test
+    default void getChangesShouldIgnoreDuplicatedValues() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .updated(TestMessageId.of(1), TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(1), TestMessageId.of(2))
+            .created(TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        EmailChanges emailChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block();
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(emailChanges.getUpdated()).containsExactly(TestMessageId.of(1), TestMessageId.of(2));
+            softly.assertThat(emailChanges.getCreated()).containsExactly(TestMessageId.of(3));
+        });
+    }
+
+    @Test
+    default void getChangesShouldFailWhenSinceStateNotFound() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block())
+            .isInstanceOf(ChangeNotFoundException.class);
+    }
+}


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


[james-project] 15/16: JAMES-3465 Mailbox/changes updatedProperties handling

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

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

commit 27dedd5a72a9fcbe79402efa47ed6cc464afc2a3
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Dec 22 16:47:38 2020 +0700

    JAMES-3465 Mailbox/changes updatedProperties handling
---
 .../james/jmap/api/change/MailboxChange.java       |  41 ++++-
 .../james/jmap/api/change/MailboxChanges.java      |  17 ++-
 .../change/MailboxChangeRepositoryContract.java    | 106 +++++++------
 .../contract/MailboxChangesMethodContract.scala    | 166 ++++++++++++++++++---
 .../contract/MailboxSetMethodContract.scala        |   5 +-
 .../james/jmap/method/MailboxChangesMethod.scala   |   7 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |  18 +--
 7 files changed, 271 insertions(+), 89 deletions(-)

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
index f8af13e..caa954e 100644
--- 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
@@ -64,7 +64,12 @@ public class MailboxChange {
 
     @FunctionalInterface
     public interface RequiredDate {
-        Builder date(ZonedDateTime date);
+        RequiredIsCountChange date(ZonedDateTime date);
+    }
+
+    @FunctionalInterface
+    public interface RequiredIsCountChange {
+        Builder isCountChange(boolean isCountChange);
     }
 
     public static class Builder {
@@ -72,11 +77,12 @@ public class MailboxChange {
         private final State state;
         private final ZonedDateTime date;
         private boolean delegated;
+        private boolean isCountChange;
         private Optional<List<MailboxId>> created;
         private Optional<List<MailboxId>> updated;
         private Optional<List<MailboxId>> destroyed;
 
-        private Builder(AccountId accountId, State state, ZonedDateTime date) {
+        private Builder(AccountId accountId, State state, ZonedDateTime date, boolean isCountChange) {
             Preconditions.checkNotNull(accountId, "'accountId' cannot be null");
             Preconditions.checkNotNull(state, "'state' cannot be null");
             Preconditions.checkNotNull(date, "'date' cannot be null");
@@ -84,6 +90,7 @@ public class MailboxChange {
             this.accountId = accountId;
             this.state = state;
             this.date = date;
+            this.isCountChange = isCountChange;
             this.created = Optional.empty();
             this.updated = Optional.empty();
             this.destroyed = Optional.empty();
@@ -94,6 +101,11 @@ public class MailboxChange {
             return this;
         }
 
+        public Builder isCountChange(boolean isCountChange) {
+            this.isCountChange = isCountChange;
+            return this;
+        }
+
         public Builder created(List<MailboxId> created) {
             this.created = Optional.of(created);
             return this;
@@ -110,12 +122,12 @@ public class MailboxChange {
         }
 
         public MailboxChange build() {
-            return new MailboxChange(accountId, state, date, delegated, created.orElse(ImmutableList.of()), updated.orElse(ImmutableList.of()), destroyed.orElse(ImmutableList.of()));
+            return new MailboxChange(accountId, state, date, delegated, isCountChange, created.orElse(ImmutableList.of()), updated.orElse(ImmutableList.of()), destroyed.orElse(ImmutableList.of()));
         }
     }
 
     public static RequiredAccountId builder() {
-        return accountId -> state -> date -> new Builder(accountId, state, date);
+        return accountId -> state -> date -> isCountChange -> new Builder(accountId, state, date, isCountChange);
     }
 
     public static class Factory {
@@ -139,6 +151,7 @@ public class MailboxChange {
                     .accountId(AccountId.fromUsername(mailboxAdded.getUsername()))
                     .state(stateFactory.generate())
                     .date(now)
+                    .isCountChange(false)
                     .created(ImmutableList.of(mailboxAdded.getMailboxId()))
                     .build());
             }
@@ -149,6 +162,7 @@ public class MailboxChange {
                     .accountId(AccountId.fromUsername(mailboxRenamed.getUsername()))
                     .state(stateFactory.generate())
                     .date(now)
+                    .isCountChange(false)
                     .updated(ImmutableList.of(mailboxRenamed.getMailboxId()))
                     .build();
 
@@ -157,6 +171,7 @@ public class MailboxChange {
                         .accountId(AccountId.fromString(name))
                         .state(stateFactory.generate())
                         .date(now)
+                        .isCountChange(false)
                         .updated(ImmutableList.of(mailboxRenamed.getMailboxId()))
                         .delegated()
                         .build());
@@ -171,6 +186,7 @@ public class MailboxChange {
                     .accountId(AccountId.fromUsername(mailboxACLUpdated.getUsername()))
                     .state(stateFactory.generate())
                     .date(now)
+                    .isCountChange(false)
                     .updated(ImmutableList.of(mailboxACLUpdated.getMailboxId()))
                     .build();
 
@@ -179,6 +195,7 @@ public class MailboxChange {
                         .accountId(AccountId.fromString(name))
                         .state(stateFactory.generate())
                         .date(now)
+                        .isCountChange(false)
                         .updated(ImmutableList.of(mailboxACLUpdated.getMailboxId()))
                         .delegated()
                         .build());
@@ -193,6 +210,7 @@ public class MailboxChange {
                     .accountId(AccountId.fromUsername(mailboxDeletion.getUsername()))
                     .state(stateFactory.generate())
                     .date(now)
+                    .isCountChange(false)
                     .destroyed(ImmutableList.of(mailboxDeletion.getMailboxId()))
                     .build();
 
@@ -206,6 +224,7 @@ public class MailboxChange {
                         .accountId(AccountId.fromString(name))
                         .state(stateFactory.generate())
                         .date(now)
+                        .isCountChange(false)
                         .destroyed(ImmutableList.of(mailboxDeletion.getMailboxId()))
                         .delegated()
                         .build());
@@ -220,6 +239,7 @@ public class MailboxChange {
                     .accountId(AccountId.fromUsername(messageAdded.getUsername()))
                     .state(stateFactory.generate())
                     .date(now)
+                    .isCountChange(true)
                     .updated(ImmutableList.of(messageAdded.getMailboxId()))
                     .build();
 
@@ -228,6 +248,7 @@ public class MailboxChange {
                         .accountId(AccountId.fromString(name))
                         .state(stateFactory.generate())
                         .date(now)
+                        .isCountChange(true)
                         .updated(ImmutableList.of(messageAdded.getMailboxId()))
                         .delegated()
                         .build());
@@ -245,6 +266,7 @@ public class MailboxChange {
                         .accountId(AccountId.fromUsername(messageFlagUpdated.getUsername()))
                         .state(stateFactory.generate())
                         .date(now)
+                        .isCountChange(true)
                         .updated(ImmutableList.of(messageFlagUpdated.getMailboxId()))
                         .build();
 
@@ -253,6 +275,7 @@ public class MailboxChange {
                             .accountId(AccountId.fromString(name))
                             .state(stateFactory.generate())
                             .date(now)
+                            .isCountChange(true)
                             .updated(ImmutableList.of(messageFlagUpdated.getMailboxId()))
                             .delegated()
                             .build());
@@ -267,6 +290,7 @@ public class MailboxChange {
                     .accountId(AccountId.fromUsername(expunged.getUsername()))
                     .state(stateFactory.generate())
                     .date(now)
+                    .isCountChange(true)
                     .updated(ImmutableList.of(expunged.getMailboxId()))
                     .build();
 
@@ -275,6 +299,7 @@ public class MailboxChange {
                         .accountId(AccountId.fromString(name))
                         .state(stateFactory.generate())
                         .date(now)
+                        .isCountChange(true)
                         .updated(ImmutableList.of(expunged.getMailboxId()))
                         .delegated()
                         .build());
@@ -305,15 +330,17 @@ public class MailboxChange {
     private final State state;
     private final ZonedDateTime date;
     private final boolean delegated;
+    private final boolean isCountChange;
     private final List<MailboxId> created;
     private final List<MailboxId> updated;
     private final List<MailboxId> destroyed;
 
-    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, boolean delegated, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
+    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, boolean delegated, boolean isCountChange, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
         this.accountId = accountId;
         this.state = state;
         this.date = date;
         this.delegated = delegated;
+        this.isCountChange = isCountChange;
         this.created = created;
         this.updated = updated;
         this.destroyed = destroyed;
@@ -335,6 +362,10 @@ public class MailboxChange {
         return delegated;
     }
 
+    public boolean isCountChange() {
+        return isCountChange;
+    }
+
     public List<MailboxId> getCreated() {
         return created;
     }
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
index bbe718b..30493fb 100644
--- 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
@@ -77,6 +77,7 @@ public class MailboxChanges {
         private boolean hasMoreChanges;
         private boolean canAddMoreItem;
         private Limit limit;
+        private boolean isCountChangeOnly;
         private Set<MailboxId> created;
         private Set<MailboxId> updated;
         private Set<MailboxId> destroyed;
@@ -85,6 +86,7 @@ public class MailboxChanges {
             this.limit = limit;
             this.state = state;
             this.hasMoreChanges = false;
+            this.isCountChangeOnly = false;
             this.canAddMoreItem = true;
             this.created = new HashSet<>();
             this.updated = new HashSet<>();
@@ -121,6 +123,11 @@ public class MailboxChanges {
                 return this;
             }
 
+            if (created.isEmpty() && updated.isEmpty() && destroyed.isEmpty()) {
+                isCountChangeOnly = change.isCountChange();
+            } else {
+                isCountChangeOnly = isCountChangeOnly && change.isCountChange();
+            }
             state = change.getState();
             created = createdTemp;
             updated = updatedTemp;
@@ -130,19 +137,21 @@ public class MailboxChanges {
         }
 
         public MailboxChanges build() {
-            return new MailboxChanges(state, hasMoreChanges, created, updated, destroyed);
+            return new MailboxChanges(state, hasMoreChanges, isCountChangeOnly, created, updated, destroyed);
         }
     }
 
     private State newState;
     private final boolean hasMoreChanges;
+    private final boolean isCountChangesOnly;
     private final Set<MailboxId> created;
     private final Set<MailboxId> updated;
     private final Set<MailboxId> destroyed;
 
-    private MailboxChanges(State newState, boolean hasMoreChanges, Set<MailboxId> created, Set<MailboxId> updated, Set<MailboxId> destroyed) {
+    private MailboxChanges(State newState, boolean hasMoreChanges, boolean isCountChangesOnly, Set<MailboxId> created, Set<MailboxId> updated, Set<MailboxId> destroyed) {
         this.newState = newState;
         this.hasMoreChanges = hasMoreChanges;
+        this.isCountChangesOnly = isCountChangesOnly;
         this.created = created;
         this.updated = updated;
         this.destroyed = destroyed;
@@ -156,6 +165,10 @@ public class MailboxChanges {
         return hasMoreChanges;
     }
 
+    public boolean isCountChangesOnly() {
+        return isCountChangesOnly;
+    }
+
     public Set<MailboxId> getCreated() {
         return created;
     }
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
index 36a64b5..8a06e96 100644
--- 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
@@ -48,7 +48,7 @@ public interface MailboxChangeRepositoryContract {
         MailboxChangeRepository repository = mailboxChangeRepository();
         State state = stateFactory().generate();
 
-        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
 
         assertThatCode(() -> repository.save(change).block())
             .doesNotThrowAnyException();
@@ -67,9 +67,9 @@ public interface MailboxChangeRepositoryContract {
         MailboxChangeRepository repository = mailboxChangeRepository();
         State.Factory stateFactory = stateFactory();
 
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(change1).block();
         repository.save(change2).block();
         repository.save(change3).block();
@@ -83,12 +83,13 @@ public interface MailboxChangeRepositoryContract {
         MailboxChangeRepository repository = mailboxChangeRepository();
         State.Factory stateFactory = stateFactory();
 
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
         MailboxChange change3 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE)
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(4)))
             .delegated()
             .build();
@@ -113,9 +114,9 @@ public interface MailboxChangeRepositoryContract {
         MailboxChangeRepository repository = mailboxChangeRepository();
         State.Factory stateFactory = stateFactory();
 
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(change1).block();
         repository.save(change2).block();
         repository.save(change3).block();
@@ -129,12 +130,13 @@ public interface MailboxChangeRepositoryContract {
         MailboxChangeRepository repository = mailboxChangeRepository();
         State.Factory stateFactory = stateFactory();
 
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
         MailboxChange change3 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE)
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(4)))
             .delegated()
             .build();
@@ -152,8 +154,8 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).updated(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState);
         repository.save(change);
 
@@ -167,7 +169,7 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState);
 
         assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getAllChanges())
@@ -180,7 +182,7 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState);
 
         assertThat(repository.getSinceState(ACCOUNT_ID, referenceState, Optional.empty()).block().getNewState())
@@ -193,10 +195,10 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState);
         repository.save(change1);
         repository.save(change2);
@@ -212,10 +214,10 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -231,11 +233,11 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
         State state2 = stateFactory.generate();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(state2).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
-        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(state2).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -252,9 +254,9 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(7))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(7))).build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -270,9 +272,9 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(4), TestId.of(5))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(4), TestId.of(5))).build();
         repository.save(oldState);
         repository.save(change1);
         repository.save(change2);
@@ -287,8 +289,8 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState);
         repository.save(change1);
 
@@ -302,9 +304,9 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState);
         repository.save(change1);
         repository.save(change2);
@@ -319,9 +321,9 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState);
         repository.save(change1);
         repository.save(change2);
@@ -336,9 +338,9 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
-        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState);
         repository.save(change1);
         repository.save(change2);
@@ -357,18 +359,21 @@ public interface MailboxChangeRepositoryContract {
             .accountId(ACCOUNT_ID)
             .state(referenceState)
             .date(DATE.minusHours(3))
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(1), TestId.of(9), TestId.of(10)))
             .build();
         MailboxChange change1 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE.minusHours(2))
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5)))
             .build();
         MailboxChange change2 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE.minusHours(1))
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(6), TestId.of(7)))
             .updated(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(9)))
             .destroyed(ImmutableList.of(TestId.of(4))).build();
@@ -376,6 +381,7 @@ public interface MailboxChangeRepositoryContract {
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE)
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(8)))
             .updated(ImmutableList.of(TestId.of(6), TestId.of(7)))
             .destroyed(ImmutableList.of(TestId.of(5), TestId.of(10))).build();
@@ -400,12 +406,13 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).updated(ImmutableList.of(TestId.of(1), TestId.of(2))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(1)).isCountChange(false).updated(ImmutableList.of(TestId.of(1), TestId.of(2))).build();
         MailboxChange change2 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE)
+            .isCountChange(false)
             .created(ImmutableList.of(TestId.of(3)))
             .updated(ImmutableList.of(TestId.of(1), TestId.of(2)))
             .build();
@@ -427,11 +434,12 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(2)).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
         MailboxChange change1 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE.minusHours(1))
+            .isCountChange(false)
             .updated(ImmutableList.of(TestId.of(1)))
             .delegated()
             .build();
@@ -450,8 +458,8 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange currentState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange currentState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE).isCountChange(false).created(ImmutableList.of(TestId.of(2))).build();
         repository.save(currentState);
         repository.save(change);
 
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/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index 46fd74f..918f062 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -133,7 +133,6 @@ trait MailboxChangesMethodContract {
             |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
             |        "oldState": "${oldState.getValue}",
             |        "hasMoreChanges": false,
-            |        "updatedProperties": [],
             |        "created": ["$mailboxId1", "$mailboxId2", "$mailboxId3"],
             |        "updated": [],
             |        "destroyed": []
@@ -191,7 +190,6 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
            |        "created": [],
            |        "updated": ["$mailboxId"],
            |        "destroyed": []
@@ -255,7 +253,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
+           |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
            |        "created": [],
            |        "updated": ["$mailboxId"],
            |        "destroyed": []
@@ -321,7 +319,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
+           |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
            |        "created": [],
            |        "updated": ["$mailboxId"],
            |        "destroyed": []
@@ -389,7 +387,7 @@ trait MailboxChangesMethodContract {
            |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |      "oldState": "${oldState.getValue}",
            |      "hasMoreChanges": false,
-           |      "updatedProperties": [],
+           |      "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
            |      "created": [],
            |      "updated": ["$mailboxId"],
            |      "destroyed": []
@@ -455,7 +453,7 @@ trait MailboxChangesMethodContract {
            |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |      "oldState": "${oldState.getValue}",
            |      "hasMoreChanges": false,
-           |      "updatedProperties": [],
+           |      "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
            |      "created": [],
            |      "updated": ["$mailboxId"],
            |      "destroyed": []
@@ -526,7 +524,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
+             |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
              |        "created": [],
              |        "updated": ["$mailboxId"],
              |        "destroyed": []
@@ -590,7 +588,6 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
              |        "created": [],
              |        "updated": ["$mailboxId"],
              |        "destroyed": []
@@ -661,7 +658,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
+             |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
              |        "created": [],
              |        "updated": ["$mailboxId"],
              |        "destroyed": []
@@ -731,7 +728,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
+             |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
              |        "created": [],
              |        "updated": ["$mailboxId"],
              |        "destroyed": []
@@ -802,7 +799,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
+             |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
              |        "created": [],
              |        "updated": ["$mailboxId"],
              |        "destroyed": []
@@ -873,7 +870,6 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
              |        "created": [],
              |        "updated": [],
              |        "destroyed": []
@@ -937,7 +933,6 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
-             |        "updatedProperties": [],
              |        "created": [],
              |        "updated": [],
              |        "destroyed": ["$mailboxId"]
@@ -998,7 +993,6 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
            |        "created": [],
            |        "updated": [],
            |        "destroyed": ["$mailboxId"]
@@ -1072,7 +1066,6 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
            |        "created": ["$mailboxId3"],
            |        "updated": ["$mailboxId1"],
            |        "destroyed": ["$mailboxId2"]
@@ -1145,7 +1138,6 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
            |        "created": ["$mailboxId2"],
            |        "updated": [],
            |        "destroyed": []
@@ -1220,7 +1212,6 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": true,
-           |        "updatedProperties": [],
            |        "created": ["$mailboxId1", "$mailboxId2", "$mailboxId3", "$mailboxId4", "$mailboxId5"],
            |        "updated": [],
            |        "destroyed": []
@@ -1355,7 +1346,6 @@ trait MailboxChangesMethodContract {
            |        "oldState": "${oldState.getValue}",
            |        "newState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
            |        "created": [],
            |        "updated": [],
            |        "destroyed": []
@@ -1480,7 +1470,6 @@ trait MailboxChangesMethodContract {
            |        "oldState": "$newState",
            |        "newState": "$newState",
            |        "hasMoreChanges": false,
-           |        "updatedProperties": [],
            |        "created": [],
            |        "updated": [],
            |        "destroyed": []
@@ -1489,11 +1478,148 @@ trait MailboxChangesMethodContract {
            |}""".stripMargin)
   }
 
+  @Test
+  def mailboxChangesShouldReturnUpdatedPropertiesWhenOnlyCountChanges(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
+    val path = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId: String = mailboxProbe
+      .createMailbox(path)
+      .serialize
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+    val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+    val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+    JmapRequests.destroyEmail(messageId2)
+    JmapRequests.markEmailAsSeen(messageId3)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Mailbox/changes",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |      [ "Mailbox/changes", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "oldState": "${oldState.getValue}",
+           |        "hasMoreChanges": false,
+           |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
+           |        "created": [],
+           |        "updated": ["$mailboxId"],
+           |        "destroyed": []
+           |      }, "c1"]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def mailboxChangesShouldNotReturnUpdatedPropertiesWhenMixedChanges(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
+    val path = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId1: String = mailboxProbe
+      .createMailbox(path)
+      .serialize
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    val path2 = MailboxPath.forUser(BOB, "mailbox2")
+    val mailboxId2: String = mailboxProbe
+      .createMailbox(path2)
+      .serialize
+
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Mailbox/changes",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |      [ "Mailbox/changes", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "oldState": "${oldState.getValue}",
+           |        "hasMoreChanges": false,
+           |        "created": ["$mailboxId2"],
+           |        "updated": ["$mailboxId1"],
+           |        "destroyed": []
+           |      }, "c1"]
+           |    ]
+           |}""".stripMargin)
+  }
+
   private def storeReferenceState(server: GuiceJamesServer, username: Username): State = {
     val state: State = stateFactory.generate()
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
 
-    jmapGuiceProbe.saveMailboxChange(MailboxChange.builder.accountId(AccountId.fromUsername(username)).state(state).date(ZonedDateTime.now()).updated(List(TestId.of(0)).asJava).build)
+    jmapGuiceProbe.saveMailboxChange(MailboxChange.builder.accountId(AccountId.fromUsername(username)).state(state).date(ZonedDateTime.now()).isCountChange(false).updated(List(TestId.of(0)).asJava).build)
 
     state
   }
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/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
index 3e034fd..4292b01 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
@@ -35,7 +35,6 @@ import org.apache.james.jmap.core.State.INSTANCE
 import org.apache.james.jmap.draft.MessageIdProbe
 import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, BOB, BOB_PASSWORD, CEDRIC, DAVID, DOMAIN, authScheme, baseRequestSpecBuilder}
-import org.apache.james.jmap.rfc8621.contract.MailboxGetMethodContract.ARGUMENTS
 import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.{EntryKey, Right}
@@ -7674,7 +7673,7 @@ trait MailboxSetMethodContract {
         s"""{
            |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |  "hasMoreChanges": false,
-           |  "updatedProperties": [],
+           |  "updatedProperties": null,
            |  "created": [],
            |  "updated": [],
            |  "destroyed": []
@@ -7735,7 +7734,7 @@ trait MailboxSetMethodContract {
         s"""{
            |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |  "hasMoreChanges": false,
-           |  "updatedProperties": [],
+           |  "updatedProperties": null,
            |  "created": [],
            |  "updated": ["${mailboxId.serialize}"],
            |  "destroyed": []
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
index ab0003c..c1f985b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
@@ -29,6 +29,7 @@ import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.{CapabilityIdentifier, Invocation, Properties, State}
 import org.apache.james.jmap.json.{MailboxSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.{HasMoreChanges, MailboxChangesRequest, MailboxChangesResponse}
+import org.apache.james.jmap.method.MailboxChangesMethod.updatedProperties
 import org.apache.james.jmap.routes.SessionSupplier
 import org.apache.james.mailbox.MailboxSession
 import org.apache.james.metrics.api.MetricFactory
@@ -38,6 +39,10 @@ import reactor.core.scala.publisher.SMono
 import scala.jdk.CollectionConverters._
 import scala.jdk.OptionConverters._
 
+object MailboxChangesMethod {
+  val updatedProperties: Properties = Properties("totalEmails", "unreadEmails", "totalThreads", "unreadThreads")
+}
+
 class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
                                      val metricFactory: MetricFactory,
                                      val sessionSupplier: SessionSupplier,
@@ -59,7 +64,7 @@ class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
         oldState = request.sinceState,
         newState = State.fromMailboxChanges(mailboxChanges),
         hasMoreChanges = HasMoreChanges.fromMailboxChanges(mailboxChanges),
-        updatedProperties = Some(Properties()),
+        updatedProperties = if (mailboxChanges.isCountChangesOnly) Some(updatedProperties) else None,
         created = mailboxChanges.getCreated.asScala.toSet,
         updated = mailboxChanges.getUpdated.asScala.toSet,
         destroyed = mailboxChanges.getDestroyed.asScala.toSet))
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
index 7c0c39d..edced0c 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
@@ -74,7 +74,7 @@ class MailboxChangeListenerTest {
   @Test
   def createMailboxShouldStoreCreatedEvent(): Unit = {
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
@@ -91,7 +91,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.renameMailbox(path, newPath, mailboxSession)
 
@@ -119,7 +119,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 
@@ -140,7 +140,7 @@ class MailboxChangeListenerTest {
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -159,7 +159,7 @@ class MailboxChangeListenerTest {
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.REMOVE, MessageRange.all(), mailboxSession)
 
@@ -176,7 +176,7 @@ class MailboxChangeListenerTest {
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.ANSWERED), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -195,7 +195,7 @@ class MailboxChangeListenerTest {
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.DELETED), FlagsUpdateMode.REPLACE, MessageRange.all(), mailboxSession)
 
@@ -212,7 +212,7 @@ class MailboxChangeListenerTest {
     val appendResult: AppendResult = messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
     messageManager.delete(List(appendResult.getId.getUid).asJava, mailboxSession)
 
     assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
@@ -226,7 +226,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
+    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).isCountChange(false).created(List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.deleteMailbox(inboxId, mailboxSession)
 


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


[james-project] 11/16: JAMES-3469 Add null checks within MailboxChange POJO builder

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

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

commit 75293e5d8986097da19478a5dc4ffe1a52686b72
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 14:43:51 2020 +0700

    JAMES-3469 Add null checks within MailboxChange POJO builder
    
    Enforces accountId and state to always be here, implementations can safely skip these checks
---
 .../james/jmap/api/change/MailboxChange.java       | 40 ++++++++++++
 .../change/MemoryMailboxChangeRepository.java      |  3 -
 .../change/MailboxChangeRepositoryContract.java    | 21 -------
 .../james/jmap/api/change/MailboxChangeTest.java   | 72 ++++++++++++++++++++++
 4 files changed, 112 insertions(+), 24 deletions(-)

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
index f242cc0..f8af13e 100644
--- 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
@@ -22,6 +22,7 @@ package org.apache.james.jmap.api.change;
 import java.time.Clock;
 import java.time.ZonedDateTime;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Stream;
 
@@ -46,6 +47,8 @@ import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MailboxPath;
 
 import com.github.steveash.guavate.Guavate;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 public class MailboxChange {
@@ -74,6 +77,10 @@ public class MailboxChange {
         private Optional<List<MailboxId>> destroyed;
 
         private Builder(AccountId accountId, State state, ZonedDateTime date) {
+            Preconditions.checkNotNull(accountId, "'accountId' cannot be null");
+            Preconditions.checkNotNull(state, "'state' cannot be null");
+            Preconditions.checkNotNull(date, "'date' cannot be null");
+
             this.accountId = accountId;
             this.state = state;
             this.date = date;
@@ -339,4 +346,37 @@ public class MailboxChange {
     public List<MailboxId> getDestroyed() {
         return destroyed;
     }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof MailboxChange) {
+            MailboxChange that = (MailboxChange) o;
+            return Objects.equals(accountId, that.accountId)
+                && Objects.equals(state, that.state)
+                && Objects.equals(date, that.date)
+                && Objects.equals(delegated, that.delegated)
+                && Objects.equals(created, that.created)
+                && Objects.equals(updated, that.updated)
+                && Objects.equals(destroyed, that.destroyed);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(accountId, state, date, delegated, created, updated, destroyed);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("accountId", accountId)
+            .add("state", state)
+            .add("date", date)
+            .add("isDelegated", delegated)
+            .add("created", created)
+            .add("updated", updated)
+            .add("destroyed", destroyed)
+            .toString();
+    }
 }
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
index b50b497..6dc9105 100644
--- 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
@@ -51,9 +51,6 @@ public class MemoryMailboxChangeRepository implements MailboxChangeRepository {
 
     @Override
     public Mono<Void> save(MailboxChange change) {
-        Preconditions.checkNotNull(change.getAccountId());
-        Preconditions.checkNotNull(change.getState());
-
         return Mono.just(mailboxChangeMap.put(change.getAccountId(), change)).then();
     }
 
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
index 81a9266..5be5163 100644
--- 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
@@ -147,27 +147,6 @@ public interface MailboxChangeRepositoryContract {
     }
 
     @Test
-    default void saveChangeShouldFailWhenNoAccountId() {
-        MailboxChangeRepository repository = mailboxChangeRepository();
-        State.Factory stateFactory = stateFactory();
-
-        MailboxChange change = MailboxChange.builder().accountId(null).state(stateFactory.generate()).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
-
-        assertThatThrownBy(() -> repository.save(change).block())
-            .isInstanceOf(NullPointerException.class);
-    }
-
-    @Test
-    default void saveChangeShouldFailWhenNoState() {
-        MailboxChangeRepository repository = mailboxChangeRepository();
-
-        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(null).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
-
-        assertThatThrownBy(() -> repository.save(change).block())
-            .isInstanceOf(NullPointerException.class);
-    }
-
-    @Test
     default void getChangesShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
         State.Factory stateFactory = stateFactory();
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeTest.java
new file mode 100644
index 0000000..06b0ed9
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/MailboxChangeTest.java
@@ -0,0 +1,72 @@
+/****************************************************************
+ * 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.assertThatThrownBy;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+
+import org.apache.james.jmap.api.model.AccountId;
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class MailboxChangeTest {
+    AccountId ACCOUNT_ID = AccountId.fromUsername(BOB);
+    ZonedDateTime DATE = ZonedDateTime.now();
+
+    @Test
+    void shouldMatchBeanContract() {
+        EqualsVerifier.forClass(MailboxChange.class)
+            .verify();
+    }
+
+    @Test
+    void shouldThrowOnNullAccountId() {
+        assertThatThrownBy(() ->
+            MailboxChange.builder()
+                .accountId(null)
+                .state(State.of(UUID.randomUUID()))
+                .date(DATE.minusHours(2)))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void shouldThrowOnNullState() {
+        assertThatThrownBy(() ->
+            MailboxChange.builder()
+                .accountId(ACCOUNT_ID)
+                .state(null)
+                .date(DATE.minusHours(2)))
+            .isInstanceOf(NullPointerException.class);;
+    }
+
+    @Test
+    void shouldThrowOnNullDate() {
+        assertThatThrownBy(() ->
+            MailboxChange.builder()
+                .accountId(ACCOUNT_ID)
+                .state(State.of(UUID.randomUUID()))
+                .date(null))
+            .isInstanceOf(NullPointerException.class);;
+    }
+}
\ No newline at end of file


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


[james-project] 04/16: JAMES-3469 Add EmailChange POJO

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

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

commit bf6aa0c7a9d95fe7fe357d839bb30979c88ce58d
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 11:05:22 2020 +0700

    JAMES-3469 Add EmailChange POJO
---
 .../apache/james/jmap/api/change/EmailChange.java  | 195 +++++++++++++++++++++
 .../james/jmap/api/change/EmailChangeTest.java     |  75 ++++++++
 2 files changed, 270 insertions(+)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChange.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChange.java
new file mode 100644
index 0000000..238b898
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChange.java
@@ -0,0 +1,195 @@
+/****************************************************************
+ * 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.Collection;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.model.MessageId;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+public class EmailChange {
+    public static class Builder {
+        @FunctionalInterface
+        public interface RequireAccountId {
+            RequireState accountId(AccountId accountId);
+        }
+
+        @FunctionalInterface
+        public interface RequireState {
+            RequireDate state(State state);
+        }
+
+        @FunctionalInterface
+        public interface RequireDate {
+            RequireIsDelegated date(ZonedDateTime date);
+        }
+
+        @FunctionalInterface
+        public interface RequireIsDelegated {
+            Builder isDelegated(boolean isDelegated);
+        }
+
+        private final AccountId accountId;
+        private final State state;
+        private final ZonedDateTime date;
+        private final boolean isDelegated;
+        private final ImmutableList.Builder<MessageId> created;
+        private final ImmutableList.Builder<MessageId> updated;
+        private final ImmutableList.Builder<MessageId> destroyed;
+
+        private Builder(AccountId accountId, State state, ZonedDateTime date, boolean isDelegated) {
+            Preconditions.checkNotNull(accountId, "'accountId' should not be null");
+            Preconditions.checkNotNull(state, "'state' should not be null");
+            Preconditions.checkNotNull(date, "'date' should not be null");
+
+            this.accountId = accountId;
+            this.state = state;
+            this.date = date;
+            this.isDelegated = isDelegated;
+            this.destroyed = ImmutableList.builder();
+            this.updated = ImmutableList.builder();
+            this.created = ImmutableList.builder();
+        }
+
+        public Builder updated(MessageId... messageId) {
+            updated.add(messageId);
+            return this;
+        }
+
+        public Builder destroyed(MessageId... messageId) {
+            destroyed.add(messageId);
+            return this;
+        }
+
+        public Builder created(MessageId... messageId) {
+            created.add(messageId);
+            return this;
+        }
+
+        public Builder created(Collection<MessageId> messageIds) {
+            created.addAll(messageIds);
+            return this;
+        }
+
+        public Builder destroyed(Collection<MessageId> messageIds) {
+            destroyed.addAll(messageIds);
+            return this;
+        }
+
+        public Builder updated(Collection<MessageId> messageIds) {
+            updated.addAll(messageIds);
+            return this;
+        }
+
+        public EmailChange build() {
+            return new EmailChange(accountId, state, date, isDelegated, created.build(), updated.build(), destroyed.build());
+        }
+    }
+
+    public static Builder.RequireAccountId builder() {
+        return accountId -> state -> date -> isDelegated -> new Builder(accountId, state, date, isDelegated);
+    }
+
+    private final AccountId accountId;
+    private final State state;
+    private final ZonedDateTime date;
+    private final boolean isDelegated;
+    private final ImmutableList<MessageId> created;
+    private final ImmutableList<MessageId> updated;
+    private final ImmutableList<MessageId> destroyed;
+
+    private EmailChange(AccountId accountId, State state, ZonedDateTime date, boolean isDelegated, ImmutableList<MessageId> created, ImmutableList<MessageId> updated, ImmutableList<MessageId> destroyed) {
+        this.accountId = accountId;
+        this.state = state;
+        this.date = date;
+        this.isDelegated = isDelegated;
+        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<MessageId> getCreated() {
+        return created;
+    }
+
+    public List<MessageId> getUpdated() {
+        return updated;
+    }
+
+    public List<MessageId> getDestroyed() {
+        return destroyed;
+    }
+
+    public boolean isDelegated() {
+        return isDelegated;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof EmailChange) {
+            EmailChange that = (EmailChange) o;
+            return Objects.equals(accountId, that.accountId)
+                && Objects.equals(state, that.state)
+                && Objects.equals(date, that.date)
+                && Objects.equals(isDelegated, that.isDelegated)
+                && Objects.equals(created, that.created)
+                && Objects.equals(updated, that.updated)
+                && Objects.equals(destroyed, that.destroyed);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(accountId, state, date, isDelegated, created, updated, destroyed);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("accountId", accountId)
+            .add("state", state)
+            .add("date", date)
+            .add("isDelegated", isDelegated)
+            .add("created", created)
+            .add("updated", updated)
+            .add("destroyed", destroyed)
+            .toString();
+    }
+}
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeTest.java
new file mode 100644
index 0000000..9f77771
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeTest.java
@@ -0,0 +1,75 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.api.change;
+
+import static org.apache.james.mailbox.fixture.MailboxFixture.BOB;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+
+import org.apache.james.jmap.api.model.AccountId;
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class EmailChangeTest {
+    AccountId ACCOUNT_ID = AccountId.fromUsername(BOB);
+    ZonedDateTime DATE = ZonedDateTime.now();
+
+    @Test
+    void shouldMatchBeanContract() {
+        EqualsVerifier.forClass(EmailChange.class)
+            .verify();
+    }
+
+    @Test
+    void shouldThrowOnNullAccountId() {
+        assertThatThrownBy(() ->
+            EmailChange.builder()
+                .accountId(null)
+                .state(State.of(UUID.randomUUID()))
+                .date(DATE.minusHours(2))
+                .isDelegated(false))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void shouldThrowOnNullState() {
+        assertThatThrownBy(() ->
+            EmailChange.builder()
+                .accountId(ACCOUNT_ID)
+                .state(null)
+                .date(DATE.minusHours(2))
+                .isDelegated(false))
+            .isInstanceOf(NullPointerException.class);;
+    }
+
+    @Test
+    void shouldThrowOnNullDate() {
+        assertThatThrownBy(() ->
+            EmailChange.builder()
+                .accountId(ACCOUNT_ID)
+                .state(State.of(UUID.randomUUID()))
+                .date(null)
+                .isDelegated(false))
+            .isInstanceOf(NullPointerException.class);;
+    }
+}
\ No newline at end of file


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


[james-project] 10/16: JAMES-3469 EmailChangeRepositoryContract: tests for delegation

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

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

commit 27f92005c7b0c55359a9648e6a13eb58841a2872
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 12:24:21 2020 +0700

    JAMES-3469 EmailChangeRepositoryContract: tests for delegation
---
 .../api/change/EmailChangeRepositoryContract.java  | 637 +++++++++++++++++++++
 1 file changed, 637 insertions(+)

diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
index 1dbb9ca..fc0ac24 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
@@ -99,6 +99,39 @@ public interface EmailChangeRepositoryContract {
     }
 
     @Test
+    default void getLatestStateShouldReturnLastNonDelegatedPersistedState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(true)
+            .created(TestMessageId.of(4))
+            .build();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getLatestState(ACCOUNT_ID).block())
+            .isEqualTo(change2.getState());
+    }
+
+    @Test
     default void getChangesShouldSuccess() {
         EmailChangeRepository repository = emailChangeRepository();
 
@@ -540,6 +573,50 @@ public interface EmailChangeRepositoryContract {
     }
 
     @Test
+    default void changesShouldNotReturnDelegatedChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(true)
+            .created(TestMessageId.of(6), TestMessageId.of(7))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(8))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        EmailChanges emailChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
+
+        assertThat(emailChanges.getCreated())
+            .containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(8));
+    }
+
+    @Test
     default void getChangesShouldIgnoreDuplicatedValues() {
         EmailChangeRepository repository = emailChangeRepository();
 
@@ -583,4 +660,564 @@ public interface EmailChangeRepositoryContract {
         assertThatThrownBy(() -> repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block())
             .isInstanceOf(ChangeNotFoundException.class);
     }
+
+    @Test
+    default void getLatestStateWithDelegationShouldReturnInitialWhenEmpty() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        assertThat(repository.getLatestStateWithDelegation(ACCOUNT_ID).block())
+            .isEqualTo(State.INITIAL);
+    }
+
+    @Test
+    default void getLatestStateWithDelegationShouldReturnLastPersistedState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getLatestStateWithDelegation(ACCOUNT_ID).block())
+            .isEqualTo(change3.getState());
+    }
+
+    @Test
+    default void getLatestStateWithDelegationShouldReturnLastDelegatedPersistedState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(true)
+            .created(TestMessageId.of(4))
+            .build();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getLatestStateWithDelegation(ACCOUNT_ID).block())
+            .isEqualTo(change3.getState());
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldSuccess() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(1))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .hasSameElementsAs(change.getUpdated());
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnEmptyWhenNoNewerState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        repository.save(oldState).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .isEmpty();
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnCurrentStateWhenNoNewerState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        repository.save(oldState).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+            .isEqualTo(oldState.getState());
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldLimitChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+        EmailChange change4 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.plusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+        repository.save(change4).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getCreated())
+            .containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4));
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnAllFromInitial() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(3))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, State.INITIAL, Optional.of(Limit.of(3))).block().getCreated())
+            .containsExactlyInAnyOrder(TestMessageId.of(1), TestMessageId.of(2), TestMessageId.of(3));
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldLimitChangesWhenMaxChangesOmitted() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(6))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(7))
+            .build();
+
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
+            .hasSameElementsAs(change1.getCreated());
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldNotReturnMoreThanMaxChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block().getAllChanges())
+            .hasSameElementsAs(change1.getCreated());
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnEmptyWhenNumberOfChangesExceedMaxChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().getAllChanges())
+            .isEmpty();
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnNewState() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
+            .isEqualTo(change2.getState());
+    }
+
+    @Test
+    default void getSinceStateWithDelegationHasMoreChangesShouldBeTrueWhenMoreChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(1))).block().hasMoreChanges())
+            .isTrue();
+    }
+
+    @Test
+    default void getSinceStateWithDelegationHasMoreChangesShouldBeFalseWhenNoMoreChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(2), TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(4))).block().hasMoreChanges())
+            .isFalse();
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnChangesInTheirRespectiveType() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1), TestMessageId.of(9), TestMessageId.of(10))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .created(TestMessageId.of(6), TestMessageId.of(7))
+            .updated(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(10))
+            .destroyed(TestMessageId.of(4), TestMessageId.of(9))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(8))
+            .updated(TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(1))
+            .destroyed(TestMessageId.of(5), TestMessageId.of(10))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        EmailChanges emailChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
+
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(emailChanges.getCreated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(8));
+            softly.assertThat(emailChanges.getUpdated()).containsExactlyInAnyOrder(TestMessageId.of(1));
+            softly.assertThat(emailChanges.getDestroyed()).containsExactlyInAnyOrder(TestMessageId.of(9), TestMessageId.of(10));
+        });
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldReturnDelegatedChanges() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(3))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(true)
+            .created(TestMessageId.of(6), TestMessageId.of(7))
+            .build();
+        EmailChange change3 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .created(TestMessageId.of(8))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+        repository.save(change3).block();
+
+        EmailChanges emailChanges = repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
+
+        assertThat(emailChanges.getCreated())
+            .containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(8));
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldIgnoreDuplicatedValues() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        EmailChange oldState = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(STATE_0)
+            .date(DATE.minusHours(2))
+            .isDelegated(false)
+            .created(TestMessageId.of(1))
+            .build();
+        EmailChange change1 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .isDelegated(false)
+            .updated(TestMessageId.of(1), TestMessageId.of(2))
+            .build();
+        EmailChange change2 = EmailChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .isDelegated(false)
+            .updated(TestMessageId.of(1), TestMessageId.of(2))
+            .created(TestMessageId.of(3))
+            .build();
+        repository.save(oldState).block();
+        repository.save(change1).block();
+        repository.save(change2).block();
+
+        EmailChanges emailChanges = repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(3))).block();
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(emailChanges.getUpdated()).containsExactly(TestMessageId.of(1), TestMessageId.of(2));
+            softly.assertThat(emailChanges.getCreated()).containsExactly(TestMessageId.of(3));
+        });
+    }
+
+    @Test
+    default void getSinceStateWithDelegationShouldFailWhenSinceStateNotFound() {
+        EmailChangeRepository repository = emailChangeRepository();
+
+        assertThatThrownBy(() -> repository.getSinceStateWithDelegation(ACCOUNT_ID, STATE_0, Optional.empty()).block())
+            .isInstanceOf(ChangeNotFoundException.class);
+    }
 }


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


[james-project] 01/16: JAMES-3117 PeriodicalHealthChecks should not fail upon slow checks

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

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

commit 1e66405e5e7f24ca6eda3d6b01288ba4d1ed49c5
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Dec 25 09:00:20 2020 +0700

    JAMES-3117 PeriodicalHealthChecks should not fail upon slow checks
    
    If some checks exceeds the reporting period, we end up overrunning
    intermediate buffers.
    
    ```
    reactor.core.Exceptions$ErrorCallbackNotImplemented: reactor.core.Exceptions$OverflowException: Could not emit tick 6784 due to lack of requests (interval doesn't support small downstream requests that replenish slower than the ticks)
    Caused by: reactor.core.Exceptions$OverflowException: Could not emit tick 6784 due to lack of requests (interval doesn't support small downstream requests that replenish slower than the ticks)
    at reactor.core.Exceptions.failWithOverflow(Exceptions.java:234)
    at reactor.core.publisher.FluxInterval$IntervalRunnable.run(FluxInterval.java:130)
    at reactor.core.scheduler.PeriodicWorkerTask.call(PeriodicWorkerTask.java:59)
    at reactor.core.scheduler.PeriodicWorkerTask.run(PeriodicWorkerTask.java:73)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source)
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)
    ```
    
    It looks safe to consider a check exceeding the reporting period
    as failed, thus preventing the buffer overrun.
---
 .../java/org/apache/james/PeriodicalHealthChecks.java    |  6 +++++-
 .../org/apache/james/PeriodicalHealthChecksTest.java     | 16 ++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/PeriodicalHealthChecks.java b/server/container/guice/guice-common/src/main/java/org/apache/james/PeriodicalHealthChecks.java
index 0f9d1bf..e99d3d4 100644
--- a/server/container/guice/guice-common/src/main/java/org/apache/james/PeriodicalHealthChecks.java
+++ b/server/container/guice/guice-common/src/main/java/org/apache/james/PeriodicalHealthChecks.java
@@ -22,6 +22,7 @@ package org.apache.james;
 import static org.apache.james.util.ReactorUtils.DEFAULT_CONCURRENCY;
 
 import java.util.Set;
+import java.util.concurrent.TimeoutException;
 
 import javax.annotation.PreDestroy;
 import javax.inject.Inject;
@@ -62,7 +63,10 @@ public class PeriodicalHealthChecks implements Startable {
     public void start() {
         disposable = Flux.interval(configuration.getPeriod(), scheduler)
             .flatMapIterable(any -> healthChecks)
-            .flatMap(healthCheck -> Mono.from(healthCheck.check()), DEFAULT_CONCURRENCY)
+            .flatMap(healthCheck -> Mono.from(healthCheck.check())
+                .timeout(configuration.getPeriod())
+                .onErrorResume(TimeoutException.class, e -> Mono.just(Result.unhealthy(healthCheck.componentName(), e.getMessage()))),
+                DEFAULT_CONCURRENCY)
             .doOnNext(this::logResult)
             .onErrorContinue(this::logError)
             .subscribeOn(Schedulers.elastic())
diff --git a/server/container/guice/guice-common/src/test/java/org/apache/james/PeriodicalHealthChecksTest.java b/server/container/guice/guice-common/src/test/java/org/apache/james/PeriodicalHealthChecksTest.java
index a325098..98da701 100644
--- a/server/container/guice/guice-common/src/test/java/org/apache/james/PeriodicalHealthChecksTest.java
+++ b/server/container/guice/guice-common/src/test/java/org/apache/james/PeriodicalHealthChecksTest.java
@@ -21,6 +21,7 @@ package org.apache.james;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -100,6 +101,21 @@ public class PeriodicalHealthChecksTest {
     }
 
     @Test
+    void healthChecksShouldBeConsideredFailedIfExceedingTimeout() {
+        testee = new PeriodicalHealthChecks(ImmutableSet.of(mockHealthCheck1, mockHealthCheck2),
+            scheduler,
+            new PeriodicalHealthChecksConfiguration(Duration.ofMillis(1)));
+
+        when(mockHealthCheck1.check()).thenReturn(Mono.just(Result.healthy(new ComponentName("mockHealthCheck1"))).delayElement(Duration.ofMillis(10)));
+        when(mockHealthCheck2.check()).thenReturn(Mono.just(Result.healthy(new ComponentName("mockHealthCheck2"))).delayElement(Duration.ofMillis(10)));
+
+        testee.start();
+
+        assertThatCode(() -> scheduler.advanceTimeBy(PERIOD))
+            .doesNotThrowAnyException();
+    }
+
+    @Test
     void startShouldCallHealthCheckAtLeastOnce() {
         testee.start();
 


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


[james-project] 13/16: MAILBOX-392 Allow mailbox names to contain `#`

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

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

commit 8d2ff807be5ae57e993889a4b59e7c559f120a57
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Dec 25 10:16:01 2020 +0700

    MAILBOX-392 Allow mailbox names to contain `#`
    
    We still reject the `#` prefix as it is reserved to denote
    a namespace. However, the use of # within the name does not lead
    to further ambiguities and can be relaxed.
    
    Names containing `#` are allowed by Cyrus, so relaxing this
    conditions helps with data migration from Cyrus servers.
    
    https://tools.ietf.org/html/rfc3501#section-5.1 simply discourages the use of `#` due to conventions but do
    not require it to be banned.
    
    ```
    Two characters, "#" and "&", have meanings by convention, and
    should be avoided except when used in that convention.
    ```
---
 .../pages/distributed/operate/webadmin.adoc        |  3 +-
 .../apache/james/mailbox/model/MailboxPath.java    |  7 ++--
 .../james/mailbox/model/MailboxPathTest.java       | 11 +++++-
 .../apache/james/imap/scripts/ListSpecialChar.test |  1 +
 .../imapmailbox/inmemory/InMemoryListingTest.java  |  1 +
 .../org/apache/james/cli/MailboxManageTest.java    |  2 +-
 .../webadmin/routes/UserMailboxesRoutesTest.java   | 43 ++++++++++++++++++----
 src/site/markdown/server/manage-webadmin.md        |  2 +-
 8 files changed, 53 insertions(+), 17 deletions(-)

diff --git a/docs/modules/servers/pages/distributed/operate/webadmin.adoc b/docs/modules/servers/pages/distributed/operate/webadmin.adoc
index c88a5e5..be53056 100644
--- a/docs/modules/servers/pages/distributed/operate/webadmin.adoc
+++ b/docs/modules/servers/pages/distributed/operate/webadmin.adoc
@@ -1108,8 +1108,7 @@ curl -XPUT http://ip:port/users/{usernameToBeUsed}/mailboxes/{mailboxNameToBeCre
 ....
 
 Resource name `usernameToBeUsed` should be an existing user Resource
-name `mailboxNameToBeCreated` should not be empty, nor contain # % *
-characters.
+name `mailboxNameToBeCreated` should not be empty, nor contain % * characters, nor starting with #.
 
 Response codes:
 
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java
index 58044a7..6a37d22 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java
@@ -62,7 +62,7 @@ public class MailboxPath {
         return new MailboxPath(MailboxConstants.USER_NAMESPACE, username, mailboxName);
     }
 
-    private static final String INVALID_CHARS = "%*#";
+    private static final String INVALID_CHARS = "%*";
     private static final CharMatcher INVALID_CHARS_MATCHER = CharMatcher.anyOf(INVALID_CHARS);
     // This is the size that all mailbox backend should support
     public  static final int MAX_MAILBOX_NAME_LENGTH = 200;
@@ -176,7 +176,7 @@ public class MailboxPath {
                 String.format("'%s' has an empty part within its mailbox name considering %s as a delimiter", asString(), pathDelimiter));
         }
         if (nameContainsForbiddenCharacters()) {
-            throw new MailboxNameException(asString() + " contains one of the forbidden characters " + INVALID_CHARS);
+            throw new MailboxNameException(asString() + " contains one of the forbidden characters " + INVALID_CHARS + " or starts with #");
         }
         if (isMailboxNameTooLong()) {
             throw new TooLongMailboxNameException("Mailbox name exceeds maximum size of " + MAX_MAILBOX_NAME_LENGTH + " characters");
@@ -185,7 +185,8 @@ public class MailboxPath {
     }
 
     private boolean nameContainsForbiddenCharacters() {
-        return INVALID_CHARS_MATCHER.matchesAnyOf(name);
+        return INVALID_CHARS_MATCHER.matchesAnyOf(name)
+            || name.startsWith("#");
     }
 
     private boolean isMailboxNameTooLong() {
diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java
index a160b3c..dd23a21 100644
--- a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java
+++ b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java
@@ -304,13 +304,20 @@ class MailboxPathTest {
     }
 
     @Test
-    void assertAcceptableShouldThrowOnSharp() {
-        assertThatThrownBy(() -> MailboxPath.forUser(USER, "a#b")
+    void assertAcceptableShouldThrowWhenStartsWithSharp() {
+        assertThatThrownBy(() -> MailboxPath.forUser(USER, "#ab")
                 .assertAcceptable('.'))
             .isInstanceOf(MailboxNameException.class);
     }
 
     @Test
+    void assertAcceptableShouldNotThrowWhenSharpInTheMiddle() {
+        assertThatCode(() -> MailboxPath.forUser(USER, "mailbox #17")
+                .assertAcceptable('.'))
+            .doesNotThrowAnyException();
+    }
+
+    @Test
     void assertAcceptableShouldThrowOnPercent() {
         assertThatThrownBy(() -> MailboxPath.forUser(USER, "a%b")
                 .assertAcceptable('.'))
diff --git a/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ListSpecialChar.test b/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ListSpecialChar.test
index 2600f85..a2c8fc8 100644
--- a/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ListSpecialChar.test
+++ b/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/ListSpecialChar.test
@@ -23,5 +23,6 @@ SUB {
 S: \* LIST \(\\HasNoChildren\) \"\.\" \"INBOX\"
 S: \* LIST \(\\HasNoChildren\) \"\.\" \"projects &- abc\"
 S: \* LIST \(\\HasNoChildren\) \"\.\" \"&AOk-valuations\"
+S: \* LIST \(\\HasNoChildren\) \"\.\" \"mailbox #17\"
 }
 S: a3 OK LIST completed.
diff --git a/mpt/impl/imap-mailbox/inmemory/src/test/java/org/apache/james/mpt/imapmailbox/inmemory/InMemoryListingTest.java b/mpt/impl/imap-mailbox/inmemory/src/test/java/org/apache/james/mpt/imapmailbox/inmemory/InMemoryListingTest.java
index 01ff5e7..5635007 100644
--- a/mpt/impl/imap-mailbox/inmemory/src/test/java/org/apache/james/mpt/imapmailbox/inmemory/InMemoryListingTest.java
+++ b/mpt/impl/imap-mailbox/inmemory/src/test/java/org/apache/james/mpt/imapmailbox/inmemory/InMemoryListingTest.java
@@ -49,6 +49,7 @@ public class InMemoryListingTest extends Listing {
     @Test
     public void listShouldUTF7EscapeSpecialChar() throws Exception {
         system.createMailbox(MailboxPath.forUser(USER, "projects & abc"));
+        system.createMailbox(MailboxPath.forUser(USER, "mailbox #17"));
         system.createMailbox(MailboxPath.forUser(USER, "évaluations"));
 
         simpleScriptedTestProtocol
diff --git a/server/protocols/webadmin-cli/src/test/java/org/apache/james/cli/MailboxManageTest.java b/server/protocols/webadmin-cli/src/test/java/org/apache/james/cli/MailboxManageTest.java
index 4835e9a..98dc2ee 100644
--- a/server/protocols/webadmin-cli/src/test/java/org/apache/james/cli/MailboxManageTest.java
+++ b/server/protocols/webadmin-cli/src/test/java/org/apache/james/cli/MailboxManageTest.java
@@ -256,7 +256,7 @@ public class MailboxManageTest {
                 .addUser("hqtran@linagora.com", "123456");
 
         int exitCode = WebAdminCli.executeFluent(new PrintStream(outputStreamCaptor), new PrintStream(errorStreamCaptor),
-                "--url", "http://127.0.0.1:" + port.getValue(), "mailbox", "delete", "hqtran@linagora.com", "IN#BOX");
+                "--url", "http://127.0.0.1:" + port.getValue(), "mailbox", "delete", "hqtran@linagora.com", "#INBOX");
 
         assertThat(exitCode).isEqualTo(1);
         assertThat(errorStreamCaptor.toString()).contains("Attempt to delete an invalid mailbox");
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesTest.java b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesTest.java
index fbd28e9..7724fec 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserMailboxesRoutesTest.java
@@ -141,7 +141,7 @@ class UserMailboxesRoutesTest {
     private static final Username USERNAME = Username.of("username");
     private static final String MAILBOX_NAME = "myMailboxName";
     private static final String MAILBOX_NAME_WITH_DOTS = "my..MailboxName";
-    private static final String INVALID_MAILBOX_NAME = "myMailboxName#";
+    private static final String INVALID_MAILBOX_NAME = "#myMailboxName";
     private static final MailboxPath INBOX = MailboxPath.inbox(USERNAME);
     private static final String ERROR_TYPE_NOTFOUND = "notFound";
     
@@ -332,7 +332,7 @@ class UserMailboxesRoutesTest {
                 .containsEntry("statusCode", BAD_REQUEST_400)
                 .containsEntry("type", "InvalidArgument")
                 .containsEntry("message", "Attempt to create an invalid mailbox")
-                .containsEntry("details", "#private:username:myMailboxName# contains one of the forbidden characters %*#");
+                .containsEntry("details", "#private:username:#myMailboxName contains one of the forbidden characters %* or starts with #");
         }
 
         @Test
@@ -372,7 +372,7 @@ class UserMailboxesRoutesTest {
                 .containsEntry("statusCode", BAD_REQUEST_400)
                 .containsEntry("type", "InvalidArgument")
                 .containsEntry("message", "Attempt to test existence of an invalid mailbox")
-                .containsEntry("details", "#private:username:myMailboxName* contains one of the forbidden characters %*#");
+                .containsEntry("details", "#private:username:myMailboxName* contains one of the forbidden characters %* or starts with #");
         }
 
         @Test
@@ -427,7 +427,7 @@ class UserMailboxesRoutesTest {
                 .containsEntry("statusCode", BAD_REQUEST_400)
                 .containsEntry("type", "InvalidArgument")
                 .containsEntry("message", "Attempt to test existence of an invalid mailbox")
-                .containsEntry("details", "#private:username:myMailboxName% contains one of the forbidden characters %*#");
+                .containsEntry("details", "#private:username:myMailboxName% contains one of the forbidden characters %* or starts with #");
         }
 
         @Test
@@ -469,7 +469,7 @@ class UserMailboxesRoutesTest {
         @Test
         void getShouldReturnUserErrorWithInvalidSharpMailboxName() throws Exception {
             Map<String, Object> errors = when()
-                .get(MAILBOX_NAME + "#")
+                .get("#" + MAILBOX_NAME)
             .then()
                 .statusCode(BAD_REQUEST_400)
                 .contentType(JSON)
@@ -482,13 +482,24 @@ class UserMailboxesRoutesTest {
                 .containsEntry("statusCode", BAD_REQUEST_400)
                 .containsEntry("type", "InvalidArgument")
                 .containsEntry("message", "Attempt to test existence of an invalid mailbox")
-                .containsEntry("details", "#private:username:myMailboxName# contains one of the forbidden characters %*#");
+                .containsEntry("details", "#private:username:#myMailboxName contains one of the forbidden characters %* or starts with #");
+        }
+
+        @Test
+        void getShouldReturnOkWhenSharpInTheMiddleOfTheName() throws Exception {
+            with()
+                .put("a#b");
+
+            when()
+                .get("a#b")
+            .then()
+                .statusCode(NO_CONTENT_204);
         }
 
         @Test
         void putShouldReturnUserErrorWithInvalidSharpMailboxName() throws Exception {
             Map<String, Object> errors = when()
-                .put(MAILBOX_NAME + "#")
+                .put("#" + MAILBOX_NAME)
             .then()
                 .statusCode(BAD_REQUEST_400)
                 .contentType(JSON)
@@ -504,9 +515,17 @@ class UserMailboxesRoutesTest {
         }
 
         @Test
+        void putShouldAcceptMailboxNamesContainingSharp() throws Exception {
+            when()
+                .put("a#b")
+            .then()
+                .statusCode(NO_CONTENT_204);
+        }
+
+        @Test
         void deleteShouldReturnUserErrorWithInvalidSharpMailboxName() throws Exception {
             Map<String, Object> errors = when()
-                .put(MAILBOX_NAME + "#")
+                .put("#" + MAILBOX_NAME)
             .then()
                 .statusCode(BAD_REQUEST_400)
                 .contentType(JSON)
@@ -522,6 +541,14 @@ class UserMailboxesRoutesTest {
         }
 
         @Test
+        void deleteShouldAcceptSharpInTheMiddleOfTheName() throws Exception {
+            when()
+                .put("a#b")
+            .then()
+                .statusCode(NO_CONTENT_204);
+        }
+
+        @Test
         void getShouldReturnNotFoundWithAndMailboxName() throws Exception {
             when()
                 .get(MAILBOX_NAME + "&")
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index cb6c067..a386681 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -959,7 +959,7 @@ curl -XPUT http://ip:port/users/{usernameToBeUsed}/mailboxes/{mailboxNameToBeCre
 ```
 
 Resource name `usernameToBeUsed` should be an existing user
-Resource name `mailboxNameToBeCreated` should not be empty, nor contain # % * characters.
+Resource name `mailboxNameToBeCreated` should not be empty, nor contain `% *` characters, nor starting with `#`.
 
 Response codes:
 


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


[james-project] 07/16: JAMES-3469 EmailChangeRepository memory implementation

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

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

commit 4309efd195b0d98f0091fead7482eb7230320b21
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 11:06:23 2020 +0700

    JAMES-3469 EmailChangeRepository memory implementation
---
 .../apache/james/jmap/api/change/EmailChanges.java | 165 +++++++++++++++++++++
 .../memory/change/MemoryEmailChangeRepository.java | 121 +++++++++++++++
 .../change/MemoryEmailChangeRepositoryTest.java    |  39 +++++
 3 files changed, 325 insertions(+)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
new file mode 100644
index 0000000..8bc28d8
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
@@ -0,0 +1,165 @@
+/****************************************************************
+ * 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.MessageId;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+public class EmailChanges {
+
+    public static class Builder {
+
+        public static class EmailChangeCollector implements Collector<EmailChange, Builder, EmailChanges> {
+            private final Limit limit;
+            private final State state;
+
+            public EmailChangeCollector(State state, Limit limit) {
+                this.limit = limit;
+                this.state = state;
+            }
+
+            @Override
+            public Supplier<Builder> supplier() {
+                return () -> new Builder(state, limit);
+            }
+
+            public BiConsumer<Builder, EmailChange> accumulator() {
+                return Builder::add;
+            }
+
+            @Override
+            public BinaryOperator<Builder> combiner() {
+                throw new NotImplementedException("Not supported");
+            }
+
+            @Override
+            public Function<Builder, EmailChanges> finisher() {
+                return Builder::build;
+            }
+
+            @Override
+            public Set<Characteristics> characteristics() {
+                return Sets.immutableEnumSet(Characteristics.UNORDERED);
+            }
+        }
+
+        private State state;
+        private boolean hasMoreChanges;
+        private boolean canAddMoreItem;
+        private Limit limit;
+        private Set<MessageId> created;
+        private Set<MessageId> updated;
+        private Set<MessageId> destroyed;
+
+        public Builder(State state, Limit limit) {
+            this.limit = limit;
+            this.state = state;
+            this.hasMoreChanges = false;
+            this.canAddMoreItem = true;
+            this.created = new HashSet<>();
+            this.updated = new HashSet<>();
+            this.destroyed = new HashSet<>();
+        }
+
+        public Builder add(EmailChange change) {
+            if (!canAddMoreItem) {
+                return this;
+            }
+
+            Set<MessageId> createdTemp = new HashSet<>(created);
+            Set<MessageId> updatedTemp = new HashSet<>(updated);
+            Set<MessageId> 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 EmailChanges build() {
+            return new EmailChanges(state, hasMoreChanges, created, updated, destroyed);
+        }
+    }
+
+    private State newState;
+    private final boolean hasMoreChanges;
+    private final Set<MessageId> created;
+    private final Set<MessageId> updated;
+    private final Set<MessageId> destroyed;
+
+    private EmailChanges(State newState, boolean hasMoreChanges, Set<MessageId> created, Set<MessageId> updated, Set<MessageId> destroyed) {
+        this.newState = newState;
+        this.hasMoreChanges = hasMoreChanges;
+        this.created = created;
+        this.updated = updated;
+        this.destroyed = destroyed;
+    }
+
+    public State getNewState() {
+        return newState;
+    }
+
+    public boolean hasMoreChanges() {
+        return hasMoreChanges;
+    }
+
+    public Set<MessageId> getCreated() {
+        return created;
+    }
+
+    public Set<MessageId> getUpdated() {
+        return updated;
+    }
+
+    public Set<MessageId> getDestroyed() {
+        return destroyed;
+    }
+
+    public List<MessageId> getAllChanges() {
+        return ImmutableList.<MessageId>builder()
+            .addAll(created)
+            .addAll(updated)
+            .addAll(destroyed)
+            .build();
+    }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepository.java
new file mode 100644
index 0000000..994dd65
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepository.java
@@ -0,0 +1,121 @@
+/****************************************************************
+ * 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.EmailChange;
+import org.apache.james.jmap.api.change.EmailChangeRepository;
+import org.apache.james.jmap.api.change.EmailChanges;
+import org.apache.james.jmap.api.change.EmailChanges.Builder.EmailChangeCollector;
+import org.apache.james.jmap.api.change.Limit;
+import org.apache.james.jmap.api.change.State;
+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 com.google.common.collect.Multimaps;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class MemoryEmailChangeRepository implements EmailChangeRepository {
+    public static final Limit DEFAULT_NUMBER_OF_CHANGES = Limit.of(5);
+
+    private final Multimap<AccountId, EmailChange> emailChangeMap;
+
+    public MemoryEmailChangeRepository() {
+        this.emailChangeMap = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
+    }
+
+    @Override
+    public Mono<Void> save(EmailChange change) {
+        Preconditions.checkNotNull(change.getAccountId());
+        Preconditions.checkNotNull(change.getState());
+
+        return Mono.just(emailChangeMap.put(change.getAccountId(), change)).then();
+    }
+
+    @Override
+    public Mono<State> getLatestState(AccountId accountId) {
+        return allChanges(accountId)
+            .filter(change -> !change.isDelegated())
+            .sort(Comparator.comparing(EmailChange::getDate))
+            .map(EmailChange::getState)
+            .last(State.INITIAL);
+    }
+
+    @Override
+    public Mono<EmailChanges> 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 resolveAllChanges(accountId, state)
+            .filter(change -> !change.isDelegated())
+            .collect(new EmailChangeCollector(state, maxChanges.orElse(DEFAULT_NUMBER_OF_CHANGES)));
+    }
+
+    @Override
+    public Mono<EmailChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxChanges) {
+        Preconditions.checkNotNull(accountId);
+        Preconditions.checkNotNull(state);
+
+        return resolveAllChanges(accountId, state)
+            .collect(new EmailChangeCollector(state, maxChanges.orElse(DEFAULT_NUMBER_OF_CHANGES)));
+    }
+
+    @Override
+    public Mono<State> getLatestStateWithDelegation(AccountId accountId) {
+        return allChanges(accountId)
+            .sort(Comparator.comparing(EmailChange::getDate))
+            .map(EmailChange::getState)
+            .last(State.INITIAL);
+    }
+
+    private Flux<EmailChange> resolveAllChanges(AccountId accountId, State state) {
+        if (state.equals(State.INITIAL)) {
+            return allChanges(accountId);
+        }
+        return allChangesSince(accountId, state);
+    }
+
+    private Flux<EmailChange> allChangesSince(AccountId accountId, State state) {
+        return findByState(accountId, state)
+            .flatMapMany(currentState -> Flux.fromIterable(emailChangeMap.get(accountId))
+                .filter(change -> change.getDate().isAfter(currentState.getDate()))
+                .sort(Comparator.comparing(EmailChange::getDate)));
+    }
+
+    private Flux<EmailChange> allChanges(AccountId accountId) {
+        return Flux.fromIterable(emailChangeMap.get(accountId))
+            .sort(Comparator.comparing(EmailChange::getDate));
+    }
+
+    private Mono<EmailChange> findByState(AccountId accountId, State state) {
+        return Flux.fromIterable(emailChangeMap.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/test/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepositoryTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepositoryTest.java
new file mode 100644
index 0000000..3b1abc1
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryEmailChangeRepositoryTest.java
@@ -0,0 +1,39 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.memory.change;
+
+import org.apache.james.jmap.api.change.EmailChangeRepository;
+import org.apache.james.jmap.api.change.EmailChangeRepositoryContract;
+import org.apache.james.jmap.api.change.MailboxChangeRepository;
+import org.junit.jupiter.api.BeforeEach;
+
+public class MemoryEmailChangeRepositoryTest implements EmailChangeRepositoryContract {
+    EmailChangeRepository emailChangeRepository;
+
+    @BeforeEach
+    void setup() {
+        emailChangeRepository = new MemoryEmailChangeRepository();
+    }
+
+    @Override
+    public EmailChangeRepository emailChangeRepository() {
+        return emailChangeRepository;
+    }
+}


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


[james-project] 16/16: JAMES-3465 Mailbox/changes response should not filter null updatedProperties

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

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

commit 3785ad22c78fba4e46c7b66e7e0b98fd8e5f1dbd
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Dec 23 10:35:37 2020 +0700

    JAMES-3465 Mailbox/changes response should not filter null updatedProperties
---
 .../james/jmap/api/change/MailboxChange.java       |   7 +-
 .../james/jmap/api/change/MailboxChanges.java      |  14 +-
 .../change/MailboxChangeRepositoryContract.java    |  88 +++++++++
 .../contract/MailboxChangesMethodContract.scala    | 216 ++++++++++++++++++++-
 .../contract/MailboxGetMethodContract.scala        |   3 +-
 .../apache/james/jmap/json/MailboxSerializer.scala |  11 +-
 .../james/jmap/method/MailboxChangesMethod.scala   |  14 +-
 7 files changed, 333 insertions(+), 20 deletions(-)

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
index caa954e..501a7b8 100644
--- 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
@@ -77,7 +77,7 @@ public class MailboxChange {
         private final State state;
         private final ZonedDateTime date;
         private boolean delegated;
-        private boolean isCountChange;
+        private final boolean isCountChange;
         private Optional<List<MailboxId>> created;
         private Optional<List<MailboxId>> updated;
         private Optional<List<MailboxId>> destroyed;
@@ -101,11 +101,6 @@ public class MailboxChange {
             return this;
         }
 
-        public Builder isCountChange(boolean isCountChange) {
-            this.isCountChange = isCountChange;
-            return this;
-        }
-
         public Builder created(List<MailboxId> created) {
             this.created = Optional.of(created);
             return this;
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
index 30493fb..c1af012 100644
--- 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
@@ -123,11 +123,7 @@ public class MailboxChanges {
                 return this;
             }
 
-            if (created.isEmpty() && updated.isEmpty() && destroyed.isEmpty()) {
-                isCountChangeOnly = change.isCountChange();
-            } else {
-                isCountChangeOnly = isCountChangeOnly && change.isCountChange();
-            }
+            isCountChangeOnly = calculateIsChangeOnly(change);
             state = change.getState();
             created = createdTemp;
             updated = updatedTemp;
@@ -139,6 +135,14 @@ public class MailboxChanges {
         public MailboxChanges build() {
             return new MailboxChanges(state, hasMoreChanges, isCountChangeOnly, created, updated, destroyed);
         }
+
+        private boolean calculateIsChangeOnly(MailboxChange change) {
+            if (created.isEmpty() && updated.isEmpty() && destroyed.isEmpty()) {
+                return change.isCountChange();
+            } else {
+                return isCountChangeOnly && change.isCountChange();
+            }
+        }
     }
 
     private State newState;
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
index 8a06e96..5ba92d0 100644
--- 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
@@ -449,7 +449,95 @@ public interface MailboxChangeRepositoryContract {
 
         assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, referenceState, Optional.empty()).block().getUpdated())
             .containsExactly(TestId.of(1));
+    }
+
+    @Test
+    default void isCountChangeOnlyShouldBeFalseWhenNoChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, State.INITIAL, Optional.empty()).block().isCountChangesOnly())
+            .isFalse();
+    }
 
+    @Test
+    default void isCountChangeOnlyShouldBeFalseWhenAllNonCountChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+
+        MailboxChange change1 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE.minusHours(1))
+            .isCountChange(false)
+            .created(ImmutableList.of(TestId.of(1)))
+            .build();
+        MailboxChange change2 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE)
+            .isCountChange(false)
+            .created(ImmutableList.of(TestId.of(2)))
+            .build();
+
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, State.INITIAL, Optional.empty()).block().isCountChangesOnly())
+            .isFalse();
+    }
+
+    @Test
+    default void isCountChangeOnlyShouldBeFalseWhenMixedChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+
+        MailboxChange change1 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE.minusHours(1))
+            .isCountChange(false)
+            .created(ImmutableList.of(TestId.of(1)))
+            .build();
+        MailboxChange change2 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE)
+            .isCountChange(false)
+            .updated(ImmutableList.of(TestId.of(2)))
+            .build();
+
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, State.INITIAL, Optional.empty()).block().isCountChangesOnly())
+            .isFalse();
+    }
+
+    @Test
+    default void isCountChangeOnlyShouldBeTrueWhenAllCountChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+        State.Factory stateFactory = stateFactory();
+
+        MailboxChange change1 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE.minusHours(1))
+            .isCountChange(true)
+            .updated(ImmutableList.of(TestId.of(1)))
+            .build();
+        MailboxChange change2 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE)
+            .isCountChange(true)
+            .updated(ImmutableList.of(TestId.of(2)))
+            .build();
+
+        repository.save(change1);
+        repository.save(change2);
+
+        assertThat(repository.getSinceStateWithDelegation(ACCOUNT_ID, State.INITIAL, Optional.empty()).block().isCountChangesOnly())
+            .isTrue();
     }
 
     @Test
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/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index 918f062..85f88e6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -133,6 +133,7 @@ trait MailboxChangesMethodContract {
             |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
             |        "oldState": "${oldState.getValue}",
             |        "hasMoreChanges": false,
+            |        "updatedProperties":null,
             |        "created": ["$mailboxId1", "$mailboxId2", "$mailboxId3"],
             |        "updated": [],
             |        "destroyed": []
@@ -190,6 +191,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
+           |        "updatedProperties":null,
            |        "created": [],
            |        "updated": ["$mailboxId"],
            |        "destroyed": []
@@ -588,6 +590,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
+             |        "updatedProperties":null,
              |        "created": [],
              |        "updated": ["$mailboxId"],
              |        "destroyed": []
@@ -870,6 +873,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
+             |        "updatedProperties":null,
              |        "created": [],
              |        "updated": [],
              |        "destroyed": []
@@ -933,6 +937,7 @@ trait MailboxChangesMethodContract {
              |        "accountId": "$ANDRE_ACCOUNT_ID",
              |        "oldState": "${oldState.getValue}",
              |        "hasMoreChanges": false,
+             |        "updatedProperties": null,
              |        "created": [],
              |        "updated": [],
              |        "destroyed": ["$mailboxId"]
@@ -993,6 +998,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
+           |        "updatedProperties":null,
            |        "created": [],
            |        "updated": [],
            |        "destroyed": ["$mailboxId"]
@@ -1066,6 +1072,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
+           |        "updatedProperties": null,
            |        "created": ["$mailboxId3"],
            |        "updated": ["$mailboxId1"],
            |        "destroyed": ["$mailboxId2"]
@@ -1138,6 +1145,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
+           |        "updatedProperties":null,
            |        "created": ["$mailboxId2"],
            |        "updated": [],
            |        "destroyed": []
@@ -1212,6 +1220,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": true,
+           |        "updatedProperties":null,
            |        "created": ["$mailboxId1", "$mailboxId2", "$mailboxId3", "$mailboxId4", "$mailboxId5"],
            |        "updated": [],
            |        "destroyed": []
@@ -1346,6 +1355,7 @@ trait MailboxChangesMethodContract {
            |        "oldState": "${oldState.getValue}",
            |        "newState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
+           |        "updatedProperties":null,
            |        "created": [],
            |        "updated": [],
            |        "destroyed": []
@@ -1470,6 +1480,7 @@ trait MailboxChangesMethodContract {
            |        "oldState": "$newState",
            |        "newState": "$newState",
            |        "hasMoreChanges": false,
+           |        "updatedProperties":null,
            |        "created": [],
            |        "updated": [],
            |        "destroyed": []
@@ -1587,9 +1598,9 @@ trait MailboxChangesMethodContract {
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
-      .when
+    .when
       .post
-      .`then`
+    .`then`
       .statusCode(SC_OK)
       .contentType(JSON)
       .extract
@@ -1607,6 +1618,7 @@ trait MailboxChangesMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
+           |        "updatedProperties":null,
            |        "created": ["$mailboxId2"],
            |        "updated": ["$mailboxId1"],
            |        "destroyed": []
@@ -1615,6 +1627,206 @@ trait MailboxChangesMethodContract {
            |}""".stripMargin)
   }
 
+  @Test
+  def mailboxChangesShouldSupportBackReferenceWithUpdatedProperties(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
+    val path = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId: String = mailboxProbe
+      .createMailbox(path)
+      .serialize
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+    val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+    JmapRequests.destroyEmail(messageId2)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Mailbox/changes", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    }, "c1"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "#properties": {
+         |        "resultOf": "c1",
+         |        "name": "Mailbox/changes",
+         |        "path": "updatedProperties"
+         |      },
+         |      "#ids": {
+         |        "resultOf": "c1",
+         |        "name": "Mailbox/changes",
+         |        "path": "/updated"
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState", "methodResponses[1][1].state")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |      [ "Mailbox/changes", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "oldState": "${oldState.getValue}",
+           |        "hasMoreChanges": false,
+           |        "updatedProperties": ["totalEmails", "unreadEmails", "totalThreads", "unreadThreads"],
+           |        "created": [],
+           |        "updated": ["$mailboxId"],
+           |        "destroyed": []
+           |      }, "c1"],
+           |      ["Mailbox/get", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "notFound": [],
+           |        "list": [
+           |          {
+           |            "id": "$mailboxId",
+           |            "totalEmails": 1,
+           |            "unreadEmails": 1,
+           |            "totalThreads": 1,
+           |            "unreadThreads": 1
+           |          }
+           |        ]
+           |      }, "c2"]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def mailboxChangesShouldSupportBackReferenceWithNullUpdatedProperties(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
+    val path1 = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId1: String = mailboxProbe
+      .createMailbox(path1)
+      .serialize
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    val path2 = MailboxPath.forUser(BOB, "mailbox2")
+    val mailboxId2: String = mailboxProbe
+      .createMailbox(path2)
+      .serialize
+
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    mailboxProbe.appendMessage(BOB.asString(), path1, AppendCommand.from(message)).getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Mailbox/changes", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    }, "c1"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "#properties": {
+         |        "resultOf": "c1",
+         |        "name": "Mailbox/changes",
+         |        "path": "updatedProperties"
+         |      },
+         |      "#ids": {
+         |        "resultOf": "c1",
+         |        "name": "Mailbox/changes",
+         |        "path": "/updated"
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState", "methodResponses[1][1].state")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |      [ "Mailbox/changes", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "oldState": "${oldState.getValue}",
+           |        "hasMoreChanges": false,
+           |        "updatedProperties": null,
+           |        "created": ["$mailboxId2"],
+           |        "updated": ["$mailboxId1"],
+           |        "destroyed": []
+           |      }, "c1"],
+           |      ["Mailbox/get", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "notFound": [],
+           |        "list": [
+           |          {
+           |            "id": "$mailboxId1",
+           |            "name": "mailbox1",
+           |            "sortOrder": 1000,
+           |            "totalEmails": 1,
+           |            "unreadEmails": 1,
+           |            "totalThreads": 1,
+           |            "unreadThreads": 1,
+           |            "myRights": {
+           |              "mayReadItems": true,
+           |              "mayAddItems": true,
+           |              "mayRemoveItems": true,
+           |              "maySetSeen": true,
+           |              "maySetKeywords": true,
+           |              "mayCreateChild": true,
+           |              "mayRename": true,
+           |              "mayDelete": true,
+           |              "maySubmit": true
+           |            },
+           |            "isSubscribed": false
+           |          }
+           |        ]
+           |      }, "c2"]
+           |    ]
+           |}""".stripMargin)
+  }
+
   private def storeReferenceState(server: GuiceJamesServer, username: Username): State = {
     val state: State = stateFactory.generate()
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
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/MailboxGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala
index b26a148..ef5d404 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala
@@ -33,7 +33,6 @@ import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
 import org.apache.james.core.quota.{QuotaCountLimit, QuotaSizeLimit}
 import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
-import org.apache.james.jmap.core.State.INSTANCE
 import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture._
 import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
@@ -1586,7 +1585,7 @@ trait MailboxGetMethodContract {
         s"""{
            |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |  "hasMoreChanges": false,
-           |  "updatedProperties": [],
+           |  "updatedProperties": null,
            |  "created": [],
            |  "updated": [],
            |  "destroyed": []
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
index 573f107..90ef9e9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
@@ -154,7 +154,16 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
     mapWrites[MailboxId, MailboxUpdateResponse](_.serialize(), mailboxSetUpdateResponseWrites)
 
   private implicit val mailboxSetResponseWrites: Writes[MailboxSetResponse] = Json.writes[MailboxSetResponse]
-  private implicit val changesResponseWrites: OWrites[MailboxChangesResponse] = Json.writes[MailboxChangesResponse]
+  private implicit val changesResponseWrites: Writes[MailboxChangesResponse] = response =>
+    Json.obj(
+      "accountId" -> response.accountId,
+      "oldState" -> response.oldState,
+      "newState" -> response.newState,
+      "hasMoreChanges" -> response.hasMoreChanges,
+      "updatedProperties" -> response.updatedProperties,
+      "created" -> response.created,
+      "updated" -> response.updated,
+      "destroyed" -> response.destroyed)
 
   def serializeChanges(changesResponse: MailboxChangesResponse): JsObject = Json.toJson(changesResponse).as[JsObject]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
index c1f985b..9fb7c6c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
@@ -20,9 +20,7 @@
 package org.apache.james.jmap.method
 
 import eu.timepit.refined.auto._
-import javax.inject.Inject
-import org.apache.james.jmap.api.change.{State => JavaState}
-import org.apache.james.jmap.api.change.MailboxChangeRepository
+import org.apache.james.jmap.api.change.{MailboxChangeRepository, MailboxChanges, State => JavaState}
 import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
@@ -36,6 +34,7 @@ import org.apache.james.metrics.api.MetricFactory
 import play.api.libs.json.{JsError, JsSuccess}
 import reactor.core.scala.publisher.SMono
 
+import javax.inject.Inject
 import scala.jdk.CollectionConverters._
 import scala.jdk.OptionConverters._
 
@@ -64,7 +63,7 @@ class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
         oldState = request.sinceState,
         newState = State.fromMailboxChanges(mailboxChanges),
         hasMoreChanges = HasMoreChanges.fromMailboxChanges(mailboxChanges),
-        updatedProperties = if (mailboxChanges.isCountChangesOnly) Some(updatedProperties) else None,
+        updatedProperties = updateProperties(mailboxChanges),
         created = mailboxChanges.getCreated.asScala.toSet,
         updated = mailboxChanges.getUpdated.asScala.toSet,
         destroyed = mailboxChanges.getDestroyed.asScala.toSet))
@@ -80,4 +79,11 @@ class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
       case JsSuccess(mailboxGetRequest, _) => Right(mailboxGetRequest)
       case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
+
+  private def updateProperties(mailboxChanges: MailboxChanges): Option[Properties] =
+    if (mailboxChanges.isCountChangesOnly) {
+      Some(updatedProperties)
+    } else {
+      None
+    }
 }


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


[james-project] 14/16: JAMES-3431 Mock SMTP server docker image should use a fixed version

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

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

commit 57534cab0839b8341fb5f647d00d4be440a825b1
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Dec 23 11:08:03 2020 +0700

    JAMES-3431 Mock SMTP server docker image should use a fixed version
---
 server/testing/src/main/java/org/apache/james/util/docker/Images.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/testing/src/main/java/org/apache/james/util/docker/Images.java b/server/testing/src/main/java/org/apache/james/util/docker/Images.java
index 79bab14..06ab8e9 100644
--- a/server/testing/src/main/java/org/apache/james/util/docker/Images.java
+++ b/server/testing/src/main/java/org/apache/james/util/docker/Images.java
@@ -27,5 +27,5 @@ public interface Images {
     String NGINX = "nginx:1.15.1";
     String TIKA = "apache/tika:1.24";
     String SPAMASSASSIN = "dinkel/spamassassin:3.4.0";
-    String MOCK_SMTP_SERVER = "linagora/mock-smtp-server";
+    String MOCK_SMTP_SERVER = "linagora/mock-smtp-server:0.1";
 }


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


[james-project] 08/16: JAMES-3469 Avoid returning duplicated changes

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

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

commit 2ffb39b1530c7a97249ef477d9a528ab0cdc85bf
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 11:23:58 2020 +0700

    JAMES-3469 Avoid returning duplicated changes
    
    If a record created then updated within the same change request we can safely
    return only the created record.
    
    https://jmap.io/spec-core.html#changes
    
    ```
    If a record has been created AND updated since the old state, the server SHOULD just return the id in the created list but MAY return it in the updated list as well.
    ```
---
 .../java/org/apache/james/jmap/api/change/EmailChanges.java   | 11 +++++++----
 .../james/jmap/api/change/EmailChangeRepositoryContract.java  |  5 ++---
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
index 8bc28d8..0c7e54c 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
@@ -32,6 +32,7 @@ import org.apache.commons.lang3.NotImplementedException;
 import org.apache.james.mailbox.model.MessageId;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 
 public class EmailChanges {
@@ -99,7 +100,9 @@ public class EmailChanges {
             Set<MessageId> updatedTemp = new HashSet<>(updated);
             Set<MessageId> destroyedTemp = new HashSet<>(destroyed);
             createdTemp.addAll(change.getCreated());
-            updatedTemp.addAll(change.getUpdated());
+            updatedTemp.addAll(
+                Sets.difference(ImmutableSet.copyOf(change.getUpdated()),
+                createdTemp));
             destroyedTemp.addAll(change.getDestroyed());
 
             if (createdTemp.size() + updatedTemp.size() + destroyedTemp.size() > limit.getValue()) {
@@ -109,9 +112,9 @@ public class EmailChanges {
             }
 
             state = change.getState();
-            this.created.addAll(change.getCreated());
-            this.updated.addAll(change.getUpdated());
-            this.destroyed.addAll(change.getDestroyed());
+            created = createdTemp;
+            updated = updatedTemp;
+            destroyed = destroyedTemp;
 
             return this;
         }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
index e917729..bb5f60c 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
@@ -28,7 +28,6 @@ import java.time.ZonedDateTime;
 import java.util.Optional;
 import java.util.UUID;
 
-import org.apache.james.jmap.api.change.State;
 import org.apache.james.jmap.api.exception.ChangeNotFoundException;
 import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.mailbox.model.TestMessageId;
@@ -523,7 +522,7 @@ public interface EmailChangeRepositoryContract {
             .date(DATE)
             .isDelegated(false)
             .created(TestMessageId.of(8))
-            .updated(TestMessageId.of(6), TestMessageId.of(7))
+            .updated(TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(1))
             .destroyed(TestMessageId.of(5))
             .build();
         repository.save(oldState).block();
@@ -535,7 +534,7 @@ public interface EmailChangeRepositoryContract {
 
         SoftAssertions.assertSoftly(softly -> {
             softly.assertThat(emailChanges.getCreated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(8));
-            softly.assertThat(emailChanges.getUpdated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(6), TestMessageId.of(7));
+            softly.assertThat(emailChanges.getUpdated()).containsExactlyInAnyOrder(TestMessageId.of(1));
             softly.assertThat(emailChanges.getDestroyed()).containsExactlyInAnyOrder(TestMessageId.of(4), TestMessageId.of(5));
         });
     }


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


[james-project] 12/16: JAMES-3469 Mailbox/changes should as well not return creates+updates when created or deleted within the same API call

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

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

commit ed215c41fdff420ab3d15ca43213a711b0d0a73c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 19:22:28 2020 +0700

    JAMES-3469 Mailbox/changes should as well not return creates+updates when created or deleted within the same API call
    
    https://jmap.io/spec-core.html#changes
    
    ```
    If a record has been created AND updated since the old state, the server SHOULD just return the id in the created list but MAY return it in the updated list as well.
    
    If a record has been updated AND destroyed since the old state, the server SHOULD just return the id in the destroyed list but MAY return it in the updated list as well.
    
    If a record has been created AND destroyed since the old state, the server SHOULD remove the id from the response entirely. However, it MAY include it in just the destroyed list or in both the destroyed and created lists.
    ```
    
    Of course not returning duplicates enable faster re-synchronisations
---
 .../apache/james/jmap/draft/JmapGuiceProbe.java    |  5 ++
 .../james/jmap/api/change/MailboxChanges.java      | 29 +++++---
 .../change/MailboxChangeRepositoryContract.java    | 24 +++++--
 .../contract/MailboxChangesMethodContract.scala    | 80 +++++++++++++++++++++-
 .../jmap/change/MailboxChangeListenerTest.scala    |  4 +-
 5 files changed, 121 insertions(+), 21 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 c6d19fb..9c9affe 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
@@ -27,6 +27,7 @@ import org.apache.james.core.Username;
 import org.apache.james.jmap.JMAPServer;
 import org.apache.james.jmap.api.change.MailboxChange;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
+import org.apache.james.jmap.api.change.State;
 import org.apache.james.jmap.api.model.AccountId;
 import org.apache.james.jmap.api.projections.MessageFastViewProjection;
 import org.apache.james.jmap.api.vacation.Vacation;
@@ -94,4 +95,8 @@ public class JmapGuiceProbe implements GuiceProbe {
     public void saveMailboxChange(MailboxChange change) {
         mailboxChangeRepository.save(change).block();
     }
+
+    public State latestState(AccountId accountId) {
+        return mailboxChangeRepository.getLatestState(accountId).block();
+    }
 }
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
index cbd1deb..bbe718b 100644
--- 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
@@ -32,6 +32,7 @@ import org.apache.commons.lang3.NotImplementedException;
 import org.apache.james.mailbox.model.MailboxId;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 
 public class MailboxChanges {
@@ -95,12 +96,24 @@ public class MailboxChanges {
                 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());
+
+            Set<MailboxId> createdTemp = Sets.difference(
+                ImmutableSet.<MailboxId>builder()
+                    .addAll(created)
+                    .addAll(change.getCreated())
+                    .build(),
+                ImmutableSet.copyOf(change.getDestroyed()));
+            Set<MailboxId> updatedTemp = Sets.difference(
+                ImmutableSet.<MailboxId>builder()
+                    .addAll(updated)
+                    .addAll(Sets.difference(ImmutableSet.copyOf(change.getUpdated()),
+                        createdTemp))
+                    .build(),
+                ImmutableSet.copyOf(change.getDestroyed()));
+            destroyedTemp.addAll(Sets.difference(
+                ImmutableSet.copyOf(change.getDestroyed()),
+                created));
 
             if (createdTemp.size() + updatedTemp.size() + destroyedTemp.size() > limit.getValue()) {
                 hasMoreChanges = true;
@@ -109,9 +122,9 @@ public class MailboxChanges {
             }
 
             state = change.getState();
-            this.created.addAll(change.getCreated());
-            this.updated.addAll(change.getUpdated());
-            this.destroyed.addAll(change.getDestroyed());
+            created = createdTemp;
+            updated = updatedTemp;
+            destroyed = destroyedTemp;
 
             return this;
         }
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
index 5be5163..36a64b5 100644
--- 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
@@ -353,14 +353,24 @@ public interface MailboxChangeRepositoryContract {
         State.Factory stateFactory = stateFactory();
         State referenceState = stateFactory.generate();
 
-        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(referenceState).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
-        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(stateFactory.generate()).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5))).build();
+        MailboxChange oldState = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(referenceState)
+            .date(DATE.minusHours(3))
+            .created(ImmutableList.of(TestId.of(1), TestId.of(9), TestId.of(10)))
+            .build();
+        MailboxChange change1 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(stateFactory.generate())
+            .date(DATE.minusHours(2))
+            .created(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5)))
+            .build();
         MailboxChange change2 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
             .state(stateFactory.generate())
             .date(DATE.minusHours(1))
             .created(ImmutableList.of(TestId.of(6), TestId.of(7)))
-            .updated(ImmutableList.of(TestId.of(2), TestId.of(3)))
+            .updated(ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(9)))
             .destroyed(ImmutableList.of(TestId.of(4))).build();
         MailboxChange change3 = MailboxChange.builder()
             .accountId(ACCOUNT_ID)
@@ -368,7 +378,7 @@ public interface MailboxChangeRepositoryContract {
             .date(DATE)
             .created(ImmutableList.of(TestId.of(8)))
             .updated(ImmutableList.of(TestId.of(6), TestId.of(7)))
-            .destroyed(ImmutableList.of(TestId.of(5))).build();
+            .destroyed(ImmutableList.of(TestId.of(5), TestId.of(10))).build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -378,9 +388,9 @@ public interface MailboxChangeRepositoryContract {
         MailboxChanges mailboxChanges = repository.getSinceState(ACCOUNT_ID, referenceState, 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));
+            softly.assertThat(mailboxChanges.getCreated()).containsExactlyInAnyOrder(TestId.of(2), TestId.of(3), TestId.of(6), TestId.of(7), TestId.of(8));
+            softly.assertThat(mailboxChanges.getUpdated()).containsExactlyInAnyOrder(TestId.of(9));
+            softly.assertThat(mailboxChanges.getDestroyed()).containsExactlyInAnyOrder(TestId.of(10));
         });
     }
 
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/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index 5d9164a..46fd74f 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -1013,6 +1013,80 @@ trait MailboxChangesMethodContract {
 
     provisionSystemMailboxes(server)
 
+    val path1 = MailboxPath.forUser(BOB, "mailbox1")
+    val mailboxId1: String = mailboxProbe
+      .createMailbox(path1)
+      .serialize
+    val path2 = MailboxPath.forUser(BOB, "mailbox2")
+    val mailboxId2: String = mailboxProbe
+      .createMailbox(path2)
+      .serialize
+    val path3 = MailboxPath.forUser(BOB, "mailbox3")
+    val oldState: State = server.getProbe(classOf[JmapGuiceProbe]).latestState(AccountId.fromUsername(BOB))
+
+    val mailboxId3: String = mailboxProbe
+      .createMailbox(path3)
+      .serialize
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    mailboxProbe.appendMessage(BOB.asString(), path1, AppendCommand.from(message))
+
+    server.getProbe(classOf[MailboxProbeImpl])
+      .deleteMailbox(path1.getNamespace, BOB.asString(), path2.getName)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Mailbox/changes",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "sinceState": "${oldState.getValue}"
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].newState")
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |      [ "Mailbox/changes", {
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |        "oldState": "${oldState.getValue}",
+           |        "hasMoreChanges": false,
+           |        "updatedProperties": [],
+           |        "created": ["$mailboxId3"],
+           |        "updated": ["$mailboxId1"],
+           |        "destroyed": ["$mailboxId2"]
+           |      }, "c1"]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def returnedIdsShouldNotReturnDuplicatesAccrossCreatedUpdatedOrDestroyed(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+    provisionSystemMailboxes(server)
+
     val oldState: State = storeReferenceState(server, BOB)
 
     val path1 = MailboxPath.forUser(BOB, "mailbox1")
@@ -1072,9 +1146,9 @@ trait MailboxChangesMethodContract {
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
            |        "updatedProperties": [],
-           |        "created": ["$mailboxId1", "$mailboxId2"],
-           |        "updated": ["$mailboxId1", "$mailboxId2"],
-           |        "destroyed": ["$mailboxId1"]
+           |        "created": ["$mailboxId2"],
+           |        "updated": [],
+           |        "destroyed": []
            |      }, "c1"]
            |    ]
            |}""".stripMargin)
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
index 32d4be2..7c0c39d 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
@@ -101,12 +101,10 @@ class MailboxChangeListenerTest {
 
   @Test
   def updateMailboxACLShouldStoreUpdatedEvent(): Unit = {
-    val state = stateFactory.generate()
-    repository.save(MailboxChange.builder().accountId(ACCOUNT_ID).state(state).date(ZonedDateTime.now).created(List[MailboxId](TestId.of(0)).asJava).build).block()
-
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val path = MailboxPath.inbox(BOB)
     val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
+    val state: State = repository.getLatestState(ACCOUNT_ID).block()
 
     mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 


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


[james-project] 09/16: JAMES-3469 Purge changes for deleted records

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

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

commit ae03324c90e3d3e6d6c07f244be06fc3d60c6489
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 11:35:43 2020 +0700

    JAMES-3469 Purge changes for deleted records
    
    https://jmap.io/spec-core.html#changes
    
    ```
    If a record has been updated AND destroyed since the old state, the server SHOULD just return the id in the destroyed list but MAY return it in the updated list as well.
    ```
---
 .../apache/james/jmap/api/change/EmailChanges.java | 24 +++++++++++++++-------
 .../api/change/EmailChangeRepositoryContract.java  | 12 +++++------
 2 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
index 0c7e54c..c037f76 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChanges.java
@@ -96,14 +96,24 @@ public class EmailChanges {
                 return this;
             }
 
-            Set<MessageId> createdTemp = new HashSet<>(created);
-            Set<MessageId> updatedTemp = new HashSet<>(updated);
             Set<MessageId> destroyedTemp = new HashSet<>(destroyed);
-            createdTemp.addAll(change.getCreated());
-            updatedTemp.addAll(
-                Sets.difference(ImmutableSet.copyOf(change.getUpdated()),
-                createdTemp));
-            destroyedTemp.addAll(change.getDestroyed());
+
+            Set<MessageId> createdTemp = Sets.difference(
+                ImmutableSet.<MessageId>builder()
+                    .addAll(created)
+                    .addAll(change.getCreated())
+                    .build(),
+                ImmutableSet.copyOf(change.getDestroyed()));
+            Set<MessageId> updatedTemp = Sets.difference(
+                ImmutableSet.<MessageId>builder()
+                    .addAll(updated)
+                    .addAll(Sets.difference(ImmutableSet.copyOf(change.getUpdated()),
+                        createdTemp))
+                    .build(),
+                ImmutableSet.copyOf(change.getDestroyed()));
+            destroyedTemp.addAll(Sets.difference(
+                ImmutableSet.copyOf(change.getDestroyed()),
+                created));
 
             if (createdTemp.size() + updatedTemp.size() + destroyedTemp.size() > limit.getValue()) {
                 hasMoreChanges = true;
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
index bb5f60c..1dbb9ca 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/EmailChangeRepositoryContract.java
@@ -498,7 +498,7 @@ public interface EmailChangeRepositoryContract {
             .state(STATE_0)
             .date(DATE.minusHours(3))
             .isDelegated(false)
-            .created(TestMessageId.of(1))
+            .created(TestMessageId.of(1), TestMessageId.of(9), TestMessageId.of(10))
             .build();
         EmailChange change1 = EmailChange.builder()
             .accountId(ACCOUNT_ID)
@@ -513,8 +513,8 @@ public interface EmailChangeRepositoryContract {
             .date(DATE.minusHours(1))
             .isDelegated(false)
             .created(TestMessageId.of(6), TestMessageId.of(7))
-            .updated(TestMessageId.of(2), TestMessageId.of(3))
-            .destroyed(TestMessageId.of(4))
+            .updated(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(10))
+            .destroyed(TestMessageId.of(4), TestMessageId.of(9))
             .build();
         EmailChange change3 = EmailChange.builder()
             .accountId(ACCOUNT_ID)
@@ -523,7 +523,7 @@ public interface EmailChangeRepositoryContract {
             .isDelegated(false)
             .created(TestMessageId.of(8))
             .updated(TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(1))
-            .destroyed(TestMessageId.of(5))
+            .destroyed(TestMessageId.of(5), TestMessageId.of(10))
             .build();
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -533,9 +533,9 @@ public interface EmailChangeRepositoryContract {
         EmailChanges emailChanges = repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.of(Limit.of(20))).block();
 
         SoftAssertions.assertSoftly(softly -> {
-            softly.assertThat(emailChanges.getCreated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(4), TestMessageId.of(5), TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(8));
+            softly.assertThat(emailChanges.getCreated()).containsExactlyInAnyOrder(TestMessageId.of(2), TestMessageId.of(3), TestMessageId.of(6), TestMessageId.of(7), TestMessageId.of(8));
             softly.assertThat(emailChanges.getUpdated()).containsExactlyInAnyOrder(TestMessageId.of(1));
-            softly.assertThat(emailChanges.getDestroyed()).containsExactlyInAnyOrder(TestMessageId.of(4), TestMessageId.of(5));
+            softly.assertThat(emailChanges.getDestroyed()).containsExactlyInAnyOrder(TestMessageId.of(9), TestMessageId.of(10));
         });
     }
 


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


[james-project] 02/16: JAMES-3469 Pojoify */changes State & Limit

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

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

commit 626b74041b6e240c221cf917d56c0a20a3a50b2c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 10:57:52 2020 +0700

    JAMES-3469 Pojoify */changes State & Limit
    
    This enable reuse of these POJOs for Email/changes.
    
    Also, add unit tests for these POJOs
    
    Move data validation from *ChangeRepository into the POJOs themselves.
    
    Other improvements includes:
     - toString method for state
     - State.INITIAL constant should be final
     - State.of should reject null values
     - Limit.of should reject negative or zero values
---
 .../james/modules/mailbox/MemoryMailboxModule.java |  2 +-
 .../change/CassandraMailboxChangeRepository.java   | 15 ++--
 .../Limit.java}                                    | 22 +++---
 .../james/jmap/api/change/MailboxChange.java       | 65 -----------------
 .../jmap/api/change/MailboxChangeRepository.java   |  2 -
 .../james/jmap/api/change/MailboxChanges.java      | 18 ++---
 .../org/apache/james/jmap/api/change/State.java    | 81 ++++++++++++++++++++++
 .../api/exception/ChangeNotFoundException.java     |  8 +--
 .../change/MemoryMailboxChangeRepository.java      |  6 +-
 .../apache/james/jmap/api/change/LimitTest.java}   | 44 ++++++------
 .../change/MailboxChangeRepositoryContract.java    |  2 -
 .../apache/james/jmap/api/change/StateTest.java}   | 49 +++++++------
 .../change/MemoryMailboxChangeRepositoryTest.java  |  8 +--
 .../contract/MailboxChangesMethodContract.scala    |  2 +-
 .../memory/MemoryMailboxChangesMethodTest.java     |  6 +-
 .../scala/org/apache/james/jmap/core/Session.scala |  4 +-
 .../apache/james/jmap/json/MailboxSerializer.scala |  2 +-
 .../org/apache/james/jmap/mail/MailboxGet.scala    |  2 +-
 .../james/jmap/method/MailboxChangesMethod.scala   |  2 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |  3 +-
 20 files changed, 185 insertions(+), 158 deletions(-)

diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
index 6bd4002..da8950e 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
+++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
@@ -25,8 +25,8 @@ import javax.inject.Singleton;
 
 import org.apache.james.adapter.mailbox.UserRepositoryAuthenticator;
 import org.apache.james.adapter.mailbox.UserRepositoryAuthorizator;
-import org.apache.james.jmap.api.change.MailboxChange.State;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
+import org.apache.james.jmap.api.change.State;
 import org.apache.james.jmap.memory.change.MemoryMailboxChangeRepository;
 import org.apache.james.mailbox.AttachmentContentLoader;
 import org.apache.james.mailbox.AttachmentManager;
diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java
index b17175a..4fe0c0e 100644
--- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java
+++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/change/CassandraMailboxChangeRepository.java
@@ -21,10 +21,11 @@ package org.apache.james.jmap.cassandra.change;
 
 import java.util.Optional;
 
+import org.apache.james.jmap.api.change.Limit;
 import org.apache.james.jmap.api.change.MailboxChange;
-import org.apache.james.jmap.api.change.MailboxChange.Limit;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
 import org.apache.james.jmap.api.change.MailboxChanges;
+import org.apache.james.jmap.api.change.State;
 import org.apache.james.jmap.api.model.AccountId;
 
 import reactor.core.publisher.Mono;
@@ -37,22 +38,22 @@ public class CassandraMailboxChangeRepository implements MailboxChangeRepository
     }
 
     @Override
-    public Mono<MailboxChanges> getSinceState(AccountId accountId, MailboxChange.State state, Optional<Limit> maxChanges) {
+    public Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges) {
         return Mono.empty();
     }
 
     @Override
-    public Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, MailboxChange.State state, Optional<Limit> maxChanges) {
+    public Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxChanges) {
         return Mono.empty();
     }
 
     @Override
-    public Mono<MailboxChange.State> getLatestState(AccountId accountId) {
-        return Mono.just(MailboxChange.State.INITIAL);
+    public Mono<State> getLatestState(AccountId accountId) {
+        return Mono.just(State.INITIAL);
     }
 
     @Override
-    public Mono<MailboxChange.State> getLatestStateWithDelegation(AccountId accountId) {
-        return Mono.just(MailboxChange.State.INITIAL);
+    public Mono<State> getLatestStateWithDelegation(AccountId accountId) {
+        return Mono.just(State.INITIAL);
     }
 }
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/Limit.java
similarity index 74%
copy from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
copy to server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/Limit.java
index 9fcc4a0..ebec8db 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/Limit.java
@@ -17,19 +17,23 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.api.exception;
+package org.apache.james.jmap.api.change;
 
-import org.apache.james.jmap.api.change.MailboxChange;
+import com.google.common.base.Preconditions;
 
-public class ChangeNotFoundException extends RuntimeException {
-    private final MailboxChange.State state;
+public class Limit {
+    public static Limit of(int value) {
+        Preconditions.checkArgument(value > 0, "'limit' needs to be strictly positive");
+        return new Limit(value);
+    }
+
+    private final int value;
 
-    public ChangeNotFoundException(MailboxChange.State state, String msg) {
-        super(msg);
-        this.state = state;
+    private Limit(int value) {
+        this.value = value;
     }
 
-    public MailboxChange.State getState() {
-        return state;
+    public int getValue() {
+        return value;
     }
 }
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
index 4bdcb2c..f242cc0 100644
--- 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
@@ -22,9 +22,7 @@ package org.apache.james.jmap.api.change;
 import java.time.Clock;
 import java.time.ZonedDateTime;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
-import java.util.UUID;
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
@@ -51,69 +49,6 @@ import com.github.steveash.guavate.Guavate;
 import com.google.common.collect.ImmutableList;
 
 public class MailboxChange {
-
-    public static class State {
-        public static State INITIAL = of(UUID.fromString("2c9f1b12-b35a-43e6-9af2-0106fb53a943"));
-
-        public interface Factory {
-            State generate();
-        }
-
-        public static class DefaultFactory implements Factory {
-
-            @Override
-            public State generate() {
-                return of(UUID.randomUUID());
-            }
-        }
-
-        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;
-        }
-    }
-
     @FunctionalInterface
     public interface RequiredAccountId {
         RequiredState accountId(AccountId accountId);
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
index 39779b6..d90b968 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
@@ -21,8 +21,6 @@ package org.apache.james.jmap.api.change;
 
 import java.util.Optional;
 
-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;
 
 import reactor.core.publisher.Mono;
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
index a3f9366..cbd1deb 100644
--- 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
@@ -39,10 +39,10 @@ public class MailboxChanges {
     public static class MailboxChangesBuilder {
 
         public static class MailboxChangeCollector implements Collector<MailboxChange, MailboxChangesBuilder, MailboxChanges> {
-            private final MailboxChange.Limit limit;
-            private final MailboxChange.State state;
+            private final Limit limit;
+            private final State state;
 
-            public MailboxChangeCollector(MailboxChange.State state, MailboxChange.Limit limit) {
+            public MailboxChangeCollector(State state, Limit limit) {
                 this.limit = limit;
                 this.state = state;
             }
@@ -72,15 +72,15 @@ public class MailboxChanges {
             }
         }
 
-        private MailboxChange.State state;
+        private State state;
         private boolean hasMoreChanges;
         private boolean canAddMoreItem;
-        private MailboxChange.Limit limit;
+        private Limit limit;
         private Set<MailboxId> created;
         private Set<MailboxId> updated;
         private Set<MailboxId> destroyed;
 
-        public MailboxChangesBuilder(MailboxChange.State state, MailboxChange.Limit limit) {
+        public MailboxChangesBuilder(State state, Limit limit) {
             this.limit = limit;
             this.state = state;
             this.hasMoreChanges = false;
@@ -121,13 +121,13 @@ public class MailboxChanges {
         }
     }
 
-    private MailboxChange.State newState;
+    private 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) {
+    private MailboxChanges(State newState, boolean hasMoreChanges, Set<MailboxId> created, Set<MailboxId> updated, Set<MailboxId> destroyed) {
         this.newState = newState;
         this.hasMoreChanges = hasMoreChanges;
         this.created = created;
@@ -135,7 +135,7 @@ public class MailboxChanges {
         this.destroyed = destroyed;
     }
 
-    public MailboxChange.State getNewState() {
+    public State getNewState() {
         return newState;
     }
 
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/State.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/State.java
new file mode 100644
index 0000000..f4ed004
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/State.java
@@ -0,0 +1,81 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.api.change;
+
+import java.util.Objects;
+import java.util.UUID;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+public class State {
+    public interface Factory {
+        Factory DEFAULT = new DefaultFactory();
+
+        State generate();
+    }
+
+    public static class DefaultFactory implements Factory {
+        @Override
+        public State generate() {
+            return of(UUID.randomUUID());
+        }
+    }
+
+    public static final State INITIAL = of(UUID.fromString("2c9f1b12-b35a-43e6-9af2-0106fb53a943"));
+
+    public static State of(UUID value) {
+        Preconditions.checkNotNull(value, "State 'value' should not be null.");
+
+        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);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("value", value)
+            .toString();
+    }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
index 9fcc4a0..d0663c3 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/exception/ChangeNotFoundException.java
@@ -19,17 +19,17 @@
 
 package org.apache.james.jmap.api.exception;
 
-import org.apache.james.jmap.api.change.MailboxChange;
+import org.apache.james.jmap.api.change.State;
 
 public class ChangeNotFoundException extends RuntimeException {
-    private final MailboxChange.State state;
+    private final State state;
 
-    public ChangeNotFoundException(MailboxChange.State state, String msg) {
+    public ChangeNotFoundException(State state, String msg) {
         super(msg);
         this.state = state;
     }
 
-    public MailboxChange.State getState() {
+    public State getState() {
         return state;
     }
 }
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
index 44e5aee..ac45191 100644
--- 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
@@ -23,12 +23,12 @@ import java.util.Comparator;
 import java.util.Optional;
 import java.util.function.Predicate;
 
+import org.apache.james.jmap.api.change.Limit;
 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.change.State;
 import org.apache.james.jmap.api.exception.ChangeNotFoundException;
 import org.apache.james.jmap.api.model.AccountId;
 
@@ -60,7 +60,7 @@ public class MemoryMailboxChangeRepository implements MailboxChangeRepository {
     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"));
+
         if (state.equals(State.INITIAL)) {
             return Flux.fromIterable(mailboxChangeMap.get(accountId))
                 .sort(Comparator.comparing(MailboxChange::getDate))
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/LimitTest.java
similarity index 65%
copy from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
copy to server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/LimitTest.java
index 39779b6..4d02219 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/LimitTest.java
@@ -19,23 +19,27 @@
 
 package org.apache.james.jmap.api.change;
 
-import java.util.Optional;
-
-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;
-
-import reactor.core.publisher.Mono;
-
-public interface MailboxChangeRepository {
-
-    Mono<Void> save(MailboxChange change);
-
-    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges);
-
-    Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxChanges);
-
-    Mono<State> getLatestState(AccountId accountId);
-
-    Mono<State> getLatestStateWithDelegation(AccountId accountId);
-}
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+class LimitTest {
+    @Test
+    void ofShouldThrowWhenNegative() {
+        assertThatThrownBy(() -> Limit.of(-1))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void ofShouldThrowWhenZero() {
+        assertThatThrownBy(() -> Limit.of(0))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void getValueShouldReturnSuppliedValue() {
+        assertThat(Limit.of(36).getValue())
+            .isEqualTo(36);
+    }
+}
\ No newline at end of file
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
index 922d0e9..81a9266 100644
--- 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
@@ -27,8 +27,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import java.time.ZonedDateTime;
 import java.util.Optional;
 
-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;
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/StateTest.java
similarity index 63%
copy from server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
copy to server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/StateTest.java
index 39779b6..cbea897 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChangeRepository.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/change/StateTest.java
@@ -19,23 +19,32 @@
 
 package org.apache.james.jmap.api.change;
 
-import java.util.Optional;
-
-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;
-
-import reactor.core.publisher.Mono;
-
-public interface MailboxChangeRepository {
-
-    Mono<Void> save(MailboxChange change);
-
-    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges);
-
-    Mono<MailboxChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxChanges);
-
-    Mono<State> getLatestState(AccountId accountId);
-
-    Mono<State> getLatestStateWithDelegation(AccountId accountId);
-}
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class StateTest {
+    @Test
+    void shouldMatchBeanContract() {
+        EqualsVerifier.forClass(State.class)
+            .verify();
+    }
+
+    @Test
+    void ofShouldThrowOnNull() {
+        assertThatThrownBy(() -> State.of(null))
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void getValueShouldReturnSuppliedValue() {
+        UUID uuid = UUID.randomUUID();
+        assertThat(State.of(uuid).getValue())
+            .isEqualTo(uuid);
+    }
+}
\ No newline at end of file
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
index 6c3b74f..ae3bd27 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/change/MemoryMailboxChangeRepositoryTest.java
@@ -19,19 +19,19 @@
 
 package org.apache.james.jmap.memory.change;
 
-import org.apache.james.jmap.api.change.MailboxChange;
 import org.apache.james.jmap.api.change.MailboxChangeRepository;
 import org.apache.james.jmap.api.change.MailboxChangeRepositoryContract;
+import org.apache.james.jmap.api.change.State;
 import org.junit.jupiter.api.BeforeEach;
 
 public class MemoryMailboxChangeRepositoryTest implements MailboxChangeRepositoryContract {
     MailboxChangeRepository mailboxChangeRepository;
-    MailboxChange.State.Factory stateFactory;
+    State.Factory stateFactory;
 
     @BeforeEach
     void setup() {
         mailboxChangeRepository = new MemoryMailboxChangeRepository();
-        stateFactory = new MailboxChange.State.DefaultFactory();
+        stateFactory = new State.DefaultFactory();
     }
 
     @Override
@@ -40,7 +40,7 @@ public class MemoryMailboxChangeRepositoryTest implements MailboxChangeRepositor
     }
 
     @Override
-    public MailboxChange.State.Factory stateFactory() {
+    public State.Factory stateFactory() {
         return stateFactory;
     }
 }
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/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index a9ce132..5d9164a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -35,7 +35,7 @@ import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
 import org.apache.james.core.Username
 import org.apache.james.jmap.api.change.MailboxChange
-import org.apache.james.jmap.api.change.MailboxChange.State
+import org.apache.james.jmap.api.change.State
 import org.apache.james.jmap.api.model.AccountId
 import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
 import org.apache.james.jmap.draft.JmapGuiceProbe
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
index 93104a9..58e6359 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
@@ -24,7 +24,7 @@ import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
-import org.apache.james.jmap.api.change.MailboxChange;
+import org.apache.james.jmap.api.change.State;
 import org.apache.james.jmap.rfc8621.contract.MailboxChangesMethodContract;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
@@ -38,7 +38,7 @@ public class MemoryMailboxChangesMethodTest implements MailboxChangesMethodContr
         .build();
 
     @Override
-    public MailboxChange.State.Factory stateFactory() {
-        return new MailboxChange.State.DefaultFactory();
+    public State.Factory stateFactory() {
+        return new State.DefaultFactory();
     }
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
index 6949488..81c3f4b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
@@ -25,12 +25,10 @@ import java.util.UUID
 
 import com.google.common.hash.Hashing
 import eu.timepit.refined.api.Refined
-import eu.timepit.refined.auto._
 import eu.timepit.refined.refineV
 import eu.timepit.refined.string.Uuid
 import org.apache.james.core.Username
-import org.apache.james.jmap.api.change.MailboxChanges
-import org.apache.james.jmap.api.change.MailboxChange.{State => JavaState}
+import org.apache.james.jmap.api.change.{MailboxChanges, State => JavaState}
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.State.INSTANCE
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
index f85c08e..573f107 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
@@ -23,7 +23,7 @@ import eu.timepit.refined._
 import eu.timepit.refined.collection.NonEmpty
 import javax.inject.Inject
 import org.apache.james.core.{Domain, Username}
-import org.apache.james.jmap.api.change.MailboxChange.Limit
+import org.apache.james.jmap.api.change.Limit
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.core.{ClientId, Properties, SetError, State}
 import org.apache.james.jmap.mail.MailboxGet.{UnparsedMailboxId, UnparsedMailboxIdConstraint}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
index add1c6e..a3a33e0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
@@ -22,7 +22,7 @@ package org.apache.james.jmap.mail
 import eu.timepit.refined
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.collection.NonEmpty
-import org.apache.james.jmap.api.change.MailboxChange.Limit
+import org.apache.james.jmap.api.change.Limit
 import org.apache.james.jmap.api.change.MailboxChanges
 import org.apache.james.jmap.core.{AccountId, Properties, State}
 import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
index c60e038..ab0003c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
@@ -21,7 +21,7 @@ package org.apache.james.jmap.method
 
 import eu.timepit.refined.auto._
 import javax.inject.Inject
-import org.apache.james.jmap.api.change.MailboxChange.{State => JavaState}
+import org.apache.james.jmap.api.change.{State => JavaState}
 import org.apache.james.jmap.api.change.MailboxChangeRepository
 import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_MAIL}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
index a207f72..32d4be2 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
@@ -22,8 +22,7 @@ package org.apache.james.jmap.change
 import java.time.{Clock, ZonedDateTime}
 
 import javax.mail.Flags
-import org.apache.james.jmap.api.change.MailboxChange.State
-import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository}
+import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository, State}
 import org.apache.james.jmap.api.model.AccountId
 import org.apache.james.jmap.change.MailboxChangeListenerTest.ACCOUNT_ID
 import org.apache.james.jmap.memory.change.MemoryMailboxChangeRepository


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


[james-project] 03/16: JAMES-3469 MemoryMailboxChangeRepository should be Thread safe

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

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

commit 39cef3a96aa386708159596cc1f4205c737d6fcb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 10:58:50 2020 +0700

    JAMES-3469 MemoryMailboxChangeRepository should be Thread safe
---
 .../apache/james/jmap/memory/change/MemoryMailboxChangeRepository.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

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
index ac45191..b50b497 100644
--- 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
@@ -35,6 +35,7 @@ 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 com.google.common.collect.Multimaps;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -45,7 +46,7 @@ public class MemoryMailboxChangeRepository implements MailboxChangeRepository {
     private final Multimap<AccountId, MailboxChange> mailboxChangeMap;
 
     public MemoryMailboxChangeRepository() {
-        this.mailboxChangeMap = ArrayListMultimap.create();
+        this.mailboxChangeMap = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
     }
 
     @Override


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


[james-project] 05/16: JAMES-3469 EmailChangeRepository interface

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

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

commit d3bfd8e171ce9ac1e42559f3736931a50b6ed5a6
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Dec 21 11:05:46 2020 +0700

    JAMES-3469 EmailChangeRepository interface
---
 .../jmap/api/change/EmailChangeRepository.java     | 38 ++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChangeRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChangeRepository.java
new file mode 100644
index 0000000..dd611bb
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/EmailChangeRepository.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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.Optional;
+
+import org.apache.james.jmap.api.model.AccountId;
+
+import reactor.core.publisher.Mono;
+
+public interface EmailChangeRepository {
+    Mono<Void> save(EmailChange change);
+
+    Mono<EmailChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxIdsToReturn);
+
+    Mono<EmailChanges> getSinceStateWithDelegation(AccountId accountId, State state, Optional<Limit> maxIdsToReturn);
+
+    Mono<State> getLatestState(AccountId accountId);
+
+    Mono<State> getLatestStateWithDelegation(AccountId accountId);
+}


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