Merge pull request #559 from chibenwa/upgrade-homepage-dockerfile

[UPGRADE] Homepage dockerfile
diff --git a/.scalafix.conf b/.scalafix.conf
new file mode 100644
index 0000000..b00dda3
--- /dev/null
+++ b/.scalafix.conf
@@ -0,0 +1,26 @@
+rules = [
+    DisableSyntax,
+    ProcedureSyntax,
+    NoValInForComprehension,
+    LeakingImplicitClassVal
+]
+
+RemoveUnused.imports = true
+RemoveUnused.privates = false
+RemoveUnused.locals = true
+RemoveUnused.patternvars = false
+RemoveUnused.implicits = false
+
+DisableSyntax.noVars = false
+DisableSyntax.noThrows = false
+DisableSyntax.noNulls = false
+DisableSyntax.noReturns = false
+DisableSyntax.noWhileLoops = false
+DisableSyntax.noAsInstanceOf = false
+DisableSyntax.noIsInstanceOf = false
+DisableSyntax.noXml = false
+DisableSyntax.noDefaultArgs = false
+DisableSyntax.noFinalVal = true
+DisableSyntax.noFinalize = true
+DisableSyntax.noValPatterns = false
+DisableSyntax.noUniversalEquality = false
\ No newline at end of file
diff --git a/event-sourcing/event-sourcing-core/pom.xml b/event-sourcing/event-sourcing-core/pom.xml
index f843e5a..a7b6287 100644
--- a/event-sourcing/event-sourcing-core/pom.xml
+++ b/event-sourcing/event-sourcing-core/pom.xml
@@ -87,6 +87,13 @@
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>io.github.evis</groupId>
+                <artifactId>scalafix-maven-plugin</artifactId>
+                <configuration>
+                    <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/event-sourcing/event-sourcing-pojo/pom.xml b/event-sourcing/event-sourcing-pojo/pom.xml
index a6c5cc6..1d24afb 100644
--- a/event-sourcing/event-sourcing-pojo/pom.xml
+++ b/event-sourcing/event-sourcing-pojo/pom.xml
@@ -52,6 +52,13 @@
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>io.github.evis</groupId>
+                <artifactId>scalafix-maven-plugin</artifactId>
+                <configuration>
+                    <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
index 6ee804b..b117baf 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
@@ -34,6 +34,7 @@
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.MessageRange;
 import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
+import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.model.search.MailboxQuery;
 import org.reactivestreams.Publisher;
 
@@ -322,7 +323,18 @@
      * @param session
      *            the context for this call, not null
      */
-    Publisher<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) throws MailboxException;
+    Publisher<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit);
+
+    /**
+     * Returns the list of MessageId of messages belonging to that Thread
+     *
+     * @param threadId
+     *          target Thread
+     * @param session
+     *          the context for this call, not null
+     * @return  the list of MessageId of messages belonging to that Thread
+     */
+    Publisher<MessageId> getThread(ThreadId threadId, MailboxSession session);
 
     /**
      * Does the given mailbox exist?
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/exception/ThreadNotFoundException.java b/mailbox/api/src/main/java/org/apache/james/mailbox/exception/ThreadNotFoundException.java
new file mode 100644
index 0000000..f6f4c3e
--- /dev/null
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/exception/ThreadNotFoundException.java
@@ -0,0 +1,28 @@
+/******************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one     *
+ * or more contributor license agreements.  See the NOTICE file   *
+ * distributed with this work for additional information          *
+ * regarding copyright ownership.  The ASF licenses this file     *
+ * to you under the Apache License, Version 2.0 (the              *
+ * "License"); you may not use this file except in compliance     *
+ * with the License.  You may obtain a copy of the License at     *
+ *                                                                *
+ * http://www.apache.org/licenses/LICENSE-2.0                     *
+ *                                                                *
+ * Unless required by applicable law or agreed to in writing,     *
+ * software distributed under the License is distributed on an    *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY         *
+ * KIND, either express or implied.  See the License for the      *
+ * specific language governing permissions and limitations        *
+ * under the License.                                             *
+ ******************************************************************/
+
+package org.apache.james.mailbox.exception;
+
+import org.apache.james.mailbox.model.ThreadId;
+
+public class ThreadNotFoundException extends MailboxException {
+    public ThreadNotFoundException(ThreadId threadId) {
+        super("Thread " + threadId.getBaseMessageId().serialize() + " can not be found");
+    }
+}
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java
index ec49426..919b6ad 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java
@@ -21,10 +21,26 @@
 
 import java.util.Objects;
 
+import javax.inject.Inject;
+
 import com.google.common.base.MoreObjects;
 
 
 public class ThreadId {
+    public static class Factory {
+        private final MessageId.Factory messageIdFactory;
+
+        @Inject
+        public Factory(MessageId.Factory messageIdFactory) {
+            this.messageIdFactory = messageIdFactory;
+        }
+
+        public ThreadId fromString(String serialized) {
+            MessageId messageId = messageIdFactory.fromString(serialized);
+            return fromBaseMessageId(messageId);
+        }
+    }
+
     public static ThreadId fromBaseMessageId(MessageId baseMessageId) {
         return new ThreadId(baseMessageId);
     }
diff --git a/mailbox/event/json/pom.xml b/mailbox/event/json/pom.xml
index e232ab8..055f8ab 100644
--- a/mailbox/event/json/pom.xml
+++ b/mailbox/event/json/pom.xml
@@ -99,6 +99,13 @@
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>io.github.evis</groupId>
+                <artifactId>scalafix-maven-plugin</artifactId>
+                <configuration>
+                    <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java b/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java
index 83ab8c1..dfa79f2 100644
--- a/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java
+++ b/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java
@@ -19,21 +19,31 @@
 
 package org.apache.james.mailbox.store.search;
 
+import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.inmemory.InMemoryCombinationManagerTestSystem;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
 import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.mailbox.inmemory.mail.InMemoryMapperProvider;
 import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.store.CombinationManagerTestSystem;
 import org.apache.james.mailbox.store.ThreadIdGuessingAlgorithmContract;
+import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.SearchThreadIdGuessingAlgorithm;
 import org.apache.james.mailbox.store.mail.ThreadIdGuessingAlgorithm;
+import org.apache.james.mailbox.store.mail.model.MapperProvider;
 
 public class SearchThreadIdGuessingAlgorithmTest extends ThreadIdGuessingAlgorithmContract {
+    private InMemoryMailboxManager mailboxManager;
 
     @Override
     protected CombinationManagerTestSystem createTestingData() {
         InMemoryIntegrationResources resources = InMemoryIntegrationResources.defaultResources();
 
+        mailboxManager = resources.getMailboxManager();
+        eventBus = resources.getEventBus();
+        messageIdFactory = resources.getMessageIdFactory();
+
         return new InMemoryCombinationManagerTestSystem(
             resources.getMailboxManager(),
             resources.getMessageIdManager());
@@ -45,7 +55,22 @@
     }
 
     @Override
+    protected MessageMapper createMessageMapper(MailboxSession mailboxSession) {
+        return mailboxManager.getMapperFactory().createMessageMapper(mailboxSession);
+    }
+
+    @Override
+    protected MapperProvider provideMapper() {
+        return new InMemoryMapperProvider();
+    }
+
+    @Override
     protected MessageId initNewBasedMessageId() {
         return InMemoryMessageId.of(100);
     }
+
+    @Override
+    protected MessageId initOtherBasedMessageId() {
+        return InMemoryMessageId.of(1000);
+    }
 }
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index e228e7e..85d47e5 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -74,6 +74,7 @@
 import org.apache.james.mailbox.model.MessageRange;
 import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
 import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.model.UidValidity;
 import org.apache.james.mailbox.model.search.MailboxQuery;
 import org.apache.james.mailbox.model.search.PrefixedWildcard;
@@ -782,14 +783,19 @@
     }
 
     @Override
-    public Flux<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) throws MailboxException {
+    public Flux<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) {
         return getInMailboxIds(expression, session)
             .filter(id -> !expression.getNotInMailboxes().contains(id))
             .collect(Guavate.toImmutableSet())
             .flatMapMany(Throwing.function(ids -> index.search(session, ids, expression.getSearchQuery(), limit)));
     }
 
-    private Flux<MailboxId> getInMailboxIds(MultimailboxesSearchQuery expression, MailboxSession session) throws MailboxException {
+    @Override
+    public Flux<MessageId> getThread(ThreadId threadId, MailboxSession session) {
+        return threadIdGuessingAlgorithm.getMessageIdsInThread(threadId, session);
+    }
+
+    private Flux<MailboxId> getInMailboxIds(MultimailboxesSearchQuery expression, MailboxSession session) {
         if (expression.getInMailboxes().isEmpty()) {
             return accessibleMailboxIds(expression.getNamespace(), Right.Read, session);
         } else {
@@ -799,7 +805,7 @@
         }
     }
 
-    private Flux<Mailbox> filterReadable(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) throws MailboxException {
+    private Flux<Mailbox> filterReadable(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) {
         MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session);
         return Flux.fromIterable(inMailboxes)
             .concatMap(mailboxMapper::findMailboxById)
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java
index b6334ca..eed7eb2 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java
@@ -28,6 +28,7 @@
 import org.apache.james.mailbox.store.mail.model.MimeMessageId;
 import org.apache.james.mailbox.store.mail.model.Subject;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public class NaiveThreadIdGuessingAlgorithm implements ThreadIdGuessingAlgorithm {
@@ -35,4 +36,9 @@
     public Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> thisMimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) {
         return Mono.just(ThreadId.fromBaseMessageId(messageId));
     }
+
+    @Override
+    public Flux<MessageId> getMessageIdsInThread(ThreadId threadId, MailboxSession session) {
+        return Flux.just(threadId.getBaseMessageId());
+    }
 }
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java
index a229fee..551e737 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java
@@ -29,7 +29,7 @@
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageIdManager;
-import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.exception.ThreadNotFoundException;
 import org.apache.james.mailbox.model.FetchGroup;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.MessageResult;
@@ -56,7 +56,7 @@
     }
 
     @Override
-    public Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) throws MailboxException {
+    public Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) {
         MultimailboxesSearchQuery expression = buildSearchQuery(mimeMessageId, inReplyTo, references, subject);
 
         return Flux.from(mailboxManager.search(expression, session, 1))
@@ -67,6 +67,21 @@
             .switchIfEmpty(Mono.just(ThreadId.fromBaseMessageId(messageId)));
     }
 
+    @Override
+    public Flux<MessageId> getMessageIdsInThread(ThreadId threadId, MailboxSession session) {
+        SearchQuery searchQuery = SearchQuery.builder()
+            .andCriteria(SearchQuery.threadId(threadId))
+            .sorts(new SearchQuery.Sort(SearchQuery.Sort.SortClause.Arrival, SearchQuery.Sort.Order.NATURAL))
+            .build();
+
+        MultimailboxesSearchQuery expression = MultimailboxesSearchQuery
+            .from(searchQuery)
+            .build();
+
+        return Flux.from(mailboxManager.search(expression, session, Integer.MAX_VALUE))
+            .switchIfEmpty(Mono.error(() -> new ThreadNotFoundException(threadId)));
+    }
+
     private MultimailboxesSearchQuery buildSearchQuery(Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject) {
         Set<MimeMessageId> mimeMessageIds = buildMimeMessageIdSet(mimeMessageId, inReplyTo, references);
 
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java
index 4461fc9..2b6d91a 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java
@@ -23,14 +23,16 @@
 import java.util.Optional;
 
 import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.store.mail.model.MimeMessageId;
 import org.apache.james.mailbox.store.mail.model.Subject;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public interface ThreadIdGuessingAlgorithm {
-    Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> thisMimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) throws MailboxException;
+    Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> thisMimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session);
+
+    Flux<MessageId> getMessageIdsInThread(ThreadId threadId, MailboxSession session);
 }
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java
index c28757c..c54ee33 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java
@@ -20,22 +20,40 @@
 package org.apache.james.mailbox.store;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.nio.charset.StandardCharsets;
+import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import javax.mail.Flags;
+
 import org.apache.james.core.Username;
+import org.apache.james.events.EventBus;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.events.MailboxIdRegistrationKey;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.exception.ThreadNotFoundException;
+import org.apache.james.mailbox.model.ByteContent;
+import org.apache.james.mailbox.model.Mailbox;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageMetaData;
 import org.apache.james.mailbox.model.ThreadId;
+import org.apache.james.mailbox.model.UidValidity;
+import org.apache.james.mailbox.store.event.EventFactory;
+import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.ThreadIdGuessingAlgorithm;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.MapperProvider;
 import org.apache.james.mailbox.store.mail.model.MimeMessageId;
 import org.apache.james.mailbox.store.mail.model.Subject;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
 import org.apache.james.mime4j.dom.Message;
 import org.apache.james.mime4j.stream.RawField;
 import org.junit.jupiter.api.BeforeEach;
@@ -44,35 +62,55 @@
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 
+import com.google.common.collect.ImmutableList;
+
+import reactor.core.publisher.Flux;
+
 public abstract class ThreadIdGuessingAlgorithmContract {
     public static final Username USER = Username.of("quan");
+    private static final UidValidity UID_VALIDITY = UidValidity.of(42);
+
+    protected EventBus eventBus;
+    protected MessageId.Factory messageIdFactory;
     private MailboxManager mailboxManager;
     private MessageManager inbox;
+    private MessageMapper messageMapper;
     private ThreadIdGuessingAlgorithm testee;
     private MailboxSession mailboxSession;
     private CombinationManagerTestSystem testingData;
     private MessageId newBasedMessageId;
+    private MessageId otherBasedMessageId;
+    private Mailbox mailbox;
 
     protected abstract CombinationManagerTestSystem createTestingData();
 
     protected abstract ThreadIdGuessingAlgorithm initThreadIdGuessingAlgorithm(CombinationManagerTestSystem testingData);
 
+    protected abstract MessageMapper createMessageMapper(MailboxSession mailboxSession);
+
+    protected abstract MapperProvider provideMapper();
+
     protected abstract MessageId initNewBasedMessageId();
 
+    protected abstract MessageId initOtherBasedMessageId();
+
     @BeforeEach
     void setUp() throws Exception {
         testingData = createTestingData();
         testee = initThreadIdGuessingAlgorithm(testingData);
         newBasedMessageId = initNewBasedMessageId();
+        otherBasedMessageId = initOtherBasedMessageId();
 
         mailboxManager = testingData.getMailboxManager();
         mailboxSession = mailboxManager.createSystemSession(USER);
         mailboxManager.createMailbox(MailboxPath.inbox(USER), mailboxSession);
+        messageMapper = createMessageMapper(mailboxSession);
         inbox = mailboxManager.getMailbox(MailboxPath.inbox(USER), mailboxSession);
+        mailbox = inbox.getMailboxEntity();
     }
 
     @Test
-    void givenNonMailWhenAddAMailThenGuessingThreadIdShouldBasedOnGeneratedMessageId() throws Exception {
+    void givenNonMailWhenAddAMailThenGuessingThreadIdShouldBasedOnGeneratedMessageId() {
         ThreadId threadId = testee.guessThreadIdReactive(newBasedMessageId, Optional.of(new MimeMessageId("abc")), Optional.empty(), Optional.empty(), Optional.of(new Subject("test")), mailboxSession).block();
 
         assertThat(threadId.getBaseMessageId()).isEqualTo(newBasedMessageId);
@@ -202,4 +240,86 @@
         assertThat(threadId.getBaseMessageId()).isEqualTo(newBasedMessageId);
     }
 
+    @Test
+    void givenThreeMailsInAThreadThenGetThreadShouldReturnAListWithThreeMessageIdsSortedByArrivalDate() throws MailboxException {
+        MailboxMessage message1 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+        MailboxMessage message2 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+        MailboxMessage message3 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+
+        appendMessageThenDispatchAddedEvent(mailbox, message1);
+        appendMessageThenDispatchAddedEvent(mailbox, message2);
+        appendMessageThenDispatchAddedEvent(mailbox, message3);
+
+        Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
+
+        assertThat(messageIds.collectList().block())
+            .isEqualTo(ImmutableList.of(message1.getMessageId(), message2.getMessageId(), message3.getMessageId()));
+    }
+
+    @Test
+    void givenNonMailInAThreadThenGetThreadShouldThrowThreadNotFoundException() {
+        Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
+
+        assertThatThrownBy(() -> testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession).collectList().block())
+            .getCause()
+            .isInstanceOf(ThreadNotFoundException.class);
+    }
+
+    @Test
+    void givenAMailInAThreadThenGetThreadShouldReturnAListWithOnlyOneMessageIdInThatThread() throws MailboxException {
+        MailboxMessage message1 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+
+        appendMessageThenDispatchAddedEvent(mailbox, message1);
+
+        Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
+
+        assertThat(messageIds.collectList().block())
+            .containsOnly(message1.getMessageId());
+    }
+
+    @Test
+    void givenTwoDistinctThreadsThenGetThreadShouldNotReturnUnrelatedMails() throws MailboxException {
+        // given message1 and message2 in thread1, message3 in thread2
+        ThreadId threadId1 = ThreadId.fromBaseMessageId(newBasedMessageId);
+        ThreadId threadId2 = ThreadId.fromBaseMessageId(otherBasedMessageId);
+        MailboxMessage message1 = createMessage(mailbox, threadId1);
+        MailboxMessage message2 = createMessage(mailbox, threadId1);
+        MailboxMessage message3 = createMessage(mailbox, threadId2);
+
+        appendMessageThenDispatchAddedEvent(mailbox, message1);
+        appendMessageThenDispatchAddedEvent(mailbox, message2);
+        appendMessageThenDispatchAddedEvent(mailbox, message3);
+
+        // then get thread2 should not return unrelated message1 and message2
+        Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(otherBasedMessageId), mailboxSession);
+
+        assertThat(messageIds.collectList().block())
+            .doesNotContain(message1.getMessageId(), message2.getMessageId());
+    }
+
+    private SimpleMailboxMessage createMessage(Mailbox mailbox, ThreadId threadId) {
+        MessageId messageId = messageIdFactory.generate();
+        String content = "Some content";
+        int bodyStart = 16;
+        return new SimpleMailboxMessage(messageId,
+            threadId,
+            new Date(),
+            content.length(),
+            bodyStart,
+            new ByteContent(content.getBytes()),
+            new Flags(),
+            new PropertyBuilder().build(),
+            mailbox.getMailboxId());
+    }
+
+    private void appendMessageThenDispatchAddedEvent(Mailbox mailbox, MailboxMessage mailboxMessage) throws MailboxException {
+        MessageMetaData messageMetaData = messageMapper.add(mailbox, mailboxMessage);
+        eventBus.dispatch(EventFactory.added()
+                .randomEventId()
+                .mailboxSession(mailboxSession)
+                .mailbox(mailbox)
+                .addMetaData(messageMetaData)
+                .build(),
+            new MailboxIdRegistrationKey(mailbox.getMailboxId())).block();
+    }
 }
