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 898a76a157a41aacfd4738bf24cc93fb22aaafac Author: Quan Tran <hqt...@linagora.com> AuthorDate: Wed Feb 1 12:19:30 2023 +0700 JAMES-3830 Adapt Quota RFC-9245 changes in code Quota object changes: - "dataTypes" -> "types" - "limit" -> "hardLimit" Quota filter condition object changes: - "dataTypes" String[] -> "type" String - "resourceTypes" ResourceType[] -> "resourceType" String - "scopes" Scope[] -> "scope" String --- .../rfc8621/contract/QuotaGetMethodContract.scala | 64 +++++++++++----------- .../contract/QuotaQueryMethodContract.scala | 30 +++++----- .../scala/org/apache/james/jmap/mail/Quotas.scala | 24 ++++---- .../james/jmap/method/QuotaQueryMethod.scala | 13 +++-- .../james/jmap/json/QuotaSerializerTest.scala | 20 +++---- 5 files changed, 77 insertions(+), 74 deletions(-) diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/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 1b36049303..a095d89ac2 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 @@ -153,10 +153,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -165,10 +165,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:octets:Mail", | "id": "eab6ce8ac5d9730a959e614854410cf39df98ff3760a623b8e540f36f5184947", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 99, + | "hardLimit": 99, | "warnLimit": 89, | "resourceType": "octets", | "scope": "account" @@ -229,10 +229,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -341,10 +341,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -353,10 +353,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:octets:Mail", | "id": "eab6ce8ac5d9730a959e614854410cf39df98ff3760a623b8e540f36f5184947", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 99, + | "hardLimit": 99, | "warnLimit": 89, | "resourceType": "octets", | "scope": "account" @@ -467,10 +467,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -540,10 +540,10 @@ trait QuotaGetMethodContract { | "used": 1, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -552,10 +552,10 @@ trait QuotaGetMethodContract { | "used": 85, | "name": "#private&b...@domain.tld@domain.tld:account:octets:Mail", | "id": "eab6ce8ac5d9730a959e614854410cf39df98ff3760a623b8e540f36f5184947", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 900, + | "hardLimit": 900, | "warnLimit": 810, | "resourceType": "octets", | "scope": "account" @@ -844,7 +844,7 @@ trait QuotaGetMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "ids": null, - | "properties": ["name","used","limit"] + | "properties": ["name","used","hardLimit"] | }, | "c1"]] |}""".stripMargin) @@ -870,7 +870,7 @@ trait QuotaGetMethodContract { | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", - | "limit": 100 + | "hardLimit": 100 | } | ], | "notFound": [] @@ -1033,10 +1033,10 @@ trait QuotaGetMethodContract { | "used": 1, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -1045,10 +1045,10 @@ trait QuotaGetMethodContract { | "used": 85, | "name": "#private&b...@domain.tld@domain.tld:account:octets:Mail", | "id": "eab6ce8ac5d9730a959e614854410cf39df98ff3760a623b8e540f36f5184947", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 101, + | "hardLimit": 101, | "warnLimit": 90, | "resourceType": "octets", | "scope": "account" @@ -1116,10 +1116,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -1188,10 +1188,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -1201,10 +1201,10 @@ trait QuotaGetMethodContract { | "name": "#private&an...@domain.tld@domain.tld:account:count:Mail", | "warnLimit": 79, | "id": "04cbe4578878e02a74e47ae6be66c88cc8aafd3a5fc698457d712ee5f9a5b4ca", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 88, + | "hardLimit": 88, | "resourceType": "count", | "scope": "account" | } @@ -1270,10 +1270,10 @@ trait QuotaGetMethodContract { | "used": 0, | "name": "#private&b...@domain.tld@domain.tld:account:count:Mail", | "id": "08417be420b6dd6fa77d48fb2438e0d19108cd29424844bb109b52d356fab528", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 100, + | "hardLimit": 100, | "warnLimit": 90, | "resourceType": "count", | "scope": "account" @@ -1343,10 +1343,10 @@ trait QuotaGetMethodContract { | "name": "#private&an...@domain.tld@domain.tld:account:count:Mail", | "warnLimit": 79, | "id": "04cbe4578878e02a74e47ae6be66c88cc8aafd3a5fc698457d712ee5f9a5b4ca", - | "dataTypes": [ + | "types": [ | "Mail" | ], - | "limit": 88, + | "hardLimit": 88, | "resourceType": "count", | "scope": "account" | } 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/QuotaQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaQueryMethodContract.scala index 102bd80d67..d5215bc60c 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaQueryMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaQueryMethodContract.scala @@ -178,7 +178,7 @@ trait QuotaQueryMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { - | "resourceTypes": ["count"] + | "resourceType": "count" | } | }, | "c1"]] @@ -232,7 +232,7 @@ trait QuotaQueryMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { - | "resourceTypes": ["invalid"] + | "resourceType": "invalid" | } | }, | "c1"]] @@ -254,7 +254,7 @@ trait QuotaQueryMethodContract { | "error", | { | "type": "invalidArguments", - | "description": "'/filter/resourceTypes(0)' property is not valid: Unexpected value invalid, only 'count' and 'octets' are managed" + | "description": "'/filter/resourceType' property is not valid: Unexpected value invalid, only 'count' and 'octets' are managed" | }, | "c1" | ] @@ -263,7 +263,7 @@ trait QuotaQueryMethodContract { } @Test - def filterDataTypesShouldWork(server: GuiceJamesServer): Unit = { + def filterDataTypeShouldWork(server: GuiceJamesServer): Unit = { val quotaProbe = server.getProbe(classOf[QuotaProbesImpl]) val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB)) quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L)) @@ -280,7 +280,7 @@ trait QuotaQueryMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { - | "dataTypes": ["Mail"] + | "type": "Mail" | } | }, | "c1"]] @@ -318,7 +318,7 @@ trait QuotaQueryMethodContract { } @Test - def filterDataTypesShouldFailWhenInvalidDataTypes(server: GuiceJamesServer): Unit = { + def filterDataTypeShouldFailWhenInvalidDataType(server: GuiceJamesServer): Unit = { val quotaProbe = server.getProbe(classOf[QuotaProbesImpl]) val bobQuotaRoot = quotaProbe.getQuotaRoot(MailboxPath.inbox(BOB)) quotaProbe.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(100L)) @@ -335,7 +335,7 @@ trait QuotaQueryMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { - | "dataTypes": ["invalid"] + | "type": "invalid" | } | }, | "c1"]] @@ -357,7 +357,7 @@ trait QuotaQueryMethodContract { | "error", | { | "type": "invalidArguments", - | "description": "'/filter/dataTypes(0)' property is not valid: Unexpected value invalid, only 'Mail' are managed" + | "description": "'/filter/type' property is not valid: Unexpected value invalid, only 'Mail' are managed" | }, | "c1" | ] @@ -383,7 +383,7 @@ trait QuotaQueryMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { - | "scope": ["account"] + | "scope": "account" | } | }, | "c1"]] @@ -438,7 +438,7 @@ trait QuotaQueryMethodContract { | { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { - | "scope": ["invalidScope"] + | "scope": "invalidScope" | } | }, | "c1"]] @@ -460,7 +460,7 @@ trait QuotaQueryMethodContract { | "error", | { | "type": "invalidArguments", - | "description": "'/filter/scope(0)' property is not valid: Unexpected value invalidScope, only 'account' is managed" + | "description": "'/filter/scope' property is not valid: Unexpected value invalidScope, only 'account' is managed" | }, | "c1" | ] @@ -593,9 +593,9 @@ trait QuotaQueryMethodContract { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { | "name": "#private&b...@domain.tld@domain.tld:account:octets:Mail", - | "dataTypes": ["Mail"], - | "scope": ["account"], - | "resourceTypes": ["octets"] + | "type": "Mail", + | "scope": "account", + | "resourceType": "octets" | } | }, | "c1"]] @@ -650,7 +650,7 @@ trait QuotaQueryMethodContract { | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", | "filter" : { | "name": "#private&b...@domain.tld@domain.tld:account:octets:Mail", - | "resourceTypes": ["count"] + | "resourceType": "count" | } | }, | "c1"]] 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 9e743d066e..d2aaadeec2 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 @@ -84,7 +84,7 @@ case class QuotaGetRequest(accountId: AccountId, object JmapQuota { val WARN_LIMIT_PERCENTAGE = 0.9 - val allProperties: Properties = Properties("id", "resourceType", "used", "limit", "scope", "name", "dataTypes", "warnLimit", "softLimit", "description") + val allProperties: Properties = Properties("id", "resourceType", "used", "hardLimit", "scope", "name", "types", "warnLimit", "softLimit", "description") val idProperty: Properties = Properties("id") def propertiesFiltered(requestedProperties: Properties): Properties = idProperty ++ requestedProperties @@ -96,10 +96,10 @@ object JmapQuota { id = countQuotaIdPlaceHolder, resourceType = CountResourceType, used = UnsignedInt.liftOrThrow(quota.getUsed.asLong()), - limit = UnsignedInt.liftOrThrow(limit.asLong()), + hardLimit = UnsignedInt.liftOrThrow(limit.asLong()), scope = AccountScope, name = QuotaName.from(quotaRoot, AccountScope, CountResourceType, List(MailDataType)), - dataTypes = List(MailDataType), + types = List(MailDataType), warnLimit = Some(UnsignedInt.liftOrThrow((limit.asLong() * WARN_LIMIT_PERCENTAGE).toLong)))) def extractUserMessageSizeQuota(quota: ModelQuota[QuotaSizeLimit, QuotaSizeUsage], sizeQuotaIdPlaceHolder: Id, quotaRoot: ModelQuotaRoot): Option[JmapQuota] = @@ -109,24 +109,24 @@ object JmapQuota { id = sizeQuotaIdPlaceHolder, resourceType = OctetsResourceType, used = UnsignedInt.liftOrThrow(quota.getUsed.asLong()), - limit = UnsignedInt.liftOrThrow(limit.asLong()), + hardLimit = UnsignedInt.liftOrThrow(limit.asLong()), scope = AccountScope, name = QuotaName.from(quotaRoot, AccountScope, OctetsResourceType, List(MailDataType)), - dataTypes = List(MailDataType), + types = 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}" + UuidState(UUID.nameUUIDFromBytes(s"${quotas.sortBy(_.name.string).map(_.name.string).mkString("_")}:${quotas.map(_.used.value).sum + quotas.map(_.hardLimit.value).sum}" .getBytes(StandardCharsets.UTF_8))) } case class JmapQuota(id: Id, resourceType: ResourceType, used: UnsignedInt, - limit: UnsignedInt, + hardLimit: UnsignedInt, scope: Scope, name: QuotaName, - dataTypes: List[DataType], + types: List[DataType], warnLimit: Option[UnsignedInt] = None, softLimit: Option[UnsignedInt] = None, description: Option[QuotaDescription] = None) @@ -233,13 +233,13 @@ case class QuotaChangesResponse(accountId: AccountId, case class QuotaQueryRequest(accountId: AccountId, filter: QuotaQueryFilter) extends WithAccountId object QuotaQueryFilter { - val SUPPORTED: Set[String] = Set("scope", "name", "resourceTypes", "dataTypes") + val SUPPORTED: Set[String] = Set("scope", "name", "resourceType", "type") } -case class QuotaQueryFilter(scope: Option[Set[Scope]], +case class QuotaQueryFilter(scope: Option[Scope], name: Option[QuotaName], - resourceTypes: Option[Set[ResourceType]], - dataTypes: Option[Set[DataType]]) + resourceType: Option[ResourceType], + `type`: Option[DataType]) case class QuotaQueryResponse(accountId: AccountId, queryState: QueryState, diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaQueryMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaQueryMethod.scala index 5592641450..6a956840d0 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaQueryMethod.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/QuotaQueryMethod.scala @@ -74,12 +74,15 @@ class QuotaQueryMethod @Inject()(val metricFactory: MetricFactory, private def filterPredicate(filter: QuotaQueryFilter): (JmapQuota) => scala.Boolean = quota => { - ((filter.name match { + (filter.name match { case None => true case Some(value) => value.string == quota.name.string - }) - && filter.scope.forall(_.contains(quota.scope)) - && filter.resourceTypes.forall(_.contains(quota.resourceType)) - && filter.dataTypes.forall(dataTypesValue => quota.dataTypes.toSet.subsetOf(dataTypesValue))) + }) && (filter.scope match { + case None => true + case Some(scope) => scope.asString() == quota.scope.asString() + }) && (filter.resourceType match { + case None => true + case Some(resourceType) => resourceType.asString() == quota.resourceType.asString() + }) && filter.`type`.forall(dataType => quota.types.contains(dataType)) } } 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 26a6a13e1a..f78deb29c6 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 @@ -121,10 +121,10 @@ class QuotaSerializerTest extends AnyWordSpec with Matchers { id = Id.validate("aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8").toOption.get, resourceType = CountResourceType, used = UnsignedInt.liftOrThrow(1), - limit = UnsignedInt.liftOrThrow(2), + hardLimit = UnsignedInt.liftOrThrow(2), scope = AccountScope, name = QuotaName("name1"), - dataTypes = List(MailDataType), + types = List(MailDataType), warnLimit = Some(UnsignedInt.liftOrThrow(123)), softLimit = Some(UnsignedInt.liftOrThrow(456)), description = Some(QuotaDescription("Description 1"))) @@ -144,10 +144,10 @@ class QuotaSerializerTest extends AnyWordSpec with Matchers { | "id": "aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8", | "resourceType": "count", | "used": 1, - | "limit": 2, + | "hardLimit": 2, | "scope": "account", | "name": "name1", - | "dataTypes": [ + | "types": [ | "Mail" | ], | "warnLimit": 123, @@ -168,10 +168,10 @@ class QuotaSerializerTest extends AnyWordSpec with Matchers { id = Id.validate("aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8").toOption.get, resourceType = CountResourceType, used = UnsignedInt.liftOrThrow(1), - limit = UnsignedInt.liftOrThrow(2), + hardLimit = UnsignedInt.liftOrThrow(2), scope = AccountScope, name = QuotaName("name1"), - dataTypes = List(MailDataType), + types = List(MailDataType), description = None) val jmapQuota2 = jmapQuota.copy(id = Id.validate("aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy9").toOption.get, @@ -194,10 +194,10 @@ class QuotaSerializerTest extends AnyWordSpec with Matchers { | "id": "aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy8", | "resourceType": "count", | "used": 1, - | "limit": 2, + | "hardLimit": 2, | "scope": "account", | "name": "name1", - | "dataTypes": [ + | "types": [ | "Mail" | ] | }, @@ -205,10 +205,10 @@ class QuotaSerializerTest extends AnyWordSpec with Matchers { | "id": "aHR0cHM6Ly93d3cuYmFzZTY0ZW5jb2RlLm9yZy9", | "resourceType": "octets", | "used": 1, - | "limit": 2, + | "hardLimit": 2, | "scope": "account", | "name": "name2", - | "dataTypes": [ + | "types": [ | "Mail" | ] | } --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org