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

[james-project] 14/17: JAMES-3481 Mailbox/change handle delegated mailbox

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

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

commit 833710a5056cfc980452fee7ebc0c92a1d16451b
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Dec 15 18:14:05 2020 +0700

    JAMES-3481 Mailbox/change handle delegated mailbox
---
 .../james/jmap/api/change/MailboxChange.java       | 175 ++++-
 .../jmap/api/change/MailboxChangeRepository.java   |   2 +-
 .../change/MailboxChangeRepositoryContract.java    | 136 ++--
 .../contract/MailboxChangesMethodContract.scala    | 718 +++++++++++++++++++--
 .../james/jmap/change/MailboxChangeListener.scala  |  22 +-
 .../jmap/change/MailboxChangeListenerTest.scala    |  26 +-
 6 files changed, 964 insertions(+), 115 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 52461e6..5613db9 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
@@ -25,10 +25,14 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.stream.Stream;
 
 import javax.mail.Flags;
 
+import org.apache.james.core.Username;
 import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.events.Event;
 import org.apache.james.mailbox.events.MailboxListener.Added;
 import org.apache.james.mailbox.events.MailboxListener.Expunged;
@@ -37,8 +41,12 @@ import org.apache.james.mailbox.events.MailboxListener.MailboxACLUpdated;
 import org.apache.james.mailbox.events.MailboxListener.MailboxAdded;
 import org.apache.james.mailbox.events.MailboxListener.MailboxDeletion;
 import org.apache.james.mailbox.events.MailboxListener.MailboxRenamed;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxACL;
 import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
 
+import com.github.steveash.guavate.Guavate;
 import com.google.common.collect.ImmutableList;
 
 public class MailboxChange {
@@ -93,31 +101,139 @@ public class MailboxChange {
         }
     }
 
