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 2020/11/16 01:50:41 UTC

[james-project] 01/05: JAMES-3369 Enhance EmailGetSerializer

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 334bde6b365afa76dc807710fbf085a96dacd2a8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 6 13:08:42 2020 +0700

    JAMES-3369 Enhance EmailGetSerializer
---
 .../rfc8621/contract/EmailSetMethodContract.scala  |  14 +-
 .../org/apache/james/jmap/core/Properties.scala    |   1 +
 .../james/jmap/json/EmailGetSerializer.scala       | 174 ++++++++++++++-------
 .../james/jmap/json/EmailQuerySerializer.scala     |  14 +-
 .../james/jmap/json/EmailSetSerializer.scala       |  40 ++---
 .../jmap/json/EmailSubmissionSetSerializer.scala   |  12 +-
 .../james/jmap/json/MailboxQuerySerializer.scala   |   5 +-
 .../apache/james/jmap/json/MailboxSerializer.scala |  93 ++++++-----
 .../james/jmap/json/ResponseSerializer.scala       |  42 ++---
 .../james/jmap/json/VacationSerializer.scala       |  37 +++--
 .../scala/org/apache/james/jmap/json/package.scala |  42 ++---
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |   8 +-
 .../jmap/json/MailboxGetSerializationTest.scala    |   4 +-
 .../james/jmap/json/MailboxSerializationTest.scala |   2 +-
 .../VacationResponseGetSerializationTest.scala     |   2 +-
 .../json/VacationResponseSerializationTest.scala   |   2 +-
 .../james/jmap/routes/JMAPApiRoutesTest.scala      |   4 +-
 17 files changed, 250 insertions(+), 246 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 8bd5a9e..38bfb79 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -430,7 +430,7 @@ trait EmailSetMethodContract {
       .isEqualTo(
         s"""|{
           |   "type":"invalidPatch",
-          |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
+          |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((/movie,List(JsonValidationError(List(map marker value can only be true),ArraySeq()))))),ArraySeq()))))"
           |}""".stripMargin)
   }
 
@@ -964,7 +964,7 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notCreated.aaaaaa")
       .isEqualTo(
         s"""{
-          |  "description": "List((/mailboxIds,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))",
+          |  "description": "List((/mailboxIds/invalid,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))",
           |  "type": "invalidArguments"
           |}""".stripMargin)
   }
@@ -3312,7 +3312,7 @@ trait EmailSetMethodContract {
       .isEqualTo(
         """|{
            |   "type":"invalidPatch",
-           |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
+           |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((/mus*c,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
            |}""".stripMargin)
   }
 