diff --git a/mdn/pom.xml b/mdn/pom.xml
index 735a372..24b9687 100644
--- a/mdn/pom.xml
+++ b/mdn/pom.xml
@@ -90,6 +90,13 @@
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>io.github.evis</groupId>
+                <artifactId>scalafix-maven-plugin</artifactId>
+                <configuration>
+                    <config>${project.parent.basedir}/.scalafix.conf</config>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/pom.xml b/pom.xml
index 2009d47..5ed7565 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1358,6 +1358,12 @@
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
+                <artifactId>james-server-data-ldap</artifactId>
+                <version>${project.version}</version>
+                <type>test-jar</type>
+            </dependency>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
                 <artifactId>james-server-data-library</artifactId>
                 <version>${project.version}</version>
             </dependency>
@@ -2819,6 +2825,24 @@
                     <version>2.7.1</version>
                 </plugin>
                 <plugin>
+                    <groupId>io.github.evis</groupId>
+                    <artifactId>scalafix-maven-plugin</artifactId>
+                    <version>0.1.4_0.9.23</version>
+                    <configuration>
+                        <config>${basedir}/.scalafix.conf</config>
+                        <mode>CHECK</mode>
+                    </configuration>
+                    <executions>
+                        <execution>
+                            <id>scala-check-style</id>
+                            <goals>
+                                <goal>scalafix</goal>
+                            </goals>
+                            <phase>compile</phase>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
                     <groupId>org.scalatest</groupId>
                     <artifactId>scalatest-maven-plugin</artifactId>
                     <version>2.0.0</version>
@@ -2847,7 +2871,15 @@
                             <arg>-unchecked</arg>
                             <arg>-deprecation</arg>
                             <arg>-explaintypes</arg>
+                            <arg>-Ywarn-unused</arg>
                         </args>
+                        <compilerPlugins>
+                            <compilerPlugin>
+                                <groupId>org.scalameta</groupId>
+                                <artifactId>semanticdb-scalac_${scala.version}</artifactId>
+                                <version>4.4.23</version>
+                            </compilerPlugin>
+                        </compilerPlugins>
                     </configuration>
                     <executions>
                         <execution>
diff --git a/server/apps/cassandra-app/pom.xml b/server/apps/cassandra-app/pom.xml
index 51d93b5..11dd2a8 100644
--- a/server/apps/cassandra-app/pom.xml
+++ b/server/apps/cassandra-app/pom.xml
@@ -106,7 +106,6 @@
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-ldap</artifactId>
-            <version>${project.version}</version>
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
@@ -126,6 +125,12 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-data-ldap</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-elasticsearch</artifactId>
         </dependency>
         <dependency>
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java
index 0c3a91a..b248f47 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java
+++ b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java
@@ -29,6 +29,7 @@
 
 import org.apache.commons.net.imap.IMAPClient;
 import org.apache.james.core.Domain;
+import org.apache.james.data.LdapTestExtension;
 import org.apache.james.data.UsersRepositoryModuleChooser;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.apache.james.modules.protocols.ImapGuiceProbe;
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java
index 536fabc..6bcb98e 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java
+++ b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.james;
 
+import org.apache.james.data.LdapTestExtension;
 import org.apache.james.data.UsersRepositoryModuleChooser;
 import org.apache.james.jmap.draft.JmapJamesServerContract;
 import org.apache.james.modules.TestJMAPServerModule;
diff --git a/server/apps/distributed-app/pom.xml b/server/apps/distributed-app/pom.xml
index acfaaf7..9e90b98 100644
--- a/server/apps/distributed-app/pom.xml
+++ b/server/apps/distributed-app/pom.xml
@@ -141,7 +141,6 @@
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-ldap</artifactId>
-            <version>${project.version}</version>
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
@@ -161,6 +160,12 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-data-ldap</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-distributed</artifactId>
         </dependency>
         <dependency>
diff --git a/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java b/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java
index 623d15e..40b6f4b 100644
--- a/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java
+++ b/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java
@@ -25,6 +25,7 @@
 import java.io.IOException;
 
 import org.apache.commons.net.imap.IMAPClient;
+import org.apache.james.data.LdapTestExtension;
 import org.apache.james.data.UsersRepositoryModuleChooser;
 import org.apache.james.jmap.draft.JmapJamesServerContract;
 import org.apache.james.modules.AwsS3BlobStoreExtension;
diff --git a/server/apps/jpa-app/pom.xml b/server/apps/jpa-app/pom.xml
index cc395da..7727845 100644
--- a/server/apps/jpa-app/pom.xml
+++ b/server/apps/jpa-app/pom.xml
@@ -72,6 +72,12 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-ldap</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-common</artifactId>
         </dependency>
         <dependency>
@@ -82,6 +88,16 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-data-ldap</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-data-ldap</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-es-resporter</artifactId>
         </dependency>
         <dependency>
diff --git a/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesConfiguration.java b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
new file mode 100644
index 0000000..de35304
--- /dev/null
+++ b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
@@ -0,0 +1,127 @@
+/****************************************************************
+ * 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;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.james.data.UsersRepositoryModuleChooser;
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.filesystem.api.JamesDirectoriesProvider;
+import org.apache.james.server.core.JamesServerResourceLoader;
+import org.apache.james.server.core.MissingArgumentException;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+
+public class JPAJamesConfiguration implements Configuration {
+    public static class Builder {
+        private Optional<String> rootDirectory;
+        private Optional<ConfigurationPath> configurationPath;
+        private Optional<UsersRepositoryModuleChooser.Implementation> usersRepositoryImplementation;
+
+        private Builder() {
+            rootDirectory = Optional.empty();
+            configurationPath = Optional.empty();
+            usersRepositoryImplementation = Optional.empty();
+        }
+
+        public Builder workingDirectory(String path) {
+            rootDirectory = Optional.of(path);
+            return this;
+        }
+
+        public Builder workingDirectory(File file) {
+            rootDirectory = Optional.of(file.getAbsolutePath());
+            return this;
+        }
+
+        public Builder useWorkingDirectoryEnvProperty() {
+            rootDirectory = Optional.ofNullable(System.getProperty(WORKING_DIRECTORY));
+            if (!rootDirectory.isPresent()) {
+                throw new MissingArgumentException("Server needs a working.directory env entry");
+            }
+            return this;
+        }
+
+        public Builder configurationPath(ConfigurationPath path) {
+            configurationPath = Optional.of(path);
+            return this;
+        }
+
+        public Builder configurationFromClasspath() {
+            configurationPath = Optional.of(new ConfigurationPath(FileSystem.CLASSPATH_PROTOCOL));
+            return this;
+        }
+
+        public Builder usersRepository(UsersRepositoryModuleChooser.Implementation implementation) {
+            this.usersRepositoryImplementation = Optional.of(implementation);
+            return this;
+        }
+
+        public JPAJamesConfiguration build() {
+            ConfigurationPath configurationPath = this.configurationPath.orElse(new ConfigurationPath(FileSystem.FILE_PROTOCOL_AND_CONF));
+            JamesServerResourceLoader directories = new JamesServerResourceLoader(rootDirectory
+                .orElseThrow(() -> new MissingArgumentException("Server needs a working.directory env entry")));
+
+            FileSystemImpl fileSystem = new FileSystemImpl(directories);
+
+            FileConfigurationProvider configurationProvider = new FileConfigurationProvider(fileSystem, Basic.builder()
+                .configurationPath(configurationPath)
+                .workingDirectory(directories.getRootDirectory())
+                .build());
+            UsersRepositoryModuleChooser.Implementation usersRepositoryChoice = usersRepositoryImplementation.orElseGet(
+                () -> UsersRepositoryModuleChooser.Implementation.parse(configurationProvider));
+
+            return new JPAJamesConfiguration(
+                configurationPath,
+                directories,
+                usersRepositoryChoice);
+        }
+    }
+
+    public static JPAJamesConfiguration.Builder builder() {
+        return new Builder();
+    }
+
+    private final ConfigurationPath configurationPath;
+    private final JamesDirectoriesProvider directories;
+    private final UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation;
+
+    public JPAJamesConfiguration(ConfigurationPath configurationPath, JamesDirectoriesProvider directories, UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation) {
+        this.configurationPath = configurationPath;
+        this.directories = directories;
+        this.usersRepositoryImplementation = usersRepositoryImplementation;
+    }
+
+    @Override
+    public ConfigurationPath configurationPath() {
+        return configurationPath;
+    }
+
+    @Override
+    public JamesDirectoriesProvider directories() {
+        return directories;
+    }
+
+    public UsersRepositoryModuleChooser.Implementation getUsersRepositoryImplementation() {
+        return usersRepositoryImplementation;
+    }
+}
diff --git a/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java
index 7b33cb6..a840ee6 100644
--- a/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java
+++ b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java
@@ -19,9 +19,11 @@
 
 package org.apache.james;
 
+import org.apache.james.data.UsersRepositoryModuleChooser;
 import org.apache.james.modules.MailboxModule;
 import org.apache.james.modules.MailetProcessingModule;
 import org.apache.james.modules.data.JPADataModule;
+import org.apache.james.modules.data.JPAUsersRepositoryModule;
 import org.apache.james.modules.data.SieveJPARepositoryModules;
 import org.apache.james.modules.mailbox.DefaultEventModule;
 import org.apache.james.modules.mailbox.JPAMailboxModule;
@@ -51,7 +53,6 @@
 import org.apache.james.modules.server.WebAdminReIndexingTaskSerializationModule;
 import org.apache.james.modules.server.WebAdminServerModule;
 import org.apache.james.modules.spamassassin.SpamAssassinListenerModule;
-import org.apache.james.server.core.configuration.Configuration;
 
 import com.google.inject.Module;
 import com.google.inject.util.Modules;
@@ -99,7 +100,7 @@
         new MailetProcessingModule(), JPA_SERVER_MODULE, PROTOCOLS);
 
     public static void main(String[] args) throws Exception {
-        Configuration configuration = Configuration.builder()
+        JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
             .useWorkingDirectoryEnvProperty()
             .build();
 
@@ -110,9 +111,10 @@
         JamesServerMain.main(server);
     }
 
-    static GuiceJamesServer createServer(Configuration configuration) {
+    static GuiceJamesServer createServer(JPAJamesConfiguration configuration) {
         return GuiceJamesServer.forConfiguration(configuration)
-            .combineWith(JPA_MODULE_AGGREGATE);
+            .combineWith(JPA_MODULE_AGGREGATE)
+            .combineWith(new UsersRepositoryModuleChooser(new JPAUsersRepositoryModule())
+                .chooseModules(configuration.getUsersRepositoryImplementation()));
     }
-
 }
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java
index 9cd0cd6..db1284b 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.james;
 
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Durations.FIVE_HUNDRED_MILLISECONDS;
 import static org.awaitility.Durations.ONE_MINUTE;
@@ -41,7 +42,12 @@
 class JPAJamesServerTest implements JamesServerContract {
 
     @RegisterExtension
-    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+        JPAJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
         .server(configuration -> JPAJamesServerMain.createServer(configuration)
             .overrideWith(new TestJPAConfigurationModule()))
         .lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java
index d8359e4..95f14b4 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java
@@ -19,12 +19,19 @@
 
 package org.apache.james;
 
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 class JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest extends JPAJamesServerWithSqlValidationTest {
 
     @RegisterExtension
-    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+        JPAJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
         .server(configuration -> JPAJamesServerMain.createServer(configuration)
             .overrideWith(new TestJPAConfigurationModuleWithSqlValidation.WithDatabaseAuthentication()))
         .lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java
index 89f4dc0..2581b10 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java
@@ -19,12 +19,18 @@
 
 package org.apache.james;
 
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 class JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest extends JPAJamesServerWithSqlValidationTest {
-
     @RegisterExtension
-    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+        JPAJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
         .server(configuration -> JPAJamesServerMain.createServer(configuration)
             .overrideWith(new TestJPAConfigurationModuleWithSqlValidation.NoDatabaseAuthentication()))
         .lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAWithLDAPJamesServerTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAWithLDAPJamesServerTest.java
new file mode 100644
index 0000000..8d86124
--- /dev/null
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAWithLDAPJamesServerTest.java
@@ -0,0 +1,57 @@
+/****************************************************************
+ * 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;
+
+import static org.apache.james.MailsShouldBeWellReceived.JAMES_SERVER_HOST;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.LDAP;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+
+import org.apache.commons.net.imap.IMAPClient;
+import org.apache.james.data.LdapTestExtension;
+import org.apache.james.modules.protocols.ImapGuiceProbe;
+import org.apache.james.user.ldap.DockerLdapSingleton;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class JPAWithLDAPJamesServerTest {
+    @RegisterExtension
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+        JPAJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(LDAP)
+            .build())
+        .server(configuration -> JPAJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJPAConfigurationModule()))
+        .lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
+        .extension(new LdapTestExtension())
+        .build();
+
+
+    @Test
+    void userFromLdapShouldLoginViaImapProtocol(GuiceJamesServer server) throws IOException {
+        IMAPClient imapClient = new IMAPClient();
+        imapClient.connect(JAMES_SERVER_HOST, server.getProbe(ImapGuiceProbe.class).getImapPort());
+
+        assertThat(imapClient.login(DockerLdapSingleton.JAMES_USER.asString(), DockerLdapSingleton.PASSWORD)).isTrue();
+    }
+}
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java
index 3150e58..c08f88c 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james;
 
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -40,7 +41,12 @@
     }
 
     @RegisterExtension
-    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+        JPAJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
         .server(configuration -> JPAJamesServerMain.createServer(configuration)
             .overrideWith(new TestJPAConfigurationModule())
             .overrideWith(binder -> binder.bind(MailboxManager.class).toInstance(mailboxManager())))
diff --git a/server/apps/jpa-smtp-app/pom.xml b/server/apps/jpa-smtp-app/pom.xml
index ced7ff6..84c8cee 100644
--- a/server/apps/jpa-smtp-app/pom.xml
+++ b/server/apps/jpa-smtp-app/pom.xml
@@ -69,6 +69,13 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-ldap</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-common</artifactId>
         </dependency>
         <dependency>
@@ -79,6 +86,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-data-ldap</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-es-resporter</artifactId>
         </dependency>
         <dependency>
diff --git a/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesConfiguration.java b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
new file mode 100644
index 0000000..9614230
--- /dev/null
+++ b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
@@ -0,0 +1,127 @@
+/****************************************************************
+ * 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;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.james.data.UsersRepositoryModuleChooser;
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.filesystem.api.JamesDirectoriesProvider;
+import org.apache.james.server.core.JamesServerResourceLoader;
+import org.apache.james.server.core.MissingArgumentException;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+
+public class JPAJamesConfiguration implements Configuration {
+    public static class Builder {
+        private Optional<String> rootDirectory;
+        private Optional<ConfigurationPath> configurationPath;
+        private Optional<UsersRepositoryModuleChooser.Implementation> usersRepositoryImplementation;
+
+        private Builder() {
+            rootDirectory = Optional.empty();
+            configurationPath = Optional.empty();
+            usersRepositoryImplementation = Optional.empty();
+        }
+
+        public Builder workingDirectory(String path) {
+            rootDirectory = Optional.of(path);
+            return this;
+        }
+
+        public Builder workingDirectory(File file) {
+            rootDirectory = Optional.of(file.getAbsolutePath());
+            return this;
+        }
+
+        public Builder useWorkingDirectoryEnvProperty() {
+            rootDirectory = Optional.ofNullable(System.getProperty(WORKING_DIRECTORY));
+            if (!rootDirectory.isPresent()) {
+                throw new MissingArgumentException("Server needs a working.directory env entry");
+            }
+            return this;
+        }
+
+        public Builder configurationPath(ConfigurationPath path) {
+            configurationPath = Optional.of(path);
+            return this;
+        }
+
+        public Builder configurationFromClasspath() {
+            configurationPath = Optional.of(new ConfigurationPath(FileSystem.CLASSPATH_PROTOCOL));
+            return this;
+        }
+
+        public Builder usersRepository(UsersRepositoryModuleChooser.Implementation implementation) {
+            this.usersRepositoryImplementation = Optional.of(implementation);
+            return this;
+        }
+
+        public JPAJamesConfiguration build() {
+            ConfigurationPath configurationPath = this.configurationPath.orElse(new ConfigurationPath(FileSystem.FILE_PROTOCOL_AND_CONF));
+            JamesServerResourceLoader directories = new JamesServerResourceLoader(rootDirectory
+                .orElseThrow(() -> new MissingArgumentException("Server needs a working.directory env entry")));
+
+            FileSystemImpl fileSystem = new FileSystemImpl(directories);
+
+            FileConfigurationProvider configurationProvider = new FileConfigurationProvider(fileSystem, Basic.builder()
+                .configurationPath(configurationPath)
+                .workingDirectory(directories.getRootDirectory())
+                .build());
+            UsersRepositoryModuleChooser.Implementation usersRepositoryChoice = usersRepositoryImplementation.orElseGet(
+                () -> UsersRepositoryModuleChooser.Implementation.parse(configurationProvider));
+
+            return new JPAJamesConfiguration(
+                configurationPath,
+                directories,
+                usersRepositoryChoice);
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private final ConfigurationPath configurationPath;
+    private final JamesDirectoriesProvider directories;
+    private final UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation;
+
+    public JPAJamesConfiguration(ConfigurationPath configurationPath, JamesDirectoriesProvider directories, UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation) {
+        this.configurationPath = configurationPath;
+        this.directories = directories;
+        this.usersRepositoryImplementation = usersRepositoryImplementation;
+    }
+
+    @Override
+    public ConfigurationPath configurationPath() {
+        return configurationPath;
+    }
+
+    @Override
+    public JamesDirectoriesProvider directories() {
+        return directories;
+    }
+
+    public UsersRepositoryModuleChooser.Implementation getUsersRepositoryImplementation() {
+        return usersRepositoryImplementation;
+    }
+}
diff --git a/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java
index bc6d1fd..a7f684c 100644
--- a/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java
+++ b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java
@@ -19,9 +19,11 @@
 
 package org.apache.james;
 
+import org.apache.james.data.UsersRepositoryModuleChooser;
 import org.apache.james.modules.MailetProcessingModule;
 import org.apache.james.modules.data.JPADataModule;
 import org.apache.james.modules.data.JPAEntityManagerModule;
+import org.apache.james.modules.data.JPAUsersRepositoryModule;
 import org.apache.james.modules.protocols.ProtocolHandlerModule;
 import org.apache.james.modules.protocols.SMTPServerModule;
 import org.apache.james.modules.queue.activemq.ActiveMQQueueModule;
@@ -35,7 +37,6 @@
 import org.apache.james.modules.server.RawPostDequeueDecoratorModule;
 import org.apache.james.modules.server.TaskManagerModule;
 import org.apache.james.modules.server.WebAdminServerModule;
-import org.apache.james.server.core.configuration.Configuration;
 
 import com.google.inject.Module;
 import com.google.inject.util.Modules;
@@ -62,7 +63,7 @@
         new ElasticSearchMetricReporterModule());
 
     public static void main(String[] args) throws Exception {
-        Configuration configuration = Configuration.builder()
+        JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
             .useWorkingDirectoryEnvProperty()
             .build();
 
@@ -72,9 +73,11 @@
         JamesServerMain.main(server);
     }
 
-    public static GuiceJamesServer createServer(Configuration configuration) {
+    public static GuiceJamesServer createServer(JPAJamesConfiguration configuration) {
         return GuiceJamesServer.forConfiguration(configuration)
-            .combineWith(JPA_SERVER_MODULE,  PROTOCOLS, new DKIMMailetModule());
+            .combineWith(JPA_SERVER_MODULE,  PROTOCOLS, new DKIMMailetModule())
+            .combineWith(new UsersRepositoryModuleChooser(new JPAUsersRepositoryModule())
+                .chooseModules(configuration.getUsersRepositoryImplementation()));
     }
 
 }
diff --git a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
index 542022a..cdbeaae 100644
--- a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
+++ b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.james;
 
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.IOException;
@@ -34,7 +35,6 @@
 import org.apache.james.mailrepository.jpa.JPAUrl;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
 import org.apache.james.rrt.jpa.model.JPARecipientRewrite;
-import org.apache.james.server.core.configuration.Configuration;
 import org.apache.james.user.jpa.model.JPAUser;
 import org.junit.After;
 import org.junit.Before;
@@ -56,9 +56,10 @@
     }
 
     private org.apache.james.GuiceJamesServer createJamesServer() throws IOException {
-        Configuration configuration = Configuration.builder()
+        JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
             .workingDirectory(temporaryFolder.newFolder())
             .configurationFromClasspath()
+            .usersRepository(DEFAULT)
             .build();
 
         return JPAJamesServerMain.createServer(configuration)
diff --git a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java
index be9a34b..50df95b 100644
--- a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java
+++ b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.mariadb;
 
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.IOException;
@@ -28,9 +29,9 @@
 import java.nio.charset.StandardCharsets;
 
 import org.apache.james.GuiceJamesServer;
+import org.apache.james.JPAJamesConfiguration;
 import org.apache.james.JPAJamesServerMain;
 import org.apache.james.modules.protocols.SmtpGuiceProbe;
-import org.apache.james.server.core.configuration.Configuration;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -62,8 +63,9 @@
     }
 
     private org.apache.james.GuiceJamesServer createJamesServer(String mariaDBUrl) throws IOException {
-        Configuration configuration = Configuration.builder()
+        JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
             .workingDirectory(temporaryFolder.newFolder())
+            .usersRepository(DEFAULT)
             .configurationFromClasspath()
             .build();
 
diff --git a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
index 8f45e0f..aa4a6b6 100644
--- a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
+++ b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
@@ -131,4 +131,9 @@
     public Publisher<Void> deleteBucket(BucketName bucketName) {
         return underlying.deleteBucket(bucketName);
     }
+
+    @Override
+    public Publisher<BucketName> listBuckets() {
+        return underlying.listBuckets();
+    }
 }
diff --git a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
index 8be3728..ad58cd0 100644
--- a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
+++ b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
@@ -30,6 +30,7 @@
 import org.apache.james.blob.api.BlobStoreDAOContract;
 import org.apache.james.blob.memory.MemoryBlobStoreDAO;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import com.google.common.io.ByteSource;
@@ -83,4 +84,10 @@
 
         assertThat(bytes).isNotEqualTo(SHORT_BYTEARRAY);
     }
+
+    @Override
+    @Disabled("Not supported by the Memory blob store")
+    public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+    }
 }
\ No newline at end of file
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
index 3f7cd67..cf315d2 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
@@ -58,6 +58,8 @@
 
     BucketName getDefaultBucketName();
 
+    Publisher<BucketName> listBuckets();
+
     Publisher<Void> deleteBucket(BucketName bucketName);
 
     Publisher<Boolean> delete(BucketName bucketName, BlobId blobId);
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
index f0e2844..5288284 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
@@ -99,4 +99,6 @@
      *  otherwise an IOObjectStoreException in its error channel
      */
     Publisher<Void> deleteBucket(BucketName bucketName);