-    public static MailboxChange of(AccountId accountId, State state,  ZonedDateTime date, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
-        return new MailboxChange(accountId, state, date, created, updated, destroyed);
+    @FunctionalInterface
+    interface RequiredAccountId {
+        RequiredState accountId(AccountId accountId);
     }
 
-    public static Optional<MailboxChange> fromEvent(Event event) {
+    @FunctionalInterface
+    interface RequiredState {
+        RequiredDate state(State state);
+    }
+
+    @FunctionalInterface
+    interface RequiredDate {
+        Builder date(ZonedDateTime date);
+    }
+
+    public static class Builder {
+        private final AccountId accountId;
+        private final State state;
+        private final ZonedDateTime date;
+        private boolean delegated;
+        private Optional<List<MailboxId>> created;
+        private Optional<List<MailboxId>> updated;
+        private Optional<List<MailboxId>> destroyed;
+
+        private Builder(AccountId accountId, State state, ZonedDateTime date) {
+            this.accountId = accountId;
+            this.state = state;
+            this.date = date;
+            this.created = Optional.empty();
+            this.updated = Optional.empty();
+            this.destroyed = Optional.empty();
+        }
+
+        public Builder delegated() {
+            this.delegated = true;
+            return this;
+        }
+
+        public Builder created(List<MailboxId> created) {
+            this.created = Optional.of(created);
+            return this;
+        }
+
+        public Builder updated(List<MailboxId> updated) {
+            this.updated = Optional.of(updated);
+            return this;
+        }
+
+        public Builder destroyed(List<MailboxId> destroyed) {
+            this.destroyed = Optional.of(destroyed);
+            return this;
+        }
+
+        public MailboxChange build() {
+            return new MailboxChange(accountId, state, date, delegated, 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);
+    }
+
+    public static Builder created(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> created) {
+        return MailboxChange.builder()
+            .accountId(accountId)
+            .state(state)
+            .date(date)
+            .created(created);
+    }
+
+    public static Builder updated(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> updated) {
+        return MailboxChange.builder()
+            .accountId(accountId)
+            .state(state)
+            .date(date)
+            .updated(updated);
+    }
+
+    public static Builder destroyed(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> destroyed) {
+        return MailboxChange.builder()
+            .accountId(accountId)
+            .state(state)
+            .date(date)
+            .destroyed(destroyed);
+    }
+
+    public static Optional<List<MailboxChange>> fromEvent(Event event, MailboxManager mailboxManager) {
         ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC());
         if (event instanceof MailboxAdded) {
             MailboxAdded mailboxAdded = (MailboxAdded) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxAdded.getMailboxId()), ImmutableList.of(), ImmutableList.of()));
+            return Optional.of(ImmutableList.of(MailboxChange.created(AccountId.fromUsername(mailboxAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxAdded.getMailboxId())).build()));
         }
         if (event instanceof MailboxRenamed) {
             MailboxRenamed mailboxRenamed = (MailboxRenamed) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxRenamed.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(mailboxRenamed.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(mailboxRenamed.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxRenamed.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(mailboxRenamed.getNewPath(), mailboxRenamed.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxRenamed.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            List<MailboxChange> blah = Stream.concat(Stream.of(ownerChange), shareeChanges).collect(Guavate.toImmutableList());
+            return Optional.of(blah);
         }
         if (event instanceof MailboxACLUpdated) {
             MailboxACLUpdated mailboxACLUpdated = (MailboxACLUpdated) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxACLUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(mailboxACLUpdated.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(mailboxACLUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxACLUpdated.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(mailboxACLUpdated.getMailboxPath(), mailboxACLUpdated.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxACLUpdated.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            return Optional.of(
+                Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList()));
         }
         if (event instanceof MailboxDeletion) {
             MailboxDeletion mailboxDeletion = (MailboxDeletion) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxDeletion.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(), ImmutableList.of(mailboxDeletion.getMailboxId())));
+            return Optional.of(ImmutableList.of(MailboxChange.destroyed(AccountId.fromUsername(mailboxDeletion.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxDeletion.getMailboxId())).build()));
         }
         if (event instanceof Added) {
             Added messageAdded = (Added) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(messageAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(messageAdded.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(messageAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(messageAdded.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(messageAdded.getMailboxPath(), messageAdded.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(messageAdded.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            return Optional.of(
+                Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList()));
         }
         if (event instanceof FlagsUpdated) {
             FlagsUpdated messageFlagUpdated = (FlagsUpdated) event;
@@ -125,28 +241,61 @@ public class MailboxChange {
                 .stream()
                 .anyMatch(flags -> flags.isChanged(Flags.Flag.SEEN));
             if (isSeenChanged) {
-                return Optional.of(MailboxChange.of(AccountId.fromUsername(messageFlagUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(messageFlagUpdated.getMailboxId()), ImmutableList.of()));
+
+                MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(messageFlagUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(messageFlagUpdated.getMailboxId())).build();
+                Stream<MailboxChange> shareeChanges = getSharees(messageFlagUpdated.getMailboxPath(), messageFlagUpdated.getUsername(), mailboxManager)
+                    .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(messageFlagUpdated.getMailboxId()))
+                        .delegated()
+                        .build());
+
+                return Optional.of(
+                    Stream.concat(Stream.of(ownerChange), shareeChanges)
+                        .collect(Guavate.toImmutableList()));
             }
         }
         if (event instanceof Expunged) {
             Expunged expunged = (Expunged) event;
-            return Optional.of(MailboxChange.of(AccountId.fromUsername(expunged.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(expunged.getMailboxId()), ImmutableList.of()));
+
+            MailboxChange ownerChange = MailboxChange.updated(AccountId.fromUsername(expunged.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(expunged.getMailboxId())).build();
+            Stream<MailboxChange> shareeChanges = getSharees(expunged.getMailboxPath(), expunged.getUsername(), mailboxManager)
+                .map(name -> MailboxChange.updated(AccountId.fromString(name), State.of(UUID.randomUUID()), now, ImmutableList.of(expunged.getMailboxId()))
+                    .delegated()
+                    .build());
+
+            return Optional.of(
+                Stream.concat(Stream.of(ownerChange), shareeChanges)
+                    .collect(Guavate.toImmutableList()));
         }
 
         return Optional.empty();
     }
 
+    private static Stream<String> getSharees(MailboxPath path, Username username, MailboxManager mailboxManager) {
+        MailboxSession mailboxSession = mailboxManager.createSystemSession(username);
+        try {
+            MailboxACL mailboxACL = mailboxManager.listRights(path, mailboxSession);
+            return mailboxACL.getEntries().keySet()
+                .stream()
+                .filter(rfc4314Rights -> !rfc4314Rights.isNegative())
+                .map(MailboxACL.EntryKey::getName);
+        } catch (MailboxException e) {
+            return Stream.of();
+        }
+    }
+
     private final AccountId accountId;
     private final State state;
     private final ZonedDateTime date;
+    private final boolean delegated;
     private final List<MailboxId> created;
     private final List<MailboxId> updated;
     private final List<MailboxId> destroyed;
 
-    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
+    private MailboxChange(AccountId accountId, State state, ZonedDateTime date, boolean delegated, List<MailboxId> created, List<MailboxId> updated, List<MailboxId> destroyed) {
         this.accountId = accountId;
         this.state = state;
         this.date = date;
+        this.delegated = delegated;
         this.created = created;
         this.updated = updated;
         this.destroyed = destroyed;
@@ -164,6 +313,10 @@ public class MailboxChange {
         return date;
     }
 
+    public boolean isDelegated() {
+        return delegated;
+    }
+
     public List<MailboxId> getCreated() {
         return created;
     }
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 ccc0a36..006316b 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
@@ -31,7 +31,7 @@ public interface MailboxChangeRepository {
 
     Mono<Void> save(MailboxChange change);
 
-    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxIdsToReturn);
+    Mono<MailboxChanges> getSinceState(AccountId accountId, State state, Optional<Limit> maxChanges);
 
     Mono<State> getLatestState(AccountId accountId);
 }
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 9cb83475..26c04ba 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
@@ -49,7 +49,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1))).build();
 
         assertThatCode(() -> repository.save(change).block())
             .doesNotThrowAnyException();
@@ -67,9 +67,9 @@ public interface MailboxChangeRepositoryContract {
     default void getLatestStateShouldReturnLastPersistedState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(change1).block();
         repository.save(change2).block();
         repository.save(change3).block();
@@ -82,7 +82,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldFailWhenNoAccountId() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.of(null, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.created(null, STATE_0, DATE, ImmutableList.of(TestId.of(1))).build();
 
         assertThatThrownBy(() -> repository.save(change).block())
             .isInstanceOf(NullPointerException.class);
@@ -92,7 +92,7 @@ public interface MailboxChangeRepositoryContract {
     default void saveChangeShouldFailWhenNoState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, null, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange change = MailboxChange.created(ACCOUNT_ID, null, DATE, ImmutableList.of(TestId.of(1))).build();
 
         assertThatThrownBy(() -> repository.save(change).block())
             .isInstanceOf(NullPointerException.class);
@@ -102,8 +102,8 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldSuccess() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(1), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(1)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState).block();
         repository.save(change).block();
 
@@ -115,7 +115,7 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnEmptyWhenNoNewerState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState).block();
 
         assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getAllChanges())
@@ -126,7 +126,7 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnCurrentStateWhenNoNewerState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
         repository.save(oldState).block();
 
         assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getNewState())
@@ -137,10 +137,10 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldLimitChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -154,10 +154,10 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnAllFromInitial() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(3))).build();
+        MailboxChange change3 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -171,11 +171,11 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesFromInitialShouldReturnNewState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(2))).build();
         State state2 = State.of(UUID.randomUUID());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, state2, DATE.minusHours(1), ImmutableList.of(TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4)), ImmutableList.of(), ImmutableList.of());