@@ -5694,7 +5694,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((/invalid,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
@@ -5755,7 +5755,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((/${messageId.serialize},List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
@@ -5816,7 +5816,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((/${messageId.serialize},List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
@@ -5897,7 +5897,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId2.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((/invalid,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Properties.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Properties.scala
index bfbe18d..90f4627 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Properties.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Properties.scala
@@ -44,6 +44,7 @@ case class Properties(value: Set[NonEmptyString]) {
   def isEmpty(): Boolean = value.isEmpty
 
   def contains(property: NonEmptyString): Boolean = value.contains(property)
+  def containsString(property: String): Boolean = refineV[NonEmpty](property).fold(e => false, refined => contains(refined))
 
   def format(): String = value.mkString(", ")
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index a53b734..0eafd02 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -21,6 +21,7 @@ package org.apache.james.jmap.json
 
 import org.apache.james.jmap.api.model.Preview
 import org.apache.james.jmap.core.Properties
+import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.{AddressesHeaderValue, BlobId, Charset, DateHeaderValue, Disposition, EmailAddress, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, Group [...]
 import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
 import play.api.libs.functional.syntax._
@@ -28,6 +29,35 @@ import play.api.libs.json._
 
 import scala.language.implicitConversions
 
+object EmailBodyPartToSerialize {
+  def from(part: EmailBodyPart): EmailBodyPartToSerialize = EmailBodyPartToSerialize(
+    partId = part.partId,
+    blobId = part.blobId,
+    headers = part.headers,
+    size = part.size,
+    `type` = part.`type`,
+    charset = part.charset,
+    disposition = part.disposition,
+    cid = part.cid,
+    language = part.language,
+    location = part.location,
+    name = part.name,
+    subParts = part.subParts.map(list => list.map(EmailBodyPartToSerialize.from)))
+}
+
+case class EmailBodyPartToSerialize(partId: PartId,
+                                    blobId: Option[BlobId],
+                                    headers: List[EmailHeader],
+                                    size: Size,
+                                    name: Option[Name],
+                                    `type`: Type,
+                                    charset: Option[Charset],
+                                    disposition: Option[Disposition],
+                                    cid: Option[Cid],
+                                    language: Option[Languages],
+                                    location: Option[Location],
+                                    subParts: Option[List[EmailBodyPartToSerialize]])
+
 object EmailGetSerializer {
   private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize)
 
@@ -88,65 +118,97 @@ object EmailGetSerializer {
     case (keyword, b) => (keyword.flagName, JsBoolean(b))
   })
 
-  private implicit def bodyValueMapWrites(implicit bodyValueWriter: Writes[EmailBodyValue]): Writes[Map[PartId, EmailBodyValue]] =
-    mapWrites[PartId, EmailBodyValue](_.value.toString(), bodyValueWriter)
-  private def bodyPartWritesWithPropertyFilter(properties: Properties): Writes[EmailBodyPart] =
-    new Writes[EmailBodyPart] {
-      def removeJsNull(obj: JsObject): JsObject =
-        JsObject(obj.fields.filter({
-          case (_, JsNull) => false
-          case _ => true
-        }))
-      def writes(part: EmailBodyPart): JsValue = properties.filter(
-        removeJsNull(
-          Json.obj("partId" -> Json.toJson(part.partId),
-            "blobId" -> Json.toJson(part.blobId),
-            "headers" -> Json.toJson(part.headers),
-            "size" -> Json.toJson(part.size),
-            "name" -> Json.toJson(part.name),
-            "type" -> Json.toJson(part.`type`),
-            "charset" -> Json.toJson(part.charset),
-            "disposition" -> Json.toJson(part.disposition),
-            "cid" -> Json.toJson(part.cid),
-            "language" -> Json.toJson(part.language),
-            "location" -> Json.toJson(part.location),
-            "subParts" -> part.subParts.map(list => list.map(writes)))))
-    }
-
-  private def emailWritesWithPropertyFilter(properties: Properties)(implicit partsWrites: Writes[EmailBodyPart]): Writes[EmailView] = {
-    implicit val emailMetadataWrites: OWrites[EmailMetadata] = Json.writes[EmailMetadata]
-    implicit val emailHeadersWrites: Writes[EmailHeaders] = Json.writes[EmailHeaders]
-    implicit val emailBodyWrites: Writes[EmailBody] = Json.writes[EmailBody]
-    implicit val emailBodyMetadataWrites: Writes[EmailBodyMetadata] = Json.writes[EmailBodyMetadata]
-
-    val emailFullViewWrites: OWrites[EmailFullView] = (JsPath.write[EmailMetadata] and
-        JsPath.write[EmailHeaders] and
-        JsPath.write[EmailBody] and
-        JsPath.write[EmailBodyMetadata] and
-        JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFullView.unapply))
-
-    val emailFastViewWrites: OWrites[EmailFastView] = (JsPath.write[EmailMetadata] and
-        JsPath.write[EmailHeaders] and
-        JsPath.write[EmailBodyMetadata] and
-        JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFastView.unapply))
-    val emailHeaderViewWrites: OWrites[EmailHeaderView] = (JsPath.write[EmailMetadata] and
-        JsPath.write[EmailHeaders] and
-        JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailHeaderView.unapply))
-    val emailMetadataViewWrites: OWrites[EmailMetadataView] = view => Json.toJsObject(view.metadata)
-
-    val emailWrites: OWrites[EmailView] = {
-      case view: EmailMetadataView => emailMetadataViewWrites.writes(view)
-      case view: EmailHeaderView => emailHeaderViewWrites.writes(view)
-      case view: EmailFastView => emailFastViewWrites.writes(view)
-      case view: EmailFullView => emailFullViewWrites.writes(view)
-    }
-
-    emailWrites.transform(properties.filter(_))
+  private implicit val bodyValueMapWrites: Writes[Map[PartId, EmailBodyValue]] =
+    mapWrites[PartId, EmailBodyValue](_.value.toString(), bodyValueWrites)
+
+  private implicit val bodyPartWritesToSerializeWrites: Writes[EmailBodyPartToSerialize] = (
+      (__ \ "partId").write[PartId] and
+      (__ \ "blobId").writeNullable[BlobId] and
+      (__ \ "headers").write[List[EmailHeader]] and
+      (__ \ "size").write[Size] and
+      (__ \ "name").writeNullable[Name] and
+      (__ \ "type").write[Type] and
+      (__ \ "charset").writeNullable[Charset] and
+      (__ \ "disposition").writeNullable[Disposition] and
+      (__ \ "cid").writeNullable[Cid] and
+      (__ \ "language").writeNullable[Languages] and
+      (__ \ "location").writeNullable[Location] and
+      (__ \ "subParts").lazyWriteNullable(implicitly[Writes[List[EmailBodyPartToSerialize]]])
+    )(unlift(EmailBodyPartToSerialize.unapply))
+
+  private implicit val bodyPartWrites: Writes[EmailBodyPart] = part => bodyPartWritesToSerializeWrites.writes(EmailBodyPartToSerialize.from(part))
+
+  private implicit val emailMetadataWrites: OWrites[EmailMetadata] = Json.writes[EmailMetadata]
+  private implicit val emailHeadersWrites: Writes[EmailHeaders] = Json.writes[EmailHeaders]
+  private implicit val emailBodyMetadataWrites: Writes[EmailBodyMetadata] = Json.writes[EmailBodyMetadata]
+
+  private val emailFastViewWrites: OWrites[EmailFastView] = (JsPath.write[EmailMetadata] and
+    JsPath.write[EmailHeaders] and
+    JsPath.write[EmailBodyMetadata] and
+    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFastView.unapply))
+  private val emailHeaderViewWrites: OWrites[EmailHeaderView] = (JsPath.write[EmailMetadata] and
+    JsPath.write[EmailHeaders] and
+    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailHeaderView.unapply))
+  private val emailMetadataViewWrites: OWrites[EmailMetadataView] = view => Json.toJsObject(view.metadata)
+  private implicit val emailBodyWrites: Writes[EmailBody] = Json.writes[EmailBody]
+  private implicit val emailFullViewWrites: OWrites[EmailFullView] = (JsPath.write[EmailMetadata] and
+    JsPath.write[EmailHeaders] and
+    JsPath.write[EmailBody] and
+    JsPath.write[EmailBodyMetadata] and
+    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFullView.unapply))
+  private implicit val emailWrites: OWrites[EmailView] = {
+    case view: EmailMetadataView => emailMetadataViewWrites.writes(view)
+    case view: EmailHeaderView => emailHeaderViewWrites.writes(view)
+    case view: EmailFastView => emailFastViewWrites.writes(view)
+    case view: EmailFullView => emailFullViewWrites.writes(view)
   }
-  private implicit def emailGetResponseWrites(implicit emailWrites: Writes[EmailView]): Writes[EmailGetResponse] = Json.writes[EmailGetResponse]
+  private implicit val emailGetResponseWrites: Writes[EmailGetResponse] = Json.writes[EmailGetResponse]
 
   def serialize(emailGetResponse: EmailGetResponse, properties: Properties, bodyProperties: Properties): JsValue =
-    Json.toJson(emailGetResponse)(emailGetResponseWrites(emailWritesWithPropertyFilter(properties)(bodyPartWritesWithPropertyFilter(bodyProperties))))
+    Json.toJson(emailGetResponse)
+      .transform((__ \ "list").json.update {
+        case JsArray(underlying) => JsSuccess(JsArray(underlying.map(js => js.transform {
+          case jsonObject: JsObject =>
+           bodyPropertiesFilteringTransformation(bodyProperties)
+             .reads(properties.filter(jsonObject))
+          case js => JsSuccess(js)
+        }.fold(_ => JsArray(underlying), o => o))))
+        case jsValue => JsSuccess(jsValue)
+      }).get
+
+  private def bodyPropertiesFilteringTransformation(bodyProperties: Properties): Reads[JsValue] = {
+    case serializedMailbox: JsObject =>
+      val bodyPropertiesToRemove = EmailBodyPart.allowedProperties -- bodyProperties
+      val noop: JsValue => JsValue = o => o
+
+      JsSuccess(Seq(
+          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "attachments"),
+          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "bodyStructure"),
+          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "textBody"),
+          bodyPropertiesFilteringTransformation(bodyPropertiesToRemove, "htmlBody"))
+        .reduceLeftOption(_ compose _)
+        .getOrElse(noop)
+        .apply(serializedMailbox))
+    case js => JsSuccess(js)
+  }
+
+  private def bodyPropertiesFilteringTransformation(properties: Properties, field: String): JsValue => JsValue =
+  {
+    case JsObject(underlying) =>JsObject(underlying.map {
+      case (key, jsValue) if key.equals(field) => (field, removeFieldsRecursively(properties).apply(jsValue))
+      case (key, jsValue) => (key, jsValue)
+    })
+    case jsValue => jsValue
+  }
+
+  private def removeFieldsRecursively(properties: Properties): JsValue => JsValue = {
+    case JsObject(underlying) => JsObject(underlying.flatMap {
+      case (key, _) if properties.containsString(key) => None
+      case (key, value) => Some((key, removeFieldsRecursively(properties).apply(value)))
+    })
+    case JsArray(others) => JsArray(others.map(removeFieldsRecursively(properties)))
+    case o: JsValue => o
+  }
 
   def deserializeEmailGetRequest(input: JsValue): JsResult[EmailGetRequest] = Json.fromJson[EmailGetRequest](input)
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
index ab020df..7a12c2b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
@@ -20,7 +20,7 @@
 package org.apache.james.jmap.json
 
 import javax.inject.Inject
-import org.apache.james.jmap.core.{AccountId, CanCalculateChanges, LimitUnparsed, PositionUnparsed, QueryState}
+import org.apache.james.jmap.core.{CanCalculateChanges, LimitUnparsed, PositionUnparsed, QueryState}
 import org.apache.james.jmap.mail.{AllInThreadHaveKeywordSortProperty, Anchor, AnchorOffset, And, Bcc, Body, Cc, CollapseThreads, Collation, Comparator, EmailQueryRequest, EmailQueryResponse, FilterCondition, FilterOperator, FilterQuery, From, FromSortProperty, HasAttachment, HasKeywordSortProperty, Header, HeaderContains, HeaderExist, IsAscending, Keyword, Not, Operator, Or, ReceivedAtSortProperty, SentAtSortProperty, SizeSortProperty, SomeInThreadHaveKeywordSortProperty, SortProperty,  [...]
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
 import play.api.libs.json._
@@ -29,8 +29,6 @@ import scala.language.implicitConversions
 import scala.util.Try
 
 class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
-  private implicit val accountIdWrites: Format[AccountId] = Json.valueFormat[AccountId]
-
   private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize)
   private implicit val mailboxIdReads: Reads[MailboxId] = {
     case JsString(serializedMailboxId) => Try(JsSuccess(mailboxIdFactory.fromString(serializedMailboxId))).getOrElse(JsError())
@@ -81,16 +79,16 @@ class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
     case _ => JsError(s"Expecting a JsString to represent a known operator")
   }
 
+  private val filterConditionRawRead: Reads[FilterCondition] = Json.reads[FilterCondition]
   private implicit val filterConditionReads: Reads[FilterCondition] = {
-    case JsObject(underlying) => {
+    case JsObject(underlying) =>
       val unsupported: collection.Set[String] = underlying.keySet.diff(FilterCondition.SUPPORTED)
       if (unsupported.nonEmpty) {
         JsError(s"These '${unsupported.mkString("[", ", ", "]")}' was unsupported filter options")
       } else {
-        Json.reads[FilterCondition].reads(JsObject(underlying))
+        filterConditionRawRead.reads(JsObject(underlying))
       }
-    }
-    case jsValue => Json.reads[FilterCondition].reads(jsValue)
+    case jsValue => filterConditionRawRead.reads(jsValue)
   }
 
   private implicit val limitUnparsedReads: Reads[LimitUnparsed] = Json.valueReads[LimitUnparsed]
@@ -133,7 +131,7 @@ class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
 
   private implicit val emailQueryRequestReads: Reads[EmailQueryRequest] = Json.reads[EmailQueryRequest]
 
-  private implicit def emailQueryResponseWrites: OWrites[EmailQueryResponse] = Json.writes[EmailQueryResponse]
+  private implicit val emailQueryResponseWrites: OWrites[EmailQueryResponse] = Json.writes[EmailQueryResponse]
 
   def serialize(emailQueryResponse: EmailQueryResponse): JsObject = Json.toJsObject(emailQueryResponse)
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index fa97676..6ebc2c6 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -173,12 +173,7 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   }
 
   private implicit val mailboxIdsMapReads: Reads[Map[MailboxId, Boolean]] =
