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

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


The following commit(s) were added to refs/heads/master by this push:
     new 533b94b140 JAMES-3830 JMAP Quota draft compatibility (#1644)
533b94b140 is described below

commit 533b94b1404475391996a260119bb5009b9923c6
Author: Trần Hồng Quân <55171818+quantranhong1...@users.noreply.github.com>
AuthorDate: Tue Jul 18 08:18:20 2023 +0700

    JAMES-3830 JMAP Quota draft compatibility (#1644)
---
 .../docs/modules/ROOT/pages/configure/jvm.adoc     |  13 +++
 .../rfc8621/contract/QuotaGetMethodContract.scala  | 128 +++++++++++++++++++++
 .../scala/org/apache/james/jmap/mail/Quotas.scala  |  20 +++-
 .../apache/james/jmap/method/QuotaGetMethod.scala  |   7 +-
 .../james/jmap/json/QuotaSerializerTest.scala      |  49 ++++++++
 src/site/xdoc/server/config-system.xml             |   4 +
 6 files changed, 216 insertions(+), 5 deletions(-)

diff --git 
a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jvm.adoc 
b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jvm.adoc
index d95acb2414..f1b9055822 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jvm.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/jvm.adoc
@@ -65,3 +65,16 @@ james.blob.id.hash.encoding=base16
 
 Optional. String. Defaults to base64Url.
 
+== JMAP Quota draft compatibility
+
+Some JMAP clients depend on the JMAP Quota draft specifications. The property 
`james.jmap.quota.draft.compatibility` allows
+to enable JMAP Quota draft compatibility for those clients and allow them a 
time window to adapt to the RFC-9245 JMAP Quota.
+
+Optional. Boolean. Default to false.
+
+Ex in `jvm.properties`
+----
+james.jmap.quota.draft.compatibility=true
+----
+To enable the compatibility.
+
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 a095d89ac2..d4f80c22a5 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
@@ -64,6 +64,8 @@ trait QuotaGetMethodContract {
       .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
       .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .build
+
+    System.clearProperty("james.jmap.quota.draft.compatibility")
   }
 
   @Test
@@ -1359,4 +1361,130 @@ trait QuotaGetMethodContract {
          |""".stripMargin)
   }
 
+  @Test
+  def shouldSupportQuotaGetDraftCompatibilityWhenEnabled(server: 
GuiceJamesServer): Unit = {
+    System.setProperty("james.jmap.quota.draft.compatibility", "true")
+
+    val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+    val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+    quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+    quotaProbe.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.unlimited())
+
+    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": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
+         |                "list": [
+         |                    {
+         |                        "used": 0,
+         |                        "name": 
"#private&b...@domain.tld@domain.tld:account:count:Mail",
+         |                        "id": 
"08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528",
+         |                        "types": ["Mail"],
+         |                        "dataTypes": ["Mail"],
+         |                        "hardLimit": 100,
+         |                        "limit": 100,
+         |                        "warnLimit": 90,
+         |                        "resourceType": "count",
+         |                        "scope": "account"
+         |                    }
+         |                ]
+         |            },
+         |            "c1"
+         |        ]
+         |    ]
+         |}
+         |""".stripMargin)
+  }
+
+  @Test
+  def quotaGetDraftCompatibilityShouldStillSupportPropertiesFiltering(server: 
GuiceJamesServer): Unit = {
+    System.setProperty("james.jmap.quota.draft.compatibility", "true")
+
+    val quotaProbe = server.getProbe(classOf[QuotaProbesImpl])
+    val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB))
+    quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L))
+    quotaProbe.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.unlimited())
+
+    val response = `given`
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:quota"],
+           |  "methodCalls": [[
+           |    "Quota/get",
+           |    {
+           |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |      "ids": null,
+           |      "properties": ["limit", "dataTypes"]
+           |    },
+           |    "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": "84c40a2e-76a1-3f84-a1e8-862104c7a697",
+         |                "list": [
+         |                    {
+         |                        "id": 
"08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528",
+         |                        "dataTypes": ["Mail"],
+         |                        "limit": 100
+         |                    }
+         |                ]
+         |            },
+         |            "c1"
+         |        ]
+         |    ]
+         |}
+         |""".stripMargin)
+  }
+
 }
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 d2aaadeec2..63d60cb641 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
@@ -83,12 +83,20 @@ case class QuotaGetRequest(accountId: AccountId,
                            properties: Option[Properties]) extends 
WithAccountId
 
 object JmapQuota {
-  val WARN_LIMIT_PERCENTAGE = 0.9
-  val allProperties: Properties = Properties("id", "resourceType", "used", 
"hardLimit", "scope", "name", "types", "warnLimit", "softLimit", "description")
-  val idProperty: Properties = Properties("id")
+  private val WARN_LIMIT_PERCENTAGE = 0.9
+  private val allRfc9245Properties: Properties = Properties("id", 
"resourceType", "used", "hardLimit", "scope", "name", "types", "warnLimit", 
"softLimit", "description")
+  private val allRfc9245PropertiesWithDraftProperties: Properties = 
allRfc9245Properties ++ Properties("dataTypes", "limit")
+  private val idProperty: Properties = Properties("id")
 
   def propertiesFiltered(requestedProperties: Properties): Properties = 
idProperty ++ requestedProperties
 
+  def allProperties(draftBackwardCompatibility: Boolean): Properties =
+    if (draftBackwardCompatibility) {
+      allRfc9245PropertiesWithDraftProperties
+    } else {
+      allRfc9245Properties
+    }
+
   def extractUserMessageCountQuota(quota: ModelQuota[QuotaCountLimit, 
QuotaCountUsage], countQuotaIdPlaceHolder: Id, quotaRoot: ModelQuotaRoot): 
Option[JmapQuota] =
     Option(quota.getLimit)
       .filter(_.isLimited)
@@ -97,9 +105,11 @@ object JmapQuota {
         resourceType = CountResourceType,
         used = UnsignedInt.liftOrThrow(quota.getUsed.asLong()),
         hardLimit = UnsignedInt.liftOrThrow(limit.asLong()),
+        limit = Some(UnsignedInt.liftOrThrow(limit.asLong())),
         scope = AccountScope,
         name = QuotaName.from(quotaRoot, AccountScope, CountResourceType, 
List(MailDataType)),
         types = List(MailDataType),
+        dataTypes = Some(List(MailDataType)),
         warnLimit = Some(UnsignedInt.liftOrThrow((limit.asLong() * 
WARN_LIMIT_PERCENTAGE).toLong))))
 
   def extractUserMessageSizeQuota(quota: ModelQuota[QuotaSizeLimit, 
QuotaSizeUsage], sizeQuotaIdPlaceHolder: Id, quotaRoot: ModelQuotaRoot): 
Option[JmapQuota] =
@@ -110,9 +120,11 @@ object JmapQuota {
         resourceType = OctetsResourceType,
         used = UnsignedInt.liftOrThrow(quota.getUsed.asLong()),
         hardLimit = UnsignedInt.liftOrThrow(limit.asLong()),
+        limit = Some(UnsignedInt.liftOrThrow(limit.asLong())),
         scope = AccountScope,
         name = QuotaName.from(quotaRoot, AccountScope, OctetsResourceType, 
List(MailDataType)),
         types = List(MailDataType),
+        dataTypes = Some(List(MailDataType)),
         warnLimit = Some(UnsignedInt.liftOrThrow((limit.asLong() * 
WARN_LIMIT_PERCENTAGE).toLong))))
 
   def correspondingState(quotas: Seq[JmapQuota]): UuidState =
@@ -124,9 +136,11 @@ case class JmapQuota(id: Id,
                      resourceType: ResourceType,
                      used: UnsignedInt,
                      hardLimit: UnsignedInt,
+                     limit: Option[UnsignedInt] = None,
                      scope: Scope,
                      name: QuotaName,
                      types: List[DataType],
+                     dataTypes: Option[List[DataType]] = None,
                      warnLimit: Option[UnsignedInt] = None,
                      softLimit: Option[UnsignedInt] = None,
                      description: Option[QuotaDescription] = None)
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 9e8ea70acc..09d8848a6d 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
@@ -48,10 +48,13 @@ class QuotaGetMethod @Inject()(val metricFactory: 
MetricFactory,
   override val requiredCapabilities: Set[CapabilityIdentifier] = 
Set(JMAP_QUOTA, JMAP_CORE)
   val jmapQuotaManagerWrapper: JmapQuotaManagerWrapper = 
JmapQuotaManagerWrapper(quotaManager, quotaRootResolver)
 
+  private lazy val JMAP_QUOTA_DRAFT_COMPATIBILITY: Boolean = 
Option(System.getProperty("james.jmap.quota.draft.compatibility"))
+    .exists(_.toBoolean)
+
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: 
InvocationWithContext, mailboxSession: MailboxSession, request: 
QuotaGetRequest): Publisher[InvocationWithContext] = {
-    val requestedProperties: Properties = 
request.properties.getOrElse(JmapQuota.allProperties)
+    val requestedProperties: Properties = 
request.properties.getOrElse(JmapQuota.allProperties(JMAP_QUOTA_DRAFT_COMPATIBILITY))
 
-    (requestedProperties -- JmapQuota.allProperties match {
+    (requestedProperties -- 
JmapQuota.allProperties(JMAP_QUOTA_DRAFT_COMPATIBILITY) match {
       case invalidProperties if invalidProperties.isEmpty() => 
getQuotaGetResponse(request, mailboxSession.getUser, capabilities)
         .map(result => result.asResponse(accountId = request.accountId))
         .map(response => Invocation(
diff --git 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/QuotaSerializerTest.scala
 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/QuotaSerializerTest.scala
index f78deb29c6..7fa4d6d852 100644
--- 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/QuotaSerializerTest.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/QuotaSerializerTest.scala
@@ -163,6 +163,55 @@ class QuotaSerializerTest extends AnyWordSpec with 
Matchers {
       
assertThatJson(Json.stringify(QuotaSerializer.serialize(actualValue))).isEqualTo(expectedJson)
     }
 
+    "succeed when draft compatibility" in {
+      val jmapQuota: JmapQuota = JmapQuota(
+        id = 
Id.validate("aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8").toOption.get,
+        resourceType = CountResourceType,
+        used = UnsignedInt.liftOrThrow(1),
+        hardLimit = UnsignedInt.liftOrThrow(2),
+        limit = Some(UnsignedInt.liftOrThrow(2)),
+        scope = AccountScope,
+        name = QuotaName("name1"),
+        types = List(MailDataType),
+        dataTypes = Some(List(MailDataType)),
+        warnLimit = Some(UnsignedInt.liftOrThrow(123)),
+        softLimit = Some(UnsignedInt.liftOrThrow(456)),
+        description = Some(QuotaDescription("Description 1")))
+
+      val actualValue: QuotaGetResponse = QuotaGetResponse(
+        accountId = ACCOUNT_ID,
+        state = UuidState.INSTANCE,
+        list = List(jmapQuota),
+        notFound = QuotaNotFound(Set(UnparsedQuotaId("notfound2"))))
+
+      val expectedJson: String =
+        """{
+          |    "accountId": "aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8",
+          |    "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          |    "list": [
+          |        {
+          |            "id": "aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8",
+          |            "resourceType": "count",
+          |            "used": 1,
+          |            "hardLimit": 2,
+          |            "limit": 2,
+          |            "scope": "account",
+          |            "name": "name1",
+          |            "types": [ "Mail" ],
+          |            "dataTypes": [ "Mail" ],
+          |            "warnLimit": 123,
+          |            "softLimit": 456,
+          |            "description": "Description 1"
+          |        }
+          |    ],
+          |    "notFound": [
+          |        "notfound2"
+          |    ]
+          |}""".stripMargin
+
+      
assertThatJson(Json.stringify(QuotaSerializer.serialize(actualValue))).isEqualTo(expectedJson)
+    }
+
     "succeed when list has multiple quota" in {
       val jmapQuota: JmapQuota = JmapQuota(
         id = 
Id.validate("aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8").toOption.get,
diff --git a/src/site/xdoc/server/config-system.xml 
b/src/site/xdoc/server/config-system.xml
index 86b499b03a..89a45c50c4 100644
--- a/src/site/xdoc/server/config-system.xml
+++ b/src/site/xdoc/server/config-system.xml
@@ -237,6 +237,10 @@
                 <dt><strong>james.blob.id.hash.encoding</strong></dt>
                 <dd>Optional. String. Defaults to base64Url. <br/>
                     The encoding type when encode blobId. The support value 
are: base16, hex, base32, base32Hex, base64, base64Url.</dd>
+
+                <dt><strong>james.jmap.quota.draft.compatibility</strong></dt>
+                <dd>Optional. Boolean. Default to false. <br/>
+                    This property allows to enable JMAP Quota draft 
compatibility for some JMAP clients and allow them a time window to adapt to 
the RFC-9245 JMAP Quota.</dd>
             </dl>
 
             </subsection>


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

Reply via email to