+        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(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -190,9 +190,9 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldLimitChangesWhenMaxChangesOmitted() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(7)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.created(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5), TestId.of(6))).build();
+        MailboxChange change2 = MailboxChange.created(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(7))).build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -206,9 +206,9 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldNotReturnMoreThanMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(4), TestId.of(5)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(4), TestId.of(5))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -221,8 +221,8 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnEmptyWhenNumberOfChangesExceedMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
 
@@ -234,9 +234,9 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldReturnNewState() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -249,9 +249,9 @@ public interface MailboxChangeRepositoryContract {
     default void hasMoreChangesShouldBeTrueWhenMoreChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -264,9 +264,9 @@ public interface MailboxChangeRepositoryContract {
     default void hasMoreChangesShouldBeFalseWhenNoMoreChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(2)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE.minusHours(1)).created(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
+        MailboxChange change2 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).updated(ImmutableList.of(TestId.of(2), TestId.of(3))).build();
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -279,10 +279,23 @@ public interface MailboxChangeRepositoryContract {
     default void changesShouldBeStoredInTheirRespectiveType() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(3), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(2), ImmutableList.of(TestId.of(2), TestId.of(3), TestId.of(4), TestId.of(5)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(6), TestId.of(7)), ImmutableList.of(TestId.of(2), TestId.of(3)), ImmutableList.of(TestId.of(4)));
-        MailboxChange change3 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(8)), ImmutableList.of(TestId.of(6), TestId.of(7)), ImmutableList.of(TestId.of(5)));
+        MailboxChange oldState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE.minusHours(3)).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).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(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .created(ImmutableList.of(TestId.of(6), TestId.of(7)))
+            .updated(ImmutableList.of(TestId.of(2), TestId.of(3)))
+            .destroyed(ImmutableList.of(TestId.of(4))).build();
+        MailboxChange change3 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .created(ImmutableList.of(TestId.of(8)))
+            .updated(ImmutableList.of(TestId.of(6), TestId.of(7)))
+            .destroyed(ImmutableList.of(TestId.of(5))).build();
+
         repository.save(oldState).block();
         repository.save(change1).block();
         repository.save(change2).block();
@@ -301,9 +314,15 @@ public interface MailboxChangeRepositoryContract {
     default void getChangesShouldIgnoreDuplicatedValues() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange oldState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change1 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(), ImmutableList.of(TestId.of(1), TestId.of(2)), ImmutableList.of());
-        MailboxChange change2 = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(3)), ImmutableList.of(TestId.of(1), TestId.of(2)), ImmutableList.of());
+        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.updated(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE.minusHours(1), ImmutableList.of(TestId.of(1), TestId.of(2))).build();
+        MailboxChange change2 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE)
+            .created(ImmutableList.of(TestId.of(3)))
+            .updated(ImmutableList.of(TestId.of(1), TestId.of(2)))
+            .build();
 
         repository.save(oldState).block();
         repository.save(change1).block();