-    readMapEntry[MailboxId, Boolean](s => Try(mailboxIdFactory.fromString(s)).toEither.left.map(error => error.getMessage),
-      {
-        case JsBoolean(true) => JsSuccess(true)
-        case JsBoolean(false) => JsError("mailboxId value can only be true")
-        case _ => JsError("Expecting mailboxId value to be a boolean")
-      })
+    Reads.mapReads[MailboxId, Boolean] {s => Try(mailboxIdFactory.fromString(s)).fold(e => JsError(e.getMessage), JsSuccess(_)) } (mapMarkerReads)
 
   private implicit val mailboxIdsReads: Reads[MailboxIds] = jsValue => mailboxIdsMapReads.reads(jsValue).map(
     mailboxIdsMap => MailboxIds(mailboxIdsMap.keys.toList))
@@ -189,18 +184,10 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   }
 
   private implicit val updatesMapReads: Reads[Map[UnparsedMessageId, JsObject]] =
-    readMapEntry[UnparsedMessageId, JsObject](s => refineV[UnparsedMessageIdConstraint](s),
-      {
-        case o: JsObject => JsSuccess(o)
-        case _ => JsError("Expecting a JsObject as an update entry")
-      })
+    Reads.mapReads[UnparsedMessageId, JsObject] {string => refineV[UnparsedMessageIdConstraint](string).fold(JsError(_), id => JsSuccess(id)) }
 
   private implicit val createsMapReads: Reads[Map[EmailCreationId, JsObject]] =
-    readMapEntry[EmailCreationId, JsObject](s => refineV[IdConstraint](s),
-      {
-        case o: JsObject => JsSuccess(o)
-        case _ => JsError("Expecting a JsObject as an update entry")
-      })
+    Reads.mapReads[EmailCreationId, JsObject] {s => refineV[IdConstraint](s).fold(JsError(_), JsSuccess(_)) }
 
   private implicit val keywordReads: Reads[Keyword] = {
     case jsString: JsString => Keyword.parse(jsString.value)
@@ -210,12 +197,7 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   }
 
   private implicit val keywordsMapReads: Reads[Map[Keyword, Boolean]] =
-    readMapEntry[Keyword, Boolean](s => Keyword.parse(s),
-      {
-        case JsBoolean(true) => JsSuccess(true)
-        case JsBoolean(false) => JsError("keyword value can only be true")
-        case _ => JsError("Expecting keyword value to be a boolean")
-      })
+    Reads.mapReads[Keyword, Boolean] {string => Keyword.parse(string).fold(JsError(_), JsSuccess(_)) } (mapMarkerReads)
   private implicit val keywordsReads: Reads[Keywords] = jsValue => keywordsMapReads.reads(jsValue).flatMap(
     keywordsMap => STRICT_KEYWORDS_FACTORY.fromSet(keywordsMap.keys.toSet)
       .fold(e => JsError(e.getMessage), keywords => JsSuccess(keywords)))
@@ -227,6 +209,10 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   private implicit val destroyIdsWrites: Writes[DestroyIds] = Json.valueWrites[DestroyIds]
   private implicit val emailRequestSetReads: Reads[EmailSetRequest] = Json.reads[EmailSetRequest]
   private implicit val emailCreationResponseWrites: Writes[EmailCreationResponse] = Json.writes[EmailCreationResponse]
+  private implicit val createsMapWrites: Writes[Map[EmailCreationId, EmailCreationResponse]] =
+    mapWrites[EmailCreationId, EmailCreationResponse](_.value, emailCreationResponseWrites)
+  private implicit val notCreatedMapWrites: Writes[Map[EmailCreationId, SetError]] =
+    mapWrites[EmailCreationId, SetError](_.value, setErrorWrites)
   private implicit val emailResponseSetWrites: OWrites[EmailSetResponse] = Json.writes[EmailSetResponse]
 
   private implicit val subjectReads: Reads[Subject] = Json.valueReads[Subject]
@@ -248,17 +234,21 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   private implicit val clientEmailBodyValueReads: Reads[ClientEmailBodyValue] = Json.reads[ClientEmailBodyValue]
   private implicit val typeReads: Reads[Type] = Json.valueReads[Type]
   private implicit val clientPartIdReads: Reads[ClientPartId] = Json.valueReads[ClientPartId]
