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

btellier 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 1367ff1  JAMES-3440 EmailQueryView support for sort by receivedAt 
(#710)
1367ff1 is described below

commit 1367ff1ebef1482b36f8613755759cc5d854e205
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Oct 27 16:15:12 2021 +0700

    JAMES-3440 EmailQueryView support for sort by receivedAt (#710)
    
    Sample JMAP requests:
    
    ```
    ["Email/query",
    {
      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
      "filter: {
         "inMailbox":"abcd"
      }
      "sort": [{
        "property":"sentAt",
        "isAscending": false
      }]
    },
    "c1"]
    ```
    
    And
    
    ```
    ["Email/query",
    {
      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
      "filter: {
          "inMailbox":"abcd",
          "after":"aDate"
      }
      "sort": [{
        "property":"receivedAt",
        "isAscending": false
      }]
    },
    "c1"]
    ```
---
 .../projections/CassandraEmailQueryView.java       |  42 ++-
 .../projections/CassandraEmailQueryViewTest.java   |   2 +-
 .../james/jmap/api/projections/EmailQueryView.java |  50 ++-
 .../memory/projections/MemoryEmailQueryView.java   |  17 +
 .../api/projections/EmailQueryViewContract.java    |  20 ++
 .../contract/EmailQueryMethodContract.scala        | 350 +++++++++++++++++++++
 .../org/apache/james/jmap/mail/EmailQuery.scala    |   1 +
 .../james/jmap/method/EmailQueryMethod.scala       |  44 +++
 8 files changed, 517 insertions(+), 9 deletions(-)

diff --git 
a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
 
b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
index 979296e..ca3e593 100644
--- 
a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
+++ 
b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java
@@ -62,9 +62,9 @@ import reactor.core.publisher.Mono;
 public class CassandraEmailQueryView implements EmailQueryView {
     private static final String LIMIT_MARKER = "LIMIT_BIND_MARKER";
 
-    private final CassandraMessageId.Factory messageIdFactory;
     private final CassandraAsyncExecutor executor;
     private final PreparedStatement listMailboxContentBySentAt;
+    private final PreparedStatement listMailboxContentByReceivedAt;
     private final PreparedStatement listMailboxContentSinceSentAt;
     private final PreparedStatement listMailboxContentSinceReceivedAt;
     private final PreparedStatement insertInLookupTable;
@@ -79,8 +79,7 @@ public class CassandraEmailQueryView implements 
EmailQueryView {
     private final PreparedStatement lookupDate;
 
     @Inject
-    public CassandraEmailQueryView(CassandraMessageId.Factory 
messageIdFactory, Session session) {
-        this.messageIdFactory = messageIdFactory;
+    public CassandraEmailQueryView(Session session) {
         this.executor = new CassandraAsyncExecutor(session);
 
         listMailboxContentBySentAt = session.prepare(select()
@@ -89,6 +88,12 @@ public class CassandraEmailQueryView implements 
EmailQueryView {
             .orderBy(desc(SENT_AT))
             .limit(bindMarker(LIMIT_MARKER)));
 
+        listMailboxContentByReceivedAt = session.prepare(select()
+            .from(TABLE_NAME_RECEIVED_AT)
+            .where(eq(MAILBOX_ID, bindMarker(MAILBOX_ID)))
+            .orderBy(desc(RECEIVED_AT))
+            .limit(bindMarker(LIMIT_MARKER)));
+
         listMailboxContentSinceSentAt = session.prepare(select()
             .from(TABLE_NAME_SENT_AT)
             .where(eq(MAILBOX_ID, bindMarker(MAILBOX_ID)))
@@ -161,7 +166,18 @@ public class CassandraEmailQueryView implements 
EmailQueryView {
         return executor.executeRows(listMailboxContentBySentAt.bind()
                 .setUUID(MAILBOX_ID, cassandraId.asUuid())
                 .setInt(LIMIT_MARKER, limit.getLimit().get()))
-            .map(row -> messageIdFactory.of(row.getUUID(MESSAGE_ID)));
+            .map(row -> 
CassandraMessageId.Factory.of(row.getUUID(MESSAGE_ID)));
+    }
+
+    @Override
+    public Flux<MessageId> listMailboxContentSortedByReceivedAt(MailboxId 
mailboxId, Limit limit) {
+        Preconditions.checkArgument(!limit.isUnlimited(), "Limit should be 
defined");
+
+        CassandraId cassandraId = (CassandraId) mailboxId;
+        return executor.executeRows(listMailboxContentByReceivedAt.bind()
+            .setUUID(MAILBOX_ID, cassandraId.asUuid())
+            .setInt(LIMIT_MARKER, limit.getLimit().get()))
+            .map(row -> 
CassandraMessageId.Factory.of(row.getUUID(MESSAGE_ID)));
     }
 
     @Override
@@ -175,7 +191,7 @@ public class CassandraEmailQueryView implements 
EmailQueryView {
                 .setUUID(MAILBOX_ID, cassandraId.asUuid())
                 .setTimestamp(RECEIVED_AT, sinceDate))
             .map(row -> {
-                CassandraMessageId messageId = 
messageIdFactory.of(row.getUUID(MESSAGE_ID));
+                CassandraMessageId messageId = 
CassandraMessageId.Factory.of(row.getUUID(MESSAGE_ID));
                 Date receivedAt = row.getTimestamp(RECEIVED_AT);
                 Date sentAt = row.getTimestamp(SENT_AT);
 
@@ -189,6 +205,20 @@ public class CassandraEmailQueryView implements 
EmailQueryView {
     }
 
     @Override
+    public Flux<MessageId> 
listMailboxContentSinceReceivedAtSortedByReceivedAt(MailboxId mailboxId, 
ZonedDateTime since, Limit limit) {
+        Preconditions.checkArgument(!limit.isUnlimited(), "Limit should be 
defined");
+
+        Date sinceDate = Date.from(since.toInstant());
+        CassandraId cassandraId = (CassandraId) mailboxId;
+
+        return executor.executeRows(listMailboxContentSinceReceivedAt.bind()
+            .setUUID(MAILBOX_ID, cassandraId.asUuid())
+            .setTimestamp(RECEIVED_AT, sinceDate))
+            .<MessageId>map(row -> 
CassandraMessageId.Factory.of(row.getUUID(MESSAGE_ID)))
+            .take(limit.getLimit().get());
+    }
+
+    @Override
     public Flux<MessageId> listMailboxContentSinceSentAt(MailboxId mailboxId, 
ZonedDateTime since, Limit limit) {
         Preconditions.checkArgument(!limit.isUnlimited(), "Limit should be 
defined");
 
@@ -199,7 +229,7 @@ public class CassandraEmailQueryView implements 
EmailQueryView {
             .setUUID(MAILBOX_ID, cassandraId.asUuid())
             .setInt(LIMIT_MARKER, limit.getLimit().get())
             .setTimestamp(SENT_AT, sinceDate))
-            .map(row -> 
messageIdFactory.of(row.getUUID(MESSAGE_ID_LOWERCASE)));
+            .map(row -> 
CassandraMessageId.Factory.of(row.getUUID(MESSAGE_ID_LOWERCASE)));
     }
 
     @Override
diff --git 
a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewTest.java
 
b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewTest.java
index 08692e7..a883689 100644
--- 
a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewTest.java
+++ 
b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewTest.java
@@ -45,7 +45,7 @@ public class CassandraEmailQueryViewTest implements 
EmailQueryViewContract {
 
     @BeforeEach
     void setUp(CassandraCluster cassandra) {
-        testee = new CassandraEmailQueryView(MESSAGE_ID_FACTORY, 
cassandra.getConf());
+        testee = new CassandraEmailQueryView(cassandra.getConf());
     }
 
     @Override
diff --git 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java
 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java
index 4c33af7..ff0e9ca 100644
--- 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java
+++ 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java
@@ -90,7 +90,7 @@ public interface EmailQueryView {
      *       "filter: {
      *           "inMailbox":"abcd"
      *       }
-     *       "comparator": [{
+     *       "sort": [{
      *         "property":"sentAt",
      *         "isAscending": false
      *       }]
@@ -106,6 +106,29 @@ public interface EmailQueryView {
     Flux<MessageId> listMailboxContent(MailboxId mailboxId, Limit limit);
 
     /**
+     *
+     * Sample JMAP requests:
+     *
+     *    - RFC-8621:
+     *
+     *    ["Email/query",
+     *     {
+     *       "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+     *       "filter: {
+     *           "inMailbox":"abcd"
+     *       }
+     *       "sort": [{
+     *         "property":"receivedAt",
+     *         "isAscending": false
+     *       }]
+     *     },
+     *     "c1"]
+     *
+     * @return messageIds of the messages in this mailbox, sorted by sentAt.
+     */
+    Flux<MessageId> listMailboxContentSortedByReceivedAt(MailboxId mailboxId, 
Limit limit);
+
+    /**
      *  Sample JMAP requests:
      *
      *      - RFC-8621:
@@ -117,7 +140,7 @@ public interface EmailQueryView {
      *           "inMailbox":"abcd",
      *           "after":"aDate"
      *       }
-     *       "comparator": [{
+     *       "sort": [{
      *         "property":"sentAt",
      *         "isAscending": false
      *       }]
@@ -131,6 +154,29 @@ public interface EmailQueryView {
     /**
      *  Sample JMAP requests:
      *
+     *      - RFC-8621:
+     *
+     *    ["Email/query",
+     *     {
+     *       "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+     *       "filter: {
+     *           "inMailbox":"abcd",
+     *           "after":"aDate"
+     *       }
+     *       "sort": [{
+     *         "property":"receivedAt",
+     *         "isAscending": false
+     *       }]
+     *     },
+     *     "c1"]
+     *
+     * @return messageIds of the messages in this mailbox, since being 
receivedAt. Sorted by sentAt.
+     */
+    Flux<MessageId> 
listMailboxContentSinceReceivedAtSortedByReceivedAt(MailboxId mailboxId, 
ZonedDateTime since, Limit limit);
+
+    /**
+     *  Sample JMAP requests:
+     *
      *   - Draft
      *
      *   [["getMessageList", {"filter":{"after":"aDate", "inMailboxes": 
["abcd"]}, "sort": ["date desc"]}, "#0"]]
diff --git 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java
 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java
index fd72560..c5cbfea 100644
--- 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java
+++ 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java
@@ -78,6 +78,23 @@ public class MemoryEmailQueryView implements EmailQueryView {
     }
 
     @Override
+    public Flux<MessageId> listMailboxContentSortedByReceivedAt(MailboxId 
mailboxId, Limit limit) {
+        return Flux.fromIterable(entries.row(mailboxId).values())
+            .sort(Comparator.comparing(Entry::getReceivedAt).reversed())
+            .map(Entry::getMessageId)
+            .take(limit.getLimit().get());
+    }
+
+    @Override
+    public Flux<MessageId> 
listMailboxContentSinceReceivedAtSortedByReceivedAt(MailboxId mailboxId, 
ZonedDateTime since, Limit limit) {
+        return Flux.fromIterable(entries.row(mailboxId).values())
+            .filter(e -> e.getReceivedAt().isAfter(since) || 
e.getReceivedAt().isEqual(since))
+            .sort(Comparator.comparing(Entry::getReceivedAt).reversed())
+            .map(Entry::getMessageId)
+            .take(limit.getLimit().get());
+    }
+
+    @Override
     public Mono<Void> delete(MailboxId mailboxId, MessageId messageId) {
         return Mono.fromRunnable(() -> entries.remove(mailboxId, messageId));
     }
diff --git 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java
 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java
index b48d19f..b792795 100644
--- 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java
+++ 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java
@@ -228,6 +228,26 @@ public interface EmailQueryViewContract {
     }
 
     @Test
+    default void 
listMailboxContentSortedByReceivedAtShouldBeSortedByReceivedAt() {
+        testee().save(mailboxId1(), DATE_1, DATE_4, messageId1()).block();
+        testee().save(mailboxId1(), DATE_2, DATE_3, messageId2()).block();
+        testee().save(mailboxId1(), DATE_5, DATE_6, messageId3()).block();
+
+        assertThat(testee().listMailboxContentSortedByReceivedAt(mailboxId1(), 
Limit.limit(12)).collectList().block())
+            .containsExactly(messageId3(), messageId1(), messageId2());
+    }
+
+    @Test
+    default void 
listMailboxContentSinceSortedByReceivedAtShouldBeSortedByReceivedAt() {
+        testee().save(mailboxId1(), DATE_1, DATE_4, messageId1()).block();
+        testee().save(mailboxId1(), DATE_2, DATE_3, messageId2()).block();
+        testee().save(mailboxId1(), DATE_5, DATE_6, messageId3()).block();
+
+        
assertThat(testee().listMailboxContentSinceReceivedAtSortedByReceivedAt(mailboxId1(),
 DATE_4, Limit.limit(12)).collectList().block())
+            .containsExactly(messageId3(), messageId1());
+    }
+
+    @Test
     default void clearShouldNotFailWhenEmpty() {
         assertThatCode(() -> 
testee().delete(mailboxId1()).block()).doesNotThrowAnyException();
     }
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 ad16a8b..c8f9a92 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
@@ -1511,6 +1511,356 @@ trait EmailQueryMethodContract {
   }
 
   @Test
+  def inMailboxAfterSortedByReceivedAtShouldYieldExpectedResult(server: 
GuiceJamesServer): Unit = {
+    val beforeRequestDate1 = 
Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val beforeRequestDate2 = 
Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val requestDate = ZonedDateTime.now().minusDays(1)
+    val afterRequestDate1 = Date.from(ZonedDateTime.now().toInstant)
+    val afterRequestDate2 = 
Date.from(ZonedDateTime.now().plusDays(1).toInstant)
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    val messageId1: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(beforeRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId2: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+          .withInternalDate(beforeRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val messageId3: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(afterRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId4: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+        .withInternalDate(afterRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize()}",
+         |        "after": 
"${UTCDate(requestDate).asUTC.format(UTC_DATE_FORMAT)}"
+         |       },
+         |      "sort": [{
+         |        "property":"receivedAt",
+         |        "isAscending": false
+         |      }]
+         |    },
+         |    "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(messageId4, 
messageId3)}",
+           |                "canCalculateChanges": false,
+           |                "position": 0,
+           |                "limit": 256,
+           |                "ids": ["${messageId4.serialize}", 
"${messageId3.serialize}"]
+           |            },
+           |            "c1"
+           |        ]]
+           |}""".stripMargin)
+    }
+  }
+
+  @Test
+  def 
inMailboxAfterSortedByReceivedAtShouldYieldExpectedResultWithOffsetAndLimit(server:
 GuiceJamesServer): Unit = {
+    val beforeRequestDate1 = 
Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val beforeRequestDate2 = 
Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val requestDate = ZonedDateTime.now().minusDays(1)
+    val afterRequestDate1 = Date.from(ZonedDateTime.now().toInstant)
+    val afterRequestDate2 = 
Date.from(ZonedDateTime.now().plusDays(1).toInstant)
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    val messageId1: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(beforeRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId2: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+          .withInternalDate(beforeRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val messageId3: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(afterRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId4: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+        .withInternalDate(afterRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize()}",
+         |        "after": 
"${UTCDate(requestDate).asUTC.format(UTC_DATE_FORMAT)}"
+         |       },
+         |      "sort": [{
+         |        "property":"receivedAt",
+         |        "isAscending": false
+         |      }],
+         |      "limit": 1,
+         |      "position": 1
+         |    },
+         |    "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)}",
+           |                "canCalculateChanges": false,
+           |                "position": 1,
+           |                "ids": ["${messageId3.serialize}"]
+           |            },
+           |            "c1"
+           |        ]]
+           |}""".stripMargin)
+    }
+  }
+
+  @Test
+  def inMailboxSortedByReceivedAtShouldYieldExpectedResult(server: 
GuiceJamesServer): Unit = {
+    val beforeRequestDate1 = 
Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val beforeRequestDate2 = 
Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val requestDate = ZonedDateTime.now().minusDays(1)
+    val afterRequestDate1 = Date.from(ZonedDateTime.now().toInstant)
+    val afterRequestDate2 = 
Date.from(ZonedDateTime.now().plusDays(1).toInstant)
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    val messageId1: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(beforeRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId2: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+          .withInternalDate(beforeRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val messageId3: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(afterRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId4: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+        .withInternalDate(afterRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize()}"
+         |       },
+         |      "sort": [{
+         |        "property":"receivedAt",
+         |        "isAscending": false
+         |      }]
+         |    },
+         |    "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(messageId4, 
messageId3, messageId2, messageId1)}",
+           |                "canCalculateChanges": false,
+           |                "position": 0,
+           |                "limit": 256,
+           |                "ids": ["${messageId4.serialize}", 
"${messageId3.serialize}", "${messageId2.serialize}", "${messageId1.serialize}"]
+           |            },
+           |            "c1"
+           |        ]]
+           |}""".stripMargin)
+    }
+  }
+
+  @Test
+  def 
inMailboxSortedByReceivedAtShouldYieldExpectedResultWithOffsetAndLimit(server: 
GuiceJamesServer): Unit = {
+    val beforeRequestDate1 = 
Date.from(ZonedDateTime.now().minusDays(3).toInstant)
+    val beforeRequestDate2 = 
Date.from(ZonedDateTime.now().minusDays(2).toInstant)
+    val requestDate = ZonedDateTime.now().minusDays(1)
+    val afterRequestDate1 = Date.from(ZonedDateTime.now().toInstant)
+    val afterRequestDate2 = 
Date.from(ZonedDateTime.now().plusDays(1).toInstant)
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    val messageId1: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(beforeRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId2: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+        .withInternalDate(beforeRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val messageId3: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+        AppendCommand.builder()
+          .withInternalDate(afterRequestDate1)
+          .build(buildTestMessage))
+      .getMessageId
+
+    val messageId4: MessageId = mailboxProbe
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), 
AppendCommand.builder()
+        .withInternalDate(afterRequestDate2)
+        
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter": {
+         |        "inMailbox": "${mailboxId.serialize()}"
+         |      },
+         |      "sort": [{
+         |        "property":"receivedAt",
+         |        "isAscending": false
+         |      }],
+         |      "limit": 2,
+         |      "position": 1
+         |    },
+         |    "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": 1,
+           |                "ids": ["${messageId3.serialize}", 
"${messageId2.serialize}"]
+           |            },
+           |            "c1"
+           |        ]]
+           |}""".stripMargin)
+    }
+  }
+
+  @Test
   def shouldListMailsInAllUserMailboxes(server: GuiceJamesServer): Unit = {
     val message: Message = buildTestMessage
     
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
index de06a1e..63e19ff 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
@@ -199,6 +199,7 @@ case class Collation(value: String) extends AnyVal
 
 object Comparator {
   val SENT_AT_DESC: Comparator = Comparator(SentAtSortProperty, 
Some(IsAscending.DESCENDING), None)
+  val RECEIVED_AT_DESC: Comparator = Comparator(ReceivedAtSortProperty, 
Some(IsAscending.DESCENDING), None)
 }
 
 case class Comparator(property: SortProperty,
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 6f9de13..74f9596 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
@@ -97,6 +97,10 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
         queryViewForListingSortedBySentAt(session, position, limit, request)
       case request: EmailQueryRequest if 
matchesInMailboxAfterSortedBySentAt(request) =>
         queryViewForContentAfterSortedBySentAt(session, position, limit, 
request)
+      case request: EmailQueryRequest if 
matchesInMailboxSortedByReceivedAt(request) =>
+        queryViewForListingSortedByReceivedAt(session, position, limit, 
request)
+      case request: EmailQueryRequest if 
matchesInMailboxAfterSortedByReceivedAt(request) =>
+        queryViewForContentAfterSortedByReceivedAt(session, position, limit, 
request)
       case _ => executeQueryAgainstSearchIndex(session, searchQuery, position, 
limit)
     }
 
@@ -119,6 +123,22 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
       })
   }
 
+  private def queryViewForContentAfterSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest): SMono[Seq[MessageId]] = {
+    val condition: FilterCondition = 
request.filter.get.asInstanceOf[FilterCondition]
+    val mailboxId: MailboxId = condition.inMailbox.get
+    val after: ZonedDateTime = condition.after.get.asUTC
+    SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
+      .`then`(SFlux.fromPublisher(
+        
emailQueryView.listMailboxContentSinceReceivedAtSortedByReceivedAt(mailboxId, 
after, JavaLimit.from(limitToUse.value + position.value)))
+        .drop(position.value)
+        .take(limitToUse.value)
+        .collectSeq())
+      .onErrorResume({
+        case _: MailboxNotFoundException => SMono.just[Seq[MessageId]](Seq())
+        case e => SMono.error[Seq[MessageId]](e)
+      })
+  }
+
   private def queryViewForListingSortedBySentAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest): SMono[Seq[MessageId]] = {
     val mailboxId: MailboxId = 
request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
     SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
@@ -133,16 +153,40 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
       })
   }
 
+  private def queryViewForListingSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest): SMono[Seq[MessageId]] = {
+    val mailboxId: MailboxId = 
request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
+    SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
+      .`then`(SFlux.fromPublisher(
+        emailQueryView.listMailboxContentSortedByReceivedAt(mailboxId, 
JavaLimit.from(limitToUse.value + position.value)))
+        .drop(position.value)
+        .take(limitToUse.value)
+        .collectSeq())
+      .onErrorResume({
+        case _: MailboxNotFoundException => SMono.just[Seq[MessageId]](Seq())
+        case e => SMono.error[Seq[MessageId]](e)
+      })
+  }
+
   private def matchesInMailboxSortedBySentAt(request: EmailQueryRequest): 
Boolean =
     configuration.isEmailQueryViewEnabled &&
       request.filter.exists(_.inMailboxFilterOnly) &&
       request.sort.contains(Set(Comparator.SENT_AT_DESC))
 
+  private def matchesInMailboxSortedByReceivedAt(request: EmailQueryRequest): 
Boolean =
+    configuration.isEmailQueryViewEnabled &&
+      request.filter.exists(_.inMailboxFilterOnly) &&
+      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
   private def matchesInMailboxAfterSortedBySentAt(request: EmailQueryRequest): 
Boolean =
     configuration.isEmailQueryViewEnabled &&
       request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
       request.sort.contains(Set(Comparator.SENT_AT_DESC))
 
+  private def matchesInMailboxAfterSortedByReceivedAt(request: 
EmailQueryRequest): Boolean =
+    configuration.isEmailQueryViewEnabled &&
+      request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
+      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
   private def toResponse(request: EmailQueryRequest, position: Position, 
limitToUse: Limit, ids: Seq[MessageId]): EmailQueryResponse =
     EmailQueryResponse(accountId = request.accountId,
       queryState = QueryState.forIds(ids),

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

Reply via email to