+
+    Publisher<BucketName> listBuckets();
 }
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
index 8c57fec..4b104ac 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
@@ -102,4 +102,9 @@
         return metricFactory.decoratePublisherWithTimerMetric(DELETE_TIMER_NAME, blobStoreImpl.delete(bucketName, blobId));
     }
 
+
+    @Override
+    public Publisher<BucketName> listBuckets() {
+        return blobStoreImpl.listBuckets();
+    }
 }
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
index 75b0706..e616ee2 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
@@ -31,6 +31,7 @@
 import org.apache.james.util.concurrency.ConcurrentTestRunner;
 import org.junit.jupiter.api.Test;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public interface BucketBlobStoreContract {
@@ -168,4 +169,33 @@
             .operationCount(10)
             .runSuccessfullyWithin(Duration.ofMinutes(1));
     }
+
+    @Test
+    default void listBucketsShouldReturnDefaultBucket() {
+        BlobStore store = testee();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .containsOnly(store.getDefaultBucketName());
+    }
+
+    @Test
+    default void listBucketsShouldReturnACustomBucket() {
+        BlobStore store = testee();
+
+        Mono.from(store.save(CUSTOM, SHORT_BYTEARRAY, LOW_COST)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .containsOnly(store.getDefaultBucketName(), CUSTOM);
+    }
+
+    @Test
+    default void listBucketsShouldNotReturnADeletedBucket() {
+        BlobStore store = testee();
+
+        Mono.from(store.save(CUSTOM, SHORT_BYTEARRAY, LOW_COST)).block();
+        Mono.from(store.deleteBucket(CUSTOM)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .containsOnly(store.getDefaultBucketName());
+    }
 }
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
index acb4fe8..eb71a37 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
@@ -35,6 +35,7 @@
 import org.apache.james.util.concurrency.ConcurrentTestRunner;
 import org.junit.jupiter.api.Test;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public interface BucketBlobStoreDAOContract {
@@ -172,4 +173,68 @@
             .operationCount(10)
             .runSuccessfullyWithin(Duration.ofMinutes(1));
     }
+
+    @Test
+    default void listBucketsShouldReturnEmptyWhenNone() {
+        BlobStoreDAO store = testee();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    default void listBucketsShouldReturnBucketInUse() {
+        BlobStoreDAO store = testee();
+
+        Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .containsOnly(TEST_BUCKET_NAME);
+    }
+
+    @Test
+    default void listBucketsShouldNotReturnDuplicates() {
+        BlobStoreDAO store = testee();
+
+        Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+        Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .hasSize(1);
+    }
+
+    @Test
+    default void listBucketsShouldReturnAllBucketsInUse() {
+        BlobStoreDAO store = testee();
+
+        Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+        Mono.from(store.save(CUSTOM_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .containsOnly(TEST_BUCKET_NAME, CUSTOM_BUCKET_NAME);
+    }
+
+    @Test
+    default void listBucketsShouldNotReturnDeletedBuckets() {
+        BlobStoreDAO store = testee();
+
+        Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+        Mono.from(store.deleteBucket(TEST_BUCKET_NAME)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .isEmpty();
+    }
+
+    @Test
+    default void listBucketsShouldReturnBucketsWithNoBlob() {
+        BlobStoreDAO store = testee();
+
+        Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+        Mono.from(store.delete(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+        assertThat(Flux.from(store.listBuckets()).collectList().block())
+            .containsOnly(TEST_BUCKET_NAME);
+    }
 }
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
index ffcc179..b27ec29 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
@@ -42,6 +42,7 @@
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.util.DataChunker;
 import org.apache.james.util.ReactorUtils;
+import org.reactivestreams.Publisher;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -69,7 +70,6 @@
     private final CassandraConfiguration configuration;
     private final BucketName defaultBucket;
 
-    private final MetricFactory metricFactory;
     private final Metric metricClOneHitCount;
     private final Metric metricClOneMissCount;
 
@@ -84,7 +84,6 @@
         this.bucketDAO = bucketDAO;
         this.configuration = cassandraConfiguration;
         this.defaultBucket = defaultBucket;
-        this.metricFactory = metricFactory;
 
         this.metricClOneMissCount = metricFactory.generate(CASSANDRA_BLOBSTORE_CL_ONE_MISS_COUNT_METRIC_NAME);
         this.metricClOneHitCount = metricFactory.generate(CASSANDRA_BLOBSTORE_CL_ONE_HIT_COUNT_METRIC_NAME);
@@ -269,4 +268,11 @@
             .reduce(ByteBuffer.allocate(targetSize), ByteBuffer::put)
             .array();
     }
+
+    @Override
+    public Publisher<BucketName> listBuckets() {
+        return bucketDAO.listAll()
+            .map(Pair::getLeft)
+            .distinct();
+    }
 }
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
index 9b6d0b9..fb74113 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
@@ -322,4 +322,9 @@
         return Mono.from(metricFactory.decoratePublisherWithTimerMetric(BLOBSTORE_BACKEND_LATENCY_METRIC_NAME,
             backend.readBytes(bucketName, blobId)));
     }
+
+    @Override
+    public Publisher<BucketName> listBuckets() {
+        return backend.listBuckets();
+    }
 }
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
index 6b2ad1d..159d5a1 100644
--- a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
@@ -28,11 +28,11 @@
 import org.apache.james.blob.api.HashBlobId;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 public class CassandraBlobStoreDAOTest implements BlobStoreDAOContract {
     private static final int CHUNK_SIZE = 10240;
-    private static final int MULTIPLE_CHUNK_SIZE = 3;
 
     @RegisterExtension
     static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraBlobModule.MODULE);
@@ -60,4 +60,9 @@
         return testee;
     }
 
+    @Override
+    @Disabled("Not supported by the Cassandra blob store")
+    public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+    }
 }
\ No newline at end of file
diff --git a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
index f66cc1f..95b256f 100644
--- a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
+++ b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
@@ -29,12 +29,14 @@
 import org.apache.james.blob.api.BucketName;
 import org.apache.james.blob.api.ObjectNotFoundException;
 import org.apache.james.blob.api.ObjectStoreIOException;
+import org.reactivestreams.Publisher;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.Table;
 import com.google.common.io.ByteSource;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public class MemoryBlobStoreDAO implements BlobStoreDAO {
@@ -110,4 +112,9 @@
             }
         });
     }
+
+    @Override
+    public Publisher<BucketName> listBuckets() {
+        return Flux.fromIterable(blobs.rowKeySet());
+    }
 }
diff --git a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
index 3225301..83748a7 100644
--- a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
+++ b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
@@ -22,6 +22,7 @@
 import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.blob.api.BlobStoreDAOContract;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 
 class MemoryBlobStoreDAOTest implements BlobStoreDAOContract {
 
@@ -36,4 +37,10 @@
     public BlobStoreDAO testee() {
         return blobStore;
     }
+
+    @Override
+    @Disabled("Not supported")
+    public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+    }
 }
diff --git a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
index a0fcb71..7b13ad3 100644
--- a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
+++ b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
@@ -95,6 +95,19 @@
             .orElse(bucketName);
     }
 
+    Optional<BucketName> unresolve(BucketName bucketName) {
+        if (isNameSpace(bucketName)) {
+            return Optional.of(bucketName);
+        }
+
+        return prefix.map(p -> {
+            if (bucketName.asString().startsWith(p)) {
+                return Optional.of(BucketName.of(bucketName.asString().substring(p.length())));
+            }
+            return Optional.<BucketName>empty();
+        }).orElse(Optional.of(bucketName));
+    }
+
     private boolean isNameSpace(BucketName bucketName) {
         return namespace
             .map(existingNamespace -> existingNamespace.equals(bucketName))
diff --git a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
index 5849bf9..24a35a4 100644
--- a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
+++ b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
@@ -41,6 +41,7 @@
 import org.apache.james.lifecycle.api.Startable;
 import org.apache.james.util.DataChunker;
 import org.apache.james.util.ReactorUtils;
+import org.reactivestreams.Publisher;
 
 import com.github.fge.lambdas.Throwing;
 import com.github.steveash.guavate.Guavate;
@@ -63,11 +64,11 @@
 import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
 import software.amazon.awssdk.services.s3.S3AsyncClient;
 import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.Bucket;
 import software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException;
 import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse;
 import software.amazon.awssdk.services.s3.model.GetObjectResponse;
 import software.amazon.awssdk.services.s3.model.ListBucketsResponse;
-import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
 import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
 import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
 import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
@@ -279,10 +280,13 @@
 
     private Mono<BucketName> emptyBucket(BucketName bucketName) {
         return Mono.fromFuture(() -> client.listObjects(builder -> builder.bucket(bucketName.asString())))
-            .flatMapIterable(ListObjectsResponse::contents)
-            .window(EMPTY_BUCKET_BATCH_SIZE)
-            .flatMap(this::buildListForBatch, DEFAULT_CONCURRENCY)
-            .flatMap(identifiers -> deleteObjects(bucketName, identifiers), DEFAULT_CONCURRENCY)
+            .flatMap(response -> Flux.fromIterable(response.contents())
+                .window(EMPTY_BUCKET_BATCH_SIZE)
+                .flatMap(this::buildListForBatch, DEFAULT_CONCURRENCY)
+                .flatMap(identifiers -> deleteObjects(bucketName, identifiers), DEFAULT_CONCURRENCY)
+                .then(Mono.just(response)))
+            .flux()
+            .takeUntil(list -> !list.isTruncated())
             .then(Mono.just(bucketName));
     }
 
@@ -305,4 +309,13 @@
                 .flatMap(bucket -> deleteResolvedBucket(BucketName.of(bucket.name())), DEFAULT_CONCURRENCY)
             .then();
     }
+
+    @Override
+    public Publisher<BucketName> listBuckets() {
+        return Mono.fromFuture(client::listBuckets)
+            .flatMapIterable(ListBucketsResponse::buckets)
+            .map(Bucket::name)
+            .handle((bucket, sink) -> bucketNameResolver.unresolve(BucketName.of(bucket))
+                .ifPresent(sink::next));
+    }
 }
diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
index 43e9e84..178830d 100644
--- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
+++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
@@ -25,10 +25,25 @@
 import org.apache.james.blob.api.BucketName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 class BucketNameResolverTest {
     @Nested
     class EmptyPrefix {
+        @ParameterizedTest
+        @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+        void withShouldAddNewValuesInSet(String bucketNameString) {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .noPrefix()
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            BucketName bucketName = BucketName.of(bucketNameString);
+            assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+                .contains(bucketName);
+        }
+
         @Test
         void resolveShouldReturnPassedValue() {
             BucketNameResolver resolver = BucketNameResolver.builder()
@@ -50,10 +65,45 @@
             assertThat(resolver.resolve(BucketName.of("namespace")))
                 .isEqualTo(BucketName.of("namespace"));
         }
+
+        @Test
+        void unresolveShouldReturnPassedValue() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .noPrefix()
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("bucketName")))
+                .contains(BucketName.of("bucketName"));
+        }
+
+        @Test
+        void unresolveShouldReturnValueWhenNamespace() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .noPrefix()
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("namespace")))
+                .contains(BucketName.of("namespace"));
+        }
     }
 
     @Nested
     class EmptyNamespace {
+        @ParameterizedTest
+        @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+        void withShouldAddNewValuesInSet(String bucketNameString) {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .noNamespace()
+                .build();
+
+            BucketName bucketName = BucketName.of(bucketNameString);
+            assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+                .contains(bucketName);
+        }
+
         @Test
         void resolveShouldReturnPassedValueWithPrefix() {
             BucketNameResolver resolver = BucketNameResolver.builder()
@@ -64,10 +114,45 @@
             assertThat(resolver.resolve(BucketName.of("bucketName")))
                 .isEqualTo(BucketName.of("bucketPrefix-bucketName"));
         }
+
+        @Test
+        void unresolveShouldReturnPassedValueWithPrefix() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .noNamespace()
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("bucketPrefix-bucketName")))
+                .contains(BucketName.of("bucketName"));
+        }
+
+        @Test
+        void unresolveShouldFilterValuesWithoutPrefix() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .noNamespace()
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("bucketName")))
+                .isEmpty();
+        }
     }
 
     @Nested
     class BothAreEmpty {
+        @ParameterizedTest
+        @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+        void withShouldAddNewValuesInSet(String bucketNameString) {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .noPrefix()
+                .noNamespace()
+                .build();
+
+            BucketName bucketName = BucketName.of(bucketNameString);
+            assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+                .contains(bucketName);
+        }
+
         @Test
         void resolveShouldReturnPassedValue() {
             BucketNameResolver resolver = BucketNameResolver.builder()
@@ -78,10 +163,35 @@
             assertThat(resolver.resolve(BucketName.of("bucketName")))
                 .isEqualTo(BucketName.of("bucketName"));
         }
+
+        @Test
+        void unresolveShouldReturnPassedValue() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .noPrefix()
+                .noNamespace()
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("bucketName")))
+                .contains(BucketName.of("bucketName"));
+        }
     }
 
     @Nested
     class BothArePresent {
+
+        @ParameterizedTest
+        @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+        void withShouldAddNewValuesInSet(String bucketNameString) {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            BucketName bucketName = BucketName.of(bucketNameString);
+            assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+                .contains(bucketName);
+        }
+
         @Test
         void resolveShouldReturnPassedValueWithPrefix() {
             BucketNameResolver resolver = BucketNameResolver.builder()
@@ -103,6 +213,39 @@
             assertThat(resolver.resolve(BucketName.of("namespace")))
                 .isEqualTo(BucketName.of("namespace"));
         }
+
+        @Test
+        void unresolveShouldFilterValuesWithoutPrefix() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("bucketName")))
+                .isEmpty();
+        }
+
+        @Test
+        void unresolveShouldRemovePrefix() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("bucketPrefix-bucketName")))
+                .contains(BucketName.of("bucketName"));
+        }
+
+        @Test
+        void unresolveShouldReturnNamespaceWhenPassingNamespace() {
+            BucketNameResolver resolver = BucketNameResolver.builder()
+                .prefix("bucketPrefix-")
+                .namespace(BucketName.of("namespace"))
+                .build();
+
+            assertThat(resolver.unresolve(BucketName.of("namespace")))
+                .contains(BucketName.of("namespace"));
+        }
     }
 
 