@@ -317,11 +336,32 @@ public interface MailboxChangeRepositoryContract {
     }
 
     @Test
+    default void getChangesShouldReturnDelegatedChanges() {
+        MailboxChangeRepository repository = mailboxChangeRepository();
+
+        MailboxChange oldState = MailboxChange.created(ACCOUNT_ID, STATE_0, DATE.minusHours(2), ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change1 = MailboxChange.builder()
+            .accountId(ACCOUNT_ID)
+            .state(State.of(UUID.randomUUID()))
+            .date(DATE.minusHours(1))
+            .updated(ImmutableList.of(TestId.of(1)))
+            .delegated()
+            .build();
+
+        repository.save(oldState);
+        repository.save(change1);
+
+        assertThat(repository.getSinceState(ACCOUNT_ID, STATE_0, Optional.empty()).block().getUpdated())
+            .containsExactly(TestId.of(1));
+
+    }
+
+    @Test
     default void getChangesShouldFailWhenInvalidMaxChanges() {
         MailboxChangeRepository repository = mailboxChangeRepository();
 
-        MailboxChange currentState = MailboxChange.of(ACCOUNT_ID, STATE_0, DATE, ImmutableList.of(TestId.of(1)), ImmutableList.of(), ImmutableList.of());
-        MailboxChange change = MailboxChange.of(ACCOUNT_ID, State.of(UUID.randomUUID()), DATE, ImmutableList.of(TestId.of(2)), ImmutableList.of(), ImmutableList.of());
+        MailboxChange currentState = MailboxChange.builder().accountId(ACCOUNT_ID).state(STATE_0).date(DATE).created(ImmutableList.of(TestId.of(1))).build();
+        MailboxChange change = MailboxChange.builder().accountId(ACCOUNT_ID).state(State.of(UUID.randomUUID())).date(DATE).created(ImmutableList.of(TestId.of(2))).build();
         repository.save(currentState).block();
         repository.save(change).block();
 
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 e7de455..e07eef1 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
@@ -27,11 +27,13 @@ import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.builder.ResponseSpecBuilder
 import io.restassured.http.ContentType.JSON
+import javax.mail.Flags
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
 import net.javacrumbs.jsonunit.core.internal.Options
 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.model.AccountId
@@ -41,12 +43,12 @@ import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_ACCOUNT_ID, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.Right
-import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath}
+import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageId}
 import org.apache.james.mime4j.dom.Message
 import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
