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/11/04 02:10:25 UTC
[james-project] 02/02: JAMES-3830 Implement JMAP Quota/changes
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 8ea77b7e361fb9f42b35262b15b8f54edba68e88
Author: Tung Van TRAN <vt...@linagora.com>
AuthorDate: Wed Oct 26 17:09:24 2022 +0700
JAMES-3830 Implement JMAP Quota/changes
---
.../james/jmap/rfc8621/RFC8621MethodsModule.java | 2 +
.../DistributedQuotaChangesMethodTest.java | 55 +++
.../contract/QuotaChangesMethodContract.scala | 528 +++++++++++++++++++++
.../rfc8621/contract/QuotaGetMethodContract.scala | 173 +++----
.../memory/MemoryQuotaChangesMethodTest.java | 44 ++
.../doc/specs/spec/quotas/quota.mdown | 4 +-
.../apache/james/jmap/json/QuotaSerializer.scala | 19 +-
.../scala/org/apache/james/jmap/mail/Quotas.scala | 61 ++-
.../james/jmap/method/QuotaChangesMethod.scala | 79 +++
.../apache/james/jmap/method/QuotaGetMethod.scala | 55 +--
10 files changed, 881 insertions(+), 139 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 fbfe7c036e..571ef5434c 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
@@ -64,6 +64,7 @@ import org.apache.james.jmap.method.MailboxSetMethod;
import org.apache.james.jmap.method.Method;
import org.apache.james.jmap.method.PushSubscriptionGetMethod;
import org.apache.james.jmap.method.PushSubscriptionSetMethod;
+import org.apache.james.jmap.method.QuotaChangesMethod;
import org.apache.james.jmap.method.QuotaGetMethod;
import org.apache.james.jmap.method.SystemZoneIdProvider;
import org.apache.james.jmap.method.ThreadChangesMethod;
@@ -143,6 +144,7 @@ public class RFC8621MethodsModule extends AbstractModule {
methods.addBinding().to(MDNSendMethod.class);
methods.addBinding().to(PushSubscriptionGetMethod.class);
methods.addBinding().to(PushSubscriptionSetMethod.class);
+ methods.addBinding().to(QuotaChangesMethod.class);
methods.addBinding().to(QuotaGetMethod.class);
methods.addBinding().to(ThreadChangesMethod.class);
methods.addBinding().to(ThreadGetMethod.class);
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/DistributedQuotaChangesMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedQuotaChangesMethodTest.java
new file mode 100644
index 0000000000..3f274d9523
--- /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/DistributedQuotaChangesMethodTest.java
@@ -0,0 +1,55 @@
+/****************************************************************
+ * 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.DockerOpenSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.QuotaChangesMethodContract;
+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 DistributedQuotaChangesMethodTest implements QuotaChangesMethodContract {
+
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+ CassandraRabbitMQJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .blobStore(BlobStoreConfiguration.builder()
+ .s3()
+ .disableCache()
+ .deduplication()
+ .noCryptoConfig())
+ .build())
+ .extension(new DockerOpenSearchExtension())
+ .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/QuotaChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaChangesMethodContract.scala
new file mode 100644
index 0000000000..86029b2262
--- /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/QuotaChangesMethodContract.scala
@@ -0,0 +1,528 @@
+/****************************************************************
+ * 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.{`given`, requestSpecification}
+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.core.quota.QuotaCountLimit
+import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.MessageManager.AppendCommand
+import org.apache.james.mailbox.model.MailboxACL.Right.Read
+import org.apache.james.mailbox.model.{MailboxACL, MailboxPath}
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl, QuotaProbesImpl}
+import org.apache.james.utils.DataProbeImpl
+import org.assertj.core.api.Assertions.assertThat
+import org.awaitility.Awaitility
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+import java.nio.charset.StandardCharsets
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+
+
+trait QuotaChangesMethodContract {
+
+ private lazy val awaitAtMostTenSeconds = Awaitility.`with`
+ .await
+ .pollInterval(Duration.ofMillis(100))
+ .atMost(10, TimeUnit.SECONDS)
+
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent
+ .addDomain(DOMAIN.asString)
+ .addUser(BOB.asString, BOB_PASSWORD)
+ .addUser(ANDRE.asString, ANDRE_PASSWORD)
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+ .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .build
+ }
+
+ @Test
+ def quotaChangeShouldReturnCorrectResponse(server: GuiceJamesServer): Unit = {
+ val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+ val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+ quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${INSTANCE.value}",
+ | "newState": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
+ | "hasMoreChanges": false,
+ | "updatedProperties": null,
+ | "created": [],
+ | "updated": ["08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528"],
+ | "destroyed": []
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def quotaChangeShouldReturnSameResponseWhenSameRequest(server: GuiceJamesServer): Unit = {
+ val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+ val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+ quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+
+ val response1 = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ val response2 = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response1).isEqualTo(response2)
+ }
+
+ @Test
+ def hasMoreChangesShouldBeFalseWhenNoQuotaChanges(server: GuiceJamesServer): Unit = {
+ val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+ val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+ quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+
+ val newState: String = getLastState()
+
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${newState}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${newState}",
+ | "newState": "${newState}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": null,
+ | "created": [],
+ | "updated": [],
+ | "destroyed": []
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+
+ @Test
+ def stateShouldBeChangedWhenQuotaIsUpdated(server: GuiceJamesServer): Unit = {
+ val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+ val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+ quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+
+ val newState: String = getLastState()
+
+ // update quota usage
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString(), MailboxPath.inbox(BOB), AppendCommand.from(Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build))
+ .getMessageId.serialize()
+
+ awaitAtMostTenSeconds.untilAsserted(() => assertThat(getLastState())
+ .isNotEqualTo(newState))
+ }
+
+ private def getLastState(): String = {
+ `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract()
+ .jsonPath()
+ .get("methodResponses[0][1].newState")
+ }
+
+ @Test
+ def quotaChangesShouldFailWhenWrongAccountId(): Unit = {
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "unknownAccountId",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | ["error", {
+ | "type": "accountNotFound"
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def quotaChangesShouldFailWhenMissSinceState(): Unit = {
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "error",
+ | {
+ | "type": "invalidArguments",
+ | "description": "{\\"errors\\":[{\\"path\\":\\"obj.sinceState\\",\\"messages\\":[\\"error.path.missing\\"]}]}"
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def quotaChangesShouldFailWhenSinceStateIsInvalid(): Unit = {
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "invaLid@"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "error",
+ | {
+ | "type": "invalidArguments",
+ | "description": "{\\"errors\\":[{\\"path\\":\\"obj.sinceState\\",\\"messages\\":[\\"error.expected.uuid\\"]}]}"
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def quotaChangeShouldFailWhenOmittingOneCapability(): Unit = {
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "unknownAccountId",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [[
+ | "error",
+ | {
+ | "type": "unknownMethod",
+ | "description":"Missing capability(ies): urn:ietf:params:jmap:quota"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def quotaChangesShouldFailWhenOmittingAllCapability(): Unit = {
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "unknownAccountId",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [[
+ | "error",
+ | {
+ | "type": "unknownMethod",
+ | "description":"Missing capability(ies): urn:ietf:params:jmap:quota, urn:ietf:params:jmap:core"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def quotaChangesShouldReturnDifferenceStateWhenProvideDelegatedMailbox(server: GuiceJamesServer): Unit = {
+ val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+ val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+ quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+
+ // setup delegated Mailbox
+ val andreMailbox = MailboxPath.forUser(ANDRE, "mailbox")
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreMailbox)
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(andreMailbox, BOB.asString, new MailboxACL.Rfc4314Rights(Read))
+ quotaProbe.setMaxMessageCount(quotaProbe.getQuotaRoot(andreMailbox), QuotaCountLimit.count(88L))
+
+ val stateWithOutShareCapability: String = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract()
+ .jsonPath()
+ .get("methodResponses[0][1].newState")
+
+ val stateWithShareCapability: String = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota",
+ | "urn:apache:james:params:jmap:mail:shares"],
+ | "methodCalls": [[
+ | "Quota/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${INSTANCE.value}"
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract()
+ .jsonPath()
+ .get("methodResponses[0][1].newState")
+
+ assertThat(stateWithShareCapability).isNotEqualTo(stateWithOutShareCapability)
+ }
+}
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/QuotaGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaGetMethodContract.scala
index bd8f00ca02..3d0687a5f8 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaGetMethodContract.scala
@@ -43,9 +43,18 @@ import org.junit.jupiter.api.{BeforeEach, Test}
import java.nio.charset.StandardCharsets
import org.apache.james.mailbox.model.MailboxACL.Right.Read
import org.apache.james.mailbox.model.MailboxACL.Right.Lookup
+import org.awaitility.Awaitility
+
+import java.time.Duration
+import java.util.concurrent.TimeUnit
trait QuotaGetMethodContract {
+ private lazy val awaitAtMostTenSeconds = Awaitility.`with`
+ .await
+ .pollInterval(Duration.ofMillis(100))
+ .atMost(10, TimeUnit.SECONDS)
+
@BeforeEach
def setUp(server: GuiceJamesServer): Unit = {
server.getProbe(classOf[DataProbeImpl])
@@ -92,7 +101,7 @@ trait QuotaGetMethodContract {
| "Quota/get",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${INSTANCE.value}",
+ | "state": "1a9d5db2-2c73-3993-bf0b-42f64b396873",
| "list": [],
| "notFound": []
| },
@@ -141,7 +150,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [],
- | "state": "${INSTANCE.value}",
+ | "state": "6d7199ed-f1ce-31f3-8f02-c2e824004e55",
| "list": [
| {
| "used": 0,
@@ -214,7 +223,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [],
- | "state": "${INSTANCE.value}",
+ | "state": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
| "list": []
| },
| "c1"
@@ -262,7 +271,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [ "notfound123" ],
- | "state": "${INSTANCE.value}",
+ | "state": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
| "list": []
| },
| "c1"
@@ -313,7 +322,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [ "notfound123" ],
- | "state": "${INSTANCE.value}",
+ | "state": "461cef39-0c47-352b-a9e9-052093c20d5d",
| "list": [
| {
| "used": 0,
@@ -386,7 +395,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [ ],
- | "state": "${INSTANCE.value}",
+ | "state": "3c51d50a-d766-38b7-9fa4-c9ff12de87a4",
| "list": [
| {
| "used": 1,
@@ -576,7 +585,7 @@ trait QuotaGetMethodContract {
| "Quota/get",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${INSTANCE.value}",
+ | "state": "1a9d5db2-2c73-3993-bf0b-42f64b396873",
| "list": [],
| "notFound": []
| },
@@ -622,7 +631,7 @@ trait QuotaGetMethodContract {
| "Quota/get",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${INSTANCE.value}",
+ | "state": "1a9d5db2-2c73-3993-bf0b-42f64b396873",
| "list": [],
| "notFound": [ "${quotaId}" ]
| },
@@ -667,7 +676,7 @@ trait QuotaGetMethodContract {
| "Quota/get",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${INSTANCE.value}",
+ | "state": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
| "list": [
| {
| "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528"
@@ -716,7 +725,7 @@ trait QuotaGetMethodContract {
| "Quota/get",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${INSTANCE.value}",
+ | "state": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
| "list": [
| {
| "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528",
@@ -844,73 +853,75 @@ trait QuotaGetMethodContract {
.build))
.getMessageId.serialize()
- val response = `given`
- .body(
- s"""{
- | "using": [
- | "urn:ietf:params:jmap:core",
- | "urn:ietf:params:jmap:quota"],
- | "methodCalls": [[
- | "Quota/get",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "ids": null
- | },
- | "c1"]]
- |}""".stripMargin)
- .when
- .post
- .`then`
- .statusCode(SC_OK)
- .contentType(JSON)
- .extract
- .body
- .asString
-
- assertThatJson(response)
- .withOptions(new Options(IGNORING_ARRAY_ORDER))
- .isEqualTo(
- s"""{
- | "sessionState": "${SESSION_STATE.value}",
- | "methodResponses": [
- | [
- | "Quota/get",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "notFound": [],
- | "state": "${INSTANCE.value}",
- | "list": [
- | {
- | "used": 1,
- | "name": "#private&bob@domain.tld@domain.tld:account:count:Mail",
- | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528",
- | "dataTypes": [
- | "Mail"
- | ],
- | "limit": 100,
- | "warnLimit": 90,
- | "resourceType": "count",
- | "scope": "account"
- | },
- | {
- | "used": 85,
- | "name": "#private&bob@domain.tld@domain.tld:account:octets:Mail",
- | "id": "eab6ce8ac5d9730a959e614854410cf39df98ff3760a623b8e540f36f5184947",
- | "dataTypes": [
- | "Mail"
- | ],
- | "limit": 101,
- | "warnLimit": 90,
- | "resourceType": "octets",
- | "scope": "account"
- | }
- | ]
- | },
- | "c1"
- | ]
- | ]
- |}
- |""".stripMargin)
+ awaitAtMostTenSeconds.untilAsserted(() => {
+ val response = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:quota"],
+ | "methodCalls": [[
+ | "Quota/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "Quota/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "notFound": [],
+ | "state": "7d53031b-2819-3584-9e9d-e10ac1067906",
+ | "list": [
+ | {
+ | "used": 1,
+ | "name": "#private&bob@domain.tld@domain.tld:account:count:Mail",
+ | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528",
+ | "dataTypes": [
+ | "Mail"
+ | ],
+ | "limit": 100,
+ | "warnLimit": 90,
+ | "resourceType": "count",
+ | "scope": "account"
+ | },
+ | {
+ | "used": 85,
+ | "name": "#private&bob@domain.tld@domain.tld:account:octets:Mail",
+ | "id": "eab6ce8ac5d9730a959e614854410cf39df98ff3760a623b8e540f36f5184947",
+ | "dataTypes": [
+ | "Mail"
+ | ],
+ | "limit": 101,
+ | "warnLimit": 90,
+ | "resourceType": "octets",
+ | "scope": "account"
+ | }
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin)
+ })
}
@Test
@@ -960,7 +971,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [],
- | "state": "${INSTANCE.value}",
+ | "state": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
| "list": [
| {
| "used": 0,
@@ -1032,7 +1043,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [],
- | "state": "${INSTANCE.value}",
+ | "state": "5dc809dd-d059-3fab-bc7e-c0f1fcacf2f2",
| "list": [
| {
| "used": 0,
@@ -1114,7 +1125,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [],
- | "state": "${INSTANCE.value}",
+ | "state": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
| "list": [
| {
| "used": 0,
@@ -1186,7 +1197,7 @@ trait QuotaGetMethodContract {
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "notFound": [],
- | "state": "${INSTANCE.value}",
+ | "state": "5dc809dd-d059-3fab-bc7e-c0f1fcacf2f2",
| "list": [
| {
| "used": 0,
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/MemoryQuotaChangesMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryQuotaChangesMethodTest.java
new file mode 100644
index 0000000000..c2606b3d9d
--- /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/MemoryQuotaChangesMethodTest.java
@@ -0,0 +1,44 @@
+/****************************************************************
+ * 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.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.MemoryJamesConfiguration;
+import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.jmap.rfc8621.contract.QuotaChangesMethodContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryQuotaChangesMethodTest implements QuotaChangesMethodContract {
+
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir ->
+ MemoryJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .usersRepository(DEFAULT)
+ .build())
+ .server(configuration -> MemoryJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+}
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/quotas/quota.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/quotas/quota.mdown
index 1fef9d046d..e9215e303c 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/quotas/quota.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/quotas/quota.mdown
@@ -46,8 +46,8 @@ Standard “/get” method as described in [@!RFC8620] section 5.1. The ids argu
## Quota/changes
-> :warning:
-> Not implemented
+> :information_source:
+> Implemented
Standard “/changes” method as described in [@!RFC8620] section 5.2 but with one extra argument to the response:
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/QuotaSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/QuotaSerializer.scala
index 2ca89be9ab..bcd56b62a4 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/QuotaSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/QuotaSerializer.scala
@@ -22,7 +22,7 @@ package org.apache.james.jmap.json
import eu.timepit.refined
import org.apache.james.jmap.core.Id.IdConstraint
import org.apache.james.jmap.core.{Properties, UuidState}
-import org.apache.james.jmap.mail.{DataType, JmapQuota, QuotaDescription, QuotaGetRequest, QuotaGetResponse, QuotaIds, QuotaName, QuotaNotFound, ResourceType, Scope, UnparsedQuotaId}
+import org.apache.james.jmap.mail.{DataType, JmapQuota, QuotaChangesRequest, QuotaChangesResponse, QuotaDescription, QuotaGetRequest, QuotaGetResponse, QuotaIds, QuotaName, QuotaNotFound, ResourceType, Scope, UnparsedQuotaId}
import play.api.libs.json._
object QuotaSerializer {
@@ -53,10 +53,25 @@ object QuotaSerializer {
notFound => JsArray(notFound.value.toList.map(id => JsString(id.id.value)))
private implicit val quotaGetResponseWrites: Writes[QuotaGetResponse] = Json.writes[QuotaGetResponse]
+ private implicit val quotaChangesRequestReads: Reads[QuotaChangesRequest] = Json.reads[QuotaChangesRequest]
+
+ private implicit val quotaChangesResponseWrites: Writes[QuotaChangesResponse] = response =>
+ Json.obj(
+ "accountId" -> response.accountId,
+ "oldState" -> response.oldState,
+ "newState" -> response.newState,
+ "hasMoreChanges" -> response.hasMoreChanges,
+ "updatedProperties" -> response.updatedProperties,
+ "created" -> response.created,
+ "updated" -> response.updated,
+ "destroyed" -> response.destroyed)
+
def deserializeQuotaGetRequest(input: String): JsResult[QuotaGetRequest] = Json.parse(input).validate[QuotaGetRequest]
def deserializeQuotaGetRequest(input: JsValue): JsResult[QuotaGetRequest] = Json.fromJson[QuotaGetRequest](input)
+ def deserializeQuotaChangesRequest(input: JsValue): JsResult[QuotaChangesRequest] = Json.fromJson[QuotaChangesRequest](input)
+
def serialize(quotaGetResponse: QuotaGetResponse, properties: Properties): JsValue =
Json.toJson(quotaGetResponse)
.transform((__ \ "list").json.update {
@@ -70,4 +85,6 @@ object QuotaSerializer {
def serialize(response: QuotaGetResponse): JsValue = Json.toJson(response)
+ def serializeChanges(changesResponse: QuotaChangesResponse): JsObject = Json.toJson(changesResponse).as[JsObject]
+
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala
index 1c4545a5d9..d24076b1ce 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala
@@ -23,14 +23,15 @@ import com.google.common.hash.Hashing
import eu.timepit.refined.auto._
import org.apache.james.core.Domain
import org.apache.james.core.quota.{QuotaCountLimit, QuotaCountUsage, QuotaSizeLimit, QuotaSizeUsage}
+import org.apache.james.jmap.api.change.Limit
import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.UnsignedInt.UnsignedInt
-import org.apache.james.jmap.core.UuidState.INSTANCE
import org.apache.james.jmap.core.{AccountId, Id, Properties, UnsignedInt, UuidState}
import org.apache.james.jmap.method.WithAccountId
import org.apache.james.mailbox.model.{Quota => ModelQuota, QuotaRoot => ModelQuotaRoot}
import java.nio.charset.StandardCharsets
+import java.util.UUID
import scala.compat.java8.OptionConverters._
object QuotaRoot {
@@ -109,6 +110,10 @@ object JmapQuota {
name = QuotaName.from(quotaRoot, AccountScope, OctetsResourceType, List(MailDataType)),
dataTypes = List(MailDataType),
warnLimit = Some(UnsignedInt.liftOrThrow((limit.asLong() * WARN_LIMIT_PERCENTAGE).toLong))))
+
+ def correspondingState(quotas: Seq[JmapQuota]): UuidState =
+ UuidState(UUID.nameUUIDFromBytes(s"${quotas.sortBy(_.name.string).map(_.name.string).mkString("_")}:${quotas.map(_.used.value).sum + quotas.map(_.limit.value).sum}"
+ .getBytes(StandardCharsets.UTF_8)))
}
case class JmapQuota(id: Id,
@@ -174,19 +179,49 @@ object QuotaIdFactory {
}
object QuotaResponseGetResult {
- def empty: QuotaResponseGetResult = QuotaResponseGetResult()
-
- def merge(result1: QuotaResponseGetResult, result2: QuotaResponseGetResult): QuotaResponseGetResult = result1.merge(result2)
-}
-
-case class QuotaResponseGetResult(jmapQuotaSet: Set[JmapQuota] = Set(), notFound: QuotaNotFound = QuotaNotFound(Set())) {
- def merge(other: QuotaResponseGetResult): QuotaResponseGetResult =
- QuotaResponseGetResult(this.jmapQuotaSet ++ other.jmapQuotaSet,
- this.notFound.merge(other.notFound))
-
+ def from(quotas: Seq[JmapQuota], requestIds: Option[Set[Id]]): QuotaResponseGetResult =
+ requestIds match {
+ case None => QuotaResponseGetResult(quotas.toSet, state = JmapQuota.correspondingState(quotas))
+ case Some(value) => QuotaResponseGetResult(
+ jmapQuotaSet = quotas.filter(quota => value.contains(quota.id)).toSet,
+ notFound = QuotaNotFound(value.diff(quotas.map(_.id).toSet).map(UnparsedQuotaId)),
+ state = JmapQuota.correspondingState(quotas))
+ }
+}
+
+case class QuotaResponseGetResult(jmapQuotaSet: Set[JmapQuota] = Set(),
+ notFound: QuotaNotFound = QuotaNotFound(Set()),
+ state: UuidState) {
def asResponse(accountId: AccountId): QuotaGetResponse =
QuotaGetResponse(accountId = accountId,
- state = INSTANCE,
+ state = state,
list = jmapQuotaSet.toList,
notFound = notFound)
-}
\ No newline at end of file
+}
+
+case class QuotaChangesRequest(accountId: AccountId,
+ sinceState: UuidState,
+ maxChanges: Option[Limit]) extends WithAccountId
+
+object QuotaChangesResponse {
+ def from(oldState: UuidState, newState: (UuidState, Seq[Id]), accountId: AccountId): QuotaChangesResponse =
+ QuotaChangesResponse(
+ accountId = accountId,
+ oldState = oldState,
+ newState = newState._1,
+ hasMoreChanges = HasMoreChanges(false),
+ updated = if (oldState.value.equals(newState._1.value)) {
+ Set()
+ } else {
+ newState._2.toSet
+ })
+}
+
+case class QuotaChangesResponse(accountId: AccountId,
+ oldState: UuidState,
+ newState: UuidState,
+ hasMoreChanges: HasMoreChanges,
+ updatedProperties: Option[Properties] = None,
+ created: Set[Id] = Set(),
+ updated: Set[Id] = Set(),
+ destroyed: Set[Id] = Set())
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaChangesMethod.scala
new file mode 100644
index 0000000000..f451495402
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaChangesMethod.scala
@@ -0,0 +1,79 @@
+/****************************************************************
+ * 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 org.apache.james.core.Username
+import org.apache.james.jmap.api.change.CanNotCalculateChangesException
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_QUOTA}
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.core.{ErrorCode, Invocation, UuidState}
+import org.apache.james.jmap.json.{QuotaSerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.{JmapQuota, QuotaChangesRequest, QuotaChangesResponse}
+import org.apache.james.jmap.routes.SessionSupplier
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.mailbox.quota.{QuotaManager, UserQuotaRootResolver}
+import org.apache.james.metrics.api.MetricFactory
+import org.reactivestreams.Publisher
+import reactor.core.scala.publisher.SMono
+
+import javax.inject.Inject
+
+class QuotaChangesMethod @Inject()(val metricFactory: MetricFactory,
+ val sessionSupplier: SessionSupplier,
+ val quotaManager: QuotaManager,
+ val quotaRootResolver: UserQuotaRootResolver) extends MethodRequiringAccountId[QuotaChangesRequest] {
+
+ override val methodName: Invocation.MethodName = MethodName("Quota/changes")
+
+ override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_QUOTA, JMAP_CORE)
+
+ val quotaChangesResolver: QuotaChangesResolver = QuotaChangesResolver(quotaManager, quotaRootResolver)
+
+ override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: QuotaChangesRequest): Publisher[InvocationWithContext] =
+ quotaChangesResolver.getLatestState(mailboxSession.getUser, capabilities)
+ .map(newState => QuotaChangesResponse.from(request.sinceState, newState, request.accountId))
+ .map(response => InvocationWithContext(
+ invocation = Invocation(
+ methodName = methodName,
+ arguments = Arguments(QuotaSerializer.serializeChanges(response)),
+ methodCallId = invocation.invocation.methodCallId),
+ processingContext = invocation.processingContext))
+ .onErrorResume {
+ case e: CanNotCalculateChangesException => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.CannotCalculateChanges, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
+ case e => SMono.error(e)
+ }
+
+ override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, QuotaChangesRequest] =
+ QuotaSerializer.deserializeQuotaChangesRequest(invocation.arguments.value)
+ .asEither.left.map(ResponseSerializer.asException)
+}
+
+case class QuotaChangesResolver(private val quotaManager: QuotaManager,
+ private val quotaRootResolver: UserQuotaRootResolver) {
+ private val jmapQuotaManagerWrapper: JmapQuotaManagerWrapper = JmapQuotaManagerWrapper(quotaManager, quotaRootResolver)
+
+ def getLatestState(username: Username, capabilities: Set[CapabilityIdentifier]): SMono[(UuidState, Seq[Id])] =
+ jmapQuotaManagerWrapper.list(username, capabilities)
+ .collectSeq()
+ .map(quotas => (JmapQuota.correspondingState(quotas), quotas.map(_.id)))
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaGetMethod.scala
index 4c2d07dc7b..fb339699a8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaGetMethod.scala
@@ -26,7 +26,7 @@ import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.Invocation.{Arguments, MethodCallId, MethodName}
import org.apache.james.jmap.core.{ErrorCode, Invocation, MissingCapabilityException, Properties}
import org.apache.james.jmap.json.{QuotaSerializer, ResponseSerializer}
-import org.apache.james.jmap.mail.{CountResourceType, JmapQuota, OctetsResourceType, QuotaGetRequest, QuotaIdFactory, QuotaNotFound, QuotaResponseGetResult}
+import org.apache.james.jmap.mail.{CountResourceType, JmapQuota, OctetsResourceType, QuotaGetRequest, QuotaIdFactory, QuotaNotFound, QuotaResponseGetResult, UnparsedQuotaId}
import org.apache.james.jmap.routes.SessionSupplier
import org.apache.james.lifecycle.api.Startable
import org.apache.james.mailbox.MailboxSession
@@ -54,7 +54,6 @@ class QuotaGetMethod @Inject()(val metricFactory: MetricFactory,
(requestedProperties -- JmapQuota.allProperties match {
case invalidProperties if invalidProperties.isEmpty() => getQuotaGetResponse(request, mailboxSession.getUser, capabilities)
- .reduce(QuotaResponseGetResult.empty)(QuotaResponseGetResult.merge)
.map(result => result.asResponse(accountId = request.accountId))
.map(response => Invocation(
methodName = methodName,
@@ -75,18 +74,10 @@ class QuotaGetMethod @Inject()(val metricFactory: MetricFactory,
QuotaSerializer.deserializeQuotaGetRequest(invocation.arguments.value)
.asEither.left.map(ResponseSerializer.asException)
- private def getQuotaGetResponse(quotaGetRequest: QuotaGetRequest, username: Username, capabilities: Set[CapabilityIdentifier]): SFlux[QuotaResponseGetResult] =
- quotaGetRequest.ids match {
- case None =>
- jmapQuotaManagerWrapper.list(username, capabilities)
- .collectSeq()
- .map(listJmapQuota => QuotaResponseGetResult(jmapQuotaSet = listJmapQuota.toSet))
- .flatMapMany(result => SFlux.just(result))
- case Some(ids) => SFlux.fromIterable(ids.value)
- .flatMap(id => jmapQuotaManagerWrapper.get(username, id.id, capabilities)
- .map(jmapQuota => QuotaResponseGetResult(jmapQuotaSet = Set(jmapQuota)))
- .switchIfEmpty(SMono.just(QuotaResponseGetResult(notFound = QuotaNotFound(Set(id))))))
- }
+ private def getQuotaGetResponse(request: QuotaGetRequest, username: Username, capabilities: Set[CapabilityIdentifier]): SMono[QuotaResponseGetResult] =
+ jmapQuotaManagerWrapper.list(username, capabilities)
+ .collectSeq()
+ .map(quotas => QuotaResponseGetResult.from(quotas, request.ids.map(_.value.map(_.id).toSet)))
private def handleRequestValidationErrors(exception: Exception, methodCallId: MethodCallId): SMono[Invocation] = exception match {
case _: MissingCapabilityException => SMono.just(Invocation.error(ErrorCode.UnknownMethod, methodCallId))
@@ -94,15 +85,11 @@ class QuotaGetMethod @Inject()(val metricFactory: MetricFactory,
}
}
-case class JmapQuotaManagerWrapper(private var quotaManager: QuotaManager,
- private var quotaRootResolver: UserQuotaRootResolver) {
- def get(username: Username, quotaId: Id, capabilities: Set[CapabilityIdentifier]): SFlux[JmapQuota] =
- retrieveQuotaRoot(username, capabilities)
- .flatMap(quotaRoot => getJmapQuota(quotaRoot, Some(quotaId)))
-
+case class JmapQuotaManagerWrapper(private val quotaManager: QuotaManager,
+ private val quotaRootResolver: UserQuotaRootResolver) {
def list(username: Username, capabilities: Set[CapabilityIdentifier]): SFlux[JmapQuota] =
retrieveQuotaRoot(username, capabilities)
- .flatMap(quotaRoot => getJmapQuota(quotaRoot))
+ .flatMap(quotaRoot => getJmapQuota(quotaRoot), ReactorUtils.DEFAULT_CONCURRENCY)
def retrieveQuotaRoot(username: Username, capabilities: Set[CapabilityIdentifier]): SFlux[QuotaRoot] =
SMono.just(capabilities)
@@ -111,26 +98,10 @@ case class JmapQuotaManagerWrapper(private var quotaManager: QuotaManager,
.flatMapMany(_ => SFlux(quotaRootResolver.listAllAccessibleQuotaRoots(username)))
.switchIfEmpty(SFlux.just(quotaRootResolver.forUser(username)))
- private def getJmapQuota(quotaRoot: QuotaRoot, quotaId: Option[Id] = None): SFlux[JmapQuota] =
- (quotaId match {
- case None => SMono(quotaManager.getQuotasReactive(quotaRoot))
- .flatMapMany(quotas => SMono.fromCallable(() => JmapQuota.extractUserMessageCountQuota(quotas.getMessageQuota, QuotaIdFactory.from(quotaRoot, CountResourceType), quotaRoot))
- .mergeWith(SMono.fromCallable(() => JmapQuota.extractUserMessageSizeQuota(quotas.getStorageQuota, QuotaIdFactory.from(quotaRoot, OctetsResourceType), quotaRoot))))
-
- case Some(quotaIdValue) =>
- val quotaCountPublisher = SMono.fromCallable(() => QuotaIdFactory.from(quotaRoot, CountResourceType))
- .filter(countQuotaId => countQuotaId.value.equals(quotaIdValue.value))
- .flatMap(_ => SMono.fromCallable(() => quotaManager.getMessageQuota(quotaRoot))
- .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER))
- .map(quota => JmapQuota.extractUserMessageCountQuota(quota, quotaIdValue, quotaRoot))
-
- val quotaSizePublisher = SMono.fromCallable(() => QuotaIdFactory.from(quotaRoot, OctetsResourceType))
- .filter(sizeQuotaId => sizeQuotaId.value.equals(quotaIdValue.value))
- .flatMap(_ => SMono.fromCallable(() => quotaManager.getStorageQuota(quotaRoot))
- .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER))
- .map(quota => JmapQuota.extractUserMessageSizeQuota(quota, quotaIdValue, quotaRoot))
-
- quotaCountPublisher.mergeWith(quotaSizePublisher)
- }).flatMap(SMono.justOrEmpty)
+ private def getJmapQuota(quotaRoot: QuotaRoot): SFlux[JmapQuota] =
+ SMono(quotaManager.getQuotasReactive(quotaRoot))
+ .flatMapMany(quotas => SMono.fromCallable(() => JmapQuota.extractUserMessageCountQuota(quotas.getMessageQuota, QuotaIdFactory.from(quotaRoot, CountResourceType), quotaRoot))
+ .mergeWith(SMono.fromCallable(() => JmapQuota.extractUserMessageSizeQuota(quotas.getStorageQuota, QuotaIdFactory.from(quotaRoot, OctetsResourceType), quotaRoot))))
+ .flatMap(SMono.justOrEmpty)
}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org