diff --git a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
index 618beb4..15148c2 100644
--- a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
+++ b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
@@ -29,7 +29,7 @@
 import org.apache.commons.io.IOUtils
 import org.apache.james.blob.api.{BlobId, BlobStore, BlobStoreDAO, BucketName}
 import org.reactivestreams.Publisher
-import reactor.core.publisher.Mono
+import reactor.core.publisher.{Flux, Mono}
 import reactor.core.scala.publisher.SMono
 import reactor.util.function.{Tuple2, Tuples}
 
@@ -112,4 +112,6 @@
 
     SMono.just(Boolean.box(false))
   }
+
+  override def listBuckets(): Publisher[BucketName] = Flux.concat(blobStoreDAO.listBuckets(), Flux.just(defaultBucketName)).distinct()
 }
diff --git a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
index 6cb73e4..faf4a5d 100644
--- a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
+++ b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
@@ -26,6 +26,7 @@
 import javax.inject.{Inject, Named}
 import org.apache.james.blob.api.{BlobId, BlobStore, BlobStoreDAO, BucketName}
 import org.reactivestreams.Publisher
+import reactor.core.publisher.Flux
 import reactor.core.scala.publisher.SMono
 
 
@@ -86,4 +87,6 @@
     SMono.fromPublisher(blobStoreDAO.delete(bucketName, blobId))
       .`then`(SMono.just(Boolean.box(true)))
   }
+
+  override def listBuckets(): Publisher[BucketName] = Flux.concat(blobStoreDAO.listBuckets(), Flux.just(defaultBucketName)).distinct()
 }
diff --git a/server/container/guice/data-ldap/pom.xml b/server/container/guice/data-ldap/pom.xml
index 5bc6f60..752804c 100644
--- a/server/container/guice/data-ldap/pom.xml
+++ b/server/container/guice/data-ldap/pom.xml
@@ -43,10 +43,23 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-ldap</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-common</artifactId>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-common</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-testing</artifactId>
             <scope>test</scope>
         </dependency>
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/DockerLdapRule.java b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/DockerLdapRule.java
similarity index 96%
rename from server/apps/cassandra-app/src/test/java/org/apache/james/DockerLdapRule.java
rename to server/container/guice/data-ldap/src/test/java/org/apache/james/data/DockerLdapRule.java
index 4ce36af..2cdada9 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/DockerLdapRule.java
+++ b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/DockerLdapRule.java
@@ -17,9 +17,10 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james;
+package org.apache.james.data;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.james.GuiceModuleTestRule;
 import org.apache.james.user.ldap.DockerLdapSingleton;
 import org.apache.james.user.ldap.LdapRepositoryConfiguration;
 import org.junit.runner.Description;
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/LdapTestExtension.java
similarity index 91%
rename from server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
rename to server/container/guice/data-ldap/src/test/java/org/apache/james/data/LdapTestExtension.java
index a468116..d64312f 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
+++ b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/LdapTestExtension.java
@@ -17,8 +17,9 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james;
+package org.apache.james.data;
 
+import org.apache.james.GuiceModuleTestExtension;
 import org.junit.jupiter.api.extension.ExtensionContext;
 
 import com.google.inject.Module;
@@ -27,11 +28,11 @@
 
     private DockerLdapRule ldapRule;
 
-    LdapTestExtension() {
+    public LdapTestExtension() {
         this(new DockerLdapRule());
     }
 
-    LdapTestExtension(DockerLdapRule ldapRule) {
+    public LdapTestExtension(DockerLdapRule ldapRule) {
         this.ldapRule = ldapRule;
     }
 
diff --git a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java
index ee79def..4082070 100644
--- a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java
+++ b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java
@@ -21,10 +21,8 @@
 import com.google.inject.AbstractModule;
 
 public class JPADataModule extends AbstractModule {
-
     @Override
     protected void configure() {
-        install(new JPAUsersRepositoryModule());
         install(new JPADomainListModule());
         install(new JPARecipientRewriteTableModule());
         install(new JPAMailRepositoryModule());
diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
index 10755c1..56e31fd 100644
--- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
+++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
@@ -173,6 +173,13 @@
         return messageManager.appendMessage(appendCommand, mailboxSession).getId();
     }
 
+    public MessageManager.AppendResult appendMessageAndGetAppendResult(String username, MailboxPath mailboxPath, MessageManager.AppendCommand appendCommand)
+        throws MailboxException {
+        MailboxSession mailboxSession = mailboxManager.createSystemSession(Username.of(username));
+        MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, mailboxSession);
+        return messageManager.appendMessage(appendCommand, mailboxSession);
+    }
+
     @Override
     public Collection<String> listSubscriptions(String user) throws Exception {
         MailboxSession mailboxSession = mailboxManager.createSystemSession(Username.of(user));
diff --git a/server/container/guice/pom.xml b/server/container/guice/pom.xml
index ea45f6f..dea05b8 100644
--- a/server/container/guice/pom.xml
+++ b/server/container/guice/pom.xml
@@ -104,6 +104,12 @@
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
+                <artifactId>james-server-guice-data-ldap</artifactId>
+                <version>${project.version}</version>
+                <type>test-jar</type>
+            </dependency>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
                 <artifactId>james-server-guice-distributed</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
index 1cb9130..3c29347 100644
--- a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
+++ b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
@@ -19,9 +19,13 @@
 
 package org.apache.james.user.cassandra;
 
+import java.util.Optional;
+
 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.lib.UsersRepositoryContract;
 import org.apache.james.user.lib.UsersRepositoryImpl;
 import org.apache.james.user.lib.model.Algorithm;
@@ -39,17 +43,24 @@
         @RegisterExtension
         UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
 
-        private UsersRepositoryImpl usersRepository;
+        private UsersRepositoryImpl<CassandraUsersDAO> usersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) throws Exception {
-            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+            this.testSystem = testSystem;
         }
 
         @Override
-        public UsersRepositoryImpl testee() {
+        public UsersRepositoryImpl<CassandraUsersDAO> testee() {
             return usersRepository;
         }
+
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+        }
     }
 
     @Nested
@@ -57,25 +68,33 @@
         @RegisterExtension
         UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
 
-        private UsersRepositoryImpl usersRepository;
+        private UsersRepositoryImpl<CassandraUsersDAO> usersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) throws Exception {
-            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+            this.testSystem = testSystem;
         }
 
         @Override
-        public UsersRepositoryImpl testee() {
+        public UsersRepositoryImpl<CassandraUsersDAO> testee() {
             return usersRepository;
         }
+
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+        }
     }
 
-    private static UsersRepositoryImpl getUsersRepository(DomainList domainList, boolean enableVirtualHosting) throws Exception {
+    private static UsersRepositoryImpl<CassandraUsersDAO> getUsersRepository(DomainList domainList, boolean enableVirtualHosting, Optional<Username> administrator) throws Exception {
         CassandraUsersDAO usersDAO = new CassandraUsersDAO(new Algorithm.DefaultFactory(), cassandraCluster.getCassandraCluster().getConf());
         BaseHierarchicalConfiguration configuration = new BaseHierarchicalConfiguration();
         configuration.addProperty("enableVirtualHosting", String.valueOf(enableVirtualHosting));
+        administrator.ifPresent(username -> configuration.addProperty("administratorId", username.asString()));
 
-        UsersRepositoryImpl usersRepository = new UsersRepositoryImpl(domainList, usersDAO);
+        UsersRepositoryImpl<CassandraUsersDAO> usersRepository = new UsersRepositoryImpl<>(domainList, usersDAO);
         usersRepository.configure(configuration);
         return usersRepository;
     }
diff --git a/server/data/data-jmap/pom.xml b/server/data/data-jmap/pom.xml
index 8456e56..c5972d8 100644
--- a/server/data/data-jmap/pom.xml
+++ b/server/data/data-jmap/pom.xml
@@ -45,6 +45,20 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>blob-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>blob-memory</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>blob-storage-strategy</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>event-sourcing-core</artifactId>
         </dependency>
         <dependency>
@@ -85,6 +99,11 @@
             <artifactId>guava</artifactId>
         </dependency>
         <dependency>
+            <groupId>eu.timepit</groupId>
+            <artifactId>refined_${scala.base}</artifactId>
+            <version>0.9.20</version>
+        </dependency>
+        <dependency>
             <groupId>io.projectreactor</groupId>
             <artifactId>reactor-core</artifactId>
         </dependency>
@@ -112,4 +131,13 @@
         </dependency>
     </dependencies>
 
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>net.alchim31.maven</groupId>
+                <artifactId>scala-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
 </project>
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Size.scala b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Size.scala
new file mode 100644
index 0000000..f4ed8d3
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Size.scala
@@ -0,0 +1,41 @@
+/****************************************************************
+ * 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.model
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.auto._
+import eu.timepit.refined.numeric.NonNegative
+import eu.timepit.refined.refineV
+import org.slf4j.{Logger, LoggerFactory}
+
+object Size {
+  private val logger: Logger = LoggerFactory.getLogger(classOf[Size])
+  type Size = Long Refined NonNegative
+  val Zero: Size = 0L
+
+  def sanitizeSize(value: Long): Size = {
+    val size: Either[String, Size] = refineV[NonNegative](value)
+    size.fold(e => {
+      logger.error(s"Encountered an invalid size: $e")
+      Zero
+    },
+      refinedValue => refinedValue)
+  }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Upload.scala b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Upload.scala
new file mode 100644
index 0000000..73e8578
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Upload.scala
@@ -0,0 +1,56 @@
+/****************************************************************
+ * 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.model
+
+import org.apache.james.blob.api.BlobId
+import org.apache.james.jmap.api.model.Size.Size
+import org.apache.james.mailbox.model.ContentType
+
+import java.io.InputStream
+
+object Upload {
+
+  def from(metaData: UploadMetaData, content: InputStream): Upload =
+    Upload(uploadId = metaData.uploadId,
+      size = metaData.size,
+      contentType = metaData.contentType,
+      content = content)
+}
+
+case class Upload(uploadId: UploadId,
+                  size: Size,
+                  contentType: ContentType,
+                  content: InputStream)
+
+case class UploadNotFoundException(uploadId: UploadId) extends RuntimeException(s"Upload not found $uploadId")
+
+object UploadMetaData {
+  def from(uploadId: UploadId, contentType: ContentType, size: Long, blobId: BlobId): UploadMetaData =
+    UploadMetaData(uploadId = uploadId,
+      contentType = contentType,
+      size = Size.sanitizeSize(size),
+      blobId = blobId)
+}
+
+case class UploadMetaData(uploadId: UploadId,
+                          contentType: ContentType,
+                          size: Size,
+                          blobId: BlobId)
+
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/UploadId.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/UploadId.java
new file mode 100644
index 0000000..7d58646
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/UploadId.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.model;
+
+import org.apache.commons.text.RandomStringGenerator;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+public class UploadId {
+    public static final RandomStringGenerator RANDOM_STRING_GENERATOR = new RandomStringGenerator.Builder()
+        .withinRange('a', 'z')
+        .build();
+
+    public static UploadId random() {
+        return new UploadId(RANDOM_STRING_GENERATOR.generate(20));
+    }
+
+    public static UploadId from(String id) {
+        Preconditions.checkNotNull(id);
+        Preconditions.checkArgument(!id.isEmpty());
+        return new UploadId(id);
+    }
+
+    private final String id;
+
+    public UploadId(String id) {
+        this.id = id;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof UploadId) {
+            UploadId other = (UploadId) obj;
+            return Objects.equal(id, other.id);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(id);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects
+            .toStringHelper(this)
+            .add("id", id)
+            .toString();
+    }
+
+}
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadRepository.java
similarity index 60%
copy from server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
copy to server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadRepository.java
index a468116..b104037 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadRepository.java
@@ -17,40 +17,19 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james;
+package org.apache.james.jmap.api.upload;
 
-import org.junit.jupiter.api.extension.ExtensionContext;
+import java.io.InputStream;
 
-import com.google.inject.Module;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.Upload;
+import org.apache.james.jmap.api.model.UploadId;
+import org.apache.james.mailbox.model.ContentType;
+import org.reactivestreams.Publisher;
 
-public class LdapTestExtension implements GuiceModuleTestExtension {
+public interface UploadRepository {
+    Publisher<UploadId> upload(InputStream data, ContentType contentType, Username user);
 
-    private DockerLdapRule ldapRule;
-
-    LdapTestExtension() {
-        this(new DockerLdapRule());
-    }
-
-    LdapTestExtension(DockerLdapRule ldapRule) {
-        this.ldapRule = ldapRule;
-    }
-
-    @Override
-    public void beforeAll(ExtensionContext extensionContext) {
-        ldapRule.start();
-    }
-
-    @Override
-    public void afterAll(ExtensionContext extensionContext) {
-        ldapRule.stop();
-    }
-
-    @Override
-    public Module getModule() {
-        return ldapRule.getModule();
-    }
-
-    public DockerLdapRule getLdapRule() {
-        return ldapRule;
-    }
+    Publisher<Upload> retrieve(UploadId id, Username user);
 }
+
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepository.java
new file mode 100644
index 0000000..256409a
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepository.java
@@ -0,0 +1,99 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.memory.upload;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.Upload;
+import org.apache.james.jmap.api.model.UploadId;
+import org.apache.james.jmap.api.model.UploadMetaData;
+import org.apache.james.jmap.api.model.UploadNotFoundException;
+import org.apache.james.jmap.api.upload.UploadRepository;
+import org.apache.james.mailbox.model.ContentType;
+import org.reactivestreams.Publisher;
+
+import com.google.common.base.Preconditions;
+
+import reactor.core.publisher.Mono;
+
+public class InMemoryUploadRepository implements UploadRepository {
+
+    private static final Map<UploadId, ImmutablePair<Username, UploadMetaData>> uploadStore = new HashMap<>();
+
+    private final BlobStore blobStore;
+    private final BucketName bucketName;
+
+    @Inject
+    public InMemoryUploadRepository(BlobStore blobStore) {
+        this.blobStore = blobStore;
+        this.bucketName = blobStore.getDefaultBucketName();
+    }
+
+    @Override
+    public Publisher<UploadId> upload(InputStream data, ContentType contentType, Username user) {
+        Preconditions.checkNotNull(data);
+        Preconditions.checkNotNull(contentType);
+        Preconditions.checkNotNull(user);
+
+        byte[] dataAsByte = toByteArray(data);
+        return Mono.from(blobStore.save(bucketName, dataAsByte, BlobStore.StoragePolicy.LOW_COST))
+            .map(blobId -> {
+                UploadId uploadId = UploadId.random();
+                uploadStore.put(uploadId, new ImmutablePair<>(user, UploadMetaData.from(uploadId, contentType, dataAsByte.length, blobId)));
+                return uploadId;
+            });
+    }
+
+    @Override
+    public Publisher<Upload> retrieve(UploadId id, Username user) {
+        Preconditions.checkNotNull(id);
+        Preconditions.checkNotNull(user);
+
+        return Mono.justOrEmpty(uploadStore.get(id))
+            .filter(pair -> user.equals(pair.left))
+            .flatMap(userAndMetaData -> retrieveUpload(userAndMetaData.right))
+            .switchIfEmpty(Mono.error(() -> new UploadNotFoundException(id)));
+    }
+
+    private Mono<Upload> retrieveUpload(UploadMetaData uploadMetaData) {
+        return Mono.from(blobStore.readBytes(bucketName, uploadMetaData.blobId()))
+            .map(content -> Upload.from(uploadMetaData, new ByteArrayInputStream(content)));
+    }
+
+    private byte[] toByteArray(InputStream inputStream) {
+        try {
+            return IOUtils.toByteArray(inputStream);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+}
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadRepositoryContract.scala b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadRepositoryContract.scala
new file mode 100644
index 0000000..2bb529b
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadRepositoryContract.scala
@@ -0,0 +1,101 @@
+ /***************************************************************
+ * 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.upload
+
+ import org.apache.commons.io.IOUtils
+ import org.apache.james.core.Username
+ import org.apache.james.jmap.api.model.Size.sanitizeSize
+ import org.apache.james.jmap.api.model.{Upload, UploadId, UploadNotFoundException}
+ import org.apache.james.jmap.api.upload.UploadRepositoryContract.{CONTENT_TYPE, DATA_STRING, USER}
+ import org.apache.james.mailbox.model.ContentType
+ import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}
+ import org.junit.jupiter.api.Test
+ import reactor.core.scala.publisher.SMono
+
+ import java.io.InputStream
+ import java.nio.charset.StandardCharsets
+
+ object UploadRepositoryContract {
+   private lazy val CONTENT_TYPE: ContentType = ContentType
+     .of("text/html")
+   private lazy val DATA_STRING: String = "123321"
+   private lazy val USER: Username = Username.of("Bob")
+ }
+
+ trait UploadRepositoryContract {
+
+   def testee: UploadRepository
+
+   def data(): InputStream = IOUtils.toInputStream(DATA_STRING, StandardCharsets.UTF_8)
+
+   @Test
+   def uploadShouldSuccess(): Unit = {
+     val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+     assertThat(SMono.fromPublisher(testee.retrieve(uploadId, USER)).block())
+       .isNotNull
+   }
+
+   @Test
+   def uploadShouldReturnDifferentIdWhenDifferentData(): Unit = {
+     val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+     assertThat(uploadId)
+       .isNotEqualTo(SMono.fromPublisher(testee.upload(IOUtils.toInputStream("abcxyz", StandardCharsets.UTF_8), CONTENT_TYPE, USER)).block())
+   }
+
+   @Test
+   def uploadSameContentShouldReturnDifferentId(): Unit = {
+     val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+     assertThat(uploadId)
+       .isNotEqualTo(SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block())
+   }
+
+   @Test
+   def retrieveShouldSuccess(): Unit = {
+     val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+     val actualUpload: Upload = SMono.fromPublisher(testee.retrieve(uploadId, USER)).block()
+
+     assertThat(actualUpload.uploadId)
+       .isEqualTo(uploadId)
+     assertThat(actualUpload.contentType)
+       .isEqualTo(CONTENT_TYPE)
+     assertThat(actualUpload.size)
+       .isEqualTo(sanitizeSize(DATA_STRING.length))
+     assertThat(actualUpload.content.readAllBytes())
+       .isEqualTo(DATA_STRING.getBytes)
+   }
+
+   @Test
+   def retrieveShouldThrowWhenUploadIdIsNotExist(): Unit = {
+     assertThatThrownBy(() => SMono.fromPublisher(testee.retrieve(UploadId.from("notFoundId"), USER)).block())
+       .isInstanceOf(classOf[UploadNotFoundException])
+   }
+
+   @Test
+   def retrieveShouldThrowWhenUserIsNotOwnerOfUpload(): Unit = {
+     val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+     assertThatThrownBy(() => SMono.fromPublisher(testee.retrieve(uploadId, Username.of("Alice"))).block())
+       .isInstanceOf(classOf[UploadNotFoundException])
+   }
+
+ }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepositoryTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepositoryTest.java
new file mode 100644
index 0000000..a863354
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepositoryTest.java
@@ -0,0 +1,45 @@
+/****************************************************************
+ * 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.upload;
+
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.jmap.api.upload.UploadRepository;
+import org.apache.james.jmap.api.upload.UploadRepositoryContract;
+import org.junit.jupiter.api.BeforeEach;
+import org.apache.james.blob.memory.MemoryBlobStoreDAO;
+import org.apache.james.server.blob.deduplication.DeDuplicationBlobStore;
+
+public class InMemoryUploadRepositoryTest implements UploadRepositoryContract {
+
+    private UploadRepository testee;
+
+    @BeforeEach
+    void setUp() {
+        BlobStore blobStore = new DeDuplicationBlobStore(new MemoryBlobStoreDAO(), BucketName.DEFAULT, new HashBlobId.Factory());
+        testee = new InMemoryUploadRepository(blobStore);
+    }
+
+    @Override
+    public UploadRepository testee() {
+        return testee;
+    }
+}
diff --git a/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java b/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
index 9ef1078..55355b0 100644
--- a/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
+++ b/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
@@ -18,12 +18,15 @@
  ****************************************************************/
 package org.apache.james.user.jpa;
 
+import java.util.Optional;
+
 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.james.backends.jpa.JpaTestCluster;
+import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.jpa.model.JPAUser;
 import org.apache.james.user.lib.UsersRepositoryContract;
-import org.apache.james.user.lib.UsersRepositoryImpl;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
@@ -39,16 +42,23 @@
         UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
 
         private JPAUsersRepository usersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) throws Exception {
-            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+            this.testSystem = testSystem;
         }
 
         @Override
