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