You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/08/14 09:07:34 UTC
[james-project] 06/13: JAMES-3357 Handle Mailbox/set create property
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 0b14c1527de8d4420dd08106e693449a407ffc1c
Author: RĂ©mi KOWALSKI <rk...@linagora.com>
AuthorDate: Wed Jul 29 11:34:17 2020 +0200
JAMES-3357 Handle Mailbox/set create property
---
.../james/jmap/rfc8621/RFC8621MethodsModule.java | 2 +
.../DistributedMailboxSetMethodTest.java | 54 +++
.../contract/MailboxSetMethodContract.scala | 396 +++++++++++++++++++++
.../rfc8621/memory/MemoryMailboxSetMethodTest.java | 38 ++
.../org/apache/james/jmap/json/Serializer.scala | 87 ++++-
.../org/apache/james/jmap/mail/MailboxSet.scala | 73 ++++
.../scala/org/apache/james/jmap/mail/package.scala | 28 ++
.../james/jmap/method/MailboxSetMethod.scala | 123 +++++++
.../org/apache/james/jmap/model/Invocation.scala | 5 +-
9 files changed, 800 insertions(+), 6 deletions(-)
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 3012858..fd68a61 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -36,6 +36,7 @@ import org.apache.james.jmap.json.Serializer;
import org.apache.james.jmap.jwt.JWTAuthenticationStrategy;
import org.apache.james.jmap.method.CoreEchoMethod;
import org.apache.james.jmap.method.MailboxGetMethod;
+import org.apache.james.jmap.method.MailboxSetMethod;
import org.apache.james.jmap.method.Method;
import org.apache.james.jmap.model.JmapRfc8621Configuration;
import org.apache.james.jmap.routes.JMAPApiRoutes;
@@ -62,6 +63,7 @@ public class RFC8621MethodsModule extends AbstractModule {
Multibinder<Method> methods = Multibinder.newSetBinder(binder(), Method.class);
methods.addBinding().to(CoreEchoMethod.class);
methods.addBinding().to(MailboxGetMethod.class);
+ methods.addBinding().to(MailboxSetMethod.class);
}
@ProvidesIntoSet
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedMailboxSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedMailboxSetMethodTest.java
new file mode 100644
index 0000000..60e7f40
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedMailboxSetMethodTest.java
@@ -0,0 +1,54 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.MailboxSetMethodContract;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedMailboxSetMethodTest implements MailboxSetMethodContract {
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+ CassandraRabbitMQJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .blobStore(BlobStoreConfiguration.builder()
+ .objectStorage()
+ .disableCache()
+ .deduplication())
+ .build())
+ .extension(new DockerElasticSearchExtension())
+ .extension(new CassandraExtension())
+ .extension(new RabbitMQExtension())
+ .extension(new AwsS3BlobStoreExtension())
+ .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+
+}
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
new file mode 100644
index 0000000..76a21ec
--- /dev/null
+++ 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
@@ -0,0 +1,396 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.contract
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured._
+import io.restassured.http.ContentType.JSON
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.modules.MailboxProbeImpl
+import org.apache.james.utils.DataProbeImpl
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
+
+trait MailboxSetMethodContract {
+
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent
+ .addDomain(DOMAIN.asString)
+ .addUser(BOB.asString, BOB_PASSWORD)
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+ .build
+ }
+
+ @Test
+ def mailboxSetShouldReturnNotCreatedWhenNameIsMissing(): Unit = {
+ val request=
+ """
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "create": {
+ | "C42": {
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response: String =
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notCreated": {
+ | "C42": {
+ | "type": "invalidArguments",
+ | "description": "Missing '/name' property in mailbox object"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ @Disabled("should we support that? Anyway seems hard with Play-JSON")
+ def mailboxSetShouldReturnNotCreatedWhenUnknownParameter(): Unit = {
+ val request=
+ """
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "create": {
+ | "C42": {
+ | "name": "plop",
+ | "unknown": "what?"
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response: String =
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notCreated": {
+ | "C42": {
+ | "type": "invalidArguments",
+ | "description": "Unknown 'unknown' property in mailbox object"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxSetShouldReturnNotCreatedWhenBadParameter(): Unit = {
+ val request=
+ """
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "create": {
+ | "C42": {
+ | "name": "plop",
+ | "parentId": ""
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response: String =
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notCreated": {
+ | "C42": {
+ | "type": "invalidArguments",
+ | "description": "'/parentId' property in mailbox object is not valid"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxSetShouldCreateMailboxWhenOnlyName(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"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ Assertions.assertThatCode(() => server.getProbe(classOf[MailboxProbeImpl])
+ .getMailboxId("#private", BOB.asString(), "myMailbox")).doesNotThrowAnyException()
+ }
+
+ @Test
+ def mailboxSetShouldReturnCreatedWhenOnlyName(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"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response: String =
+ `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",
+ | "isSubscribed":true,
+ | "myRights":{"mayAddItems":true,"mayCreateChild":true,"mayDelete":true,"mayReadItems":true,"mayRemoveItems":true,"mayRename":true,"maySetKeywords":true,"maySetSeen":true,"maySubmit":true},
+ | "totalEmails":0,
+ | "totalThreads":0,
+ | "unreadEmails":0,
+ | "unreadThreads":0
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxSetShouldReturnCreatedAndNotCreatedWhenOneWithOnlyNameAndOneWithoutName(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"
+ | },
+ | "C43": {
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response: String =
+ `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",
+ | "isSubscribed":true,
+ | "myRights":{"mayAddItems":true,"mayCreateChild":true,"mayDelete":true,"mayReadItems":true,"mayRemoveItems":true,"mayRename":true,"maySetKeywords":true,"maySetSeen":true,"maySubmit":true},
+ | "totalEmails":0,
+ | "totalThreads":0,
+ | "unreadEmails":0,
+ | "unreadThreads":0
+ | }
+ | },
+ | "notCreated": {
+ | "C43": {
+ | "type": "invalidArguments",
+ | "description": "Missing '/name' property in mailbox object"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxSetMethodTest.java
new file mode 100644
index 0000000..876c7ed
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxSetMethodTest.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.memory;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.MailboxSetMethodContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryMailboxSetMethodTest implements MailboxSetMethodContract {
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+ .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+}
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 09484a4..f15f1ce 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
@@ -22,10 +22,13 @@ package org.apache.james.jmap.json
import java.io.InputStream
import java.net.URL
+import eu.timepit.refined._
+import eu.timepit.refined.auto._
import javax.inject.Inject
import org.apache.james.core.{Domain, Username}
-import org.apache.james.jmap.mail.{DelegatedNamespace, Ids, IsSubscribed, Mailbox, MailboxGetRequest, MailboxGetResponse, MailboxNamespace, MailboxRights, MayAddItems, MayCreateChild, MayDelete, MayReadItems, MayRemoveItems, MayRename, MaySetKeywords, MaySetSeen, MaySubmit, NotFound, PersonalNamespace, Properties, Quota, QuotaId, QuotaRoot, Quotas, Right, Rights, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, Value}
+import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId
+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
import org.apache.james.jmap.model.Invocation.{Arguments, MethodCallId, MethodName}
@@ -150,7 +153,7 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
case _ => JsError()
}
- private implicit val roleWrites: Writes[Role] = role => JsString(role.serialize)
+ private implicit val roleWrites: Writes[Role] = Writes(role => JsString(role.serialize))
private implicit val sortOrderWrites: Writes[SortOrder] = Json.valueWrites[SortOrder]
private implicit val totalEmailsWrites: Writes[TotalEmails] = Json.valueWrites[TotalEmails]
private implicit val unreadEmailsWrites: Writes[UnreadEmails] = Json.valueWrites[UnreadEmails]
@@ -219,11 +222,84 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
private implicit val propertiesRead: Reads[Properties] = Json.valueReads[Properties]
private implicit val mailboxGetRequest: Reads[MailboxGetRequest] = Json.reads[MailboxGetRequest]
+
+ private implicit val mailboxRemoveEmailsOnDestroy: Reads[RemoveEmailsOnDestroy] = Json.reads[RemoveEmailsOnDestroy]
+ implicit val mailboxCreationRequest: Reads[MailboxCreationRequest] = Json.reads[MailboxCreationRequest]
+ private implicit val mailboxPatchObject: Reads[MailboxPatchObject] = Json.valueReads[MailboxPatchObject]
+
+ private implicit val mapPatchObjectByMailboxIdReads: Reads[Map[MailboxId, MailboxPatchObject]] = _.validate[Map[String, MailboxPatchObject]]
+ .map(mapWithStringKey => mapWithStringKey
+ .map(keyValue => (mailboxIdFactory.fromString(keyValue._1), keyValue._2)))
+
+ private implicit val mapCreationRequestByMailBoxCreationId: Reads[Map[MailboxCreationId, JsObject]] = _.validate[Map[String, JsObject]]
+ .flatMap(mapWithStringKey => {
+ mapWithStringKey
+ .foldLeft[Either[JsError, Map[MailboxCreationId, JsObject]]](scala.util.Right[JsError, Map[MailboxCreationId, JsObject]](Map.empty))((acc: Either[JsError, Map[MailboxCreationId, JsObject]], keyValue) => {
+ acc match {
+ case error@Left(_) => error
+ case scala.util.Right(validatedAcc) =>
+ val refinedKey: Either[String, MailboxCreationId] = refineV(keyValue._1)
+ refinedKey match {
+ case Left(error) => Left(JsError(error))
+ case scala.util.Right(mailboxCreationId) => scala.util.Right(validatedAcc + (mailboxCreationId -> keyValue._2))
+ }
+ }
+ }) match {
+ case Left(jsError) => jsError
+ case scala.util.Right(value) => JsSuccess(value)
+ }
+ })
+
+ private implicit val mailboxSetRequestReads: Reads[MailboxSetRequest] = Json.reads[MailboxSetRequest]
+
private implicit def notFoundWrites(implicit mailboxIdWrites: Writes[MailboxId]): Writes[NotFound] =
notFound => JsArray(notFound.value.toList.map(mailboxIdWrites.writes))
private implicit def mailboxGetResponseWrites(implicit mailboxWrites: Writes[Mailbox]): Writes[MailboxGetResponse] = Json.writes[MailboxGetResponse]
+ private implicit val mailboxSetResponseWrites: Writes[MailboxSetResponse] = Json.writes[MailboxSetResponse]
+
+
+ private implicit val mailboxSetCreationResponseWrites: Writes[MailboxCreationResponse] = Json.writes[MailboxCreationResponse]
+
+ private implicit val mailboxSetUpdateResponseWrites: Writes[MailboxUpdateResponse] = Json.writes[MailboxUpdateResponse]
+
+ private implicit val propertiesWrites: Writes[Properties] = Json.writes[Properties]
+
+ private implicit val setErrorDescriptionWrites: Writes[SetErrorDescription] = Json.valueWrites[SetErrorDescription]
+
+ private implicit val mailboxSetErrorWrites: Writes[MailboxSetError] = Json.writes[MailboxSetError]
+
+ private implicit def mailboxMapSetErrorForCreationWrites: Writes[Map[MailboxCreationId, MailboxSetError]] =
+ (m: Map[MailboxCreationId, MailboxSetError]) => {
+ m.foldLeft(JsObject.empty)((jsObject, kv) => {
+ val (mailboxCreationId: MailboxCreationId, mailboxSetError: MailboxSetError) = kv
+ jsObject.+(mailboxCreationId, mailboxSetErrorWrites.writes(mailboxSetError))
+ })
+ }
+ private implicit def mailboxMapSetErrorWrites: Writes[Map[MailboxId, MailboxSetError]] =
+ (m: Map[MailboxId, MailboxSetError]) => {
+ m.foldLeft(JsObject.empty)((jsObject, kv) => {
+ val (mailboxId: MailboxId, mailboxSetError: MailboxSetError) = kv
+ jsObject.+(mailboxId.serialize(), mailboxSetErrorWrites.writes(mailboxSetError))
+ })
+ }
+
+ private implicit def mailboxMapCreationResponseWrites: Writes[Map[MailboxCreationId, MailboxCreationResponse]] =
+ (m: Map[MailboxCreationId, MailboxCreationResponse]) => {
+ m.foldLeft(JsObject.empty)((jsObject, kv) => {
+ val (mailboxCreationId: MailboxCreationId, mailboxCreationResponse: MailboxCreationResponse) = kv
+ jsObject.+(mailboxCreationId, mailboxSetCreationResponseWrites.writes(mailboxCreationResponse))
+ })
+ }
+ private implicit def mailboxMapUpdateResponseWrites: Writes[Map[MailboxId, MailboxUpdateResponse]] =
+ (m: Map[MailboxId, MailboxUpdateResponse]) => {
+ m.foldLeft(JsObject.empty)((jsObject, kv) => {
+ val (mailboxId: MailboxId, mailboxUpdateResponse: MailboxUpdateResponse) = kv
+ jsObject.+(mailboxId.serialize(), mailboxSetUpdateResponseWrites.writes(mailboxUpdateResponse))
+ })
+ }
+
private implicit val jsonValidationErrorWrites: Writes[JsonValidationError] = error => JsString(error.message)
private implicit def jsonValidationErrorsWrites(implicit jsonValidationErrorWrites: Writes[JsonValidationError]): Writes[LegacySeq[JsonValidationError]] =
@@ -261,9 +337,10 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
def serialize(mailboxGetResponse: MailboxGetResponse)(implicit mailboxWrites: Writes[Mailbox]): JsValue = Json.toJson(mailboxGetResponse)
- def serialize(mailboxGetResponse: MailboxGetResponse, properties: Option[Properties], capabilities: Set[CapabilityIdentifier]): JsValue = {
+ def serialize(mailboxGetResponse: MailboxGetResponse, properties: Option[Properties], capabilities: Set[CapabilityIdentifier]): JsValue =
serialize(mailboxGetResponse)(mailboxWritesWithFilteredProperties(properties, capabilities))
- }
+
+ def serialize(mailboxSetResponse: MailboxSetResponse): JsValue = Json.toJson(mailboxSetResponse)(mailboxSetResponseWrites)
def serialize(errors: JsError): JsValue = Json.toJson(errors)
@@ -276,4 +353,6 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
def deserializeMailboxGetRequest(input: String): JsResult[MailboxGetRequest] = Json.parse(input).validate[MailboxGetRequest]
def deserializeMailboxGetRequest(input: JsValue): JsResult[MailboxGetRequest] = Json.fromJson[MailboxGetRequest](input)
+
+ def deserializeMailboxSetRequest(input: JsValue): JsResult[MailboxSetRequest] = Json.fromJson[MailboxSetRequest](input)
}
\ No newline at end of file
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
new file mode 100644
index 0000000..07517dc
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
@@ -0,0 +1,73 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.mail
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.collection.NonEmpty
+import org.apache.james.jmap.mail.MailboxName.MailboxName
+import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId
+import org.apache.james.jmap.model.AccountId
+import org.apache.james.jmap.model.State.State
+import org.apache.james.mailbox.Role
+import org.apache.james.mailbox.model.MailboxId
+import play.api.libs.json.JsObject
+
+case class MailboxSetRequest(accountId: AccountId,
+ ifInState: Option[State],
+ create: Option[Map[MailboxCreationId, JsObject]],
+ update: Option[Map[MailboxId, MailboxPatchObject]],
+ destroy: Option[Seq[MailboxId]],
+ onDestroyRemoveEmails: Option[RemoveEmailsOnDestroy])
+
+object MailboxSetRequest {
+ type MailboxCreationId = String Refined NonEmpty
+}
+
+case class RemoveEmailsOnDestroy(value: Boolean) extends AnyVal
+case class MailboxCreationRequest(name: MailboxName, parentId: Option[MailboxId])
+
+case class MailboxPatchObject(value: Map[String, JsObject])
+
+case class MailboxSetResponse(accountId: AccountId,
+ oldState: Option[State],
+ newState: State,
+ created: Option[Map[MailboxCreationId, MailboxCreationResponse]],
+ updated: Option[Map[MailboxId, MailboxUpdateResponse]],
+ destroyed: Option[Seq[MailboxId]],
+ notCreated: Option[Map[MailboxCreationId, MailboxSetError]],
+ notUpdated: Option[Map[MailboxId, MailboxSetError]],
+ notDestroyed: Option[Map[MailboxId, MailboxSetError]])
+
+case class MailboxSetError(`type`: SetErrorType, description: Option[SetErrorDescription], properties: Option[Properties])
+
+case class MailboxCreationResponse(id: MailboxId,
+ role: Option[Role],//TODO see if we need to return this, if a role is set by the server during creation
+ totalEmails: TotalEmails,
+ unreadEmails: UnreadEmails,
+ totalThreads: TotalThreads,
+ unreadThreads: UnreadThreads,
+ myRights: MailboxRights,
+ rights: Option[Rights],//TODO display only if RightsExtension and if some rights are set by the server during creation
+ namespace: Option[MailboxNamespace], //TODO display only if RightsExtension
+ quotas: Option[Quotas],//TODO display only if QuotasExtension
+ isSubscribed: IsSubscribed
+ )
+
+case class MailboxUpdateResponse(value: JsObject)
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/package.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/package.scala
new file mode 100644
index 0000000..981e224
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/package.scala
@@ -0,0 +1,28 @@
+/** **************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ * ***************************************************************/
+package org.apache.james.jmap
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.collection.NonEmpty
+
+
+package object mail {
+ type SetErrorType = String Refined NonEmpty
+ case class SetErrorDescription(description: String) extends AnyVal
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
new file mode 100644
index 0000000..86479c0
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
@@ -0,0 +1,123 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+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.MailboxCreationId
+import org.apache.james.jmap.mail.{IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, SetErrorDescription, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads}
+import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
+import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.model.{Invocation, State}
+import org.apache.james.mailbox.model.{MailboxId, MailboxPath}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
+import org.apache.james.metrics.api.MetricFactory
+import org.reactivestreams.Publisher
+import play.api.libs.json.{JsError, JsObject, JsPath, JsSuccess, Json, JsonValidationError}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import scala.collection.immutable
+
+class MailboxSetMethod @Inject() (serializer: Serializer,
+ mailboxManager: MailboxManager,
+ metricFactory: MetricFactory) extends Method {
+ override val methodName: MethodName = MethodName("Mailbox/set")
+
+
+ override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession): Publisher[Invocation] = {
+ metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value,
+ asMailboxSetRequest(invocation.arguments)
+ .flatMap(mailboxSetRequest => {
+ val (unparsableCreateRequests, createRequests) = parseCreateRequests(mailboxSetRequest)
+ for {
+ created <- createMailboxes(mailboxSession, createRequests)
+ } yield createResponse(invocation, mailboxSetRequest, unparsableCreateRequests, created)
+ }))
+ }
+
+ private def parseCreateRequests(mailboxSetRequest: MailboxSetRequest): (immutable.Iterable[(MailboxCreationId, MailboxSetError)], immutable.Iterable[(MailboxCreationId, MailboxCreationRequest)]) = {
+ mailboxSetRequest.create
+ .getOrElse(Map.empty)
+ .view
+ .mapValues(value => Json.fromJson(value)(serializer.mailboxCreationRequest))
+ .toMap
+ .partitionMap { case (creationId, creationRequestParseResult) =>
+ creationRequestParseResult match {
+ case JsSuccess(creationRequest, _) => Right((creationId, creationRequest))
+ case JsError(errors) => Left(creationId, mailboxSetError(errors))
+ }
+ }
+ }
+
+ private def mailboxSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): MailboxSetError =
+ errors.head match {
+ case (path, Seq()) => MailboxSetError("invalidArguments", Some(SetErrorDescription(s"'$path' property in mailbox object is not valid")), None)
+ case (path, Seq(JsonValidationError(Seq("error.path.missing")))) => MailboxSetError("invalidArguments", Some(SetErrorDescription(s"Missing '$path' property in mailbox object")), None)
+ case (path, _) => MailboxSetError("invalidArguments", Some(SetErrorDescription(s"Unknown error on property '$path'")), None)
+ }
+
+ private def createMailboxes(mailboxSession: MailboxSession, createRequests: immutable.Iterable[(MailboxCreationId, MailboxCreationRequest)]): SMono[Map[MailboxCreationId, MailboxId]] = {
+ SFlux.fromIterable(createRequests).flatMap {
+ case (mailboxCreationId: MailboxCreationId, mailboxCreationRequest: MailboxCreationRequest) => {
+ SMono.fromCallable(() => {
+ val path = MailboxPath.forUser(mailboxSession.getUser, mailboxCreationRequest.name)
+ //can safely do a get as the Optional is empty only if the mailbox name is empty which is forbidden by the type constraint on MailboxName
+ (mailboxCreationId, mailboxManager.createMailbox(path, mailboxSession).get())
+ }).subscribeOn(Schedulers.elastic())
+ }
+ }.collectMap(_._1, _._2)
+ }
+
+ private def createResponse(invocation: Invocation, mailboxSetRequest: MailboxSetRequest, createErrors: immutable.Iterable[(MailboxCreationId, MailboxSetError)], created: Map[MailboxCreationId, MailboxId]): Invocation = {
+ Invocation(methodName, Arguments(serializer.serialize(MailboxSetResponse(
+ mailboxSetRequest.accountId,
+ oldState = None,
+ newState = State.INSTANCE,
+ created = Some(created.map(creation => (creation._1, MailboxCreationResponse(
+ id = creation._2,
+ role = None,
+ totalEmails = TotalEmails(0L),
+ unreadEmails = UnreadEmails(0L),
+ totalThreads = TotalThreads(0L),
+ unreadThreads = UnreadThreads(0L),
+ myRights = MailboxRights.FULL,
+ rights = None,
+ namespace = None,
+ quotas = None,
+ isSubscribed = IsSubscribed(true)
+
+ )))).filter(_.nonEmpty),
+ notCreated = Some(createErrors.toMap).filter(_.nonEmpty),
+ updated = None,
+ notUpdated = None,
+ destroyed = None,
+ notDestroyed = None
+ )).as[JsObject]), invocation.methodCallId)
+ }
+
+ private def asMailboxSetRequest(arguments: Arguments): SMono[MailboxSetRequest] = {
+ serializer.deserializeMailboxSetRequest(arguments.value) match {
+ case JsSuccess(mailboxSetRequest, _) => SMono.just(mailboxSetRequest)
+ case errors: JsError => SMono.raiseError(new IllegalArgumentException(serializer.serialize(errors).toString))
+ }
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
index 331d2f1..5875ad8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
@@ -35,8 +35,9 @@ object Invocation {
case class MethodCallId(value: NonEmptyString)
- def error(errorCode: ErrorCode, description: String, methodCallId: MethodCallId): Invocation = Invocation(MethodName("error"),
- Arguments(JsObject(Map("type" -> JsString(errorCode.code), "description" -> JsString(description)))),
+ def error(errorCode: ErrorCode, description: Option[String], methodCallId: MethodCallId): Invocation = {
+ Invocation(MethodName("error"),
+ Arguments(JsObject(Seq("type" -> JsString(errorCode.code), "description" -> JsString(description.getOrElse(""))))),
methodCallId)
def error(errorCode: ErrorCode, methodCallId: MethodCallId): Invocation = Invocation(MethodName("error"),
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org