+import org.junit.jupiter.api.{BeforeEach, Disabled, Nested, Test}
 import play.api.libs.json.{JsString, Json}
 
 import scala.jdk.CollectionConverters._
@@ -80,7 +82,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val mailboxId1: String = mailboxProbe
       .createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
@@ -148,7 +150,7 @@ trait MailboxChangesMethodContract {
       .createMailbox(path)
       .serialize
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     renameMailbox(mailboxId, "mailbox11")
 
@@ -207,7 +209,7 @@ trait MailboxChangesMethodContract {
       .createMailbox(path)
       .serialize
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val message: Message = Message.Builder
       .of
@@ -261,28 +263,26 @@ trait MailboxChangesMethodContract {
   }
 
   @Test
-  @Disabled("Not implemented yet")
-  def mailboxChangesShouldReturnUpdatedChangesWhenAppendMessageToDelegatedMailbox(server: GuiceJamesServer): Unit = {
+  def mailboxChangesShouldReturnUpdatedChangesWhenAddSeenFlag(server: GuiceJamesServer): Unit = {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
 
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
-
     val path = MailboxPath.forUser(BOB, "mailbox1")
     val mailboxId: String = mailboxProbe
       .createMailbox(path)
       .serialize
 
-    server.getProbe(classOf[ACLProbeImpl])
-      .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
-
     val message: Message = Message.Builder
       .of
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
       .build
-    mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
+    val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    markEmailAsSeen(messageId)
 
     val request =
       s"""{
@@ -290,25 +290,23 @@ trait MailboxChangesMethodContract {
          |  "methodCalls": [[
          |    "Mailbox/changes",
          |    {
-         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |      "sinceState": "${oldState.getValue}"
          |    },
          |    "c1"]]
          |}""".stripMargin
 
-    val response = `given`(
-      baseRequestSpecBuilder(server)
-        .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
-        .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-        .setBody(request)
-        .build, new ResponseSpecBuilder().build)
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
       .post
-      .`then`
-        .statusCode(SC_OK)
-        .contentType(JSON)
-        .extract
-        .body
-        .asString
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
 
     assertThatJson(response)
       .whenIgnoringPaths("methodResponses[0][1].newState")
@@ -318,7 +316,7 @@ trait MailboxChangesMethodContract {
            |    "sessionState": "${SESSION_STATE.value}",
            |    "methodResponses": [
            |      [ "Mailbox/changes", {
-           |        "accountId": "$ANDRE_ACCOUNT_ID",
+           |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "oldState": "${oldState.getValue}",
            |        "hasMoreChanges": false,
            |        "updatedProperties": [],
@@ -331,6 +329,553 @@ trait MailboxChangesMethodContract {
   }
 
   @Test
+  def mailboxChangesShouldReturnUpdatedChangesWhenRemoveSeenFlag(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
+
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+    val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path,
+      AppendCommand.builder()
+        .withFlags(new Flags(Flags.Flag.SEEN))
+        .build("header: value\r\n\r\nbody"))
+      .getMessageId
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    markEmailAsNotSeen(messageId)
+
+    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": [],
+           |      "updated": ["$mailboxId"],
+           |      "destroyed": []
+           |    }, "c1"]
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def mailboxChangesShouldReturnUpdatedChangesWhenDestroyEmail(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 message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+    val oldState: State = storeReferenceState(server, BOB)
+
+    destroyEmail(messageId)
+
+    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": [],
+           |      "updated": ["$mailboxId"],
+           |      "destroyed": []
+           |    }, "c1"]
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Nested
+  class MailboxDelegationTest {
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenAppendMessageToMailbox(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
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      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": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .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": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenRenameMailbox(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
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      renameMailbox(mailboxId, "mailbox11")
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .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": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenAddSeenFlag(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
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val message: Message = Message.Builder
+        .of
+        .setSubject("test")
+        .setBody("testmail", StandardCharsets.UTF_8)
+        .build
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      markEmailAsSeen(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .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": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenRemoveSeenFlag(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
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path,
+        AppendCommand.builder()
+          .withFlags(new Flags(Flags.Flag.SEEN))
+          .build("header: value\r\n\r\nbody"))
+        .getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      markEmailAsNotSeen(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .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": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenDestroyEmail(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
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val message: Message = Message.Builder
+        .of
+        .setSubject("test")
+        .setBody("testmail", StandardCharsets.UTF_8)
+        .build
+      val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      destroyEmail(messageId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .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": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": ["$mailboxId"],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    @Disabled("Not implemented yet")
+    def mailboxChangesShouldReturnUpdatedChangesWhenDestroyDelegatedMailbox(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
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldState: State = storeReferenceState(server, ANDRE)
+
+      destroyMailbox(mailboxId)
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldState.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .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": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldState.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": [],
+             |        "created": [],
+             |        "updated": [],
+             |        "destroyed": ["$mailboxId"]
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+  }
+
+  @Test
   def mailboxChangesShouldReturnDestroyedChanges(server: GuiceJamesServer): Unit = {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
 
@@ -341,7 +886,7 @@ trait MailboxChangesMethodContract {
       .createMailbox(path)
       .serialize
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     mailboxProbe
       .deleteMailbox(path.getNamespace, BOB.asString(), path.getName)
@@ -396,7 +941,7 @@ trait MailboxChangesMethodContract {
 
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val path1 = MailboxPath.forUser(BOB, "mailbox1")
     val mailboxId1: String = mailboxProbe
@@ -468,7 +1013,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val mailboxId1: String = mailboxProbe
       .createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
@@ -540,7 +1085,7 @@ trait MailboxChangesMethodContract {
 
   @Test
   def mailboxChangesShouldFailWhenAccountIdNotFound(server: GuiceJamesServer): Unit = {
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val request =
       s"""{
@@ -627,7 +1172,7 @@ trait MailboxChangesMethodContract {
   def mailboxChangesShouldReturnNoChangesWhenNoNewerState(server: GuiceJamesServer): Unit = {
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
 
     val request =
       s"""{
@@ -678,7 +1223,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
 
@@ -720,7 +1265,7 @@ trait MailboxChangesMethodContract {
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     provisionSystemMailboxes(server)
 
-    val oldState: State = storeReferenceState(server)
+    val oldState: State = storeReferenceState(server, BOB)
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
     mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
 
@@ -827,10 +1372,111 @@ trait MailboxChangesMethodContract {
       .contentType(JSON)
   }
 
-  private def storeReferenceState(server: GuiceJamesServer): State = {
+  private def destroyMailbox(mailboxId: String): Unit = {
+    val request =
+      s"""
+         |{
+         |  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+         |  "methodCalls": [[
+         |    "Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["$mailboxId"]
+         |    }, "c1"]
+         |  ]
+         |}
+         |""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def markEmailAsSeen(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "${messageId.serialize}": {
+         |          "keywords": {
+         |             "$$seen": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def markEmailAsNotSeen(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "update": {
+         |        "${messageId.serialize}": {
+         |          "keywords/$$seen": null
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def destroyEmail(messageId: MessageId): Unit = {
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+  }
+
+  private def storeReferenceState(server: GuiceJamesServer, username: Username): State = {
     val state: State = State.of(UUID.randomUUID())
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
-    jmapGuiceProbe.saveMailboxChange(MailboxChange.of(AccountId.fromUsername(BOB), state, ZonedDateTime.now(), List().asJava, List(TestId.of(0)).asJava, List().asJava))
+    jmapGuiceProbe.saveMailboxChange(MailboxChange.updated(AccountId.fromUsername(username), state, ZonedDateTime.now(), List(TestId.of(0)).asJava).build)
 
     state
   }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
index c251fb5..b577eb8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
@@ -21,21 +21,29 @@ package org.apache.james.jmap.change
 
 import javax.inject.Inject
 import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository}
-import org.apache.james.mailbox.events.MailboxListener.ReactiveGroupMailboxListener
+import org.apache.james.mailbox.MailboxManager
+import org.apache.james.mailbox.events.MailboxListener.{MailboxEvent, ReactiveGroupMailboxListener}
 import org.apache.james.mailbox.events.{Event, Group}
 import org.reactivestreams.Publisher
-import reactor.core.scala.publisher.SMono
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
 
 case class MailboxChangeListenerGroup() extends Group {}
 
-case class MailboxChangeListener @Inject() (mailboxChangeRepository: MailboxChangeRepository) extends ReactiveGroupMailboxListener {
+case class MailboxChangeListener @Inject() (mailboxChangeRepository: MailboxChangeRepository,
+                                            mailboxManager: MailboxManager) extends ReactiveGroupMailboxListener {
 
-  override def reactiveEvent(event: Event): Publisher[Void] =
-    MailboxChange.fromEvent(event)
-      .map(mailboxChangeRepository.save(_))
+  override def reactiveEvent(event: Event): Publisher[Void] = {
+    MailboxChange.fromEvent(event, mailboxManager)
+      .map(changes => SFlux.fromIterable(changes.asScala)
+        .map(change => mailboxChangeRepository.save(change))
+        .`then`()
+        .`then`(SMono.empty[Void]).asJava)
       .orElse(SMono.empty[Void].asJava)
+  }
 
   override def getDefaultGroup: Group = MailboxChangeListenerGroup()
 
-  override def isHandling(event: Event): Boolean = MailboxChange.fromEvent(event).isPresent
+  override def isHandling(event: Event): Boolean = event.isInstanceOf[MailboxEvent]
 }
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 4c7acb8..aecbfc7 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
@@ -63,14 +63,14 @@ class MailboxChangeListenerTest {
 
     mailboxManager = resources.getMailboxManager
     repository = new MemoryMailboxChangeRepository()
-    listener = MailboxChangeListener(repository)
+    listener = MailboxChangeListener(repository, mailboxManager)
     resources.getEventBus.register(listener)
   }
 
   @Test
   def createMailboxShouldStoreCreatedEvent(): Unit = {
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     val mailboxSession = MailboxSessionUtil.create(BOB)
     val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
@@ -87,7 +87,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     mailboxManager.renameMailbox(path, newPath, mailboxSession)
 
@@ -98,13 +98,13 @@ class MailboxChangeListenerTest {
   @Test
   def updateMailboxACLShouldStoreUpdatedEvent(): Unit = {
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, 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
 
-    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession);
+    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 
     assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
       .containsExactly(inboxId)
@@ -117,7 +117,9 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
+
+    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession)
 
     mailboxManager
       .getMailbox(inboxId, mailboxSession)
@@ -136,7 +138,7 @@ class MailboxChangeListenerTest {
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -155,7 +157,7 @@ class MailboxChangeListenerTest {
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.REMOVE, MessageRange.all(), mailboxSession)
 
@@ -172,7 +174,7 @@ class MailboxChangeListenerTest {
     messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.ANSWERED), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
 
@@ -191,7 +193,7 @@ class MailboxChangeListenerTest {
       .build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava).build).block()
 
     messageManager.setFlags(new Flags(Flags.Flag.DELETED), FlagsUpdateMode.REPLACE, MessageRange.all(), mailboxSession)
 
@@ -208,7 +210,7 @@ class MailboxChangeListenerTest {
     val appendResult: AppendResult = messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, 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)
@@ -222,7 +224,7 @@ class MailboxChangeListenerTest {
     val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
 
     val state = State.of(UUID.randomUUID)
-    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    repository.save(MailboxChange.created(ACCOUNT_ID, state, ZonedDateTime.now, 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