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

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


The following commit(s) were added to refs/heads/master by this push:
     new 524df348d5 JAMES-4166 Supports collapseThread on top of OpenSearch 
(#2926)
524df348d5 is described below

commit 524df348d56decccc78ac36e9465c83aff07c7e7
Author: Trần Hồng Quân <[email protected]>
AuthorDate: Thu Feb 5 15:17:25 2026 +0700

    JAMES-4166 Supports collapseThread on top of OpenSearch (#2926)
    
    - Add collapseThreads option to SearchQuery
    - Implement collapseThreads support on top of OpenSearch
    - Expose collapseThreads support to the JMAP layer
---
 .../mailbox/model/MultimailboxesSearchQuery.java   |   1 +
 .../apache/james/mailbox/model/SearchQuery.java    |  23 +++-
 .../OpenSearchListeningMessageSearchIndex.java     |   2 +-
 .../opensearch/search/OpenSearchSearcher.java      |  25 +++-
 .../opensearch/OpenSearchIntegrationTest.java      |   5 +
 .../search/AbstractMessageSearchIndexTest.java     |  81 ++++++++++-
 .../contract/EmailQueryMethodContract.scala        | 149 ++++++++++++++++++++-
 .../memory/MemoryEmailQueryMethodNoViewTest.java   |  13 ++
 .../rfc8621/memory/MemoryEmailQueryMethodTest.java |  12 ++
 .../james/jmap/method/EmailQueryMethod.scala       |   1 +
 10 files changed, 298 insertions(+), 14 deletions(-)

diff --git 
a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java
 
b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java
index 7db38f8e0e..2d21d75bc7 100644
--- 
a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java
+++ 
b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MultimailboxesSearchQuery.java
@@ -178,6 +178,7 @@ public class MultimailboxesSearchQuery {
                 .andCriteria(searchQuery.getCriteria())
                 .andCriteria(criterion)
                 .sorts(searchQuery.getSorts())
+                .collapseThreads(searchQuery.shouldCollapseThreads())
                 .build(),
             inMailboxes,
             notInMailboxes,
diff --git 
a/mailbox/api/src/main/java/org/apache/james/mailbox/model/SearchQuery.java 
b/mailbox/api/src/main/java/org/apache/james/mailbox/model/SearchQuery.java
index 0653cb3191..72c3becdbc 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/SearchQuery.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/SearchQuery.java
@@ -762,11 +762,13 @@ public class SearchQuery {
         private final ImmutableList.Builder<Criterion> criterias;
         private final ImmutableSet.Builder<MessageUid> recentMessageUids;
         private Optional<ImmutableList<Sort>> sorts;
+        private boolean collapseThreads;
 
         public Builder() {
             criterias = ImmutableList.builder();
             sorts = Optional.empty();
             recentMessageUids = ImmutableSet.builder();
+            collapseThreads = false;
         }
 
         public Builder andCriteria(Criterion... criteria) {
@@ -804,10 +806,16 @@ public class SearchQuery {
             return this;
         }
 
+        public Builder collapseThreads(boolean collapseThreads) {
+            this.collapseThreads = collapseThreads;
+            return this;
+        }
+
         public SearchQuery build() {
             return new SearchQuery(criterias.build(),
                 sorts.orElse(ImmutableList.of()),
-                recentMessageUids.build());
+                recentMessageUids.build(),
+                collapseThreads);
         }
     }
 
@@ -834,11 +842,13 @@ public class SearchQuery {
     private final ImmutableList<Criterion> criteria;
     private final ImmutableList<Sort> sorts;
     private final ImmutableSet<MessageUid> recentMessageUids;
+    private final boolean collapseThreads;
 
-    private SearchQuery(ImmutableList<Criterion> criteria, ImmutableList<Sort> 
sorts, ImmutableSet<MessageUid> recentMessageUids) {
+    private SearchQuery(ImmutableList<Criterion> criteria, ImmutableList<Sort> 
sorts, ImmutableSet<MessageUid> recentMessageUids, boolean collapseThreads) {
         this.criteria = criteria;
         this.sorts = sorts;
         this.recentMessageUids = recentMessageUids;
+        this.collapseThreads = collapseThreads;
     }
 
     public List<Criterion> getCriteria() {
@@ -869,6 +879,10 @@ public class SearchQuery {
         return recentMessageUids;
     }
 
+    public boolean shouldCollapseThreads() {
+        return collapseThreads;
+    }
+
     @Override
     public String toString() {
         return "Search:" + criteria.toString();
@@ -876,7 +890,7 @@ public class SearchQuery {
 
     @Override
     public final int hashCode() {
-        return Objects.hashCode(criteria, sorts, recentMessageUids);
+        return Objects.hashCode(criteria, sorts, recentMessageUids, 
collapseThreads);
     }
 
     @Override
@@ -886,7 +900,8 @@ public class SearchQuery {
 
             return Objects.equal(this.criteria, that.criteria)
                 && Objects.equal(this.sorts, that.sorts)
-                && Objects.equal(this.recentMessageUids, 
that.recentMessageUids);
+                && Objects.equal(this.recentMessageUids, 
that.recentMessageUids)
+                && Objects.equal(this.collapseThreads, that.collapseThreads);
         }
         return false;
     }
diff --git 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/events/OpenSearchListeningMessageSearchIndex.java
 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/events/OpenSearchListeningMessageSearchIndex.java
index cfbc448c7d..4eb0f38132 100644
--- 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/events/OpenSearchListeningMessageSearchIndex.java
+++ 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/events/OpenSearchListeningMessageSearchIndex.java
@@ -370,7 +370,7 @@ public class OpenSearchListeningMessageSearchIndex extends 
ListeningMessageSearc
             return Flux.empty();
         }
 
-        return searcher.searchCollapsedByMessageId(mailboxIds, searchQuery, 
searchOptions, MESSAGE_ID_FIELD, !SEARCH_HIGHLIGHT)
+        return searcher.search(mailboxIds, searchQuery, searchOptions, 
MESSAGE_ID_FIELD, !SEARCH_HIGHLIGHT)
             .handle(this::extractMessageIdFromHit);
     }
 
diff --git 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/search/OpenSearchSearcher.java
 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/search/OpenSearchSearcher.java
index 3643ddc1bf..3717b8a6f0 100644
--- 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/search/OpenSearchSearcher.java
+++ 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/search/OpenSearchSearcher.java
@@ -106,10 +106,14 @@ public class OpenSearchSearcher {
             .searchHits();
     }
 
-    public Flux<Hit<ObjectNode>> 
searchCollapsedByMessageId(Collection<MailboxId> mailboxIds, SearchQuery query,
-                                                            SearchOptions 
searchOptions, List<String> fields,
-                                                            boolean 
searchHighlight) {
-        SearchRequest searchRequest = 
prepareCollapsedSearchByMessageId(mailboxIds, query, searchOptions, fields, 
searchHighlight);
+    public Flux<Hit<ObjectNode>> search(Collection<MailboxId> mailboxIds, 
SearchQuery query,
+                                        SearchOptions searchOptions, 
List<String> fields,
+                                        boolean searchHighlight) {
+        SearchRequest searchRequest = prepareCollapsedSearch(mailboxIds, 
query, searchOptions, fields, searchHighlight);
+        return search(searchRequest);
+    }
+
+    private Flux<Hit<ObjectNode>> search(SearchRequest searchRequest) {
         try {
             return client.search(searchRequest)
                 .flatMapMany(response -> 
Flux.fromIterable(response.hits().hits()));
@@ -144,8 +148,8 @@ public class OpenSearchSearcher {
             .build();
     }
 
-    private SearchRequest 
prepareCollapsedSearchByMessageId(Collection<MailboxId> mailboxIds, SearchQuery 
query,
-                                                            SearchOptions 
searchOptions, List<String> fields, boolean highlight) {
+    private SearchRequest prepareCollapsedSearch(Collection<MailboxId> 
mailboxIds, SearchQuery query,
+                                                 SearchOptions searchOptions, 
List<String> fields, boolean highlight) {
         List<SortOptions> sorts = query.getSorts()
             .stream()
             .flatMap(SortConverter::convertSort)
@@ -162,7 +166,7 @@ public class OpenSearchSearcher {
             .size(size)
             .storedFields(fields)
             .sort(sorts)
-            .collapse(collapse -> 
collapse.field(JsonMessageConstants.MESSAGE_ID));
+            .collapse(collapse -> 
collapse.field(retrieveCollapseField(query)));
 
         if (highlight) {
             request.highlight(highlightQuery);
@@ -174,6 +178,13 @@ public class OpenSearchSearcher {
             .build();
     }
 
+    private String retrieveCollapseField(SearchQuery query) {
+        if (query.shouldCollapseThreads()) {
+            return JsonMessageConstants.THREAD_ID;
+        }
+        return JsonMessageConstants.MESSAGE_ID;
+    }
+
     private Optional<String> toRoutingKey(Collection<MailboxId> mailboxIds) {
         if (mailboxIds.size() < MAX_ROUTING_KEY) {
             return Optional.of(mailboxIds.stream()
diff --git 
a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
 
b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
index 036decb70a..870e35b9bb 100644
--- 
a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
+++ 
b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
@@ -820,4 +820,9 @@ class OpenSearchIntegrationTest extends 
AbstractMessageSearchIndexTest {
             .untilAsserted(() -> assertThat(messageSearchIndex.search(session, 
List.of(mailboxId), SearchQuery.matchAll(), 
SearchOptions.limit(Limit.limit(100))).toStream().count())
                 .isEqualTo(expectedCountResult));
     }
+
+    @Override
+    protected boolean supportsCollapseThreads() {
+        return true;
+    }
 }
\ No newline at end of file
diff --git 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
index 3e4a35a7a6..83f766eb38 100644
--- 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
+++ 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
@@ -78,6 +78,7 @@ import org.apache.james.mime4j.message.MultipartBuilder;
 import org.apache.james.mime4j.message.SingleBodyBuilder;
 import org.apache.james.util.ClassLoaderUtils;
 import org.apache.james.util.streams.Limit;
+import org.apache.james.util.streams.Offset;
 import org.apache.james.utils.UpdatableTickingClock;
 import org.awaitility.Awaitility;
 import org.awaitility.core.ConditionFactory;
@@ -291,6 +292,10 @@ public abstract class AbstractMessageSearchIndexTest {
 
     protected abstract MessageId initOtherBasedMessageId();
 
+    protected boolean supportsCollapseThreads() {
+        return false;
+    }
+
     @Test
     void searchingMessageInMultipleMailboxShouldNotReturnTwiceTheSameMessage() 
throws MailboxException {
         assumeTrue(messageIdManager != null);
@@ -1706,6 +1711,76 @@ public abstract class AbstractMessageSearchIndexTest {
         assertThat(actual).isEmpty();
     }
 
+    @Test
+    void collapseThreadsShouldReturnOneMessagePerThread() throws 
MailboxException {
+        assumeTrue(supportsCollapseThreads(), "This test is only relevant when 
collapseThread is supported");
+
+        ThreadId threadId1 = ThreadId.fromBaseMessageId(newBasedMessageId);
+        ThreadId threadId2 = ThreadId.fromBaseMessageId(otherBasedMessageId);
+        MailboxMessage message1 = createMessage(quanMailbox, threadId1);
+        MailboxMessage message2 = createMessage(quanMailbox, threadId1);
+        MailboxMessage message3 = createMessage(quanMailbox, threadId2);
+
+        // Thread 1: message1 and message2
+        // Thread 2: message3
+        appendMessageThenDispatchAddedEvent(quanMailbox, message1);
+        appendMessageThenDispatchAddedEvent(quanMailbox, message2);
+        appendMessageThenDispatchAddedEvent(quanMailbox, message3);
+
+        awaitMessageCount(ImmutableList.of(), SearchQuery.matchAll(), 16);
+
+        SearchQuery collapseThreadsQuery = SearchQuery.builder()
+            .andCriteria(SearchQuery.all())
+            .collapseThreads(true)
+            .build();
+
+        List<MessageId> actual = messageSearchIndex.search(quanSession, 
ImmutableList.of(quanMailbox.getMailboxId()), collapseThreadsQuery, LIMIT)
+            .collectList().block();
+
+        assertThat(actual).isEqualTo(ImmutableList.of(message1.getMessageId(), 
message3.getMessageId()));
+    }
+
+    @Test
+    void collapseThreadsShouldSupportPagination() throws MailboxException {
+        assumeTrue(supportsCollapseThreads(), "This test is only relevant when 
collapseThread is supported");
+
+        ThreadId threadId1 = ThreadId.fromBaseMessageId(newBasedMessageId);
+        ThreadId threadId2 = ThreadId.fromBaseMessageId(otherBasedMessageId);
+        ThreadId threadId3 = 
ThreadId.fromBaseMessageId(messageIdFactory.generate());
+
+        // Thread1 has two messages; thread2 and thread3 have one each.
+        MailboxMessage thread1Older = createMessage(quanMailbox, threadId1, 
new Date(1000));
+        MailboxMessage thread1Newer = createMessage(quanMailbox, threadId1, 
new Date(3000));
+        MailboxMessage thread2Message = createMessage(quanMailbox, threadId2, 
new Date(2000));
+        MailboxMessage thread3Message = createMessage(quanMailbox, threadId3, 
new Date(500));
+
+        appendMessageThenDispatchAddedEvent(quanMailbox, thread1Older);
+        appendMessageThenDispatchAddedEvent(quanMailbox, thread1Newer);
+        appendMessageThenDispatchAddedEvent(quanMailbox, thread2Message);
+        appendMessageThenDispatchAddedEvent(quanMailbox, thread3Message);
+
+        awaitMessageCount(ImmutableList.of(quanMailbox.getMailboxId()), 
SearchQuery.matchAll(), 4);
+
+        SearchQuery collapseThreadsQuery = SearchQuery.builder()
+            .andCriteria(SearchQuery.all())
+            .sorts(new Sort(SortClause.Arrival, Order.REVERSE))
+            .collapseThreads(true)
+            .build();
+        List<MessageId> allCollapsedMessages = 
messageSearchIndex.search(quanSession,
+                ImmutableList.of(quanMailbox.getMailboxId()), 
collapseThreadsQuery, SearchOptions.of(Offset.none(), Limit.limit(10)))
+            .collectList().block();
+        List<MessageId> paginatedCollapsedMessages = 
messageSearchIndex.search(quanSession,
+                ImmutableList.of(quanMailbox.getMailboxId()), 
collapseThreadsQuery, SearchOptions.of(Offset.from(1), Limit.limit(2)))
+            .collectList().block();
+
+        // With Arrival sort descending, the collapsed list should be ordered 
by:
+        // 1) thread1 (newest at date=3000), 2) thread2 (date=2000), 3) 
thread3 (date=500).
+        
assertThat(allCollapsedMessages).containsExactly(thread1Newer.getMessageId(), 
thread2Message.getMessageId(), thread3Message.getMessageId());
+
+        // Request offset=1, limit=2 to fetch from the second message 
(thread2, thread3).
+        
assertThat(paginatedCollapsedMessages).containsExactly(thread2Message.getMessageId(),
 thread3Message.getMessageId());
+    }
+
     private void appendMessageThenDispatchAddedEvent(Mailbox mailbox, 
MailboxMessage mailboxMessage) throws MailboxException {
         MessageMetaData messageMetaData = messageMapper.add(mailbox, 
mailboxMessage);
         eventBus.dispatch(EventFactory.added()
@@ -1720,12 +1795,16 @@ public abstract class AbstractMessageSearchIndexTest {
     }
 
     private SimpleMailboxMessage createMessage(Mailbox mailbox, ThreadId 
threadId) {
+        return createMessage(mailbox, threadId, new Date());
+    }
+
+    private SimpleMailboxMessage createMessage(Mailbox mailbox, ThreadId 
threadId, Date date) {
         MessageId messageId = messageIdFactory.generate();
         String content = "Some content";
         int bodyStart = 16;
         return new SimpleMailboxMessage(messageId,
             threadId,
-            new Date(),
+            date,
             content.length(),
             bodyStart,
             new ByteContent(content.getBytes()),
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 079f193f55..ebc55a5dc3 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
@@ -40,6 +40,7 @@ import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
 import org.apache.james.jmap.core.UTCDate
 import org.apache.james.jmap.http.UserCredential
 import 
org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, 
ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, 
baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
 import org.apache.james.mailbox.FlagsBuilder
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.Right
@@ -54,7 +55,7 @@ import org.apache.james.util.ClassLoaderUtils
 import org.apache.james.utils.DataProbeImpl
 import org.awaitility.Awaitility
 import org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS
-import org.junit.jupiter.api.{BeforeEach, Test}
+import org.junit.jupiter.api.{BeforeEach, Tag, Test}
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.{Arguments, MethodSource, ValueSource}
 import org.threeten.extra.Seconds
@@ -7759,6 +7760,152 @@ trait EmailQueryMethodContract {
     }
   }
 
+  @Test
+  def collapseThreadsShouldApplyOnSearchIndexPath(server: GuiceJamesServer): 
Unit = {
+    val thread1Message: Message = buildTestThreadMessage("thread-1", 
"Message-ID-1")
+    val thread2Message: Message = buildTestThreadMessage("thread-2", 
"Message-ID-2")
+
+    val threeDaysBefore = Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val twoDaysBefore = Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val oneDayBefore = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+
+    // Thread 1: message 1 (oldest), message 3 (newest)
+    // Thread 2: message 2
+    val messageId1: MessageId = sendMessageToBobInbox(server, thread1Message, 
threeDaysBefore)
+    val messageId2: MessageId = sendMessageToBobInbox(server, thread2Message, 
twoDaysBefore)
+    val messageId3: MessageId = sendMessageToBobInbox(server, thread1Message, 
oneDayBefore)
+
+    val request: String =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize()}",
+         |        "text": "testmail"
+         |      },
+         |      "sort": [{
+         |        "property":"receivedAt",
+         |        "isAscending": false
+         |      }],
+         |      "collapseThreads": true
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      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": "${SESSION_STATE.value}",
+           |    "methodResponses": [[
+           |            "Email/query",
+           |            {
+           |                "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "queryState": "${generateQueryState(messageId3, 
messageId2)}",
+           |                "canCalculateChanges": false,
+           |                "position": 0,
+           |                "limit": 256,
+           |                "ids": ["${messageId3.serialize}", 
"${messageId2.serialize}"]
+           |            },
+           |            "c1"
+           |        ]]
+           |}""".stripMargin)
+    }
+  }
+
+  @Test
+  @Tag(CategoryTags.BASIC_FEATURE)
+  def collapseThreadsShouldApplyPaginationOnCollapsedResults(server: 
GuiceJamesServer): Unit = {
+    val thread1Message: Message = buildTestThreadMessage("thread-1", 
"Message-ID-1")
+    val thread2Message: Message = buildTestThreadMessage("thread-2", 
"Message-ID-2")
+    val thread3Message: Message = buildTestThreadMessage("thread-3", 
"Message-ID-3")
+
+    val fourDaysBefore = Date.from(ZonedDateTime.now().minusDays(4).toInstant)
+    val threeDaysBefore = Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val twoDaysBefore = Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val oneDayBefore = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+
+    // Thread 1: message 1 (oldest), message 2 (newest)
+    // Thread 2: message 3
+    // Thread 3: message 4
+    val messageId1: MessageId = sendMessageToBobInbox(server, thread1Message, 
twoDaysBefore)
+    val messageId2: MessageId = sendMessageToBobInbox(server, thread1Message, 
oneDayBefore)
+    val messageId3: MessageId = sendMessageToBobInbox(server, thread2Message, 
threeDaysBefore)
+    val messageId4: MessageId = sendMessageToBobInbox(server, thread3Message, 
fourDaysBefore)
+
+    val request: String =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize()}",
+         |        "text": "testmail"
+         |      },
+         |      "sort": [{
+         |        "property":"receivedAt",
+         |        "isAscending": false
+         |      }],
+         |      "position": 1,
+         |      "limit": 2,
+         |      "collapseThreads": true
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response: String = `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": "${SESSION_STATE.value}",
+           |    "methodResponses": [[
+           |            "Email/query",
+           |            {
+           |                "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "queryState": "${generateQueryState(messageId3, 
messageId4)}",
+           |                "canCalculateChanges": false,
+           |                "position": 1,
+           |                "ids": ["${messageId3.serialize}", 
"${messageId4.serialize}"]
+           |            },
+           |            "c1"
+           |        ]]
+           |}""".stripMargin)
+    }
+  }
+
   private def sendMessageToBobInbox(server: GuiceJamesServer, message: 
Message, requestDate: Date): MessageId = {
     server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java
index 49cd6cc5b3..2587a843ef 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodNoViewTest.java
@@ -101,4 +101,17 @@ public class MemoryEmailQueryMethodNoViewTest implements 
EmailQueryMethodContrac
     @Disabled("JAMES-3340 Not supported for no email query view")
     public void 
inMailboxBeforeSortedByReceivedAtShouldCollapseThreads(GuiceJamesServer server) 
{
     }
+
+    @Test
+    @Override
+    @Disabled("JAMES-4166 collapseThreads does not support Lucene 
implementation yet")
+    public void collapseThreadsShouldApplyOnSearchIndexPath(GuiceJamesServer 
server) {
+    }
+
+    @Test
+    @Override
+    @Disabled("JAMES-4166 collapseThreads does not support Lucene 
implementation yet")
+    public void 
collapseThreadsShouldApplyPaginationOnCollapsedResults(GuiceJamesServer server) 
{
+    }
+
 }
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodTest.java
index f5152dd222..970107e19c 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailQueryMethodTest.java
@@ -52,4 +52,16 @@ public class MemoryEmailQueryMethodTest extends MemoryBase 
implements EmailQuery
         
EmailQueryMethodContract.super.shouldListMailsReceivedAfterADate(server);
     }
 
+    @Test
+    @Override
+    @Disabled("JAMES-4166 collapseThreads does not support Lucene 
implementation yet")
+    public void collapseThreadsShouldApplyOnSearchIndexPath(GuiceJamesServer 
server) {
+    }
+
+    @Test
+    @Override
+    @Disabled("JAMES-4166 collapseThreads does not support Lucene 
implementation yet")
+    public void 
collapseThreadsShouldApplyPaginationOnCollapsedResults(GuiceJamesServer server) 
{
+    }
+
 }
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
index 5a7672a8a6..3b18389198 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
@@ -214,6 +214,7 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
       .flatMap(sorts => for {
         queryFilter <- QueryFilter.buildQuery(request)
       } yield {
+        queryFilter.collapseThreads(getCollapseThreads(request))
         if (sorts.isEmpty) {
           queryFilter
             .build()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to