JAMES-3377 filter before 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 c240161..f00878f 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
@@ -21,6 +21,7 @@
import java.nio.charset.StandardCharsets
import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.concurrent.TimeUnit
@@ -32,6 +33,7 @@
import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.model.UTCDate
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.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.{MailboxPath, MessageId}
@@ -51,6 +53,8 @@
.await
private lazy val awaitAtMostTenSeconds = calmlyAwait.atMost(10, TimeUnit.SECONDS)
+ private lazy val UTC_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX")
+
@BeforeEach
def setUp(server: GuiceJamesServer): Unit = {
server.getProbe(classOf[DataProbeImpl])
@@ -637,6 +641,114 @@
}
}
+ @Test
+ def shouldListMailsReceivedBeforeADate(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 requestDate = ZonedDateTime.now().minusDays(1)
+ val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withInternalDate(Date.from(requestDate.toInstant)).build(message))
+ .getMessageId
+
+
+ val otherMailboxPath = MailboxPath.forUser(BOB, "other")
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
+ 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" : {
+ | "before": "${UTCDate(requestDate.plusHours(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"""["${messageId1.serialize()}"]""")
+ }
+ }
+ @Test
+ def shouldListMailsReceivedBeforeADateInclusively(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 requestDate = ZonedDateTime.now().minusDays(1)
+ val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withInternalDate(Date.from(requestDate.toInstant)).build(message))
+ .getMessageId
+
+ val otherMailboxPath = MailboxPath.forUser(BOB, "other")
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
+ 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" : {
+ | "before": "${UTCDate(requestDate).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"""["${messageId1.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 22c09d0..30d5053 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
@@ -20,10 +20,12 @@
package org.apache.james.jmap.mail
import com.google.common.hash.Hashing
-import org.apache.james.jmap.model.AccountId
+import org.apache.james.jmap.model.{AccountId, UTCDate}
import org.apache.james.mailbox.model.{MailboxId, MessageId}
-case class FilterCondition(inMailbox: Option[MailboxId], inMailboxOtherThan: Option[Seq[MailboxId]])
+case class FilterCondition(inMailbox: Option[MailboxId],
+ inMailboxOtherThan: Option[Seq[MailboxId]],
+ before: 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 18d391a..934d28b 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
@@ -18,6 +18,8 @@
****************************************************************/
package org.apache.james.jmap.method
+import java.util.Date
+
import eu.timepit.refined.auto._
import javax.inject.Inject
import org.apache.james.jmap.json.{EmailQuerySerializer, ResponseSerializer}
@@ -27,8 +29,9 @@
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model._
import org.apache.james.jmap.routes.ProcessingContext
-import org.apache.james.mailbox.exception.{MailboxNotFoundException}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.jmap.utils.search.MailboxFilter
+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}
import org.apache.james.mailbox.{MailboxManager, MailboxSession}
@@ -37,6 +40,8 @@
import play.api.libs.json.{JsError, JsSuccess}
import reactor.core.scala.publisher.{SFlux, SMono}
+import scala.collection.JavaConverters.seqAsJavaListConverter
+
class EmailQueryMethod @Inject() (serializer: EmailQuerySerializer,
mailboxManager: MailboxManager,
metricFactory: MetricFactory) extends Method {
@@ -68,13 +73,26 @@
}
private def searchQueryFromRequest(request: EmailQueryRequest): MultimailboxesSearchQuery = {
- val query = new SearchQuery.Builder()
+ val query = queryWithCriterions(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)