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 <[email protected]>
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&[email protected]@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: [email protected]
For additional commands, e-mail: [email protected]