+  private val rawHTMLReads: Reads[ClientHtmlBody] = Json.reads[ClientHtmlBody]
   private implicit val clientHtmlBodyReads: Reads[ClientHtmlBody] = {
     case JsObject(underlying) if underlying.contains("charset") => JsError("charset must not be specified in htmlBody")
     case JsObject(underlying) if underlying.contains("size") => JsError("size must not be specified in htmlBody")
     case JsObject(underlying) if underlying.contains("header:Content-Transfer-Encoding:asText") => JsError("Content-Transfer-Encoding must not be specified in htmlBody")
-    case o: JsObject => Json.reads[ClientHtmlBody].reads(o)
+    case o: JsObject => rawHTMLReads.reads(o)
     case _ => JsError("Expecting a JsObject to represent an ClientHtmlBody")
   }
 
   private implicit val bodyValuesReads: Reads[Map[ClientPartId, ClientEmailBodyValue]] =
-    readMapEntry[ClientPartId, ClientEmailBodyValue](s => Id.validate(s).fold(e => Left(e.getMessage), partId => Right(ClientPartId(partId))),
-      clientEmailBodyValueReads)
+    Reads.mapReads[ClientPartId, ClientEmailBodyValue] {
+      s => Id.validate(s).fold(
+        e => JsError(e.getMessage),
+        partId => JsSuccess(ClientPartId(partId)))
+    }
 
   case class EmailCreationRequestWithoutHeaders(mailboxIds: MailboxIds,
                                   messageId: Option[MessageIdsHeaderValue],
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
index 645e90e..2d5460a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -34,11 +34,7 @@ import scala.util.Try
 
 class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory) {
   private implicit val mapCreationRequestByEmailSubmissionCreationId: Reads[Map[EmailSubmissionCreationId, JsObject]] =
-    readMapEntry[EmailSubmissionCreationId, JsObject](s => refineV[IdConstraint](s),
-      {
-        case o: JsObject => JsSuccess(o)
-        case _ => JsError("Expecting a JsObject as a creation entry")
-      })
+    Reads.mapReads[EmailSubmissionCreationId, JsObject] {string => refineV[IdConstraint](string).fold(JsError(_), id => JsSuccess(id)) }
 
   private implicit val messageIdReads: Reads[MessageId] = {
     case JsString(serializedMessageId) => Try(JsSuccess(messageIdFactory.fromString(serializedMessageId)))
@@ -54,11 +50,7 @@ class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory
   }
 
   private implicit val emailUpdatesMapReads: Reads[Map[UnparsedMessageId, JsObject]] =
-    readMapEntry[UnparsedMessageId, JsObject](s => refineV[UnparsedMessageIdConstraint](s),
-      {
-        case o: JsObject => JsSuccess(o)
-        case _ => JsError("Expecting a JsObject as an update entry")
-      })
+    Reads.mapReads[UnparsedMessageId, JsObject] {string => refineV[UnparsedMessageIdConstraint](string).fold(JsError(_), id => JsSuccess(id)) }
   private implicit val destroyIdsReads: Reads[DestroyIds] = Json.valueFormat[DestroyIds]
 
   private implicit val emailSubmissionSetRequestReads: Reads[EmailSubmissionSetRequest] = Json.reads[EmailSubmissionSetRequest]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala
index a10a316..8110aaf 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxQuerySerializer.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.json
 
-import org.apache.james.jmap.core.{AccountId, CanCalculateChanges, QueryState}
+import org.apache.james.jmap.core.{CanCalculateChanges, QueryState}
 import org.apache.james.jmap.mail.{MailboxFilter, MailboxQueryRequest, MailboxQueryResponse}
 import org.apache.james.mailbox.Role
 import org.apache.james.mailbox.model.MailboxId
@@ -29,7 +29,6 @@ import scala.jdk.OptionConverters._
 import scala.language.implicitConversions
 
 object MailboxQuerySerializer {
-  private implicit val accountIdWrites: Format[AccountId] = Json.valueFormat[AccountId]
   private implicit val canCalculateChangeWrites: Writes[CanCalculateChanges] = Json.valueWrites[CanCalculateChanges]
 
   private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize)
@@ -56,7 +55,7 @@ object MailboxQuerySerializer {
   private implicit val emailQueryRequestReads: Reads[MailboxQueryRequest] = Json.reads[MailboxQueryRequest]
   private implicit val queryStateWrites: Writes[QueryState] = Json.valueWrites[QueryState]
 
-  private implicit def mailboxQueryResponseWrites: OWrites[MailboxQueryResponse] = Json.writes[MailboxQueryResponse]
+  private implicit val mailboxQueryResponseWrites: OWrites[MailboxQueryResponse] = Json.writes[MailboxQueryResponse]
 
   def serialize(mailboxQueryResponse: MailboxQueryResponse): JsObject = Json.toJsObject(mailboxQueryResponse)
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
index 8a0970e..20697ef 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
@@ -83,6 +83,9 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
   }
   private implicit val mailboxJavaRightReads: Reads[JavaRight] = value => rightRead.reads(value).map(right => right.toMailboxRight)
   private implicit val mailboxRfc4314RightsReads: Reads[Rfc4314Rights] = Json.valueReads[Rfc4314Rights]
+  private implicit val rightSeqWrites: Writes[Seq[Right]] = seq => JsArray(seq.map(rightWrites.writes))
+  private implicit val rightsMapWrites: Writes[Map[Username, Seq[Right]]] =
+    mapWrites[Username, Seq[Right]](_.asString(), rightSeqWrites)
   private implicit val rightsWrites: Writes[Rights] = Json.valueWrites[Rights]
 
   private implicit val mapRightsReads: Reads[Map[Username, Seq[Right]]] = _.validate[Map[String, Seq[Right]]]
@@ -90,30 +93,21 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
       rawMap.map(entry => (Username.of(entry._1), entry._2)))
   private implicit val rightsReads: Reads[Rights] = json => mapRightsReads.reads(json).map(rawMap => Rights(rawMap))
 
-  private implicit def rightsMapWrites(implicit rightWriter: Writes[Seq[Right]]): Writes[Map[Username, Seq[Right]]] =
-    mapWrites[Username, Seq[Right]](_.asString(), rightWriter)
-
   private implicit val domainWrites: Writes[Domain] = domain => JsString(domain.asString)
   private implicit val quotaRootWrites: Writes[QuotaRoot] = Json.writes[QuotaRoot]
   private implicit val quotaIdWrites: Writes[QuotaId] = Json.valueWrites[QuotaId]
 
   private implicit val quotaValueWrites: Writes[Value] = Json.writes[Value]
+  private implicit val quotaMapWrites: Writes[Map[Quotas.Type, Value]] =
+    mapWrites[Quotas.Type, Value](_.toString, quotaValueWrites)
   private implicit val quotaWrites: Writes[Quota] = Json.valueWrites[Quota]
-
-  private implicit def quotaMapWrites(implicit valueWriter: Writes[Value]): Writes[Map[Quotas.Type, Value]] =
-    mapWrites[Quotas.Type, Value](_.toString, valueWriter)
-
+  private implicit val quotasMapWrites: Writes[Map[QuotaId, Quota]] =
+    mapWrites[QuotaId, Quota](_.getName, quotaWrites)
   private implicit val quotasWrites: Writes[Quotas] = Json.valueWrites[Quotas]
 
