You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/10/02 08:48:39 UTC

[james-project] 03/08: JAMES-3390 Reject nested inMailbox & inMailboxOtherThan

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit b69ef78a19737af3f81d1538cb6008ae6e55b850
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Sep 30 09:57:01 2020 +0700

    JAMES-3390 Reject nested inMailbox & inMailboxOtherThan
---
 .../contract/EmailQueryMethodContract.scala        | 126 +++++++++++++++++++++
 .../doc/specs/spec/mail/message.mdown              |   3 +
 .../org/apache/james/jmap/mail/EmailQuery.scala    |  28 ++++-
 .../org/apache/james/jmap/method/Method.scala      |   6 +-
 .../james/jmap/utils/search/MailboxFilter.scala    |  39 +++----
 5 files changed, 181 insertions(+), 21 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 9ab093d..8cc4b2a 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
@@ -5039,6 +5039,132 @@ trait EmailQueryMethodContract {
   }
 
   @Test
+  def inMailboxShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
+    val message: Message = buildTestMessage
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+      .getMessageId
+
+    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+      .getMessageId
+
+    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).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)
+      .isEqualTo(s"""{
+                    |    "sessionState": "75128aab4b1b",
+                    |    "methodResponses": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "unsupportedFilter",
+                    |                "description": "Nested inMailbox filters are not supported"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
+  def inMailboxOtherThanShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
+    val message: Message = buildTestMessage
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
+      .getMessageId
+
+    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
+      .getMessageId
+
+    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).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": [
+         |          { "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
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(s"""{
+                    |    "sessionState": "75128aab4b1b",
+                    |    "methodResponses": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "unsupportedFilter",
+                    |                "description": "Nested inMailboxOtherThan filter are not supported"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
   def emailQueryFilterByTextShouldIgnoreAttachmentContent(server: GuiceJamesServer): Unit = {
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(inbox(BOB))
     server.getProbe(classOf[MailboxProbeImpl])
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
index 28a62aa..52306f6 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
@@ -741,6 +741,9 @@ These properties are not supported yet for filtering:
 - noneInThreadHaveKeyword
 - text
 - body
+
+Also please note that inMailbox and inMailboxOtherThan filters are rejected
+when nested in operator. They need to be specified in a filter condition.
 </aside>
 
 The exact semantics for matching `String` fields is **deliberately not defined** to allow for flexibility in indexing implementation, subject to the following:
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 7d678a8..2badc57 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
@@ -19,6 +19,7 @@
 
 package org.apache.james.jmap.mail
 
+import cats.implicits._
 import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.IsAscending.ASCENDING
 import org.apache.james.jmap.method.WithAccountId
@@ -31,6 +32,7 @@ import org.apache.james.mailbox.model.{MailboxId, MessageId, SearchQuery}
 
 case class UnsupportedSortException(unsupportedSort: String) extends UnsupportedOperationException
 case class UnsupportedFilterException(unsupportedFilter: String) extends UnsupportedOperationException
+case class UnsupportedNestingException(message: String) extends UnsupportedOperationException
 case class UnsupportedRequestParameterException(unsupportedParam: String) extends UnsupportedOperationException
 
 sealed trait FilterQuery
@@ -79,7 +81,31 @@ case class EmailQueryRequest(accountId: AccountId,
                              comparator: Option[Set[Comparator]],
                              collapseThreads: Option[CollapseThreads],
                              anchor: Option[Anchor],
-                             anchorOffset: Option[AnchorOffset]) extends WithAccountId
+                             anchorOffset: Option[AnchorOffset]) extends WithAccountId {
+  val validatedFilter: Either[UnsupportedNestingException, Option[FilterQuery]] =
+    filter.map(validateFilter)
+      .sequence
+      .map(_.flatten)
+
+  private def validateFilter(filter: FilterQuery): Either[UnsupportedNestingException, Option[FilterQuery]] = filter match {
+    case filterCondition: FilterCondition => scala.Right(Some(filterCondition))
+    case filterOperator: FilterOperator => rejectMailboxFilters(filterOperator)
+  }
+
+  private def rejectMailboxFilters(filter: FilterQuery): Either[UnsupportedNestingException, Option[FilterQuery]] =
+    filter match {
+      case filterCondition: FilterCondition if filterCondition.inMailbox.isDefined =>
+        scala.Left(UnsupportedNestingException("Nested inMailbox filters are not supported"))
+      case filterCondition: FilterCondition if filterCondition.inMailboxOtherThan.isDefined =>
+        scala.Left(UnsupportedNestingException("Nested inMailboxOtherThan filter are not supported"))
+      case filterCondition: FilterCondition => scala.Right(Some(filterCondition))
+      case filterOperator: FilterOperator => filterOperator.conditions
+        .toList
+        .map(rejectMailboxFilters)
+        .sequence
+        .map(_ => Some(filterOperator))
+    }
+}
 
 sealed trait SortProperty {
   def toSortClause: Either[UnsupportedSortException, SortClause]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
index 5ee14cb..1322f1e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
@@ -20,7 +20,7 @@ package org.apache.james.jmap.method
 
 
 import org.apache.james.jmap.http.SessionSupplier
-import org.apache.james.jmap.mail.{UnsupportedFilterException, UnsupportedRequestParameterException, UnsupportedSortException}
+import org.apache.james.jmap.mail.{UnsupportedFilterException, UnsupportedNestingException, UnsupportedRequestParameterException, UnsupportedSortException}
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.MethodName
 import org.apache.james.jmap.model.{AccountId, Capabilities, ErrorCode, Invocation, Session}
@@ -71,6 +71,10 @@ trait MethodRequiringAccountId[REQUEST <: WithAccountId] extends Method {
           ErrorCode.UnsupportedFilter,
           s"The filter ${e.unsupportedFilter} is syntactically valid, but the server cannot process it. If the filter was the result of a user’s search input, the client SHOULD suggest that the user simplify their search.",
           invocation.invocation.methodCallId), invocation.processingContext))
+        case e: UnsupportedNestingException => SMono.just(InvocationWithContext(Invocation.error(
+          ErrorCode.UnsupportedFilter,
+          description = e.message,
+          invocation.invocation.methodCallId), invocation.processingContext))
         case e: IllegalArgumentException => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
         case e: MailboxNotFoundException => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
         case e: Throwable => SMono.raiseError(e)
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 1dcc2f4..754e590 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
@@ -100,9 +100,10 @@ object MailboxFilter {
   }
 
   object QueryFilter {
-    def buildQuery(request: EmailQueryRequest): Either[UnsupportedFilterException, SearchQuery.Builder] =
-      request.filter.map(toCriterion)
-        .getOrElse(Right(Nil))
+    def buildQuery(request: EmailQueryRequest): Either[UnsupportedOperationException, SearchQuery.Builder] =
+      request.validatedFilter.flatMap(
+        _.map(toCriterion)
+          .getOrElse(Right(Nil)))
         .map(criteria => new SearchQuery.Builder().andCriteria(criteria.asJava))
 
     def toCriterion(filterQuery: FilterQuery): Either[UnsupportedFilterException, List[Criterion]] =
@@ -121,7 +122,7 @@ object MailboxFilter {
           val strictlyBefore = SearchQuery.internalDateBefore(Date.from(before.asUTC.toInstant), Second)
           val sameDate = SearchQuery.internalDateOn(Date.from(before.asUTC.toInstant), Second)
           Right(List(SearchQuery.or(strictlyBefore, sameDate)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 
@@ -131,7 +132,7 @@ object MailboxFilter {
         case Some(after) =>
           val strictlyAfter = new InternalDateCriterion(new DateOperator(DateComparator.AFTER, Date.from(after.asUTC.toInstant), DateResolution.Second))
           Right(List(strictlyAfter))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 
@@ -139,7 +140,7 @@ object MailboxFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.hasAttachment match {
         case Some(hasAttachment) => Right(List(SearchQuery.hasAttachment(hasAttachment.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 
@@ -150,7 +151,7 @@ object MailboxFilter {
           Right(List(SearchQuery.or(
               SearchQuery.sizeGreaterThan(minSize.value),
               SearchQuery.sizeEquals(minSize.value))))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 
@@ -159,7 +160,7 @@ object MailboxFilter {
       filterCondition.maxSize match {
         case Some(maxSize) =>
           Right(List(SearchQuery.sizeLessThan(maxSize.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 
@@ -171,7 +172,7 @@ object MailboxFilter {
             case Some(systemFlag) => Right(List(SearchQuery.flagIsSet(systemFlag)))
             case None => Right(List(SearchQuery.flagIsSet(keyword.flagName)))
           }
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object NotKeyWord extends ConditionFilter {
@@ -182,28 +183,28 @@ object MailboxFilter {
             case Some(systemFlag) => Right(List(SearchQuery.flagIsUnSet(systemFlag)))
             case None => Right(List(SearchQuery.flagIsUnSet(keyword.flagName)))
           }
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object AllInThreadHaveKeyword extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.allInThreadHaveKeyword match {
         case Some(_) => Left(UnsupportedFilterException("allInThreadHaveKeyword"))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object NoneInThreadHaveKeyword extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.noneInThreadHaveKeyword match {
         case Some(_) => Left(UnsupportedFilterException("noneInThreadHaveKeyword"))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object SomeInThreadHaveKeyword extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.someInThreadHaveKeyword match {
         case Some(_) => Left(UnsupportedFilterException("someInThreadHaveKeyword"))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 
@@ -227,35 +228,35 @@ object MailboxFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.from match {
         case Some(from) => Right(List(SearchQuery.address(AddressType.From, from.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object To extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.to match {
         case Some(to) => Right(List(SearchQuery.address(AddressType.To, to.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object Cc extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.cc match {
         case Some(cc) => Right(List(SearchQuery.address(AddressType.Cc, cc.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object Bcc extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.bcc match {
         case Some(bcc) => Right(List(SearchQuery.address(AddressType.Bcc, bcc.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object Subject extends ConditionFilter {
     override def toQuery(filterCondition: FilterCondition): Either[UnsupportedFilterException, List[Criterion]] =
       filterCondition.subject match {
         case Some(subject) => Right(List(SearchQuery.headerContains("Subject", subject.value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
   case object Header extends ConditionFilter {
@@ -263,7 +264,7 @@ object MailboxFilter {
       filterCondition.header match {
         case Some(HeaderExist(name)) => Right(List(SearchQuery.headerExists(name)))
         case Some(HeaderContains(name, value)) => Right(List(SearchQuery.headerContains(name, value)))
-        case None => Right(List())
+        case None => Right(Nil)
       }
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org