JAMES-3377 filter after receivedAt
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 f00878f..a4a7068 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
@@ -749,6 +749,115 @@
}
}
+ @Test
+ def shouldListMailsReceivedAfterADate(server: GuiceJamesServer): Unit = {
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ val receivedDateMessage1 = ZonedDateTime.now().minusDays(1)
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withInternalDate(Date.from(receivedDateMessage1.toInstant)).build(message))
+ .getMessageId
+
+ val otherMailboxPath = MailboxPath.forUser(BOB, "other")
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
+ val receivedDateMessage2 = ZonedDateTime.now().minusDays(1).plusHours(2)
+ val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, otherMailboxPath, AppendCommand.builder().withInternalDate(Date.from(receivedDateMessage2.toInstant)).build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "filter" : {
+ | "after": "${UTCDate(receivedDateMessage2.minusHours(1)).asUTC.format(UTC_DATE_FORMAT)}"
+ | }
+ | },
+ | "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)
+ .inPath("$.methodResponses[0][1].ids")
+ .isEqualTo(s"""["${messageId2.serialize()}"]""")
+ }
+ }
+
+ @Test
+ def listMailsReceivedAfterADateShouldBeExclusive(server: GuiceJamesServer): Unit = {
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ val receivedDateMessage1 = ZonedDateTime.now().minusDays(1)
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withInternalDate(Date.from(receivedDateMessage1.toInstant)).build(message))
+ .getMessageId
+
+ val otherMailboxPath = MailboxPath.forUser(BOB, "other")
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
+ val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "filter" : {
+ | "after": "${UTCDate(receivedDateMessage1).asUTC.format(UTC_DATE_FORMAT)}"
+ | }
+ | },
+ | "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)
+ .inPath("$.methodResponses[0][1].ids")
+ .isEqualTo(s"""["${messageId2.serialize()}"]""")
+ }
+ }
+
private def generateQueryState(messages: MessageId*): String = {
Hashing.murmur3_32()
.hashUnencodedChars(messages.toList.map(_.serialize()).mkString(" "))
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 30d5053..5f051cc 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
@@ -25,7 +25,8 @@
case class FilterCondition(inMailbox: Option[MailboxId],
inMailboxOtherThan: Option[Seq[MailboxId]],
- before: Option[UTCDate])
+ before: Option[UTCDate],
+ after: Option[UTCDate])
case class EmailQueryRequest(accountId: AccountId, filter: Option[FilterCondition])
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 934d28b..1fe9ef9 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
@@ -31,6 +31,7 @@
import org.apache.james.jmap.routes.ProcessingContext
import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.jmap.utils.search.MailboxFilter
+import org.apache.james.jmap.utils.search.MailboxFilter.QueryFilter
import org.apache.james.mailbox.model.SearchQuery.{Conjunction, ConjunctionCriterion, Criterion, DateComparator, DateOperator, DateResolution, InternalDateCriterion}
import org.apache.james.mailbox.model.SearchQuery.Sort.SortClause
import org.apache.james.mailbox.model.{MultimailboxesSearchQuery, SearchQuery}
@@ -73,26 +74,13 @@
}
private def searchQueryFromRequest(request: EmailQueryRequest): MultimailboxesSearchQuery = {
- val query = queryWithCriterions(request)
+ val query = QueryFilter.buildQuery(request)
val defaultSort = new SearchQuery.Sort(SortClause.Arrival, SearchQuery.Sort.Order.REVERSE)
val querySorted = query.sorts(defaultSort)
MailboxFilter.buildQuery(request, querySorted.build())
}
- private def queryWithCriterions(request: EmailQueryRequest): SearchQuery.Builder = {
- request.filter.flatMap(_.before) match {
- case Some(before) => {
- val strictlyBefore = new InternalDateCriterion(new DateOperator(DateComparator.BEFORE, Date.from(before.asUTC.toInstant), DateResolution.Second))
- val sameDate = new InternalDateCriterion(new DateOperator(DateComparator.ON, Date.from(before.asUTC.toInstant), DateResolution.Second))
- new SearchQuery.Builder()
- .andCriteria(new ConjunctionCriterion(Conjunction.OR, List[Criterion](strictlyBefore, sameDate).asJava))
- }
- case None => new SearchQuery.Builder()
- }
-
- }
-
private def asEmailQueryRequest(arguments: Arguments): SMono[EmailQueryRequest] =
serializer.deserializeEmailQueryRequest(arguments.value) match {
case JsSuccess(emailQueryRequest, _) => SMono.just(emailQueryRequest)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala
index c673e5b..3bb4cf8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/utils/search/MailboxFilter.scala
@@ -18,10 +18,13 @@
****************************************************************/
package org.apache.james.jmap.utils.search
+import java.util.Date
+
import org.apache.james.jmap.mail.EmailQueryRequest
+import org.apache.james.mailbox.model.SearchQuery.{Conjunction, ConjunctionCriterion, Criterion, DateComparator, DateOperator, DateResolution, InternalDateCriterion}
import org.apache.james.mailbox.model.{MultimailboxesSearchQuery, SearchQuery}
-import scala.collection.JavaConverters.seqAsJavaListConverter
+import scala.jdk.CollectionConverters._
sealed trait MailboxFilter {
@@ -49,4 +52,36 @@
List(InMailboxFilter, NotInMailboxFilter).foldLeft(multiMailboxQueryBuilder)((builder, filter) => filter.toQuery(builder, request))
.build()
}
+
+
+ sealed trait QueryFilter {
+ def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): SearchQuery.Builder
+ }
+
+ object QueryFilter {
+ def buildQuery(request: EmailQueryRequest): SearchQuery.Builder = {
+ List(ReceivedBefore, ReceivedAfter).foldLeft(new SearchQuery.Builder())((builder, filter) => filter.toQuery(builder, request))
+ }
+ }
+
+ case object ReceivedBefore extends QueryFilter {
+ override def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): SearchQuery.Builder = request.filter.flatMap(_.before) match {
+ case Some(before) =>
+ val strictlyBefore = new InternalDateCriterion(new DateOperator(DateComparator.BEFORE, Date.from(before.asUTC.toInstant), DateResolution.Second))
+ val sameDate = new InternalDateCriterion(new DateOperator(DateComparator.ON, Date.from(before.asUTC.toInstant), DateResolution.Second))
+ new SearchQuery.Builder()
+ .andCriteria(new ConjunctionCriterion(Conjunction.OR, List[Criterion](strictlyBefore, sameDate).asJava))
+ case None => builder
+ }
+ }
+ case object ReceivedAfter extends QueryFilter {
+ override def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): SearchQuery.Builder = request.filter.flatMap(_.after) match {
+ case Some(after) => {
+ val strictlyAfter = new InternalDateCriterion(new DateOperator(DateComparator.AFTER, Date.from(after.asUTC.toInstant), DateResolution.Second))
+ new SearchQuery.Builder()
+ .andCriteria(strictlyAfter)
+ }
+ case None => builder
+ }
+ }
}