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
commit f7980946ae51f2cec10b14ca909ac906e0b8eb4a Author: Benoit Tellier <[email protected]> AuthorDate: Fri Sep 18 17:07:07 2020 +0700 JAMES-3377 Email/query implement minSize/maxSize FilterCondition --- .../contract/EmailQueryMethodContract.scala | 261 +++++++++++++++++++-- .../org/apache/james/jmap/mail/EmailQuery.scala | 7 +- .../james/jmap/utils/search/MailboxFilter.scala | 29 ++- 3 files changed, 275 insertions(+), 22 deletions(-) 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 8381c45..d56c851 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 @@ -19,6 +19,7 @@ package org.apache.james.jmap.rfc8621.contract +import java.io.ByteArrayOutputStream import java.nio.charset.StandardCharsets import java.time.format.DateTimeFormatter import java.time.{Instant, ZonedDateTime} @@ -32,14 +33,18 @@ import io.restassured.RestAssured.{`given`, requestSpecification} import io.restassured.http.ContentType.JSON import javax.mail.Flags import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson +import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER +import net.javacrumbs.jsonunit.core.internal.Options 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.inbox import org.apache.james.mailbox.model.{MailboxPath, MessageId} import org.apache.james.mime4j.dom.Message +import org.apache.james.mime4j.message.DefaultMessageWriter import org.apache.james.modules.MailboxProbeImpl import org.apache.james.utils.DataProbeImpl import org.awaitility.Awaitility @@ -154,11 +159,11 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(ANDRE, "other") server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl]) - .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(message)) + .appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message)) .getMessageId server.getProbe(classOf[MailboxProbeImpl]) .appendMessage(ANDRE.asString, otherMailboxPath, AppendCommand.from(message)) @@ -216,7 +221,7 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(BOB, "other") server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) val requestDateMessage1 = Date.from(ZonedDateTime.now().minusDays(1).toInstant) @@ -265,11 +270,11 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(BOB, "other") server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) server.getProbe(classOf[MailboxProbeImpl]) - .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(message)) + .appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message)) .getMessageId server.getProbe(classOf[MailboxProbeImpl]) .appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message)) @@ -548,11 +553,11 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(BOB, "other") val otherMailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) server.getProbe(classOf[MailboxProbeImpl]) - .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(message)) + .appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message)) .getMessageId val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl]) .appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message)) @@ -649,11 +654,11 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(BOB, "other") val otherMailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) server.getProbe(classOf[MailboxProbeImpl]) - .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(message)) + .appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message)) .getMessageId server.getProbe(classOf[MailboxProbeImpl]) .appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message)) @@ -708,17 +713,243 @@ trait EmailQueryMethodContract { } @Test + def minSizeShouldBeInclusive(server: GuiceJamesServer): Unit = { + val message1: Message = simpleMessage("short") + val size1: Int = computeSize(message1) + // One char more than message1 + val message2: Message = simpleMessage("short!") + val size2: Int = computeSize(message2) + // One char more than message2 + val message3: Message = simpleMessage("short!!") + val size3: Int = computeSize(message3) + + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + mailboxProbe.createMailbox(inbox(BOB)) + val id1 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId + val id2 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId + val id3 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": { + | "minSize": $size2 + | } + | }, + | "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) + .withOptions(new Options(IGNORING_ARRAY_ORDER)) + .inPath("$.methodResponses[0][1].ids") + .isEqualTo( + s"""[ + | "${id2.serialize()}", + | "${id3.serialize()}" + |]""".stripMargin) + } + } + + @Test + def maxSizeShouldBeExclusive(server: GuiceJamesServer): Unit = { + val message1: Message = simpleMessage("looooooooooooooong") + val size1: Int = computeSize(message1) + // One char more than message3 + val message2: Message = simpleMessage("looooooooooooooong!") + val size2: Int = computeSize(message2) + // One char more than message4 + val message3: Message = simpleMessage("looooooooooooooong!!") + val size3: Int = computeSize(message3) + + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + mailboxProbe.createMailbox(inbox(BOB)) + val id1 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId + val id2 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId + val id3 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId + + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": { + | "maxSize": $size2 + | } + | }, + | "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) + .withOptions(new Options(IGNORING_ARRAY_ORDER)) + .inPath("$.methodResponses[0][1].ids") + .isEqualTo( + s"""[ + | "${id1.serialize()}" + |]""".stripMargin) + } + } + + @Test + def maxSizeShouldRejectNegative(): Unit = { + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": { + | "maxSize": -1 + | } + | }, + | "c1"]] + |}""".stripMargin + + awaitAtMostTenSeconds.untilAsserted { () => + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post.prettyPeek() + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | [ + | "error", + | { + | "type": "invalidArguments", + | "description": "{\\"errors\\":[{\\"path\\":\\"obj.filter.maxSize\\",\\"messages\\":[\\"Predicate (-1 < 0) did not fail.\\"]}]}" + | }, + | "c1" + | ] + | ] + |}""".stripMargin) + } + } + + @Test + def minSizeShouldRejectNegative(): Unit = { + val request = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Email/query", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "filter": { + | "minSize": -1 + | } + | }, + | "c1"]] + |}""".stripMargin + + awaitAtMostTenSeconds.untilAsserted { () => + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post.prettyPeek() + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | [ + | "error", + | { + | "type": "invalidArguments", + | "description": "{\\"errors\\":[{\\"path\\":\\"obj.filter.minSize\\",\\"messages\\":[\\"Predicate (-1 < 0) did not fail.\\"]}]}" + | }, + | "c1" + | ] + | ] + |}""".stripMargin) + } + } + + private def simpleMessage(message: String) = { + Message.Builder + .of + .setSubject("test") + .setBody(message, StandardCharsets.UTF_8) + .build + } + + private def computeSize(message: Message): Int = { + val writer = new DefaultMessageWriter() + val stream = new ByteArrayOutputStream() + writer.writeMessage(message, stream) + stream.toByteArray.length + } + + @Test def shouldListMailsNotInASpecificUserMailboxes(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)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(BOB, "other") val otherMailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl]) - .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(message)) + .appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message)) .getMessageId server.getProbe(classOf[MailboxProbeImpl]) .appendMessage(BOB.asString, otherMailboxPath, AppendCommand.from(message)) @@ -766,7 +997,7 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val otherMailboxPath = MailboxPath.forUser(BOB, "other") server.getProbe(classOf[MailboxProbeImpl]).createMailbox(otherMailboxPath) val requestDateMessage1 = Date.from(ZonedDateTime.now().minusDays(1).toInstant) @@ -923,7 +1154,7 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val requestDate = ZonedDateTime.now().minusDays(1) val messageId1 = sendMessageToBobInbox(server, message, Date.from(requestDate.toInstant)) @@ -975,7 +1206,7 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val requestDate = ZonedDateTime.now().minusDays(1) val messageId1 = sendMessageToBobInbox(server, message, Date.from(requestDate.toInstant)) @@ -1027,7 +1258,7 @@ trait EmailQueryMethodContract { .setSubject("test") .setBody("testmail", StandardCharsets.UTF_8) .build - server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB)) val receivedDateMessage1 = ZonedDateTime.now().minusDays(1) sendMessageToBobInbox(server, message, Date.from(receivedDateMessage1.toInstant)) 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 8f23532..db196bc 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,9 +18,10 @@ ****************************************************************/ package org.apache.james.jmap.mail -import org.apache.james.jmap.model.{AccountId, CanCalculateChanges, Keyword, LimitUnparsed, Position, QueryState, UTCDate} +import org.apache.james.jmap.mail.Email.Size import org.apache.james.jmap.mail.IsAscending.{ASCENDING, DESCENDING} import org.apache.james.jmap.model.Limit.Limit +import org.apache.james.jmap.model.{AccountId, CanCalculateChanges, Keyword, LimitUnparsed, Position, QueryState, UTCDate} 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} @@ -30,7 +31,9 @@ case class FilterCondition(inMailbox: Option[MailboxId], before: Option[UTCDate], after: Option[UTCDate], hasKeyword: Option[Keyword], - notKeyword: Option[Keyword]) + notKeyword: Option[Keyword], + minSize: Option[Size], + maxSize: Option[Size]) case class EmailQueryRequest(accountId: AccountId, limit: Option[LimitUnparsed], filter: Option[FilterCondition], comparator: Option[Set[Comparator]]) 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 c221cdd..91be2f8 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 @@ -20,9 +20,8 @@ package org.apache.james.jmap.utils.search import java.util.Date -import javax.mail.Flags import org.apache.james.jmap.mail.EmailQueryRequest -import org.apache.james.mailbox.model.SearchQuery.{Conjunction, ConjunctionCriterion, Criterion, DateComparator, DateOperator, DateResolution, FlagCriterion, InternalDateCriterion} +import org.apache.james.mailbox.model.SearchQuery.{Conjunction, ConjunctionCriterion, Criterion, DateComparator, DateOperator, DateResolution, InternalDateCriterion} import org.apache.james.mailbox.model.{MultimailboxesSearchQuery, SearchQuery} import scala.jdk.CollectionConverters._ @@ -61,7 +60,8 @@ object MailboxFilter { object QueryFilter { def buildQuery(request: EmailQueryRequest): SearchQuery.Builder = { - List(ReceivedBefore, ReceivedAfter, HasKeyWord, NotKeyWord).foldLeft(new SearchQuery.Builder())((builder, filter) => filter.toQuery(builder, request)) + List(ReceivedBefore, ReceivedAfter, HasKeyWord, NotKeyWord, MinSize, MaxSize) + .foldLeft(SearchQuery.builder())((builder, filter) => filter.toQuery(builder, request)) } } @@ -77,11 +77,30 @@ object MailboxFilter { } case object ReceivedAfter extends QueryFilter { override def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): SearchQuery.Builder = request.filter.flatMap(_.after) match { - case Some(after) => { + case Some(after) => val strictlyAfter = new InternalDateCriterion(new DateOperator(DateComparator.AFTER, Date.from(after.asUTC.toInstant), DateResolution.Second)) builder .andCriteria(strictlyAfter) - } + case None => builder + } + } + + case object MinSize extends QueryFilter { + override def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): SearchQuery.Builder = request.filter.flatMap(_.minSize) match { + case Some(minSize) => + builder + .andCriteria(SearchQuery.or( + SearchQuery.sizeGreaterThan(minSize.value), + SearchQuery.sizeEquals(minSize.value))) + case None => builder + } + } + + case object MaxSize extends QueryFilter { + override def toQuery(builder: SearchQuery.Builder, request: EmailQueryRequest): SearchQuery.Builder = request.filter.flatMap(_.maxSize) match { + case Some(maxSize) => + builder + .andCriteria(SearchQuery.sizeLessThan(maxSize.value)) case None => builder } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
