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)))))
+ })
+ }
+
}