-  private implicit def quotasMapWrites(implicit quotaWriter: Writes[Quota]): Writes[Map[QuotaId, Quota]] =
-    mapWrites[QuotaId, Quota](_.getName, quotaWriter)
+  implicit val mailboxWrites: Writes[Mailbox] = Json.writes[Mailbox]
 
-  implicit def mailboxWrites(properties: Properties): Writes[Mailbox] = Json.writes[Mailbox]
-    .transform(properties.filter(_))
-
-  implicit def mailboxCreationResponseWrites(properties: Properties): Writes[MailboxCreationResponse] =
-    Json.writes[MailboxCreationResponse]
-      .transform(properties.filter(_))
+  implicit val mailboxCreationResponseWrites: Writes[MailboxCreationResponse] = Json.writes[MailboxCreationResponse]
 
   private implicit val idsRead: Reads[Ids] = Json.valueReads[Ids]
 
@@ -124,59 +118,62 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
   private implicit val mailboxPatchObject: Reads[MailboxPatchObject] = Json.valueReads[MailboxPatchObject]
 
   private implicit val mapPatchObjectByMailboxIdReads: Reads[Map[UnparsedMailboxId, MailboxPatchObject]] =
-    readMapEntry[UnparsedMailboxId, MailboxPatchObject](s => refineV[UnparsedMailboxIdConstraint](s),
-      mailboxPatchObject)
+    Reads.mapReads[UnparsedMailboxId, MailboxPatchObject] {string => refineV[UnparsedMailboxIdConstraint](string).fold(JsError(_), id => JsSuccess(id)) }
 
   private implicit val mapCreationRequestByMailBoxCreationId: Reads[Map[MailboxCreationId, JsObject]] =
-    readMapEntry[MailboxCreationId, JsObject](s => refineV[NonEmpty](s),
-      {
-        case o: JsObject => JsSuccess(o)
-        case _ => JsError("Expecting a JsObject as a creation entry")
-      })
+    Reads.mapReads[MailboxCreationId, JsObject] {string => refineV[NonEmpty](string).fold(JsError(_), id => JsSuccess(id)) }
 
   private implicit val mailboxSetRequestReads: Reads[MailboxSetRequest] = Json.reads[MailboxSetRequest]
 
-  private implicit def notFoundWrites(implicit mailboxIdWrites: Writes[UnparsedMailboxId]): Writes[NotFound] =
-    notFound => JsArray(notFound.value.toList.map(mailboxIdWrites.writes))
+  private implicit val notFoundWrites: Writes[NotFound] = Json.valueWrites[NotFound]
 
-  private implicit def mailboxGetResponseWrites(implicit mailboxWrites: Writes[Mailbox]): Writes[MailboxGetResponse] = Json.writes[MailboxGetResponse]
+  private implicit val mailboxGetResponseWrites: Writes[MailboxGetResponse] = Json.writes[MailboxGetResponse]
 
-  private implicit def mailboxSetResponseWrites(implicit mailboxCreationResponseWrites: Writes[MailboxCreationResponse]): Writes[MailboxSetResponse] = Json.writes[MailboxSetResponse]
 
   private implicit val mailboxSetUpdateResponseWrites: Writes[MailboxUpdateResponse] = Json.valueWrites[MailboxUpdateResponse]
 
-  private implicit def mailboxMapSetErrorForCreationWrites: Writes[Map[MailboxCreationId, SetError]] =
+  private implicit val mailboxMapSetErrorForCreationWrites: Writes[Map[MailboxCreationId, SetError]] =
     mapWrites[MailboxCreationId, SetError](_.value, setErrorWrites)
-  private implicit def mailboxMapSetErrorWrites: Writes[Map[MailboxId, SetError]] =
+  private implicit val mailboxMapSetErrorWrites: Writes[Map[MailboxId, SetError]] =
     mapWrites[MailboxId, SetError](_.serialize(), setErrorWrites)
-  private implicit def mailboxMapSetErrorWritesByClientId: Writes[Map[ClientId, SetError]] =
+  private implicit val mailboxMapSetErrorWritesByClientId: Writes[Map[ClientId, SetError]] =
     mapWrites[ClientId, SetError](_.value.value, setErrorWrites)
-  private implicit def mailboxMapCreationResponseWrites(implicit mailboxSetCreationResponseWrites: Writes[MailboxCreationResponse]): Writes[Map[MailboxCreationId, MailboxCreationResponse]] =
-    mapWrites[MailboxCreationId, MailboxCreationResponse](_.value, mailboxSetCreationResponseWrites)
-  private implicit def mailboxMapUpdateResponseWrites: Writes[Map[MailboxId, MailboxUpdateResponse]] =
+  private implicit val mailboxMapCreationResponseWrites: Writes[Map[MailboxCreationId, MailboxCreationResponse]] =
+    mapWrites[MailboxCreationId, MailboxCreationResponse](_.value, mailboxCreationResponseWrites)
+  private implicit val mailboxMapUpdateResponseWrites: Writes[Map[MailboxId, MailboxUpdateResponse]] =
     mapWrites[MailboxId, MailboxUpdateResponse](_.serialize(), mailboxSetUpdateResponseWrites)
 
-  private def mailboxWritesWithFilteredProperties(properties: Properties, capabilities: Set[CapabilityIdentifier]): Writes[Mailbox] = {
-    mailboxWrites(Mailbox.propertiesFiltered(properties, capabilities))
-  }
+  private implicit val mailboxSetResponseWrites: Writes[MailboxSetResponse] = Json.writes[MailboxSetResponse]
 
-  private def mailboxCreationResponseWritesWithFilteredProperties(capabilities: Set[CapabilityIdentifier]): Writes[MailboxCreationResponse] = {
-    mailboxCreationResponseWrites(MailboxCreationResponse.propertiesFiltered(capabilities))
-  }
-
-  def serialize(mailbox: Mailbox)(implicit mailboxWrites: Writes[Mailbox]): JsValue = Json.toJson(mailbox)
-
-  def serialize(mailboxGetResponse: MailboxGetResponse)(implicit mailboxWrites: Writes[Mailbox]): JsValue = Json.toJson(mailboxGetResponse)
+  def serialize(mailbox: Mailbox): JsValue = Json.toJson(mailbox)
 
   def serialize(mailboxGetResponse: MailboxGetResponse, properties: Properties, capabilities: Set[CapabilityIdentifier]): JsValue =
-    serialize(mailboxGetResponse)(mailboxWritesWithFilteredProperties(properties, capabilities))
-
-  def serialize(mailboxSetResponse: MailboxSetResponse)
-               (implicit mailboxCreationResponseWrites: Writes[MailboxCreationResponse]): JsValue =
-    Json.toJson(mailboxSetResponse)(mailboxSetResponseWrites(mailboxCreationResponseWrites))
+    Json.toJson(mailboxGetResponse)
+      .transform((__ \ "list").json.update {
+        case JsArray(underlying) => JsSuccess(JsArray(underlying.map {
+          case jsonObject: JsObject =>
+            Mailbox.propertiesFiltered(properties, capabilities)
+              .filter(jsonObject)
+          case jsValue => jsValue
+        }))
+      }).get
 
   def serialize(mailboxSetResponse: MailboxSetResponse, capabilities: Set[CapabilityIdentifier]): JsValue =
