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