-        public UsersRepositoryImpl testee() {
+        public UsersRepository testee() {
             return usersRepository;
         }
+
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+        }
     }
 
     @Nested
@@ -57,16 +67,23 @@
         UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
 
         private JPAUsersRepository usersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) throws Exception {
-            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+            usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+            this.testSystem = testSystem;
         }
 
         @Override
-        public UsersRepositoryImpl testee() {
+        public UsersRepository testee() {
             return usersRepository;
         }
+
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+        }
     }
 
     @AfterEach
@@ -74,11 +91,12 @@
         JPA_TEST_CLUSTER.clear("JAMES_USER");
     }
 
-    private static JPAUsersRepository getUsersRepository(DomainList domainList, boolean enableVirtualHosting) throws Exception {
+    private static JPAUsersRepository getUsersRepository(DomainList domainList, boolean enableVirtualHosting, Optional<Username> administrator) throws Exception {
         JPAUsersRepository repos = new JPAUsersRepository(domainList);
         repos.setEntityManagerFactory(JPA_TEST_CLUSTER.getEntityManagerFactory());
         BaseHierarchicalConfiguration configuration = new BaseHierarchicalConfiguration();
         configuration.addProperty("enableVirtualHosting", String.valueOf(enableVirtualHosting));
+        administrator.ifPresent(username -> configuration.addProperty("administratorId", username.asString()));
         repos.configure(configuration);
         return repos;
     }
diff --git a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
index 9638dbf..a24c698 100644
--- a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
+++ b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
@@ -28,6 +28,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.util.Optional;
+
 import org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.ex.ConversionException;
 import org.apache.commons.configuration2.plist.PropertyListConfiguration;
@@ -35,6 +37,7 @@
 import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.mock.SimpleDomainList;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.lib.UsersRepositoryContract;
 import org.apache.james.user.lib.UsersRepositoryImpl;
 import org.junit.jupiter.api.AfterAll;
@@ -142,10 +145,12 @@
         UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
 
         private ReadOnlyUsersLDAPRepository usersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) throws Exception {
             usersRepository = startUsersRepository(ldapRepositoryConfigurationWithVirtualHosting(ldapContainer), testSystem.getDomainList());
+            this.testSystem = testSystem;
         }
 
         @Override
@@ -153,6 +158,11 @@
             return usersRepository;
         }
 
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            return startUsersRepository(ldapRepositoryConfigurationWithVirtualHosting(ldapContainer, administrator), testSystem.getDomainList());
+        }
+
         @Test
         void isAdministratorShouldReturnTrueWhenConfiguredAndUserIsAdmin(TestSystem testSystem) throws Exception {
             assertThat(testee().isAdministrator(testSystem.getAdmin())).isTrue();
@@ -241,10 +251,12 @@
         UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
 
         private ReadOnlyUsersLDAPRepository usersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) throws Exception {
             usersRepository = startUsersRepository(ldapRepositoryConfiguration(ldapContainer), testSystem.getDomainList());
+            this.testSystem = testSystem;
         }
 
         @Override
@@ -252,6 +264,11 @@
             return usersRepository;
         }
 
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            return startUsersRepository(ldapRepositoryConfiguration(ldapContainer, administrator), testSystem.getDomainList());
+        }
+
         @Test
         void knownUserShouldBeAbleToLogInWhenPasswordIsCorrect() throws Exception {
             assertThat(usersRepository.test(JAMES_USER, PASSWORD)).isTrue();
@@ -371,17 +388,25 @@
     }
 
     static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfiguration(LdapGenericContainer ldapContainer) {
+        return ldapRepositoryConfiguration(ldapContainer, Optional.of(Username.of(ADMIN_LOCAL_PART)));
+    }
+
+    static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfiguration(LdapGenericContainer ldapContainer,  Optional<Username> administrator) {
         PropertyListConfiguration configuration = baseConfiguration(ldapContainer);
         configuration.addProperty("[@userIdAttribute]", "uid");
-        configuration.addProperty("[@administratorId]", ADMIN_LOCAL_PART);
+        administrator.ifPresent(username -> configuration.addProperty("[@administratorId]", username.asString()));
         return configuration;
     }
 
     static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfigurationWithVirtualHosting(LdapGenericContainer ldapContainer) {
+        return ldapRepositoryConfigurationWithVirtualHosting(ldapContainer, Optional.of(ADMIN));
+    }
+
+    static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfigurationWithVirtualHosting(LdapGenericContainer ldapContainer, Optional<Username> administrator) {
         PropertyListConfiguration configuration = baseConfiguration(ldapContainer);
         configuration.addProperty("[@userIdAttribute]", "mail");
         configuration.addProperty("supportsVirtualHosting", true);
-        configuration.addProperty("[@administratorId]", ADMIN.asString());
+        administrator.ifPresent(username -> configuration.addProperty("[@administratorId]", username.asString()));
         return configuration;
     }
 
diff --git a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
index 15e0173..d499e74 100644
--- a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
+++ b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
@@ -40,7 +40,6 @@
 import org.apache.james.user.api.model.User;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.CharMatcher;
 
 public class UsersRepositoryImpl<T extends UsersDAO> implements UsersRepository, Configurable {
@@ -164,10 +163,6 @@
         return virtualHosting;
     }
 
-    @VisibleForTesting void setAdministratorId(Optional<Username> username) {
-        this.administratorId = username;
-    }
-
     @Override
     public boolean isAdministrator(Username username) throws UsersRepositoryException {
         assertValid(username);
diff --git a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
index eabe6db..000b84a 100644
--- a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
+++ b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
@@ -34,6 +34,7 @@
 import org.apache.james.domainlist.api.mock.SimpleDomainList;
 import org.apache.james.user.api.AlreadyExistInUsersRepositoryException;
 import org.apache.james.user.api.InvalidUsernameException;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.api.model.User;
 import org.apache.james.user.lib.model.Algorithm;
@@ -53,15 +54,12 @@
 
     class UserRepositoryExtension implements BeforeEachCallback, ParameterResolver {
 
-        private static final boolean ENABLE_VIRTUAL_HOSTING = true;
-        private static final boolean DISABLE_VIRTUAL_HOSTING = !ENABLE_VIRTUAL_HOSTING;
-
         public static UserRepositoryExtension withVirtualHost() {
-            return new UserRepositoryExtension(ENABLE_VIRTUAL_HOSTING);
+            return new UserRepositoryExtension(true);
         }
 
         public static UserRepositoryExtension withoutVirtualHosting() {
-            return new UserRepositoryExtension(DISABLE_VIRTUAL_HOSTING);
+            return new UserRepositoryExtension(false);
         }
 
         private final boolean supportVirtualHosting;
@@ -93,7 +91,7 @@
 
     class TestSystem {
         static final Domain DOMAIN = Domain.of("james.org");
-        static final Domain UNKNOW_DOMAIN = Domain.of("unknown.org");
+        static final Domain UNKNOWN_DOMAIN = Domain.of("unknown.org");
 
         private final boolean supportVirtualHosting;
         private final SimpleDomainList domainList;
@@ -103,7 +101,7 @@
         private final Username user3;
         private final Username admin;
         private final Username adminCaseVariation;
-        private final Username userWithUnknowDomain;
+        private final Username userWithUnknownDomain;
         private final Username invalidUsername;
 
         TestSystem(boolean supportVirtualHosting) throws Exception {
@@ -116,7 +114,7 @@
             user1CaseVariation = toUsername("uSeRnaMe");
             admin = toUsername("admin");
             adminCaseVariation = toUsername("adMin");
-            userWithUnknowDomain = toUsername("unknown", UNKNOW_DOMAIN);
+            userWithUnknownDomain = toUsername("unknown", UNKNOWN_DOMAIN);
             invalidUsername = toUsername("userContains)*(");
         }
 
@@ -140,12 +138,15 @@
             return admin;
         }
 
-        public Username getUserWithUnknowDomain() {
-            return userWithUnknowDomain;
+        public Username getUserWithUnknownDomain() {
+            return userWithUnknownDomain;
         }
     }
 
-    UsersRepositoryImpl testee();
+    UsersRepository testee();
+
+    UsersRepository testee(Optional<Username> administrator) throws Exception;
+
 
     interface ReadOnlyContract extends UsersRepositoryContract {
         @Test
@@ -169,9 +170,9 @@
         }
 
         @Test
-        default void isAdministratorShouldBeCaseInsentive(TestSystem testSystem) throws Exception {
-            testee().setAdministratorId(Optional.of(testSystem.admin));
-            assertThat(testee().isAdministrator(testSystem.adminCaseVariation))
+        default void isAdministratorShouldBeCaseInsensitive(TestSystem testSystem) throws Exception {
+            UsersRepository testee = testee(Optional.of(testSystem.admin));
+            assertThat(testee.isAdministrator(testSystem.adminCaseVariation))
                 .isTrue();
         }
 
@@ -255,14 +256,14 @@
         }
 
         @Test
-        default void containsShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+        default void containsShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
             testee().addUser(testSystem.user1CaseVariation, "password2");
 
             assertThat(testee().contains(testSystem.user1)).isTrue();
         }
 
         @Test
-        default void containsShouldBeCaseInsentiveWhenOriginalValueLowerCased(TestSystem testSystem) throws UsersRepositoryException {
+        default void containsShouldBeCaseInsensitiveWhenOriginalValueLowerCased(TestSystem testSystem) throws UsersRepositoryException {
             testee().addUser(testSystem.user1, "password2");
 
             assertThat(testee().contains(testSystem.user1CaseVariation)).isTrue();
@@ -294,7 +295,7 @@
         }
 
         @Test
-        default void removeUserShouldBeCaseInsentiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
+        default void removeUserShouldBeCaseInsensitiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
             testee().addUser(testSystem.user1CaseVariation, "password2");
 
             testee().removeUser(testSystem.user1);
@@ -305,7 +306,7 @@
         }
 
         @Test
-        default void removeUserShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+        default void removeUserShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
             testee().addUser(testSystem.user1, "password2");
 
             testee().removeUser(testSystem.user1CaseVariation);
@@ -316,7 +317,7 @@
         }
 
         @Test
-        default void getUserByNameShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+        default void getUserByNameShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
             testee().addUser(testSystem.user1, "password2");
 
             assertThat(testee().getUserByName(testSystem.user1CaseVariation).getUserName())
@@ -333,7 +334,7 @@
 
 
         @Test
-        default void testShouldBeCaseInsentiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
+        default void testShouldBeCaseInsensitiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
             String password = "password2";
             testee().addUser(testSystem.user1CaseVariation, password);
 
@@ -342,7 +343,7 @@
         }
 
         @Test
-        default void testShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+        default void testShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
             String password = "password2";
             testee().addUser(testSystem.user1, password);
 
@@ -516,23 +517,23 @@
 
         @Test
         default void isAdministratorShouldReturnFalseWhenNotConfigured(TestSystem testSystem) throws Exception {
-            testee().setAdministratorId(Optional.empty());
+            UsersRepository testee = testee(Optional.empty());
 
-            assertThat(testee().isAdministrator(testSystem.admin)).isFalse();
+            assertThat(testee.isAdministrator(testSystem.admin)).isFalse();
         }
 
         @Test
         default void isAdministratorShouldReturnTrueWhenConfiguredAndUserIsAdmin(TestSystem testSystem) throws Exception {
-            testee().setAdministratorId(Optional.of(testSystem.admin));
+            UsersRepository testee = testee(Optional.of(testSystem.admin));
 
-            assertThat(testee().isAdministrator(testSystem.admin)).isTrue();
+            assertThat(testee.isAdministrator(testSystem.admin)).isTrue();
         }
 
         @Test
         default void isAdministratorShouldReturnFalseWhenConfiguredAndUserIsNotAdmin(TestSystem testSystem) throws Exception {
-            testee().setAdministratorId(Optional.of(testSystem.admin));
+            UsersRepository testee = testee(Optional.of(testSystem.admin));
 
-            assertThat(testee().isAdministrator(testSystem.user1)).isFalse();
+            assertThat(testee.isAdministrator(testSystem.user1)).isFalse();
         }
     }
 
@@ -552,7 +553,7 @@
 
         @Test
         default void addUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatThrownBy(() -> testee().addUser(testSystem.userWithUnknowDomain, "password"))
+            assertThatThrownBy(() -> testee().addUser(testSystem.userWithUnknownDomain, "password"))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Domain does not exist in DomainList");
         }
@@ -566,7 +567,7 @@
 
         @Test
         default void updateUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatThrownBy(() -> testee().updateUser(new DefaultUser(testSystem.userWithUnknowDomain, Algorithm.DEFAULT_FACTORY.of("hasAlg"))))
+            assertThatThrownBy(() -> testee().updateUser(new DefaultUser(testSystem.userWithUnknownDomain, Algorithm.DEFAULT_FACTORY.of("hasAlg"))))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Domain does not exist in DomainList");
         }
@@ -579,7 +580,7 @@
 
         @Test
         default void removeUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatThrownBy(() -> testee().removeUser(testSystem.userWithUnknowDomain))
+            assertThatThrownBy(() -> testee().removeUser(testSystem.userWithUnknownDomain))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Domain does not exist in DomainList");
         }
@@ -595,25 +596,25 @@
 
         @Test
         default void getUserByNameShouldNotThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatCode(() -> testee().getUserByName(testSystem.userWithUnknowDomain))
+            assertThatCode(() -> testee().getUserByName(testSystem.userWithUnknownDomain))
                 .doesNotThrowAnyException();
         }
 
         @Test
         default void containsShouldNotThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatCode(() -> testee().contains(testSystem.userWithUnknowDomain))
+            assertThatCode(() -> testee().contains(testSystem.userWithUnknownDomain))
                 .doesNotThrowAnyException();
         }
 
         @Test
         default void testShouldNotThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatCode(() -> testee().test(testSystem.userWithUnknowDomain, "password"))
+            assertThatCode(() -> testee().test(testSystem.userWithUnknownDomain, "password"))
                 .doesNotThrowAnyException();
         }
 
         @Test
         default void isAdministratorShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
-            assertThatThrownBy(() -> testee().isAdministrator(testSystem.userWithUnknowDomain))
+            assertThatThrownBy(() -> testee().isAdministrator(testSystem.userWithUnknownDomain))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Domain does not exist in DomainList");
         }
@@ -646,14 +647,14 @@
         default void assertDomainPartValidShouldThrowWhenDomainPartIsMissing() throws Exception {
             Username withoutDomainPart = Username.fromLocalPartWithoutDomain("localPartOnly");
 
-            assertThatThrownBy(() -> testee().assertDomainPartValid(withoutDomainPart))
+            assertThatThrownBy(() -> testee().assertValid(withoutDomainPart))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Given Username needs to contain a @domainpart");
         }
 
         @Test
         default void assertDomainPartValidShouldThrowWhenDomainPartIsNotManaged(TestSystem testSystem) {
-            assertThatThrownBy(() -> testee().assertDomainPartValid(testSystem.userWithUnknowDomain))
+            assertThatThrownBy(() -> testee().assertValid(testSystem.userWithUnknownDomain))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Domain does not exist in DomainList");
         }
@@ -664,7 +665,7 @@
                 "localPart",
                 TestSystem.DOMAIN);
 
-            assertThatCode(() -> testee().assertDomainPartValid(userWithManagedDomain))
+            assertThatCode(() -> testee().assertValid(userWithManagedDomain))
                 .doesNotThrowAnyException();
         }
     }
@@ -700,7 +701,7 @@
                 "localPart",
                 TestSystem.DOMAIN);
 
-            assertThatThrownBy(() -> testee().assertDomainPartValid(withDomainPart))
+            assertThatThrownBy(() -> testee().assertValid(withDomainPart))
                 .isInstanceOf(InvalidUsernameException.class)
                 .hasMessage("Given Username contains a @domainpart but virtualhosting support is disabled");
         }
@@ -709,7 +710,7 @@
         default void assertDomainPartValidShouldNotThrowWhenDomainPartIsMissing() {
             Username withOutDomainPart = Username.fromLocalPartWithoutDomain("localPartOnly");
 
-            assertThatCode(() -> testee().assertDomainPartValid(withOutDomainPart))
+            assertThatCode(() -> testee().assertValid(withOutDomainPart))
                 .doesNotThrowAnyException();
         }
     }