-    serialize(mailboxSetResponse)(mailboxCreationResponseWritesWithFilteredProperties(capabilities))
+    Json.toJson(mailboxSetResponse)
+      .transform[JsValue] {
+        case JsObject(underlying) => JsSuccess[JsValue](JsObject(underlying.map {
+          case ("created", createdEntry: JsObject) =>
+            ("created", createdEntry match {
+              case JsObject(createdEntries) => JsObject(createdEntries.map {
+                case (key, serializedMailbox: JsObject) => (key, MailboxCreationResponse.propertiesFiltered(capabilities).filter(serializedMailbox))
+                case (key, value) => (key, value)
+              })
+              case jsValue: JsValue => jsValue
+            })
+          case (key, value) => (key, value)
+        }))
+        case jsValue => JsSuccess[JsValue](jsValue)
+      }.get
 
   def deserializeMailboxGetRequest(input: String): JsResult[MailboxGetRequest] = Json.parse(input).validate[MailboxGetRequest]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index b6038e0..6d69b4e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -22,9 +22,11 @@ package org.apache.james.jmap.json
 import java.io.InputStream
 import java.net.URL
 
+import eu.timepit.refined.refineV
 import org.apache.james.core.Username
 import org.apache.james.jmap.core
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
+import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodCallId, MethodName}
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{Account, Invocation, Session, _}
@@ -38,23 +40,13 @@ object ResponseSerializer {
   // CreateIds
   private implicit val clientIdFormat: Format[ClientId] = Json.valueFormat[ClientId]
   private implicit val serverIdFormat: Format[ServerId] = Json.valueFormat[ServerId]
-  private implicit val createdIdsFormat: Format[CreatedIds] = Json.valueFormat[CreatedIds]
-
-  private def mapWrites[K, V](keyWriter: K => String, valueWriter: Writes[V]): Writes[Map[K, V]] =
-    (ids: Map[K, V]) => {
-      ids.foldLeft(JsObject.empty)((jsObject, kv) => {
-        val (key: K, value: V) = kv
-        jsObject.+(keyWriter.apply(key), valueWriter.writes(value))
-      })
-    }
 
-  private implicit def createdIdsIdWrites(implicit serverIdWriter: Writes[ServerId]): Writes[Map[ClientId, ServerId]] =
-    mapWrites[ClientId, ServerId](_.value.value, serverIdWriter)
+  private implicit val createdIdsIdWrites: Writes[Map[ClientId, ServerId]] =
+    mapWrites[ClientId, ServerId](_.value.value, serverIdFormat)
 
-  private implicit def createdIdsIdRead(implicit serverIdReader: Reads[ServerId]): Reads[Map[ClientId, ServerId]] =
-    Reads.mapReads[ClientId, ServerId] {
-      clientIdString => Json.fromJson[ClientId](JsString(clientIdString))
-    }
+  private implicit val createdIdsIdRead: Reads[Map[ClientId, ServerId]] =
+    Reads.mapReads[ClientId, ServerId] { clientIdString => refineV[IdConstraint](clientIdString).fold(JsError(_), id => JsSuccess(ClientId(id)))}
+  private implicit val createdIdsFormat: Format[CreatedIds] = Json.valueFormat[CreatedIds]
 
   // Invocation
   private implicit val methodNameFormat: Format[MethodName] = Json.valueFormat[MethodName]
@@ -97,16 +89,12 @@ object ResponseSerializer {
   private implicit val sharesCapabilityWrites: Writes[SharesCapabilityProperties] = OWrites[SharesCapabilityProperties](_ => Json.obj())
   private implicit val vacationResponseCapabilityWrites: Writes[VacationResponseCapabilityProperties] = OWrites[VacationResponseCapabilityProperties](_ => Json.obj())
 
-  private implicit def setCapabilityWrites(implicit corePropertiesWriter: Writes[CoreCapabilityProperties],
-                                   mailCapabilityWrites: Writes[MailCapabilityProperties],
-                                   quotaCapabilityWrites: Writes[QuotaCapabilityProperties],
-                                   sharesCapabilityWrites: Writes[SharesCapabilityProperties],
-                                   vacationResponseCapabilityWrites: Writes[VacationResponseCapabilityProperties]): Writes[Set[_ <: Capability]] =
+  private implicit val setCapabilityWrites: Writes[Set[_ <: Capability]] =
     (set: Set[_ <: Capability]) => {
       set.foldLeft(JsObject.empty)((jsObject, capability) => {
         capability match {
           case capability: CoreCapability =>
-            jsObject.+(capability.identifier.value, corePropertiesWriter.writes(capability.properties))
+            jsObject.+(capability.identifier.value, coreCapabilityWrites.writes(capability.properties))
           case capability: MailCapability =>
             jsObject.+(capability.identifier.value, mailCapabilityWrites.writes(capability.properties))
           case capability: QuotaCapability =>
@@ -122,8 +110,8 @@ object ResponseSerializer {
 
   private implicit val capabilitiesWrites: Writes[Capabilities] = capabilities => setCapabilityWrites.writes(capabilities.toSet)
 
-  private implicit def identifierMapWrite[Any](implicit idWriter: Writes[AccountId]): Writes[Map[CapabilityIdentifier, AccountId]] =
-    mapWrites[CapabilityIdentifier, AccountId](_.value, idWriter)
+  private implicit val identifierMapWrite: Writes[Map[CapabilityIdentifier, AccountId]] =
+    mapWrites[CapabilityIdentifier, AccountId](_.value, accountIdWrites)
 
   private implicit val isPersonalFormat: Format[IsPersonal] = Json.valueFormat[IsPersonal]
   private implicit val isReadOnlyFormat: Format[IsReadOnly] = Json.valueFormat[IsReadOnly]
@@ -134,7 +122,7 @@ object ResponseSerializer {
       (JsPath \ Account.ACCOUNT_CAPABILITIES).write[Set[_ <: Capability]]
     ) (unlift(Account.unapplyIgnoreAccountId))
 
-  private implicit def accountListWrites(implicit accountWrites: Writes[Account]): Writes[List[Account]] =
+  private implicit val accountListWrites: Writes[List[Account]] =
     (list: List[Account]) => JsObject(list.map(account => (account.accountId.id.value, accountWrites.writes(account))))
 
   private implicit val sessionWrites: Writes[Session] = Json.writes[Session]
@@ -148,12 +136,12 @@ object ResponseSerializer {
 
   private implicit val jsonValidationErrorWrites: Writes[JsonValidationError] = error => JsString(error.message)
 
-  private implicit def jsonValidationErrorsWrites(implicit jsonValidationErrorWrites: Writes[JsonValidationError]): Writes[LegacySeq[JsonValidationError]] =
+  private implicit val jsonValidationErrorsWrites: Writes[LegacySeq[JsonValidationError]] =
     (errors: LegacySeq[JsonValidationError]) => {
       JsArray(errors.map(error => jsonValidationErrorWrites.writes(error)).toArray[JsValue])
     }
 
-  private implicit def errorsWrites(implicit jsonValidationErrorsWrites: Writes[LegacySeq[JsonValidationError]]): Writes[LegacySeq[(JsPath, LegacySeq[JsonValidationError])]] =
+  private implicit val errorsWrites: Writes[LegacySeq[(JsPath, LegacySeq[JsonValidationError])]] =
     (errors: LegacySeq[(JsPath, LegacySeq[JsonValidationError])]) => {
       errors.foldLeft(JsArray.empty)((jsArray, jsError) => {
         val (path: JsPath, list: LegacySeq[JsonValidationError]) = jsError
@@ -163,7 +151,7 @@ object ResponseSerializer {
       })
     }
 
-  private implicit def jsErrorWrites: Writes[JsError] = Json.writes[JsError]
+  private implicit val jsErrorWrites: Writes[JsError] = Json.writes[JsError]
 
   private implicit val problemDetailsWrites: Writes[ProblemDetails] = Json.writes[ProblemDetails]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/VacationSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/VacationSerializer.scala
index 3a3ecbd..102e16a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/VacationSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/VacationSerializer.scala
@@ -19,11 +19,9 @@
 
 package org.apache.james.jmap.json
 
-import java.time.format.DateTimeFormatter
-
-import org.apache.james.jmap.core.{Properties, UTCDate}
+import org.apache.james.jmap.core.Properties
 import org.apache.james.jmap.mail.Subject
-import org.apache.james.jmap.vacation.VacationResponse.{UnparsedVacationResponseId, VACATION_RESPONSE_ID}
+import org.apache.james.jmap.vacation.VacationResponse.VACATION_RESPONSE_ID
 import org.apache.james.jmap.vacation.{FromDate, HtmlBody, IsEnabled, TextBody, ToDate, VacationResponse, VacationResponseGetRequest, VacationResponseGetResponse, VacationResponseId, VacationResponseIds, VacationResponseNotFound, VacationResponsePatchObject, VacationResponseSetError, VacationResponseSetRequest, VacationResponseSetResponse, VacationResponseUpdateResponse}
 import play.api.libs.json._
 
@@ -43,9 +41,6 @@ object VacationSerializer {
 
   private implicit val vacationResponseSetResponseWrites: Writes[VacationResponseSetResponse] = Json.writes[VacationResponseSetResponse]
 
-  private implicit val utcDateWrites: Writes[UTCDate] =
-    utcDate => JsString(utcDate.asUTC.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX")))
-
   private implicit val vacationResponseIdWrites: Writes[VacationResponseId] = _ => JsString(VACATION_RESPONSE_ID.value)
   private implicit val vacationResponseIdReads: Reads[VacationResponseId] = {
     case JsString("singleton") => JsSuccess(VacationResponseId())
@@ -59,29 +54,33 @@ object VacationSerializer {
   private implicit val textBodyWrites: Writes[TextBody] = Json.valueWrites[TextBody]
   private implicit val htmlBodyWrites: Writes[HtmlBody] = Json.valueWrites[HtmlBody]
 
-  implicit def vacationResponseWrites(properties: Properties): Writes[VacationResponse] = Json.writes[VacationResponse]
-    .transform(properties.filter(_))
+  private implicit val vacationResponseWrites: Writes[VacationResponse] = Json.writes[VacationResponse]
 
   private implicit val vacationResponseIdsReads: Reads[VacationResponseIds] = Json.valueReads[VacationResponseIds]
 
   private implicit val vacationResponseGetRequest: Reads[VacationResponseGetRequest] = Json.reads[VacationResponseGetRequest]
 
-  private implicit def vacationResponseNotFoundWrites(implicit idWrites: Writes[UnparsedVacationResponseId]): Writes[VacationResponseNotFound] =
-    notFound => JsArray(notFound.value.toList.map(idWrites.writes))
-
-  private implicit def vacationResponseGetResponseWrites(implicit vacationResponseWrites: Writes[VacationResponse]): Writes[VacationResponseGetResponse] =
-    Json.writes[VacationResponseGetResponse]
+  private implicit val vacationResponseNotFoundWrites: Writes[VacationResponseNotFound] =
+    notFound => JsArray(notFound.value.toList.map(id => JsString(id.value)))
 
-  private def vacationResponseWritesWithFilteredProperties(properties: Properties): Writes[VacationResponse] =
-    vacationResponseWrites(VacationResponse.propertiesFiltered(properties))
+  private implicit val vacationResponseGetResponseWrites: Writes[VacationResponseGetResponse] = Json.writes[VacationResponseGetResponse]
 
-  def serialize(vacationResponse: VacationResponse)(implicit vacationResponseWrites: Writes[VacationResponse]): JsValue = Json.toJson(vacationResponse)
+  def serialize(vacationResponse: VacationResponse): JsValue = Json.toJson(vacationResponse)
 
   def serialize(vacationResponseGetResponse: VacationResponseGetResponse)(implicit vacationResponseWrites: Writes[VacationResponse]): JsValue =
-    Json.toJson(vacationResponseGetResponse)
+    serialize(vacationResponseGetResponse, VacationResponse.allProperties)
 
   def serialize(vacationResponseGetResponse: VacationResponseGetResponse, properties: Properties): JsValue =
-    serialize(vacationResponseGetResponse)(vacationResponseWritesWithFilteredProperties(properties))
+    Json.toJson(vacationResponseGetResponse)
+      .transform((__ \ "list").json.update {
+        case JsArray(underlying) => JsSuccess(JsArray(underlying.map {
+          case jsonObject: JsObject =>
+            VacationResponse.propertiesFiltered(properties)
+              .filter(jsonObject)
+          case jsValue => jsValue
+        }))
+      }).get
+
 
   def serialize(vacationResponseSetResponse: VacationResponseSetResponse): JsValue = Json.toJson(vacationResponseSetResponse)
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala
index 8b05ba9..536a620 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala
@@ -24,7 +24,6 @@ import java.time.format.DateTimeFormatter
 
 import eu.timepit.refined.api.{RefType, Validate}
 import org.apache.james.core.MailAddress
-import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{AccountId, Properties, SetError, UTCDate}
 import play.api.libs.json._
@@ -32,6 +31,17 @@ import play.api.libs.json._
 import scala.util.{Failure, Success, Try}
 
 package object json {
+  implicit val jsObjectReads: Reads[JsObject] = {
+    case o: JsObject => JsSuccess(o)
+    case _ => JsError("Expecting a JsObject as a creation entry")
+  }
+
+  val mapMarkerReads: Reads[Boolean] = {
+    case JsBoolean(true) => JsSuccess(true)
+    case JsBoolean(false) => JsError("map marker value can only be true")
+    case _ => JsError("Expecting mailboxId value to be a boolean")
+  }
+
   def mapWrites[K, V](keyWriter: K => String, valueWriter: Writes[V]): Writes[Map[K, V]] =
     (ids: Map[K, V]) => {
       ids.foldLeft(JsObject.empty)((jsObject, kv) => {
@@ -40,31 +50,6 @@ package object json {
       })
     }
 
-  def readMapEntry[K, V](keyValidator: String => Either[String, K], valueReads: Reads[V]): Reads[Map[K, V]] =
-    _.validate[Map[String, JsValue]]
-      .flatMap(mapWithStringKey =>{
-        val firstAcc = scala.util.Right[JsError, Map[K, V]](Map.empty)
-        mapWithStringKey
-          .foldLeft[Either[JsError, Map[K, V]]](firstAcc)((acc: Either[JsError, Map[K, V]], keyValue) => {
-            acc match {
-              case error@Left(_) => error
-              case scala.util.Right(validatedAcc) =>
-                val refinedKey: Either[String, K] = keyValidator.apply(keyValue._1)
-                refinedKey match {
-                  case Left(error) => Left(JsError(error))
-                  case scala.util.Right(unparsedK) =>
-                    val transformedValue: JsResult[V] = valueReads.reads(keyValue._2)
-                    transformedValue.fold(
-                      error => Left(JsError(error)),
-                      v => scala.util.Right(validatedAcc + (unparsedK -> v)))
-                }
-            }
-          }) match {
-          case Left(jsError) => jsError
-          case scala.util.Right(value) => JsSuccess(value)
-        }
-      })
-
   // code copied from https://github.com/avdv/play-json-refined/blob/master/src/main/scala/de.cbley.refined.play.json/package.scala
   implicit def writeRefined[T, P, F[_, _]](
                                             implicit writesT: Writes[T],
@@ -85,11 +70,6 @@ package object json {
         }
       })
 
-  implicit def idMapWrite[Any](implicit vr: Writes[Any]): Writes[Map[Id, Any]] =
-    (m: Map[Id, Any]) => {
-      JsObject(m.map { case (k, v) => (k.value, vr.writes(v)) }.toSeq)
-    }
-
   private[json] implicit val UTCDateReads: Reads[UTCDate] = {
     case JsString(value) =>
       Try(UTCDate(ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME))) match {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index e7d2c8f..1e34ed7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -58,8 +58,7 @@ object JMAPApiRoutes {
 class JMAPApiRoutes (val authenticator: Authenticator,
                      userProvisioner: UserProvisioning,
                      mailboxesProvisioner: MailboxesProvisioner,
-                     methods: Set[Method],
-                     sessionSupplier: SessionSupplier) extends JMAPRoutes {
+                     methods: Set[Method]) extends JMAPRoutes {
 
   private val methodsByName: Map[MethodName, Method] = methods.map(method => method.methodName -> method).toMap
 
@@ -67,9 +66,8 @@ class JMAPApiRoutes (val authenticator: Authenticator,
   def this(@Named(InjectionKeys.RFC_8621) authenticator: Authenticator,
            userProvisioner: UserProvisioning,
            mailboxesProvisioner: MailboxesProvisioner,
-           javaMethods: java.util.Set[Method],
-           sessionSupplier: SessionSupplier) {
-    this(authenticator, userProvisioner, mailboxesProvisioner, javaMethods.asScala.toSet, sessionSupplier)
+           javaMethods: java.util.Set[Method]) {
+    this(authenticator, userProvisioner, mailboxesProvisioner, javaMethods.asScala.toSet)
   }
 
   override def routes(): stream.Stream[JMAPRoute] = Stream.of(
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
index f39412c..55a4a7b 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
@@ -21,7 +21,7 @@ package org.apache.james.jmap.json
 
 import eu.timepit.refined.auto._
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
-import org.apache.james.jmap.core.{AccountId, Properties}
+import org.apache.james.jmap.core.{AccountId, DefaultCapabilities, Properties}
 import org.apache.james.jmap.json.Fixture._
 import org.apache.james.jmap.json.MailboxGetSerializationTest._
 import org.apache.james.jmap.json.MailboxSerializationTest.MAILBOX
@@ -196,7 +196,7 @@ class MailboxGetSerializationTest extends AnyWordSpec with Matchers {
           |}
           |""".stripMargin
 
-      assertThatJson(Json.stringify(SERIALIZER.serialize(actualValue)(SERIALIZER.mailboxWrites(Mailbox.allProperties)))).isEqualTo(expectedJson)
+      assertThatJson(Json.stringify(SERIALIZER.serialize(actualValue, Mailbox.allProperties, DefaultCapabilities.SUPPORTED_CAPABILITY_IDENTIFIERS))).isEqualTo(expectedJson)
     }
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala
index 0f825df..c9f5891 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxSerializationTest.scala
@@ -130,7 +130,7 @@ class MailboxSerializationTest extends AnyWordSpec with Matchers {
           |}""".stripMargin
 
       val serializer = new MailboxSerializer(new TestId.Factory)
-      assertThatJson(Json.stringify(serializer.serialize(MAILBOX)(serializer.mailboxWrites(Mailbox.allProperties)))).isEqualTo(expectedJson)
+      assertThatJson(Json.stringify(serializer.serialize(MAILBOX))).isEqualTo(expectedJson)
     }
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseGetSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseGetSerializationTest.scala
index b37910f..dda1b9a 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseGetSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseGetSerializationTest.scala
@@ -175,7 +175,7 @@ class VacationResponseGetSerializationTest extends AnyWordSpec with Matchers {
           |}
           |""".stripMargin
 
-      assertThatJson(Json.stringify(VacationSerializer.serialize(actualValue)(VacationSerializer.vacationResponseWrites(VacationResponse.allProperties)))).isEqualTo(expectedJson)
+      assertThatJson(Json.stringify(VacationSerializer.serialize(actualValue, VacationResponse.allProperties))).isEqualTo(expectedJson)
     }
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseSerializationTest.scala
index b805538..0a6e8a1 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/VacationResponseSerializationTest.scala
@@ -68,7 +68,7 @@ class VacationResponseSerializationTest extends AnyWordSpec with Matchers {
           | "htmlBody":"<b>HTML body</b>"
           |}""".stripMargin
 
-      assertThatJson(Json.stringify(VacationSerializer.serialize(VACATION_RESPONSE)(VacationSerializer.vacationResponseWrites(VacationResponse.allProperties)))).isEqualTo(expectedJson)
+      assertThatJson(Json.stringify(VacationSerializer.serialize(VACATION_RESPONSE))).isEqualTo(expectedJson)
     }
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index 526da86..6bbce45 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -78,7 +78,7 @@ object JMAPApiRoutesTest {
   private val sessionSupplier: SessionSupplier = new SessionSupplier(JmapRfc8621Configuration(JmapRfc8621Configuration.LOCALHOST_URL_PREFIX))
   private val JMAP_METHODS: Set[Method] = Set(new CoreEchoMethod)
 
-  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, mailboxesProvisioner, JMAP_METHODS, sessionSupplier)
+  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, mailboxesProvisioner, JMAP_METHODS)
   private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE))
 
   private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8))
@@ -442,7 +442,7 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
     when(mockCoreEchoMethod.requiredCapabilities).thenReturn(Set(JMAP_CORE))
 
     val methods: Set[Method] = Set(mockCoreEchoMethod)
-    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, mailboxesProvisioner, methods, sessionSupplier)
+    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, mailboxesProvisioner, methods)
     val routesHandler: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, apiRoute))
 
     val versionParser: VersionParser = new VersionParser(SUPPORTED_VERSIONS)


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