You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2022/06/22 01:31:25 UTC
[james-project] branch master updated: JAMES-3390 Allow Email/query to put inMailbox in its top level AND operators (#1060)
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 193ce30d3a JAMES-3390 Allow Email/query to put inMailbox in its top level AND operators (#1060)
193ce30d3a is described below
commit 193ce30d3a24d6ec141063cd2300b7d76ccba883
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Wed Jun 22 08:31:19 2022 +0700
JAMES-3390 Allow Email/query to put inMailbox in its top level AND operators (#1060)
Information for upfront mailbox filtering can easily be extracted
---
.../contract/EmailQueryMethodContract.scala | 157 ++++++++++++++++++++-
.../org/apache/james/jmap/mail/EmailQuery.scala | 29 +++-
.../james/jmap/utils/search/MailboxFilter.scala | 14 ++
3 files changed, 195 insertions(+), 5 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 c8f9a92717..480d078406 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
@@ -6345,10 +6345,73 @@ trait EmailQueryMethodContract {
|}""".stripMargin)
}
+ @Test
+ def inMailboxShouldResolveSimpleFilter(server: GuiceJamesServer): Unit = {
+ val message: Message = buildTestMessage
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "other"))
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+ .getMessageId
+
+ val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+ .getMessageId
+
+ val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
+ .getMessageId
+
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+ .getMessageId
+
+ val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "filter" : {
+ | "operator": "AND",
+ | "conditions": [
+ | { "inMailbox": "${mailboxId.serialize()}" }, { "hasKeyword": "another_custom" }
+ | ]
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ 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"""["${messageId1.serialize}","${messageId2.serialize}"]""")
+ }
+
@Test
def inMailboxShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ val mailboxId2 = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "other"))
server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
.getMessageId
@@ -6361,6 +6424,14 @@ trait EmailQueryMethodContract {
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
.getMessageId
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+ .getMessageId
+
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+ .getMessageId
+
val request =
s"""{
| "using": [
@@ -6373,7 +6444,14 @@ trait EmailQueryMethodContract {
| "filter" : {
| "operator": "AND",
| "conditions": [
- | { "inMailbox": "${mailboxId.serialize()}" }, { "hasKeyword": "another_custom" }
+ | { "inMailbox": "${mailboxId.serialize()}" },
+ | {
+ | "operator": "OR",
+ | "conditions": [
+ | { "inMailbox": "${mailboxId2.serialize()}" }, { "hasKeyword": "another_custom" }
+ | ]
+ | },
+ | { "hasKeyword": "another_custom" }
| ]
| }
| },
@@ -6412,11 +6490,12 @@ trait EmailQueryMethodContract {
def inMailboxOtherThanShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "other"))
server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
.getMessageId
- server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
.getMessageId
@@ -6424,6 +6503,14 @@ trait EmailQueryMethodContract {
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
.getMessageId
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+ .getMessageId
+
+ val messageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+ .getMessageId
+
val request =
s"""{
| "using": [
@@ -6446,9 +6533,71 @@ trait EmailQueryMethodContract {
val response = `given`
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(request)
- .when
+ .when
.post
- .`then`
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .inPath("$.methodResponses[0][1].ids")
+ .isEqualTo(s"""["${messageId.serialize}"]""")
+ }
+
+ @Test
+ def inMailboxOtherThanShouldBeRejectedWhenOrOperator(server: GuiceJamesServer): Unit = {
+ val message: Message = buildTestMessage
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "other"))
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+ .getMessageId
+
+ val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+ .getMessageId
+
+ val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
+ .getMessageId
+
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+ .getMessageId
+
+ val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "other"), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "filter" : {
+ | "operator": "OR",
+ | "conditions": [
+ | { "inMailboxOtherThan": ["${mailboxId.serialize()}"] }, { "hasKeyword": "another_custom" }
+ | ]
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
.statusCode(SC_OK)
.contentType(JSON)
.extract
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 63e19ff730..2137f20059 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,6 +20,7 @@
package org.apache.james.jmap.mail
import cats.implicits._
+import com.google.common.primitives.Booleans
import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.Limit.Limit
import org.apache.james.jmap.core.Position.Position
@@ -39,6 +40,10 @@ sealed trait FilterQuery {
def inMailboxFilterOnly: Boolean
def inMailboxAndAfterFilterOnly: Boolean
+
+ def countNestedMailboxFilter: Int
+
+ def countMailboxFilter: Int
}
sealed trait Operator
@@ -51,6 +56,13 @@ case class FilterOperator(operator: Operator,
override val inMailboxFilterOnly: Boolean = false
override val inMailboxAndAfterFilterOnly: Boolean = false
+
+ override def countNestedMailboxFilter: Int = conditions.map(_.countNestedMailboxFilter).sum
+
+ override def countMailboxFilter: Int = conditions.map {
+ case condition: FilterCondition => condition.countMailboxFilter
+ case _ => 0
+ }.sum
}
case class Text(value: String) extends AnyVal
@@ -117,6 +129,10 @@ case class FilterCondition(inMailbox: Option[MailboxId],
override val inMailboxAndAfterFilterOnly: Boolean = inMailbox.nonEmpty &&
after.nonEmpty &&
noOtherFiltersThanInMailboxAndAfter
+
+ override def countNestedMailboxFilter: Int = countMailboxFilter
+
+ override def countMailboxFilter: Int = Booleans.countTrue(inMailbox.isDefined, inMailboxOtherThan.isDefined)
}
case class EmailQueryRequest(accountId: AccountId,
@@ -134,7 +150,18 @@ case class EmailQueryRequest(accountId: AccountId,
private def validateFilter(filter: FilterQuery): Either[UnsupportedNestingException, Option[FilterQuery]] = filter match {
case filterCondition: FilterCondition => scala.Right(Some(filterCondition))
- case filterOperator: FilterOperator => rejectMailboxFilters(filterOperator)
+ case filterOperator: FilterOperator if filterOperator.operator == And =>
+ val nestedCount = filter.countNestedMailboxFilter
+ val topLevelCount = filter.countMailboxFilter
+
+ if (nestedCount == 1 && topLevelCount == 1) {
+ scala.Right(Some(filter))
+ } else if (nestedCount == 0) {
+ scala.Right(Some(filter))
+ } else {
+ rejectMailboxFilters(filterOperator)
+ }
+ case operator: FilterOperator => rejectMailboxFilters(operator)
}
private def rejectMailboxFilters(filter: FilterQuery): Either[UnsupportedNestingException, Option[FilterQuery]] =
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 25914f2c41..43e7fa523a 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
@@ -40,6 +40,13 @@ case object InMailboxFilter extends MailboxFilter {
override def toQuery(builder: MultimailboxesSearchQuery.Builder, request: EmailQueryRequest): MultimailboxesSearchQuery.Builder =
request.filter.flatMap {
case filterCondition: FilterCondition => Some(filterCondition)
+ // Extract mailbox condition from simple AND
+ case filterOperator: FilterOperator if filterOperator.operator == And &&
+ filterOperator.countMailboxFilter == 1 =>
+ filterOperator.conditions.flatMap {
+ case filterCondition: FilterCondition if filterCondition.inMailbox.isDefined => Some(filterCondition)
+ case _ => None
+ }.lastOption
case _ => None
}.flatMap(_.inMailbox) match {
case Some(mailboxId) => builder.inMailboxes(mailboxId)
@@ -51,6 +58,13 @@ case object NotInMailboxFilter extends MailboxFilter {
override def toQuery(builder: MultimailboxesSearchQuery.Builder, request: EmailQueryRequest): MultimailboxesSearchQuery.Builder =
request.filter.flatMap {
case filterCondition: FilterCondition => Some(filterCondition)
+ // Extract mailbox condition from simple AND
+ case filterOperator: FilterOperator if filterOperator.operator == And &&
+ filterOperator.countMailboxFilter == 1 =>
+ filterOperator.conditions.flatMap {
+ case filterCondition: FilterCondition if filterCondition.inMailboxOtherThan.isDefined => Some(filterCondition)
+ case _ => None
+ }.lastOption
case _ => None
}.flatMap(_.inMailboxOtherThan) match {
case Some(mailboxIds) => builder.notInMailboxes(mailboxIds.asJava)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org