diff --git a/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java b/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
index b77b437..28d8f32 100644
--- a/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
+++ b/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
@@ -22,14 +22,19 @@
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.util.Optional;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.dnsservice.api.InMemoryDNSService;
 import org.apache.james.domainlist.lib.DomainListConfiguration;
 import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.lib.UsersRepositoryContract;
-import org.apache.james.user.lib.UsersRepositoryImpl;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -46,14 +51,23 @@
         UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
 
         private MemoryUsersRepository memoryUsersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) {
             memoryUsersRepository = MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+            this.testSystem = testSystem;
         }
 
         @Override
-        public UsersRepositoryImpl testee() {
+        public UsersRepository testee() {
+            return memoryUsersRepository;
+        }
+
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            MemoryUsersRepository memoryUsersRepository = MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+            memoryUsersRepository.configure(configuration(administrator, true));
             return memoryUsersRepository;
         }
 
@@ -103,14 +117,23 @@
         UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
 
         private MemoryUsersRepository memoryUsersRepository;
+        private TestSystem testSystem;
 
         @BeforeEach
         void setUp(TestSystem testSystem) {
             memoryUsersRepository = MemoryUsersRepository.withoutVirtualHosting(testSystem.getDomainList());
+            this.testSystem = testSystem;
         }
 
         @Override
-        public UsersRepositoryImpl testee() {
+        public UsersRepository testee() {
+            return memoryUsersRepository;
+        }
+
+        @Override
+        public UsersRepository testee(Optional<Username> administrator) throws Exception {
+            MemoryUsersRepository memoryUsersRepository = MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+            memoryUsersRepository.configure(configuration(administrator, false));
             return memoryUsersRepository;
         }
 
@@ -130,4 +153,12 @@
                 .doesNotThrowAnyException();
         }
     }
+
+    private HierarchicalConfiguration<ImmutableNode> configuration(Optional<Username> administrator, boolean enableVirtualHosting) {
+        BaseHierarchicalConfiguration configuration = new BaseHierarchicalConfiguration();
+        administrator.ifPresent(username -> configuration.addProperty("administratorId", username.asString()));
+
+        configuration.addProperty("enableVirtualHosting", enableVirtualHosting);
+        return configuration;
+    }
 }
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java
index 31cd912..a8a6baf 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java
@@ -27,7 +27,6 @@
 import org.apache.james.core.MailAddress;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
 import org.apache.james.mailbox.model.SearchQuery;
@@ -106,7 +105,7 @@
                 .from(SearchQuery.of(SearchQuery.mimeMessageID(messageId)))
                 .build();
             return Flux.from(mailboxManager.search(searchByRFC822MessageId, session, limit)).toStream().findFirst();
-        } catch (MailboxException | UsersRepositoryException e) {
+        } catch (UsersRepositoryException e) {
             LOGGER.error("unable to find message with Message-Id: " + messageId, e);
         }
         return Optional.empty();
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
index ae1eeb0..3811480 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
@@ -83,6 +83,13 @@
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>io.github.evis</groupId>
+                <artifactId>scalafix-maven-plugin</artifactId>
+                <configuration>
+                    <config>${project.parent.parent.parent.basedir}/.scalafix.conf</config>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
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/CustomMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
index a152b5e..d879bd6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
@@ -22,7 +22,6 @@
 import java.io.{ByteArrayInputStream, InputStream}
 import java.net.URI
 import java.nio.charset.StandardCharsets
-
 import com.google.inject.AbstractModule
 import com.google.inject.multibindings.Multibinder
 import eu.timepit.refined.auto._
@@ -31,6 +30,7 @@
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured._
 import io.restassured.http.ContentType.JSON
+
 import javax.inject.{Inject, Named}
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus.SC_OK
@@ -38,6 +38,7 @@
 import org.apache.james.events.Event.EventId
 import org.apache.james.events.EventBus
 import org.apache.james.jmap.api.model.AccountId
+import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.change.{AccountIdRegistrationKey, StateChangeEvent, TypeName}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE}
 import org.apache.james.jmap.core.Invocation.MethodName
@@ -46,7 +47,6 @@
 import org.apache.james.jmap.draft.JmapGuiceProbe
 import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.mail
-import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.method.{InvocationWithContext, Method}
 import org.apache.james.jmap.rfc8621.contract.CustomMethodContract.CUSTOM
 import org.apache.james.jmap.rfc8621.contract.DownloadContract.accountId
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/EmailChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala
index 59222f7..0ce6bee 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala
@@ -210,15 +210,15 @@
       .setBody("testmail", StandardCharsets.UTF_8)
       .build
     val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
-    val state1: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+    waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
     val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
-    val state2: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+    waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
     val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
-    val state3: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+    waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
     val messageId4: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
-    val state4: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+    waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
     val messageId5: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
-    val state5: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+    waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
     val messageId6: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
     val state6: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
 
@@ -312,7 +312,7 @@
     val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
     val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
 
