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 6e8161ff71943d486f3717c2547ee4deb53d90fe Author: Benoit Tellier <btell...@linagora.com> AuthorDate: Fri Aug 14 14:06:05 2020 +0700 JAMES-3356 Allow the use of creationIds in Mailbox/Get --- .../contract/MailboxGetMethodContract.scala | 82 ++++++++++++++++ .../contract/MailboxSetMethodContract.scala | 105 +++++++++++++++++++++ .../org/apache/james/jmap/json/Serializer.scala | 4 +- .../org/apache/james/jmap/mail/MailboxGet.scala | 6 +- .../org/apache/james/jmap/mail/MailboxSet.scala | 9 +- .../james/jmap/method/MailboxGetMethod.scala | 18 +++- .../jmap/json/MailboxGetSerializationTest.scala | 17 +++- 7 files changed, 226 insertions(+), 15 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/MailboxGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala index b2c2d31..7776b26 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala @@ -560,6 +560,88 @@ trait MailboxGetMethodContract { } @Test + def getMailboxesShouldReturnNotFoundWhenInvalid(): Unit = { + val response: String = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "properties": ["id", "name", "rights"], + | "ids": ["invalid"] + | }, + | "c1"]] + |}""".stripMargin) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [], + | "notFound": ["invalid"] + | }, + | "c1"]] + |}""".stripMargin) + } + + @Test + def getMailboxesShouldReturnNotFoundWhenUnknownClientId(): Unit = { + val response: String = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "urn:ietf:params:jmap:mail"], + | "methodCalls": [[ + | "Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "properties": ["id", "name", "rights"], + | "ids": ["#C42"] + | }, + | "c1"]] + |}""".stripMargin) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [[ + | "Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [], + | "notFound": ["#C42"] + | }, + | "c1"]] + |}""".stripMargin) + } + + @Test def getMailboxesShouldReturnInvalidArgumentsErrorWhenInvalidProperty(server: GuiceJamesServer): Unit = { val mailboxId: String = server.getProbe(classOf[MailboxProbeImpl]) .createMailbox(MailboxPath.forUser(BOB, "custom")) 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/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala index 28bd5fd..21e234e 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala @@ -268,6 +268,111 @@ trait MailboxSetMethodContract { } @Test + def mailboxGetShouldAllowTheUseOfCreationIds(server: GuiceJamesServer): Unit = { + val request= + """ + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "create": { + | "C42": { + | "name": "myMailbox" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["#C42"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + val mailboxId: String = server.getProbe(classOf[MailboxProbeImpl]) + .getMailboxId("#private", BOB.asString(), "myMailbox") + .serialize() + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "created": { + | "C42": { + | "id": "$mailboxId", + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": true + | } + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "$mailboxId", + | "name": "myMailbox", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound":[] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test def mailboxSetShouldReturnCreatedWhenOnlyName(server: GuiceJamesServer): Unit = { val request= """ diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala index 5ba43fb..e635254 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala @@ -27,7 +27,7 @@ import eu.timepit.refined.auto._ import javax.inject.Inject import org.apache.james.core.{Domain, Username} -import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId +import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId} import org.apache.james.jmap.mail.{DelegatedNamespace, Ids, IsSubscribed, Mailbox, MailboxCreationRequest, MailboxCreationResponse, MailboxGetRequest, MailboxGetResponse, MailboxNamespace, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, MayAddItems, MayCreateChild, MayDelete, MayReadItems, MayRemoveItems, MayRename, MaySetKeywords, MaySetSeen, MaySubmit, NotFound, PersonalNamespace, Properties, Quota, QuotaId, QuotaRoot, Q [...] import org.apache.james.jmap.model import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier @@ -252,7 +252,7 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) { private implicit val mailboxSetRequestReads: Reads[MailboxSetRequest] = Json.reads[MailboxSetRequest] - private implicit def notFoundWrites(implicit mailboxIdWrites: Writes[MailboxId]): Writes[NotFound] = + private implicit def notFoundWrites(implicit mailboxIdWrites: Writes[UnparsedMailboxId]): Writes[NotFound] = notFound => JsArray(notFound.value.toList.map(mailboxIdWrites.writes)) private implicit def mailboxGetResponseWrites(implicit mailboxWrites: Writes[Mailbox]): Writes[MailboxGetResponse] = Json.writes[MailboxGetResponse] diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala index 613d418..1169652 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala @@ -20,11 +20,11 @@ package org.apache.james.jmap.mail import eu.timepit.refined.types.string.NonEmptyString +import org.apache.james.jmap.mail.MailboxSetRequest.UnparsedMailboxId import org.apache.james.jmap.model.AccountId import org.apache.james.jmap.model.State.State -import org.apache.james.mailbox.model.MailboxId -case class Ids(value: List[MailboxId]) +case class Ids(value: List[UnparsedMailboxId]) case class Properties(value: List[NonEmptyString]) { def asSetOfString: Set[String] = value.map(_.toString()).toSet @@ -35,7 +35,7 @@ case class MailboxGetRequest(accountId: AccountId, ids: Option[Ids], properties: Option[Properties]) -case class NotFound(value: Set[MailboxId]) { +case class NotFound(value: Set[UnparsedMailboxId]) { def merge(other: NotFound): NotFound = NotFound(this.value ++ other.value) } diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala index fdf2d18..794a6a6 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala @@ -19,6 +19,7 @@ package org.apache.james.jmap.mail +import eu.timepit.refined import eu.timepit.refined.api.Refined import eu.timepit.refined.auto._ import eu.timepit.refined.collection.NonEmpty @@ -38,8 +39,14 @@ case class MailboxSetRequest(accountId: AccountId, onDestroyRemoveEmails: Option[RemoveEmailsOnDestroy]) object MailboxSetRequest { + type UnparsedMailboxIdConstraint = NonEmpty type MailboxCreationId = String Refined NonEmpty - type UnparsedMailboxId = String Refined NonEmpty + type UnparsedMailboxId = String Refined UnparsedMailboxIdConstraint + + def asUnparsed(mailboxId: MailboxId): UnparsedMailboxId = refined.refineV[UnparsedMailboxIdConstraint](mailboxId.serialize()) match { + case Left(e) => throw new IllegalArgumentException(e) + case scala.Right(value) => value + } } case class RemoveEmailsOnDestroy(value: Boolean) extends AnyVal diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala index 7e66b5e..992cb74 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala @@ -22,6 +22,7 @@ package org.apache.james.jmap.method import eu.timepit.refined.auto._ import javax.inject.Inject import org.apache.james.jmap.json.Serializer +import org.apache.james.jmap.mail.MailboxSetRequest.UnparsedMailboxId import org.apache.james.jmap.mail._ import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier import org.apache.james.jmap.model.Invocation.{Arguments, MethodName} @@ -42,20 +43,25 @@ import reactor.core.scheduler.Schedulers class MailboxGetMethod @Inject() (serializer: Serializer, mailboxManager: MailboxManager, quotaFactory : QuotaLoaderWithPreloadedDefaultFactory, + mailboxIdFactory: MailboxId.Factory, mailboxFactory: MailboxFactory, metricFactory: MetricFactory) extends Method { override val methodName: MethodName = MethodName("Mailbox/get") object MailboxGetResults { def found(mailbox: Mailbox): MailboxGetResults = MailboxGetResults(Set(mailbox), NotFound(Set.empty)) - def notFound(mailboxId: MailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(mailboxId))) + def notFound(mailboxId: UnparsedMailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(mailboxId))) + def notFound(mailboxId: MailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(MailboxSetRequest.asUnparsed(mailboxId)))) } case class MailboxGetResults(mailboxes: Set[Mailbox], notFound: NotFound) { def merge(other: MailboxGetResults): MailboxGetResults = MailboxGetResults(this.mailboxes ++ other.mailboxes, this.notFound.merge(other.notFound)) } - override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): Publisher[Invocation] = { + override def process(capabilities: Set[CapabilityIdentifier], + invocation: Invocation, + mailboxSession: MailboxSession, + processingContext: ProcessingContext): Publisher[Invocation] = { metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value, asMailboxGetRequest(invocation.arguments) .flatMap(mailboxGetRequest => { @@ -64,7 +70,7 @@ class MailboxGetMethod @Inject() (serializer: Serializer, SMono.just(Invocation.error(errorCode = ErrorCode.InvalidArguments, description = s"The following properties [${properties.asSetOfString.diff(Mailbox.allProperties).mkString(", ")}] do not exist.", methodCallId = invocation.methodCallId)) - case _ => getMailboxes(capabilities, mailboxGetRequest, mailboxSession) + case _ => getMailboxes(capabilities, mailboxGetRequest, processingContext, mailboxSession) .reduce(MailboxGetResults(Set.empty, NotFound(Set.empty)), (result1: MailboxGetResults, result2: MailboxGetResults) => result1.merge(result2)) .map(mailboxes => MailboxGetResponse( accountId = mailboxGetRequest.accountId, @@ -89,12 +95,16 @@ class MailboxGetMethod @Inject() (serializer: Serializer, private def getMailboxes(capabilities: Set[CapabilityIdentifier], mailboxGetRequest: MailboxGetRequest, + processingContext: ProcessingContext, mailboxSession: MailboxSession): SFlux[MailboxGetResults] = + mailboxGetRequest.ids match { case None => getAllMailboxes(capabilities, mailboxSession) .map(MailboxGetResults.found) case Some(ids) => SFlux.fromIterable(ids.value) - .flatMap(id => getMailboxResultById(capabilities, id, mailboxSession)) + .flatMap(id => processingContext.resolveMailboxId(id, mailboxIdFactory) + .fold(e => SMono.just(MailboxGetResults.notFound(id)), + mailboxId => getMailboxResultById(capabilities, mailboxId, mailboxSession))) } private def getMailboxResultById(capabilities: Set[CapabilityIdentifier], diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala index aa22699..3fed3f5 100644 --- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala @@ -24,12 +24,13 @@ import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson import org.apache.james.jmap.json.Fixture._ import org.apache.james.jmap.json.MailboxGetSerializationTest._ import org.apache.james.jmap.json.MailboxSerializationTest.MAILBOX +import org.apache.james.jmap.mail.MailboxSetRequest.UnparsedMailboxId import org.apache.james.jmap.mail._ import org.apache.james.jmap.model.AccountId import org.apache.james.mailbox.model.{MailboxId, TestId} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import play.api.libs.json.{JsError, JsSuccess, Json} +import play.api.libs.json.{JsSuccess, Json} object MailboxGetSerializationTest { private val FACTORY: MailboxId.Factory = new TestId.Factory @@ -38,22 +39,28 @@ object MailboxGetSerializationTest { private val ACCOUNT_ID: AccountId = AccountId(id) - private val MAILBOX_ID_1: MailboxId = FACTORY.fromString("1") - private val MAILBOX_ID_2: MailboxId = FACTORY.fromString("2") + private val MAILBOX_ID_1: UnparsedMailboxId = "1" + private val MAILBOX_ID_2: UnparsedMailboxId = "2" private val PROPERTIES: Properties = Properties(List("name", "role")) } class MailboxGetSerializationTest extends AnyWordSpec with Matchers { "Deserialize MailboxGetRequest" should { - "fail when MailboxId.Factory can't deserialize MailboxId" in { + "succeed on invalid mailboxId" in { + // as they are unparsed + val expectedRequestObject = MailboxGetRequest( + accountId = ACCOUNT_ID, + ids = Some(Ids(List("ab#?"))), + properties = None) + SERIALIZER.deserializeMailboxGetRequest( """ |{ | "accountId": "aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8", | "ids": ["ab#?"] |} - |""".stripMargin) shouldBe a [JsError] + |""".stripMargin) should equal(JsSuccess(expectedRequestObject)) } "succeed when properties are missing" in { --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org