blob: 16b21e44a19bf14060c0f15dae40f481d3c87727 [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.james.jmap.json
import jakarta.inject.Inject
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, Subject, SubjectSortProperty, Text, To, ToSortProperty}
import org.apache.james.mailbox.model.{MailboxId, MessageId}
import play.api.libs.json._
import scala.util.Try
class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
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())
case _ => JsError()
}
private implicit val keywordReads: Reads[Keyword] = {
case JsString(keywordValue) =>
Keyword.parse(keywordValue)
.flatMap(keyword => if (keyword.isForbiddenImapKeyword) {
Left(s"Search based on IMAP unexposed keywords is not supported for $keywordValue")
} else {
Right(keyword)
})
.fold(JsError(_), JsSuccess(_))
case _ => JsError("Expecting keywords to be represented by a JsString")
}
private implicit val hasAttachmentReads: Reads[HasAttachment] = Json.valueReads[HasAttachment]
private implicit val textReads: Reads[Text] = Json.valueReads[Text]
private implicit val fromReads: Reads[From] = Json.valueReads[From]
private implicit val toReads: Reads[To] = Json.valueReads[To]
private implicit val ccReads: Reads[Cc] = Json.valueReads[Cc]
private implicit val bccReads: Reads[Bcc] = Json.valueReads[Bcc]
private implicit val subjectReads: Reads[Subject] = Json.valueReads[Subject]
private implicit val headerReads: Reads[Header] = {
case array: JsArray if array.value.length == 1 =>
extractString(array.value.head)
.fold[JsResult[Header]](e => e, name => JsSuccess(HeaderExist(name)))
case array: JsArray if array.value.length == 2 =>
extractString(array.value.head)
.flatMap(name => extractString(array.value.last)
.map(value => (name, value)))
.fold(e => e, {
case (name, value) => JsSuccess(HeaderContains(name, value))
})
case _ => JsError("header filter needs to be an array of one or two strings")
}
private def extractString(jsValue: JsValue): Either[JsResult[Header], String] = jsValue match {
case JsString(value) => Right(value)
case _ => Left(JsError("header filter needs to be an array of one or two strings"))
}
private implicit val bodyReads: Reads[Body] = Json.valueReads[Body]
private implicit val operatorReads: Reads[Operator] = {
case JsString("AND") => JsSuccess(And)
case JsString("OR") => JsSuccess(Or)
case JsString("NOT") => JsSuccess(Not)
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) =>
val unsupported: collection.Set[String] = underlying.keySet.diff(FilterCondition.SUPPORTED)
if (unsupported.nonEmpty) {
JsError(s"These '${unsupported.mkString("[", ", ", "]")}' was unsupported filter options")
} else {
filterConditionRawRead.reads(JsObject(underlying))
}
case jsValue => filterConditionRawRead.reads(jsValue)
}
private implicit val limitUnparsedReads: Reads[LimitUnparsed] = Json.valueReads[LimitUnparsed]
private implicit val CanCalculateChangesFormat: Format[CanCalculateChanges] = Json.valueFormat[CanCalculateChanges]
private implicit val queryStateWrites: Writes[QueryState] = Json.valueWrites[QueryState]
private implicit val positionUnparsedReads: Reads[PositionUnparsed] = Json.valueReads[PositionUnparsed]
private implicit val messageIdWrites: Writes[MessageId] = id => JsString(id.serialize())
private implicit val filterOperatorReads: Reads[FilterOperator] = Json.reads[FilterOperator]
private implicit def filterQueryReads: Reads[FilterQuery] = {
case JsObject(underlying) if underlying.contains("operator") && (!underlying.contains("conditions") || underlying.contains("conditions") && underlying.size > 2) => JsError("Expecting filterOperator to contain only operator and conditions")
case jsValue@JsObject(underlying) if underlying.contains("operator") && underlying.contains("conditions") && underlying.size.equals(2) => filterOperatorReads.reads(jsValue)
case jsValue => filterConditionReads.reads(jsValue)
}
private implicit val sortPropertyReads: Reads[SortProperty] = {
case JsString("receivedAt") => JsSuccess(ReceivedAtSortProperty)
case JsString("allInThreadHaveKeyword") => JsSuccess(AllInThreadHaveKeywordSortProperty)
case JsString("someInThreadHaveKeyword") => JsSuccess(SomeInThreadHaveKeywordSortProperty)
case JsString("size") => JsSuccess(SizeSortProperty)
case JsString("from") => JsSuccess(FromSortProperty)
case JsString("to") => JsSuccess(ToSortProperty)
case JsString("subject") => JsSuccess(SubjectSortProperty)
case JsString("hasKeyword") => JsSuccess(HasKeywordSortProperty)
case JsString("sentAt") => JsSuccess(SentAtSortProperty)
case JsString(others) => JsError(s"'$others' is not a supported sort property")
case _ => JsError(s"Expecting a JsString to represent a sort property")
}
private implicit val sortPropertyWrites: Writes[SortProperty] = {
case ReceivedAtSortProperty => JsString("receivedAt")
case _ => throw new NotImplementedError()
}
private implicit val isAscendingFormat: Format[IsAscending] = Json.valueFormat[IsAscending]
private implicit val collationFormat: Format[Collation] = Json.valueFormat[Collation]
private implicit val comparatorFormat: Format[Comparator] = Json.format[Comparator]
private implicit val collapseThreadsReads: Reads[CollapseThreads] = Json.valueReads[CollapseThreads]
private implicit val anchorReads: Reads[Anchor] = Json.valueReads[Anchor]
private implicit val anchorOffsetReads: Reads[AnchorOffset] = Json.valueReads[AnchorOffset]
private implicit val emailQueryRequestReads: Reads[EmailQueryRequest] = Json.reads[EmailQueryRequest]
private implicit val emailQueryResponseWrites: OWrites[EmailQueryResponse] = Json.writes[EmailQueryResponse]
def serialize(emailQueryResponse: EmailQueryResponse): JsObject = Json.toJsObject(emailQueryResponse)
def deserializeEmailQueryRequest(input: JsValue): JsResult[EmailQueryRequest] = Json.fromJson[EmailQueryRequest](input)
}