-    val mailboxId1 = mailboxProbe.createMailbox(path)
+    mailboxProbe.createMailbox(path)
     val mailboxId2 = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
 
     val message: Message = Message.Builder
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/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index bcfdf8e..8968153 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -3198,7 +3198,7 @@
   @Test
   def receivedAtPropertyShouldBeReturned(server: GuiceJamesServer): Unit = {
     val path = MailboxPath.inbox(BOB)
-    val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
     val message: Message = Message.Builder
       .of
       .setSubject("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/EmailImportContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala
index e8716c5..18f00c4 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala
@@ -672,7 +672,7 @@
   @Test
   def importShouldFailWhenBlobNotOwned(server: GuiceJamesServer): Unit = {
     val andrePath = MailboxPath.inbox(ANDRE)
-    val andreId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
     val bobPath = MailboxPath.inbox(BOB)
     val bobId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
     val receivedAt = ZonedDateTime.now().minusDays(1)
@@ -743,7 +743,7 @@
   @Test
   def importShouldSucceedWhenBlobDelegated(server: GuiceJamesServer): Unit = {
     val andrePath = MailboxPath.inbox(ANDRE)
-    val andreId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
     val bobPath = MailboxPath.inbox(BOB)
     val bobId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
     val receivedAt = ZonedDateTime.now().minusDays(1)
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/EmailQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
index 2b19d63..dd7efb8 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
@@ -147,7 +147,7 @@
   def hasAttachmentShouldKeepMessageWithAttachmentWhenTrue(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
         AppendCommand.from(
           buildTestMessage))
@@ -459,8 +459,8 @@
   @Test
   def emailInSharedMailboxesShouldNotBeDisplayedWhenNoExtension(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe.createMailbox(inbox(ANDRE))
+    mailboxProbe
       .appendMessage(ANDRE.asString, inbox(ANDRE),
         AppendCommand.from(
           Message.Builder
@@ -521,7 +521,7 @@
   @Test
   def emailInSharedMailboxesShouldBeDisplayedWhenExtension(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
+    mailboxProbe.createMailbox(inbox(ANDRE))
     val messageId1: MessageId = mailboxProbe
       .appendMessage(ANDRE.asString, inbox(ANDRE),
         AppendCommand.from(
@@ -585,7 +585,7 @@
   def inMailboxFilterShouldReturnEmptyForSharedMailboxesWhenNoExtension(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(ANDRE.asString, inbox(ANDRE),
         AppendCommand.from(
           Message.Builder
@@ -714,7 +714,7 @@
   def inMailboxOtherThanFilterShouldReturnEmptyForSharedMailboxesWhenNoExtension(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(ANDRE.asString, inbox(ANDRE),
         AppendCommand.from(
           Message.Builder
@@ -775,8 +775,8 @@
   def inMailboxOtherThanFilterShouldAcceptSharedMailboxesWhenExtension(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
-    val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe.createMailbox(inbox(BOB))
+    mailboxProbe
       .appendMessage(ANDRE.asString, inbox(ANDRE),
         AppendCommand.from(
           Message.Builder
@@ -851,7 +851,7 @@
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(mailboxPath)
     val requestDate = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
     val messageId1: MessageId = sendMessageToBobInbox(server, message, requestDate)
-    val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, mailboxPath, AppendCommand.from(
         ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
       .getMessageId
@@ -989,7 +989,7 @@
   @Test
   def headerExistsShouldBeCaseInsentive(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+    mailboxProbe.createMailbox(inbox(BOB))
     val messageId1: MessageId = mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
@@ -1000,7 +1000,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1060,7 +1060,7 @@
   @Test
   def headerShouldAllowToMatchMailWithSpecificHeaderSet(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+    mailboxProbe.createMailbox(inbox(BOB))
     val messageId1: MessageId = mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
@@ -1071,7 +1071,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1131,7 +1131,7 @@
   @Test
   def headerShouldAllowToMatchMailWithSpecificValueHeaderSet(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+    mailboxProbe.createMailbox(inbox(BOB))
     val messageId1: MessageId = mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
@@ -1142,7 +1142,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1151,7 +1151,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId3: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1212,7 +1212,7 @@
   @Test
   def headerContainsShouldBeCaseInsentive(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+    mailboxProbe.createMailbox(inbox(BOB))
     val messageId1: MessageId = mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
@@ -1223,7 +1223,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1232,7 +1232,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId3: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1293,8 +1293,8 @@
   @Test
   def headerShouldRejectWhenMoreThanTwoItems(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe.createMailbox(inbox(BOB))
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1304,7 +1304,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1313,7 +1313,7 @@
             .setBody("testmail", StandardCharsets.UTF_8)
             .build))
       .getMessageId
-    val messageId3: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, inbox(BOB),
         AppendCommand.from(
           Message.Builder
@@ -1377,7 +1377,7 @@
           buildTestMessage))
       .getMessageId
 
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(
         ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
       .getMessageId
@@ -1435,14 +1435,14 @@
     val afterRequestDate = Date.from(ZonedDateTime.now().toInstant)
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
-    val messageId1: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
         AppendCommand.builder()
           .withInternalDate(beforeRequestDate)
           .build(buildTestMessage))
       .getMessageId
 
-    val messageId2: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
           .withInternalDate(beforeRequestDate)
         .build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
@@ -1455,7 +1455,7 @@
           .build(buildTestMessage))
       .getMessageId
 
-    val messageId4: MessageId = mailboxProbe
+    mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
         .withInternalDate(afterRequestDate)
         .build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
@@ -2902,17 +2902,17 @@
   @Test
   def minSizeShouldBeInclusive(server: GuiceJamesServer): Unit = {
     val message1: Message = simpleMessage("short")
-    val size1: Int = computeSize(message1)
+    computeSize(message1)
     // One char more than message1
     val message2: Message = simpleMessage("short!")
     val size2: Int = computeSize(message2)
     // One char more than message2
     val message3: Message = simpleMessage("short!!")
-    val size3: Int = computeSize(message3)
+    computeSize(message3)
 
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(inbox(BOB))
-    val id1 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId
+    mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId
     val id2 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId
     val id3 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId
 
@@ -2959,19 +2959,19 @@
   @Test
   def maxSizeShouldBeExclusive(server: GuiceJamesServer): Unit = {
     val message1: Message = simpleMessage("looooooooooooooong")
-    val size1: Int = computeSize(message1)
+    computeSize(message1)
     // One char more than message3
     val message2: Message = simpleMessage("looooooooooooooong!")
     val size2: Int = computeSize(message2)
     // One char more than message4
     val message3: Message = simpleMessage("looooooooooooooong!!")
-    val size3: Int = computeSize(message3)
+    computeSize(message3)
 
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(inbox(BOB))
     val id1 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId
-    val id2 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId
-    val id3 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId
+    mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId
+    mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId
 
     val request =
       s"""{
@@ -3894,7 +3894,7 @@
           .withInternalDate(Date.from(ZonedDateTime.now().minusDays(2).toInstant))
           .build(message))
         .getMessageId
-    val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
         .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
           .withInternalDate(Date.from(ZonedDateTime.now().minusDays(1).toInstant))
           .build(message))
@@ -4471,13 +4471,13 @@
           .setFrom("user@domain.tld")
           .build))
       .getMessageId
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setFrom("other@domain.tld")
           .build))
       .getMessageId
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setFrom("yet@other.tld")
@@ -4489,7 +4489,7 @@
           .setFrom("yet@other.tld", "user@domain.tld")
           .build))
       .getMessageId
-    val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -4549,7 +4549,7 @@
       .of
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setFrom("user@domain.tld")
@@ -4567,7 +4567,7 @@
           .setFrom("display@other.tld")
           .build))
       .getMessageId
-    val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -4621,13 +4621,13 @@
           .setTo("user@domain.tld")
           .build))
       .getMessageId
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setTo("other@domain.tld")
           .build))
       .getMessageId
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setTo("yet@other.tld")
@@ -4639,7 +4639,7 @@
           .setTo("yet@other.tld", "user@domain.tld")
           .build))
       .getMessageId
-    val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -4699,7 +4699,7 @@
       .of
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setTo("user@domain.tld")
@@ -4717,7 +4717,7 @@
           .setTo("display@other.tld")
           .build))
       .getMessageId
-    val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -4771,13 +4771,13 @@
           .setCc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
           .build))
       .getMessageId
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setCc(DefaultAddressParser.DEFAULT.parseMailbox("other@domain.tld"))
           .build))
       .getMessageId
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setCc(DefaultAddressParser.DEFAULT.parseMailbox("yet@other.tld"))
@@ -4790,7 +4790,7 @@
             DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
           .build))
       .getMessageId
-    val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -4850,7 +4850,7 @@
       .of
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setCc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
@@ -4868,7 +4868,7 @@
           .setCc(DefaultAddressParser.DEFAULT.parseMailbox("display@other.tld"))
           .build))
       .getMessageId
-    val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -4922,13 +4922,13 @@
           .setBcc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
           .build))
       .getMessageId
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setBcc(DefaultAddressParser.DEFAULT.parseMailbox("other@domain.tld"))
           .build))
       .getMessageId
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setBcc(DefaultAddressParser.DEFAULT.parseMailbox("yet@other.tld"))
@@ -4941,7 +4941,7 @@
             DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
           .build))
       .getMessageId
-    val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -5001,7 +5001,7 @@
       .of
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setBcc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
@@ -5019,7 +5019,7 @@
           .setBcc(DefaultAddressParser.DEFAULT.parseMailbox("display@other.tld"))
           .build))
       .getMessageId
-    val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -5072,13 +5072,13 @@
           .setSubject("Yet another day in paradise")
           .build))
       .getMessageId
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setSubject("Welcome to hell")
           .build))
       .getMessageId
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -5131,13 +5131,13 @@
           .setSubject("Yet another day in paradise")
           .build))
       .getMessageId
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
           .setSubject("Welcome to hell")
           .build))
       .getMessageId
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
@@ -5642,11 +5642,11 @@
   def emailQueryShouldSupportAndOperator(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
       .getMessageId
 
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
       .getMessageId
 
@@ -5714,7 +5714,7 @@
         .build(message))
       .getMessageId
 
-    val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(message))
       .getMessageId
 
@@ -5761,19 +5761,19 @@
   def emailQueryShouldSupportNotOperator(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
         .withFlags(new Flags("custom"))
         .build(message))
       .getMessageId
 
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
         .withFlags(new Flags("another_custom"))
         .build(message))
       .getMessageId
 
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
         .withFlags(new FlagsBuilder().add("custom", "another_custom").build())
         .build(message))
@@ -5926,15 +5926,15 @@
   def inMailboxShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
       .getMessageId
 
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
       .getMessageId
 
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
       .getMessageId
 
@@ -5989,15 +5989,15 @@
   def inMailboxOtherThanShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
       .getMessageId
 
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
       .getMessageId
 
-    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
       .getMessageId
 
@@ -6096,11 +6096,11 @@
   def nestedOperatorsShouldBeSupported(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
       .getMessageId
 
-    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
       .getMessageId
 
@@ -6197,7 +6197,7 @@
     val message: Message = buildTestMessage
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
 
-    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
         AppendCommand.builder()
           .withFlags(new FlagsBuilder().add("custom_1", "custom_2").build())
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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 413fdd8..f77c7c1 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -1101,7 +1101,7 @@
   @Test
   def createShouldRejectEmptyMailboxIds(server: GuiceJamesServer): Unit = {
     val andrePath = MailboxPath.inbox(ANDRE)
-    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
 
     val request =
       s"""{
@@ -1142,7 +1142,7 @@
   @Test
   def createShouldRejectInvalidMailboxIds(server: GuiceJamesServer): Unit = {
     val andrePath = MailboxPath.inbox(ANDRE)
-    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
 
     val request =
       s"""{
@@ -1185,7 +1185,7 @@
   @Test
   def createShouldRejectNoMailboxIds(server: GuiceJamesServer): Unit = {
     val andrePath = MailboxPath.inbox(ANDRE)
-    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
 
     val request =
       s"""{
@@ -2923,7 +2923,7 @@
     val messageId = responseAsJson
       .\("id")
       .get.asInstanceOf[JsString].value
-    val size = responseAsJson
+    responseAsJson
       .\("size")
       .get.asInstanceOf[JsNumber].value
 
@@ -3255,7 +3255,7 @@
     val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
     val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
 
-    val uploadResponse: String = `given`
+    `given`
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .contentType("text/plain")
@@ -5522,7 +5522,7 @@
   @Test
   def invalidPatchPropertyShouldFail(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
 
     val messageId: MessageId = mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
@@ -5571,7 +5571,7 @@
   @Test
   def invalidMailboxPartialUpdatePropertyShouldFail(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
 
     val messageId: MessageId = mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
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 1c1f40a..dad2ea1 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
@@ -1054,7 +1054,7 @@
     val provisioningState: State = provisionSystemMailboxes(server)
 
     val path1 = MailboxPath.forUser(BOB, "mailbox1")
-    val mailboxId1: String = mailboxProbe
+    mailboxProbe
       .createMailbox(path1)
       .serialize
 
@@ -1226,7 +1226,7 @@
       .createMailbox(MailboxPath.forUser(BOB, "mailbox5"))
       .serialize
 
-    val mailboxId6: String = mailboxProbe
+    mailboxProbe
       .createMailbox(MailboxPath.forUser(BOB, "mailbox6"))
       .serialize
 
@@ -1634,7 +1634,7 @@
       .setSubject("test")
       .setBody("testmail", StandardCharsets.UTF_8)
       .build
-    val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+    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
 
@@ -1977,7 +1977,7 @@
   }
 
   private def provisionSystemMailboxes(server: GuiceJamesServer): State = {
-    val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
     val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
 
     val request =
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/MailboxQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala
index 8cd1898..25ad0b2 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala
@@ -184,7 +184,7 @@
   @Test
   def queryByRoleShouldNotReturnDelegatedMailboxes(server: GuiceJamesServer): Unit = {
     val andreInbox = MailboxPath.inbox(ANDRE)
-    val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .createMailbox(andreInbox)
     val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
       .createMailbox(MailboxPath.inbox(BOB))
@@ -245,7 +245,7 @@
   @Test
   def queryByRoleShouldNotReturnDelegatedMailboxesWhenCaseVariation(server: GuiceJamesServer): Unit = {
     val andreInbox = MailboxPath.inbox(ANDRE)
-    val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+    server.getProbe(classOf[MailboxProbeImpl])
       .createMailbox(andreInbox)
     val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
       .createMailbox(MailboxPath.forUser(BOB, "InBoX"))
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 624c428..ea23e11 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
@@ -7644,7 +7644,7 @@
   @Test
   def updateShouldHandleNotFoundClientId(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
-    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
 
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
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/ThreadGetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala
index 04cfdd8..d31acd0 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala
@@ -19,6 +19,8 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
+import java.nio.charset.StandardCharsets
+
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType.JSON
@@ -28,6 +30,11 @@
 import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
 import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.MessageManager
+import org.apache.james.mailbox.model.MailboxPath
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.mime4j.stream.RawField
+import org.apache.james.modules.MailboxProbeImpl
 import org.apache.james.utils.DataProbeImpl
 import org.junit.jupiter.api.{BeforeEach, Test}
 
@@ -46,7 +53,7 @@
   }
 
   @Test
-  def threadsShouldReturnSuppliedIds(): Unit = {
+  def givenNonMessageThenGetThreadsShouldReturnNotFound(): Unit = {
     val request =
       s"""{
          |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
@@ -62,47 +69,6 @@
     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)
-      .inPath("methodResponses[0][1]")
-      .isEqualTo(
-        s"""{
-          |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-          |  "state": "${SESSION_STATE.value}",
-          |  "list": [
-          |      {
-          |          "id": "123456",
-          |          "emailIds": ["123456"]
-          |      }
-          |  ]
-          |}""".stripMargin)
-  }
-
-  @Test
-  def threadsShouldReturnSuppliedIdsWhenSeveralThreads(): Unit = {
-    val request =
-      s"""{
-         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
-         |  "methodCalls": [[
-         |    "Thread/get",
-         |    {
-         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "ids": ["123456", "789"]
-         |    },
-         |    "c1"]]
-         |}""".stripMargin
-
-    val response =  `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(request)
     .when
       .post
     .`then`
@@ -113,22 +79,26 @@
       .asString
 
     assertThatJson(response)
-      .inPath("methodResponses[0][1]")
       .isEqualTo(
         s"""{
-          |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-          |  "state": "${SESSION_STATE.value}",
-          |  "list": [
-          |      {
-          |          "id": "123456",
-          |          "emailIds": ["123456"]
-          |      },
-          |      {
-          |          "id": "789",
-          |          "emailIds": ["789"]
-          |      }
-          |  ]
-          |}""".stripMargin)
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Thread/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": [
+           |
+           |				],
+           |				"notFound": [
+           |					"123456"
+           |				]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
   }
 
   @Test
@@ -172,4 +142,409 @@
           |    ]
           |}""".stripMargin)
   }
+
+  @Test
+  def addRelatedMailsInAThreadThenGetThatThreadShouldReturnExactThreadObjectWithEmailIdsSortedByArrivalDate(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    // given 3 mails with related Subject and related Mime Message-ID fields
+    val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+        .setMessageId("Message-ID-1")
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    // message2 reply to message1
+    val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+          .setMessageId("Message-ID-2")
+          .setField(new RawField("In-Reply-To", "Message-ID-1"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    // message3 related to message1 through Subject and References message1's Message-ID
+    val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Fwd: Re: Test")
+          .setMessageId("Message-ID-3")
+          .setField(new RawField("In-Reply-To", "Random-InReplyTo"))
+          .addField(new RawField("References", "Message-ID-1"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    val threadId = message1.getThreadId.serialize()
+    val message1Id = message1.getId.getMessageId.serialize()
+    val message2Id = message2.getId.getMessageId.serialize()
+    val message3Id = message3.getId.getMessageId.serialize()
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Thread/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["$threadId"]
+         |    },
+         |    "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)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Thread/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": [{
+           |					"id": "$threadId",
+           |					"emailIds": ["$message1Id", "$message2Id", "$message3Id"]
+           |				}],
+           |				"notFound": [
+           |
+           |				]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def givenTwoThreadGetThatTwoThreadShouldReturnExactTwoThreadObjectWithEmailIdsSortedByArrivalDate(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    // given 2 mails with related Subject and related Mime Message-ID fields in threadA
+    val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+          .setMessageId("Message-ID-1")
+          .setBody("testmail", StandardCharsets.UTF_8)))
+    // message2 reply to message1
+    val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+          .setMessageId("Message-ID-2")
+          .setField(new RawField("In-Reply-To", "Message-ID-1"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+    val threadA = message1.getThreadId.serialize()
+
+    // message3 in threadB
+    val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Message3-SubjectLine")
+          .setMessageId("Message-ID-3")
+          .setBody("testmail", StandardCharsets.UTF_8)))
+    val threadB = message3.getThreadId.serialize()
+
+    val message1Id = message1.getId.getMessageId.serialize()
+    val message2Id = message2.getId.getMessageId.serialize()
+    val message3Id = message3.getId.getMessageId.serialize()
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Thread/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["$threadA", "$threadB"]
+         |    },
+         |    "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)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Thread/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": [{
+           |						"id": "$threadA",
+           |						"emailIds": [
+           |							"$message1Id",
+           |							"$message2Id"
+           |						]
+           |					},
+           |					{
+           |						"id": "$threadB",
+           |						"emailIds": [
+           |							"$message3Id"
+           |						]
+           |					}
+           |				],
+           |				"notFound": [
+           |
+           |				]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def givenOneThreadGetTwoThreadShouldReturnOnlyOneThreadObjectAndNotFound(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    // given 2 mails with related Subject and related Mime Message-ID fields in threadA
+    val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+          .setMessageId("Message-ID-1")
+          .setBody("testmail", StandardCharsets.UTF_8)))
+    // message2 reply to message1
+    val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+          .setMessageId("Message-ID-2")
+          .setField(new RawField("In-Reply-To", "Message-ID-1"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+    val threadA = message1.getThreadId.serialize()
+
+    val message1Id = message1.getId.getMessageId.serialize()
+    val message2Id = message2.getId.getMessageId.serialize()
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Thread/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["$threadA", "nonExistThread"]
+         |    },
+         |    "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)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Thread/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": [{
+           |					"id": "$threadA",
+           |					"emailIds": [
+           |						"$message1Id",
+           |						"$message2Id"
+           |					]
+           |				}],
+           |				"notFound": [
+           |					"nonExistThread"
+           |				]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def addThreeMailsWithRelatedSubjectButNonIdenticalMimeMessageIDThenGetThatThreadShouldNotReturnUnrelatedMails(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+          .setMessageId("Message-ID-1")
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    // message2 have related subject with message1 but non identical Mime Message-ID
+    val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+          .setMessageId("Message-ID-2")
+          .setField(new RawField("In-Reply-To", "Random-InReplyTo"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    // message3 have related subject with message1 but non identical Mime Message-ID
+    val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Fwd: Re: Test")
+          .setMessageId("Message-ID-3")
+          .setField(new RawField("In-Reply-To", "Another-Random-InReplyTo"))
+          .addField(new RawField("References", "Random-References"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    val threadId1 = message1.getThreadId.serialize()
+    val message1Id = message1.getId.getMessageId.serialize()
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Thread/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["$threadId1"]
+         |    },
+         |    "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)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Thread/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": [{
+           |					"id": "$threadId1",
+           |					"emailIds": ["$message1Id"]
+           |				}],
+           |				"notFound": [
+           |
+           |				]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def addThreeMailsWithIdenticalMimeMessageIDButNonRelatedSubjectThenGetThatThreadShouldNotReturnUnrelatedMails(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test1")
+          .setMessageId("Message-ID-1")
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    // message2 have identical Mime Message-ID with message1 through In-Reply-To field but have non related subject
+    val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test2")
+          .setMessageId("Message-ID-2")
+          .setField(new RawField("In-Reply-To", "Message-ID-1"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    // message2 have identical Mime Message-ID with message1 through References field but have non related subject
+    val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+        MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test3")
+          .setMessageId("Message-ID-3")
+          .setField(new RawField("In-Reply-To", "Random-InReplyTo"))
+          .addField(new RawField("References", "Message-ID-1"))
+          .setBody("testmail", StandardCharsets.UTF_8)))
+
+    val threadId1 = message1.getThreadId.serialize()
+    val message1Id = message1.getId.getMessageId.serialize()
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Thread/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["$threadId1"]
+         |    },
+         |    "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)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Thread/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": [{
+           |					"id": "$threadId1",
+           |					"emailIds": ["$message1Id"]
+           |				}],
+           |				"notFound": [
+           |
+           |				]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
 }
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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index 0223d8a..45ae356 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -1069,7 +1069,7 @@
   def pushEnableRequestWithPushStateShouldReturnServerState(server: GuiceJamesServer): Unit = {
     val bobPath = MailboxPath.inbox(BOB)
     val accountId: AccountId = AccountId.fromUsername(BOB)
-    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
 
     Thread.sleep(100)
 
diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index 526270c..42d6fb0 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -189,6 +189,13 @@
                 <groupId>org.scalatest</groupId>
                 <artifactId>scalatest-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>io.github.evis</groupId>
+                <artifactId>scalafix-maven-plugin</artifactId>
+                <configuration>
+                    <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala
index 140ecfb..37cdd28 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala
@@ -25,7 +25,6 @@
 import org.apache.james.core.Username
 import org.apache.james.mailbox.MailboxSession
 import org.apache.james.metrics.api.MetricFactory
-import org.apache.james.metrics.api.TimeMetric.ExecutionResult.DEFAULT_100_MS_THRESHOLD
 import org.apache.james.user.api.{AlreadyExistInUsersRepositoryException, UsersRepository, UsersRepositoryException}
 import reactor.core.scala.publisher.SMono
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index 2223077..43167af 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -21,15 +21,14 @@
 
 import eu.timepit.refined
 import org.apache.james.jmap.api.model.Preview
+import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.{Properties, UuidState}
-import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.{AddressesHeaderValue, BlobId, Charset, DateHeaderValue, Disposition, EmailAddress, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailChangesRequest, EmailChangesResponse, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, GroupName, GroupedAddressesHeaderValue, HasAttachment, HeaderMessageId, HeaderURL, IsEncodingProblem, IsTruncated, Keyword, Keywords, Language, Languages, Location, MailboxIds, MessageIdsHeaderValue, Name, PartId, RawHeaderValue, Subject, TextHeaderValue, ThreadId, Type, URLsHeaderValue, UnparsedEmailId}
 import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
 
-import scala.language.implicitConversions
 
 object EmailBodyPartToSerialize {
   def from(part: EmailBodyPart): EmailBodyPartToSerialize = EmailBodyPartToSerialize(
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
index 11ff719..c41e255 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
@@ -25,7 +25,6 @@
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
 import play.api.libs.json._
 
-import scala.language.implicitConversions
 import scala.util.Try
 
 class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
index 9f4d74b..ca5da3e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
@@ -20,16 +20,21 @@
 package org.apache.james.jmap.json
 
 import org.apache.james.jmap.core.UuidState
-import org.apache.james.jmap.mail.{Thread, ThreadChangesRequest, ThreadChangesResponse, ThreadGetRequest, ThreadGetResponse}
-import play.api.libs.json.{JsObject, JsResult, JsValue, Json, OWrites, Reads, Writes}
+import org.apache.james.jmap.mail.{Thread, ThreadChangesRequest, ThreadChangesResponse, ThreadGetRequest, ThreadGetResponse, ThreadNotFound, UnparsedThreadId}
+import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.{JsObject, JsResult, JsString, JsValue, Json, OWrites, Reads, Writes}
 
 import scala.language.implicitConversions
 
 object ThreadSerializer {
+  private implicit val messageIdWrites: Writes[MessageId] = messageId => JsString(messageId.serialize())
+  private implicit val unparsedThreadIdReads: Reads[UnparsedThreadId] = Json.valueReads[UnparsedThreadId]
   private implicit val threadGetReads: Reads[ThreadGetRequest] = Json.reads[ThreadGetRequest]
   private implicit val threadChangesReads: Reads[ThreadChangesRequest] = Json.reads[ThreadChangesRequest]
   private implicit val threadWrites: OWrites[Thread] = Json.writes[Thread]
   private implicit val stateWrites: Writes[UuidState] = Json.valueWrites[UuidState]
+  private implicit val unparsedThreadIdWrites: Writes[UnparsedThreadId] = Json.valueWrites[UnparsedThreadId]
+  private implicit val threadNotFoundWrites: Writes[ThreadNotFound] = Json.valueWrites[ThreadNotFound]
   private implicit val threadGetWrites: OWrites[ThreadGetResponse] = Json.writes[ThreadGetResponse]
   private implicit val changesResponseWrites: OWrites[ThreadChangesResponse] = Json.writes[ThreadChangesResponse]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index db66bfa..e77c41f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -21,17 +21,14 @@
 
 import cats.implicits._
 import eu.timepit.refined
-import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
-import eu.timepit.refined.numeric.NonNegative
-import eu.timepit.refined.refineV
 import eu.timepit.refined.types.string.NonEmptyString
 import org.apache.james.jmap.api.model.Preview
+import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
 import org.apache.james.jmap.api.projections.{MessageFastViewPrecomputedProperties, MessageFastViewProjection}
 import org.apache.james.jmap.core.Id.{Id, IdConstraint}
 import org.apache.james.jmap.core.{Properties, UTCDate}
 import org.apache.james.jmap.mail.BracketHeader.sanitize
-import org.apache.james.jmap.mail.Email.{Size, sanitizeSize}
 import org.apache.james.jmap.mail.EmailHeaderName.{ADDRESSES_NAMES, DATE, MESSAGE_ID_NAMES}
 import org.apache.james.jmap.mail.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
 import org.apache.james.jmap.method.ZoneIdProvider
@@ -76,18 +73,6 @@
       case scala.Right(value) => Success(UnparsedEmailId(value))
     }
 
-  type Size = Long Refined NonNegative
-  val Zero: Size = 0L
-
-  def sanitizeSize(value: Long): Size = {
-    val size: Either[String, Size] = refineV[NonNegative](value)
-    size.fold(e => {
-      logger.error(s"Encountered an invalid Email size: $e")
-      Zero
-    },
-      refinedValue => refinedValue)
-  }
-
   private[mail] def parseAsMime4JMessage(firstMessage: MessageResult): Try[Message] = {
     val defaultMessageBuilder = new DefaultMessageBuilder
     defaultMessageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index e3d08a8..09c2bb9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -20,7 +20,6 @@
 package org.apache.james.jmap.mail
 
 import java.io.OutputStream
-
 import cats.implicits._
 import com.google.common.io.CountingOutputStream
 import eu.timepit.refined.api.Refined
@@ -28,8 +27,8 @@
 import eu.timepit.refined.numeric.NonNegative
 import eu.timepit.refined.refineV
 import org.apache.commons.io.IOUtils
+import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.core.Properties
-import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.EmailBodyPart.{FILENAME_PREFIX, MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN}
 import org.apache.james.jmap.mail.PartId.PartIdValue
 import org.apache.james.mailbox.model.{Cid, MessageId, MessageResult}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
index 1a7a676..de06a1e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
@@ -20,10 +20,10 @@
 package org.apache.james.jmap.mail
 
 import cats.implicits._
+import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.core.Limit.Limit
 import org.apache.james.jmap.core.Position.Position
 import org.apache.james.jmap.core.{AccountId, CanCalculateChanges, LimitUnparsed, PositionUnparsed, QueryState, UTCDate}
-import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.IsAscending.ASCENDING
 import org.apache.james.jmap.method.WithAccountId
 import org.apache.james.mailbox.model.SearchQuery.Sort.Order.{NATURAL, REVERSE}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index ef69a2c..afa1b41 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -20,15 +20,14 @@
 
 import java.nio.charset.{StandardCharsets, Charset => NioCharset}
 import java.util.Date
-
 import cats.implicits._
 import com.google.common.net.MediaType
 import com.google.common.net.MediaType.{HTML_UTF_8, PLAIN_TEXT_UTF_8}
 import eu.timepit.refined
+import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.core.Id.{Id, IdConstraint}
 import org.apache.james.jmap.core.{AccountId, SetError, UTCDate, UuidState}
 import org.apache.james.jmap.mail.Disposition.INLINE
-import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.method.WithAccountId
 import org.apache.james.jmap.routes.{Blob, BlobResolvers}
 import org.apache.james.mailbox.MailboxSession
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
index e39c20d..e6619a5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
@@ -23,15 +23,23 @@
 import org.apache.james.jmap.core.UnsignedInt.UnsignedInt
 import org.apache.james.jmap.core.{AccountId, UuidState}
 import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.mailbox.model.MessageId
 
-case class Thread(id: Id, emailIds: List[Id])
+case class Thread(id: Id, emailIds: List[MessageId])
 
 case class ThreadGetRequest(accountId: AccountId,
-                            ids: List[Id]) extends WithAccountId
+                            ids: List[UnparsedThreadId]) extends WithAccountId
 
 case class ThreadGetResponse(accountId: AccountId,
                              state: UuidState,
-                             list: List[Thread])
+                             list: List[Thread],
+                             notFound: ThreadNotFound)
+
+case class ThreadNotFound(value: Set[UnparsedThreadId]) {
+  def merge(other: ThreadNotFound): ThreadNotFound = ThreadNotFound(this.value ++ other.value)
+}
+
+case class UnparsedThreadId(id: Id)
 
 case class ThreadChangesRequest(accountId: AccountId,
                                 sinceState: UuidState,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala
index 5598083..f0eb461 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala
@@ -20,17 +20,18 @@
 package org.apache.james.jmap.method
 
 import java.util.Date
-
 import eu.timepit.refined.auto._
+
 import javax.inject.Inject
 import org.apache.james.jmap.api.change.EmailChangeRepository
+import org.apache.james.jmap.api.model.Size.sanitizeSize
 import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JAMES_SHARES, JMAP_CORE, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{ClientId, Id, Invocation, ServerId, SetError, UuidState}
 import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
-import org.apache.james.jmap.mail.{BlobId, Email, EmailCreationId, EmailCreationResponse, EmailImport, EmailImportRequest, EmailImportResponse, ThreadId, ValidatedEmailImport}
+import org.apache.james.jmap.mail.{BlobId, EmailCreationId, EmailCreationResponse, EmailImport, EmailImportRequest, EmailImportResponse, ThreadId, ValidatedEmailImport}
 import org.apache.james.jmap.method.EmailImportMethod.{ImportFailure, ImportResult, ImportResults, ImportSuccess, ImportWithBlob}
 import org.apache.james.jmap.routes.{Blob, BlobNotFoundException, BlobResolvers, ProcessingContext, SessionSupplier}
 import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -173,7 +174,7 @@
   private def asEmailCreationResponse(appendResult: MessageManager.AppendResult): EmailCreationResponse = {
     val blobId: Option[BlobId] = BlobId.of(appendResult.getId.getMessageId).toOption
     val threadId: ThreadId = ThreadId.fromJava(appendResult.getThreadId)
-    EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, Email.sanitizeSize(appendResult.getSize))
+    EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, sanitizeSize(appendResult.getSize))
   }
 
   private def retrieveState(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession): SMono[UuidState] =
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
index 11d2e06..bdb401f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
@@ -21,15 +21,16 @@
 
 import java.time.ZonedDateTime
 import java.util.Date
-
 import eu.timepit.refined.auto._
+
 import javax.inject.Inject
 import javax.mail.Flags
 import org.apache.james.jmap.JMAPConfiguration
+import org.apache.james.jmap.api.model.Size.sanitizeSize
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{Properties, SetError, UTCDate}
 import org.apache.james.jmap.json.EmailSetSerializer
-import org.apache.james.jmap.mail.{BlobId, Email, EmailCreationId, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, ThreadId}
+import org.apache.james.jmap.mail.{BlobId, EmailCreationId, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, ThreadId}
 import org.apache.james.jmap.method.EmailSetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
 import org.apache.james.jmap.routes.{BlobNotFoundException, BlobResolvers}
 import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -112,7 +113,7 @@
     } yield {
       val blobId: Option[BlobId] = BlobId.of(appendResult.getId.getMessageId).toOption
       val threadId: ThreadId = ThreadId.fromJava(appendResult.getThreadId)
-      CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, Email.sanitizeSize(appendResult.getSize)))
+      CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, sanitizeSize(appendResult.getSize)))
     }
   }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala
index 1c3c53e..7b91276 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala
@@ -22,31 +22,59 @@
 import eu.timepit.refined.auto._
 import javax.inject.Inject
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
-import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.core.{Invocation, UuidState}
+import org.apache.james.jmap.core.{AccountId, Invocation, UuidState}
 import org.apache.james.jmap.json.{ResponseSerializer, ThreadSerializer}
-import org.apache.james.jmap.mail.{Thread, ThreadGetRequest, ThreadGetResponse}
+import org.apache.james.jmap.mail.{Thread, ThreadGetRequest, ThreadGetResponse, ThreadNotFound, UnparsedThreadId}
 import org.apache.james.jmap.routes.SessionSupplier
-import org.apache.james.mailbox.MailboxSession
+import org.apache.james.mailbox.model.{ThreadId => JavaThreadId}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
 import org.apache.james.metrics.api.MetricFactory
 import play.api.libs.json.{JsError, JsSuccess}
-import reactor.core.scala.publisher.SMono
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.util.Try
+
+object ThreadGetResult {
+  def empty: ThreadGetResult = ThreadGetResult(Set.empty, ThreadNotFound(Set.empty))
+
+  def merge(result1: ThreadGetResult, result2: ThreadGetResult): ThreadGetResult = result1.merge(result2)
+
+  def found(thread: Thread): ThreadGetResult =
+    ThreadGetResult(Set(thread), ThreadNotFound(Set.empty))
+
+  def notFound(unparsedThreadId: UnparsedThreadId): ThreadGetResult =
+    ThreadGetResult(Set.empty, ThreadNotFound(Set(unparsedThreadId)))
+}
+
+case class ThreadGetResult(threads: Set[Thread], notFound: ThreadNotFound) {
+  def merge(other: ThreadGetResult): ThreadGetResult =
+    ThreadGetResult(this.threads ++ other.threads, this.notFound.merge(other.notFound))
+
+  def asResponse(accountId: AccountId): ThreadGetResponse =
+    ThreadGetResponse(
+      accountId = accountId,
+      state = UuidState.INSTANCE,
+      list = threads.toList,
+      notFound = notFound)
+}
 
 class ThreadGetMethod @Inject()(val metricFactory: MetricFactory,
-                                          val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[ThreadGetRequest] {
+                                val sessionSupplier: SessionSupplier,
+                                val threadIdFactory: JavaThreadId.Factory,
+                                val mailboxManager: MailboxManager) extends MethodRequiringAccountId[ThreadGetRequest] {
   override val methodName: MethodName = MethodName("Thread/get")
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
 
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: ThreadGetRequest): SMono[InvocationWithContext] = {
-    val response = ThreadGetResponse(accountId = request.accountId,
-      state = UuidState.INSTANCE,
-      list = retrieveThreads(request.ids))
-    SMono.just(InvocationWithContext(invocation = Invocation(
-      methodName = methodName,
-      arguments = Arguments(ThreadSerializer.serialize(response)),
-      methodCallId = invocation.invocation.methodCallId),
-      processingContext = invocation.processingContext))
+    getThreadResponse(request, mailboxSession)
+      .reduce(ThreadGetResult.empty)(ThreadGetResult.merge)
+      .map(threadGetResult => threadGetResult.asResponse(request.accountId))
+      .map(threadGetResponse => Invocation(
+        methodName = methodName,
+        arguments = Arguments(ThreadSerializer.serialize(threadGetResponse)),
+        methodCallId = invocation.invocation.methodCallId))
+      .map(InvocationWithContext(_, invocation.processingContext))
   }
 
   override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, ThreadGetRequest] =
@@ -55,7 +83,18 @@
       case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
 
-  // Naive implementation
-  private def retrieveThreads(ids: List[Id]): List[Thread] =
-    ids.map(id => Thread(id = id, emailIds = List(id)))
+  private def getThreadResponse(threadGetRequest: ThreadGetRequest,
+                                mailboxSession: MailboxSession): SFlux[ThreadGetResult] = {
+    SFlux.fromIterable(threadGetRequest.ids)
+      .flatMap(unparsedThreadId => {
+        Try(threadIdFactory.fromString(unparsedThreadId.id.toString()))
+          .fold(e => SFlux.just(ThreadGetResult.notFound(unparsedThreadId)),
+            threadId => SFlux.fromPublisher(mailboxManager.getThread(threadId, mailboxSession))
+              .collectSeq()
+              .map(seq => Thread(id = unparsedThreadId.id, emailIds = seq.toList))
+              .map(ThreadGetResult.found)
+              .onErrorResume((_ => SMono.just(ThreadGetResult.notFound(unparsedThreadId)))))
+      })
+  }
+
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
index d1a90db..614b28f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
@@ -22,7 +22,6 @@
 import java.nio.charset.StandardCharsets
 import java.util.stream
 import java.util.stream.Stream
-
 import com.google.common.base.CharMatcher
 import eu.timepit.refined.numeric.NonNegative
 import eu.timepit.refined.refineV
@@ -30,15 +29,16 @@
 import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE}
 import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, NOT_FOUND, OK, UNAUTHORIZED}
 import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus, QueryStringDecoder}
+
 import javax.inject.{Inject, Named}
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
+import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
 import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.{AccountId, Id, ProblemDetails}
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.{BlobId, EmailBodyPart, PartId}
 import org.apache.james.jmap.routes.DownloadRoutes.{BUFFER_SIZE, LOGGER}
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
@@ -101,7 +101,7 @@
 }
 
 case class AttachmentBlob(attachmentMetadata: AttachmentMetadata, fileContent: InputStream) extends Blob {
-  override def size: Try[Size] = Success(UploadRoutes.sanitizeSize(attachmentMetadata.getSize))
+  override def size: Try[Size] = Success(sanitizeSize(attachmentMetadata.getSize))
 
   override def contentType: ContentType = attachmentMetadata.getType
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
index 418e5ab..ff30311 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -19,22 +19,12 @@
 
 package org.apache.james.jmap.routes
 
-import java.io.InputStream
-import java.nio.ByteBuffer
-import java.nio.charset.StandardCharsets
-import java.util.stream
-import java.util.stream.Stream
-
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.auto._
-import eu.timepit.refined.numeric.NonNegative
-import eu.timepit.refined.refineV
 import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE}
 import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, CREATED, FORBIDDEN, INTERNAL_SERVER_ERROR, UNAUTHORIZED}
 import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
-import javax.inject.{Inject, Named}
 import org.apache.commons.fileupload.util.LimitedInputStream
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
+import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
 import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.{AccountId, Id, JmapRfc8621Configuration, ProblemDetails}
 import org.apache.james.jmap.exceptions.UnauthorizedException
@@ -42,8 +32,7 @@
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.{ResponseSerializer, UploadSerializer}
 import org.apache.james.jmap.mail.BlobId
-import org.apache.james.jmap.mail.Email.Size
-import org.apache.james.jmap.routes.UploadRoutes.{LOGGER, sanitizeSize}
+import org.apache.james.jmap.routes.UploadRoutes.LOGGER
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.model.{AttachmentMetadata, ContentType}
 import org.apache.james.mailbox.{AttachmentManager, MailboxSession}
@@ -55,22 +44,17 @@
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
+import java.io.InputStream
+import java.nio.ByteBuffer
+import java.nio.charset.StandardCharsets
+import java.util.stream
+import java.util.stream.Stream
+import javax.inject.{Inject, Named}
+
 case class TooBigUploadException() extends RuntimeException
 
 object UploadRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[DownloadRoutes])
-
-  type Size = Long Refined NonNegative
-  val Zero: Size = 0L
-
-  def sanitizeSize(value: Long): Size = {
-    val size: Either[String, Size] = refineV[NonNegative](value)
-    size.fold(e => {
-      LOGGER.error(s"Encountered an invalid upload files size: $e")
-      Zero
-    },
-      refinedValue => refinedValue)
-  }
 }
 
 case class UploadResponse(accountId: AccountId,
diff --git a/src/adr/0048-cleanup-jmap-uploads.md b/src/adr/0048-cleanup-jmap-uploads.md
new file mode 100644
index 0000000..62ad15d
--- /dev/null
+++ b/src/adr/0048-cleanup-jmap-uploads.md
@@ -0,0 +1,87 @@
+# 48. Cleanup of JMAP uploads
+
+Date: 2021-07-21
+
+## Status
+
+Accepted (lazy consensus).
+
+Not yet implemented.
+
+## Context
+
+JMAP allows users to upload binary content called blobs to be later referenced via method calls. This includes but is not
+limited to `Email/set` for specifying the blobId of attachments and `Email/import`.
+
+The [specification](https://jmap.io/spec-core.html#binary-data) strongly encourages enforcing the cleanup of these uploads:
+
+```
+A blob that is not referenced by a JMAP object (e.g., as a message attachment) MAY be deleted by the server to free up 
+resources. Uploads (see below) are initially unreferenced blobs.
+
+[...] An unreferenced blob MUST NOT be deleted for at least 1 hour from the time of upload; if reuploaded, the same 
+blobId MAY be returned, but this SHOULD reset the expiry time.
+```
+
+Deleting such uploads in a timely manner is important as:
+
+ - It enables freeing server resources.
+ - failing to do so may compromise privacy: content the user have uploaded and long forgotten might still be accessible
+ in the underlying data-store. Failing to delete uploads in a timely fashion may jeopardize for instance GDPR compliance.
+ 
+Today, uploads are stored along side email attachments. This means:
+ - We can hardly apply a specific lifecycle that cleans up uploads, as distinguishing attachment from uploads is not 
+ trivial.
+ - We currently have a complex right resolution system on attachment, handling both the upload case (were the attachment
+ is linked to a user) and the 'true' attachment case (linked to a message, those who can access the message can access 
+ the attachment). This leads to sub-optimal code (slow).
+
+## Decision
+
+We need to create a separate interface `UploadRepository` in `data-jmap` to store uploads for each user. We would provide a memory 
+implementation as well as a distributed implementation of it.
+
+The distributed implementation would host metadata of the upload in Cassandra, and the content using the BlobStore API,
+so object storage.
+
+This `UploadRepository` would be used by JMAP RFC-8620 to back uploads (instead of the attachment manager), we will 
+provide a `BlobResolver` to enable interactions with the uploaded blob. Similarly, we will use the `UploadRepository` to
+back uploads of JMAP draft.
+
+We will implement cleanup of the distributed `UploadRepository`. This will be done via:
+ - TTLs on the Cassandra metadata.
+ - Organisation of the blobs in time ranged buckets, only the two most recent buckets are kept.
+ - A WebAdmin endpoint would allow to plan a CRON triggering the cleanup.
+
+## Consequences
+
+Upon migrating to the `UploadRepository`, previous uploads will not be carried over. No migration plan is provided as 
+the impact is minimal. Upload prior this change will never be cleaned up. This is acceptable as JMAP implementations are
+marked as experimental.
+
+We can clean up attachment storage within the `mailbox-api` and its implementation:
+ - Drop `attachmentOwners` cassandra table
+ - Remove `getOwners` `storeAttachmentForOwner` methods in the Attachment mapper
+ - Rename `storeAttachmentsForMessage*` -> `storeAttachments*` in attachment mapper
+ - Simplify resolution logic for `StoreAttachmentManager` (looking message ownership is then enough)
+ - Fusion of `attachmentMessageId` and `attachmentV2` table, `attachmentMessageId` to be dropped in next release, 
+ `attachmentV2` can be altered to add the referencing `messageId`, and a migration task will be provided to populate it.
+ In the meantime a fallback strategy can be supplied: If the messageId cell is null we should default to reading the 
+ (old) `attachmentMessageId` table.
+ 
+## Alternatives
+
+[JMAP blob draft](https://datatracker.ietf.org/doc/draft-ietf-jmap-blob/) had been proposed to have the clients explicitly
+delete its uploads once the blob had been used to create other entities, as this extension introduce a mean to delete 
+blobs.
+
+However, relying on clients to enforce effective deletion seems brittle as:
+ - In case of client failures (or malicious client), no mechanisms would ensure effective deletion
+ - The main JMAP specification does not mandate nor encourage clients to clean up their uploads using the blob extension
+ and as such interoperability issues would arise.
+
+## References
+
+ - [JIRA](https://issues.apache.org/jira/browse/JAMES-3544)
+ - [PR of this ADR](https://github.com/apache/james-project/pull/544)
+ - [Thread on server-dev mailing list](https://www.mail-archive.com/server-dev@james.apache.org/msg70591.html)
\ No newline at end of file
diff --git a/src/homepage/index.html b/src/homepage/index.html
index 85683fc..016e72d 100644
--- a/src/homepage/index.html
+++ b/src/homepage/index.html
@@ -51,9 +51,8 @@
         <li><a href="#roadmap">Roadmap</a></li>
         <li><a href="#posts">Last Posts</a></li>
         <li><a href="#second">Community</a></li>
-        <li><a href="#third">Contribute</a></li>
-        <li><a href="#use-cases">Use-cases</a></li>
         <li><a href="documentation.html"><span class="fa fa-external-link"></span> Documentation</a></li>
+        <li><a href="download.cgi"><span class="fa fa-external-link"></span> Downloads</a></li>
       </ul>
     </nav>
 
@@ -101,9 +100,23 @@
         <section id="first" class="main">
           <header class="major">
             <h2>Get Started With James</h2>
+            <h3>Get Started With the official binaries </h3>
           </header>
           <section>
             <ul class="james-ul no-padding">
+              <li class="post-template"><span class="long-arrow-right">&#8594;</span><span><b>DOWNLOADS:</b><br>
+                The Apache James project wires together the different libraries composing James to provide a running services,
+                ready to download on the Apache mirrors. See
+                <a href="download.cgi">our download page</a>.<br/><br/>
+                Follow <a href="install.html">our installation guide</a>.
+              </span></li>
+            </ul>
+          </section>
+          <section id="first_docker" class="main">
+            <header class="major">
+              <h3>Get Started With James And Docker</h3>
+            </header>
+            <ul class="james-ul no-padding">
               <li class="post-template"><span class="long-arrow-right">&#8594;</span><span><b>
 WHAT WILL YOU TRY:</b><br>
               <span>Here you will try James server v 3.6.0 thanks to an image. This James image has a default configuration
diff --git a/src/site/xdoc/download.xml b/src/site/xdoc/download.xml
index 93607c2..e9a1f9d 100644
--- a/src/site/xdoc/download.xml
+++ b/src/site/xdoc/download.xml
@@ -164,7 +164,7 @@
           [<a href="https://downloads.apache.org/james/server/3.6.0/james-project-3.6.0-source-release.zip.asc">PGP</a>]</li>
 
         <li>Binary (ZIP Format) for Spring wiring:
-          <a href="https://www.apache.org/dyn/closer.lua/james/server/3.6.0/james-server-app-3.6.0-app.zip">apache-james-3.6.0-app.zip</a>
+          <a href="https://www.apache.org/dyn/closer.lua/james/server/3.6.0/james-server-app-3.6.0-app.zip">james-server-app-3.6.0-app.zip</a>
           [<a href="https://downloads.apache.org/james/server/3.6.0/james-server-app-3.6.0-app.zip.sha512">SHA-512</a>]
           [<a href="https://downloads.apache.org/james/server/3.6.0/james-server-app-3.6.0-app.zip.asc">PGP</a>]
         </li>
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index f32ffb4..97bed20 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -15,8 +15,33 @@
 Changes to apply between 3.6.x and 3.7.x will be reported here.
 
 Change list:
-
+ - [Adding the threadId to the ElasticSearch index](#adding-the-threadid-to-the-elasticsearch-index)
  - [Rework message denormalization](#rework-message-denormalization)
+ - [Adding threadId column to message metadata tables](#adding-threadid-column-to-message-metadata-tables)
+
+### Adding the threadId to the ElasticSearch index
+
+Date 22/07/2021
+
+JIRA: https://issues.apache.org/jira/browse/JAMES-3516
+
+Concerned product: Distributed James
+
+Add threadId to James document mapping to enable thread query search.
+
+We already have this field as part of newly created mappings, but we need to explicitly add this field to existing indices by doing:
+```
+curl -X PUT \
+  http://ip:ESport/mailbox_v1/_mapping \
+  -H 'Content-Type: application/json' \
+  -d '{
+    "properties": {
+        "threadId": {
+            "type": "keyword"
+        }
+    }
+}'
+```
 
 ### Rework message denormalization