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]