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]