JAMES-3381 email/query implements limit
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 7792c9a..8381c45 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
@@ -96,10 +96,7 @@
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
val requestDate = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
- val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl])
- .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
- AppendCommand.builder().withInternalDate(requestDate).build(message))
- .getMessageId
+ val messageId1: MessageId = sendMessageToBobInbox(server, message, requestDate)
val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message))
@@ -223,20 +220,11 @@
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
val requestDateMessage1 = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
- val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl])
- .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
- AppendCommand.builder().withInternalDate(requestDateMessage1).build(message))
- .getMessageId
+ val messageId1: MessageId = sendMessageToBobInbox(server, message, requestDateMessage1)
val requestDateMessage2 = Date.from(ZonedDateTime.now().minusDays(1).plusHours(1).toInstant)
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
- .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
- AppendCommand.builder().withInternalDate(requestDateMessage2).build(message))
- .getMessageId
+ val messageId2 = sendMessageToBobInbox(server, message, requestDateMessage2)
val requestDateMessage3 = Date.from(ZonedDateTime.now().minusDays(1).plusHours(2).toInstant)
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
- .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
- AppendCommand.builder().withInternalDate(requestDateMessage3).build(message))
- .getMessageId
+ val messageId3 = sendMessageToBobInbox(server, message, requestDateMessage3)
val request =
s"""{
@@ -782,10 +770,7 @@
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
val requestDateMessage1 = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
- val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl])
- .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
- AppendCommand.builder().withInternalDate(requestDateMessage1).build(message))
- .getMessageId
+ val messageId1: MessageId = sendMessageToBobInbox(server, message, requestDateMessage1)
val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message))
.getMessageId
@@ -940,9 +925,7 @@
.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 messageId1 = sendMessageToBobInbox(server, message, Date.from(requestDate.toInstant))
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
@@ -994,9 +977,7 @@
.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 messageId1 = sendMessageToBobInbox(server, message, Date.from(requestDate.toInstant))
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
@@ -1048,13 +1029,11 @@
.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
+ sendMessageToBobInbox(server, message, Date.from(receivedDateMessage1.toInstant))
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
- val receivedDateMessage2 = ZonedDateTime.now().minusDays(1).plusHours(2)
+ val receivedDateMessage2 = receivedDateMessage1.plusHours(2)
val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, otherMailboxPath, AppendCommand.builder().withInternalDate(Date.from(receivedDateMessage2.toInstant)).build(message))
.getMessageId
@@ -1103,9 +1082,7 @@
.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
+ sendMessageToBobInbox(server, message, Date.from(receivedDateMessage1.toInstant))
val otherMailboxPath = MailboxPath.forUser(BOB, "other")
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
@@ -1148,6 +1125,230 @@
}
}
+ @Test
+ def shouldLimitResultByTheLimitProvidedByTheClient(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 otherMailboxPath = MailboxPath.forUser(BOB, "other")
+ val requestDate = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
+ sendMessageToBobInbox(server, message, Date.from(requestDate.toInstant))
+
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath)
+ val messageId2: MessageId = 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",
+ | "limit": 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": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "queryState": "${generateQueryState(messageId2)}",
+ | "canCalculateChanges": false,
+ | "position": 0,
+ | "ids": ["${messageId2.serialize()}"]
+ | },
+ | "c1"
+ | ]]
+ |}""".stripMargin)
+ }
+ }
+
+ @Test
+ def shouldReturnAnIllegalArgumentExceptionIfTheLimitIsNegative(server: GuiceJamesServer): Unit = {
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "limit": -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": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "error",
+ | {
+ | "type": "invalidArguments",
+ | "description": "The limit can not be negative. -1 was provided."
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+ }
+
+ @Test
+ def theLimitshouldBeEnforcedByTheServerIfNoLimitProvidedByTheClient(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 allMessages = (0 to 300).toList.foldLeft(List[MessageId](), ZonedDateTime.now().minusYears(1))((acc, _) => {
+ val (messageList, date) = acc
+ val dateForNewMessage = date.plusDays(1)
+ val messageId = sendMessageToBobInbox(server, message, Date.from(dateForNewMessage.toInstant))
+ (messageId :: messageList, dateForNewMessage)
+ })
+
+ val expectedMessages = allMessages._1.take(256)
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
+ | },
+ | "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("[" + expectedMessages.map(message => s""""${message.serialize()}"""").mkString(", ") + "]")
+
+ assertThatJson(response)
+ .inPath("$.methodResponses[0][1].limit")
+ .isEqualTo("256")
+ }
+ }
+
+ @Test
+ def theLimitshouldBeEnforcedByTheServerIfAGreaterLimitProvidedByTheClient(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 allMessages = (0 to 300).toList.foldLeft(List[MessageId](), ZonedDateTime.now().minusYears(1))((acc, _) => {
+ val (messageList, date) = acc
+ val dateForNewMessage = date.plusDays(1)
+ val messageId = sendMessageToBobInbox(server, message, Date.from(dateForNewMessage.toInstant))
+ (messageId :: messageList, dateForNewMessage)
+ })
+
+ val expectedMessages = allMessages._1.take(256)
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "limit": 2000
+ | },
+ | "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("[" + expectedMessages.map(message => s""""${message.serialize()}"""").mkString(", ") + "]")
+
+ assertThatJson(response)
+ .inPath("$.methodResponses[0][1].limit")
+ .isEqualTo("256")
+ }
+ }
+
+ private def sendMessageToBobInbox(server: GuiceJamesServer, message: Message, requestDate: Date) = {
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.builder().withInternalDate(requestDate).build(message))
+ .getMessageId
+ }
+
@ParameterizedTest
@MethodSource(value = Array("jmapSystemKeywords"))
def listMailsBySystemKeywordShouldReturnOnlyMailsWithThisSystemKeyword(keywordFlag: Flags, keywordName: String, server: GuiceJamesServer): Unit = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
index 8251f96..a4609a6 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
@@ -20,8 +20,8 @@
package org.apache.james.jmap.json
import javax.inject.Inject
-import org.apache.james.jmap.mail.{CanCalculateChanges, Collation, Comparator, EmailQueryRequest, EmailQueryResponse, FilterCondition, IsAscending, Limit, Position, QueryState, ReceivedAtSortProperty, SortProperty}
-import org.apache.james.jmap.model.{AccountId, Keyword}
+import org.apache.james.jmap.mail.{Collation, Comparator, EmailQueryRequest, EmailQueryResponse, FilterCondition, IsAscending, ReceivedAtSortProperty, SortProperty}
+import org.apache.james.jmap.model.{AccountId, CanCalculateChanges, Keyword, LimitUnparsed, Position, QueryState}
import org.apache.james.mailbox.model.{MailboxId, MessageId}
import play.api.libs.json._
@@ -49,11 +49,11 @@
case _ => JsError("Expecting keywords to be represented by a JsString")
}
private implicit val filterConditionReads: Reads[FilterCondition] = Json.reads[FilterCondition]
+ private implicit val limitUnparsedReads: Reads[LimitUnparsed] = Json.valueReads[LimitUnparsed]
private implicit val CanCalculateChangesFormat: Format[CanCalculateChanges] = Json.valueFormat[CanCalculateChanges]
private implicit val queryStateWrites: Writes[QueryState] = Json.valueWrites[QueryState]
private implicit val positionFormat: Format[Position] = Json.valueFormat[Position]
- private implicit val limitFormat: Format[Limit] = Json.valueFormat[Limit]
private implicit val messageIdWrites: Writes[MessageId] = id => JsString(id.serialize())
private implicit val sortPropertyReads: Reads[SortProperty] = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala
index f8580c9..e5bc83b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala
@@ -30,7 +30,7 @@
object MailboxQuerySerializer {
private implicit val accountIdWrites: Format[AccountId] = Json.valueFormat[AccountId]
- private implicit val canCalculateChangeWrites: Writes[CanCalculateChange] = Json.valueWrites[CanCalculateChange]
+ private implicit val canCalculateChangeWrites: Writes[CanCalculateChanges] = Json.valueWrites[CanCalculateChanges]
private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize)
@@ -44,7 +44,6 @@
private implicit val emailQueryRequestReads: Reads[MailboxQueryRequest] = Json.reads[MailboxQueryRequest]
private implicit val queryStateWrites: Writes[QueryState] = Json.valueWrites[QueryState]
private implicit val positionFormat: Format[Position] = Json.valueFormat[Position]
- private implicit val limitFormat: Format[Limit] = Json.valueFormat[Limit]
private implicit def mailboxQueryResponseWrites: OWrites[MailboxQueryResponse] = Json.writes[MailboxQueryResponse]
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 f5b282e..8f23532 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
@@ -18,11 +18,9 @@
****************************************************************/
package org.apache.james.jmap.mail
-
-import org.apache.james.jmap.model.{Keyword, UTCDate}
-import com.google.common.hash.Hashing
+import org.apache.james.jmap.model.{AccountId, CanCalculateChanges, Keyword, LimitUnparsed, Position, QueryState, UTCDate}
import org.apache.james.jmap.mail.IsAscending.{ASCENDING, DESCENDING}
-import org.apache.james.jmap.model.AccountId
+import org.apache.james.jmap.model.Limit.Limit
import org.apache.james.mailbox.model.SearchQuery.Sort.Order.{NATURAL, REVERSE}
import org.apache.james.mailbox.model.SearchQuery.Sort.SortClause
import org.apache.james.mailbox.model.{MailboxId, MessageId, SearchQuery}
@@ -34,17 +32,7 @@
hasKeyword: Option[Keyword],
notKeyword: Option[Keyword])
-case class EmailQueryRequest(accountId: AccountId, filter: Option[FilterCondition], comparator: Option[Set[Comparator]])
-
-case class Position(value: Int) extends AnyVal
-object Position{
- val zero: Position = Position(0)
-}
-case class Limit(value: Long) extends AnyVal
-object Limit {
- val default: Limit = Limit(256L)
-}
-case class QueryState(value: String) extends AnyVal
+case class EmailQueryRequest(accountId: AccountId, limit: Option[LimitUnparsed], filter: Option[FilterCondition], comparator: Option[Set[Comparator]])
sealed trait SortProperty {
def toSortClause: SortClause
@@ -77,19 +65,6 @@
def toSort: SearchQuery.Sort = new SearchQuery.Sort(property.toSortClause, isAscending.getOrElse(ASCENDING).toSortOrder)
}
-object QueryState {
- def forIds(ids: Seq[MessageId]): QueryState = QueryState(
- Hashing.murmur3_32()
- .hashUnencodedChars(ids.map(_.serialize()).mkString(" "))
- .toString)
-}
-
-object CanCalculateChanges {
- val CANT: CanCalculateChanges = CanCalculateChanges(false)
-}
-
-case class CanCalculateChanges(value: Boolean) extends AnyVal
-
case class EmailQueryResponse(accountId: AccountId,
queryState: QueryState,
canCalculateChanges: CanCalculateChanges,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxQuery.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxQuery.scala
index 56a7e62..6f9df68 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxQuery.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxQuery.scala
@@ -19,7 +19,8 @@
package org.apache.james.jmap.mail
-import org.apache.james.jmap.model.{AccountId, CanCalculateChange, Limit, Position, QueryState}
+import org.apache.james.jmap.model.Limit.Limit
+import org.apache.james.jmap.model.{AccountId, CanCalculateChanges, Position, QueryState}
import org.apache.james.mailbox.Role
import org.apache.james.mailbox.model.MailboxId
@@ -29,7 +30,7 @@
case class MailboxQueryResponse(accountId: AccountId,
queryState: QueryState,
- canCalculateChanges: CanCalculateChange,
+ canCalculateChanges: CanCalculateChanges,
ids: Seq[MailboxId],
position: Position,
limit: Option[Limit])
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 ab5ddb8..7437260 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
@@ -21,11 +21,12 @@
import eu.timepit.refined.auto._
import javax.inject.Inject
import org.apache.james.jmap.json.{EmailQuerySerializer, ResponseSerializer}
-import org.apache.james.jmap.mail.{CanCalculateChanges, Comparator, EmailQueryRequest, EmailQueryResponse, Limit, Position, QueryState}
+import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest, EmailQueryResponse}
import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CAPABILITY}
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.model.{Capabilities, ErrorCode, Invocation}
+import org.apache.james.jmap.model.{CanCalculateChanges, Capabilities, ErrorCode, Invocation, Limit, Position, QueryState}
+import org.apache.james.jmap.model.Limit.Limit
import org.apache.james.jmap.routes.ProcessingContext
import org.apache.james.jmap.utils.search.MailboxFilter.QueryFilter
import org.apache.james.jmap.utils.search.MailboxFilter
@@ -58,16 +59,21 @@
private def processRequest(mailboxSession: MailboxSession, invocation: Invocation, request: EmailQueryRequest): SMono[Invocation] = {
val searchQuery: MultimailboxesSearchQuery = searchQueryFromRequest(request)
+ for {
+ limitToUse <- Limit.validateRequestLimit(request.limit)
+ response <- executeQuery(mailboxSession, request, searchQuery, limitToUse)
+ } yield Invocation(methodName = methodName, arguments = Arguments(serializer.serialize(response)), methodCallId = invocation.methodCallId)
+ }
- SFlux.fromPublisher(mailboxManager.search(searchQuery, mailboxSession, Limit.default.value))
+ private def executeQuery(mailboxSession: MailboxSession, request: EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, limitToUse: Limit): SMono[EmailQueryResponse] = {
+ SFlux.fromPublisher(mailboxManager.search(searchQuery, mailboxSession, limitToUse))
.collectSeq()
.map(ids => EmailQueryResponse(accountId = request.accountId,
queryState = QueryState.forIds(ids),
- canCalculateChanges = CanCalculateChanges.CANT,
+ canCalculateChanges = CanCalculateChanges.CANNOT,
ids = ids,
position = Position.zero,
- limit = Some(Limit.default)))
- .map(response => Invocation(methodName = methodName, arguments = Arguments(serializer.serialize(response)), methodCallId = invocation.methodCallId))
+ limit = Some(limitToUse).filterNot(used => request.limit.map(_.value).contains(used.value))))
}
private def searchQueryFromRequest(request: EmailQueryRequest): MultimailboxesSearchQuery = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala
index a85b657..46e9417 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxQueryMethod.scala
@@ -59,7 +59,7 @@
.collectSeq()
.map(ids => MailboxQueryResponse(accountId = request.accountId,
queryState = QueryState.forMailboxIds(ids),
- canCalculateChanges = CanCalculateChange(false),
+ canCalculateChanges = CanCalculateChanges(false),
ids = ids,
position = Position.zero,
limit = Some(Limit.default)))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Query.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Query.scala
index 24135dd..624e3e0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Query.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Query.scala
@@ -20,15 +20,33 @@
package org.apache.james.jmap.model
import com.google.common.hash.Hashing
+import eu.timepit.refined.auto._
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.numeric.Positive
+import eu.timepit.refined.refineV
import org.apache.james.mailbox.model.{MailboxId, MessageId}
+import reactor.core.scala.publisher.SMono
case class Position(value: Int) extends AnyVal
object Position{
val zero: Position = Position(0)
}
-case class Limit(value: Long) extends AnyVal
+
+case class LimitUnparsed(value: Long) extends AnyVal
+
object Limit {
- val default: Limit = Limit(256L)
+ type Limit = Long Refined Positive
+ val default: Limit = 256L
+
+ def validateRequestLimit(requestLimit: Option[LimitUnparsed]): SMono[Limit] = {
+ val refinedLimit : Option[Either[String, Limit]] = requestLimit.map(limit => refineV[Positive](limit.value))
+
+ refinedLimit match {
+ case Some(Left(_)) => SMono.raiseError(new IllegalArgumentException(s"The limit can not be negative. ${requestLimit.map(_.value).getOrElse("")} was provided."))
+ case Some(Right(limit)) if limit.value < default.value => SMono.just(limit)
+ case _ => SMono.just(default)
+ }
+ }
}
case class QueryState(value: String) extends AnyVal
@@ -45,4 +63,8 @@
.toString)
}
-case class CanCalculateChange(value: Boolean) extends AnyVal
+case class CanCalculateChanges(value: Boolean) extends AnyVal
+
+object CanCalculateChanges {
+ val CANNOT: CanCalculateChanges = CanCalculateChanges(false)
+}