JAMES-3516 Implement non naive Thread/get method
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 a2c6c69..c9f4ee1 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
@@ -326,12 +326,13 @@
     Publisher<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) throws MailboxException;
 
     /**
+     * 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  a list of MessageId of messages belong to that Thread
+     * @return  the list of MessageId of messages belonging to that Thread
      */
     Publisher<MessageId> getThread(ThreadId threadId, MailboxSession session) throws MailboxException;
 
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
index ff31b6d..f6f4c3e 100644
--- 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
@@ -22,10 +22,6 @@
 import org.apache.james.mailbox.model.ThreadId;
 
 public class ThreadNotFoundException extends MailboxException {
-    public ThreadNotFoundException(String message) {
-        super(message);
-    }
-
     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/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 57f6bdb..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
@@ -34,7 +34,7 @@
 import org.apache.james.mailbox.store.mail.model.MapperProvider;
 
 public class SearchThreadIdGuessingAlgorithmTest extends ThreadIdGuessingAlgorithmContract {
-    InMemoryMailboxManager mailboxManager;
+    private InMemoryMailboxManager mailboxManager;
 
     @Override
     protected CombinationManagerTestSystem createTestingData() {
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 c534c1a..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
@@ -62,6 +62,8 @@
 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 {
@@ -250,14 +252,17 @@
 
         Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
 
-        assertThat(messageIds.collectList().block()).isEqualTo(List.of(message1.getMessageId(), message2.getMessageId(), message3.getMessageId()));
+        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);
+        assertThatThrownBy(() -> testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession).collectList().block())
+            .getCause()
+            .isInstanceOf(ThreadNotFoundException.class);
     }
 
     @Test
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/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/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/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/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)))))
+      })
+  }
+
 }