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 18a9f40fd6e5058f9461ccda5b96653e704fac12 Author: LanKhuat <dlkh...@linagora.com> AuthorDate: Wed Oct 28 18:35:27 2020 +0700 JAMES-3438 Email/set create htmlBody --- .../rfc8621/contract/EmailSetMethodContract.scala | 78 +++++++++++++++++++ .../james/jmap/json/EmailSetSerializer.scala | 15 +++- .../org/apache/james/jmap/mail/EmailSet.scala | 50 +++++++++---- .../apache/james/jmap/method/EmailSetMethod.scala | 87 ++++++++++++---------- 4 files changed, 171 insertions(+), 59 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 0e2f4bd..59de94d 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 @@ -851,6 +851,84 @@ trait EmailSetMethodContract { } @Test + def createShouldSupportHtmlBody(server: GuiceJamesServer): Unit = { + val bobPath = MailboxPath.inbox(BOB) + val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath) + + val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>" + + val request = + s"""{ + | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], + | "methodCalls": [ + | ["Email/set", { + | "accountId": "$ACCOUNT_ID", + | "create": { + | "aaaaaa":{ + | "mailboxIds": { + | "${mailboxId.serialize}": true + | }, + | "subject": "World domination", + | "htmlBody": [ + | { + | "partId": "a49d", + | "type": "text/html" + | } + | ], + | "bodyValues": { + | "a49d": { + | "value": "$htmlBody", + | "isTruncated": false + | } + | } + | } + | } + | }, "c1"], + | ["Email/get", + | { + | "accountId": "$ACCOUNT_ID", + | "ids": ["#aaaaaa"], + | "properties": ["mailboxIds", "subject", "bodyValues"] + | }, + | "c2"] + | ] + |}""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post.prettyPeek() + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id") + .inPath("methodResponses[0][1].created.aaaaaa") + .isEqualTo("{}".stripMargin) + + assertThatJson(response) + .whenIgnoringPaths("methodResponses[1][1].list[0].id") + .inPath(s"methodResponses[1][1].list") + .isEqualTo( + s"""[{ + | "mailboxIds": { + | "${mailboxId.serialize}": true + | }, + | "subject": "World domination", + | "bodyValues": { + | "1": { + | "value": "$htmlBody" + | } + | } + |}]""".stripMargin) + } + + @Test def shouldNotResetKeywordWhenInvalidKeyword(server: GuiceJamesServer): Unit = { val message: Message = Fixture.createTestMessage 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 caacce8..5c76a58 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 @@ -23,10 +23,10 @@ import cats.implicits._ import eu.timepit.refined.refineV import javax.inject.Inject import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId, UnparsedMessageIdConstraint} -import org.apache.james.jmap.mail.{AddressesHeaderValue, DestroyIds, EmailAddress, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, EmailSetResponse, EmailSetUpdate, EmailerName, HeaderMessageId, MailboxIds, MessageIdsHeaderValue, Subject} +import org.apache.james.jmap.mail.{AddressesHeaderValue, ClientEmailBodyValue, ClientHtmlBody, ClientPartId, DestroyIds, EmailAddress, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, EmailSetResponse, EmailSetUpdate, EmailerName, HeaderMessageId, IsEncodingProblem, IsTruncated, MailboxIds, MessageIdsHeaderValue, Subject, Type} import org.apache.james.jmap.model.Id.IdConstraint import org.apache.james.jmap.model.KeywordsFactory.STRICT_KEYWORDS_FACTORY -import org.apache.james.jmap.model.{Keyword, Keywords, SetError} +import org.apache.james.jmap.model.{Id, Keyword, Keywords, SetError} import org.apache.james.mailbox.model.{MailboxId, MessageId} import play.api.libs.json.{JsArray, JsBoolean, JsError, JsNull, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes} @@ -242,6 +242,17 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI .fold(e => JsError(e), ids => JsSuccess(MessageIdsHeaderValue(Some(ids).filter(_.nonEmpty)))) } + + private implicit val isTruncatedReads: Reads[IsTruncated] = Json.valueReads[IsTruncated] + private implicit val isEncodingProblemReads: Reads[IsEncodingProblem] = Json.valueReads[IsEncodingProblem] + 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 implicit val clientHtmlBodyReads: Reads[ClientHtmlBody] = Json.reads[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) + private implicit val emailCreationRequestReads: Reads[EmailCreationRequest] = Json.reads[EmailCreationRequest] def deserialize(input: JsValue): JsResult[EmailSetRequest] = Json.fromJson[EmailSetRequest](input) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala index 767f021..e5b31b3 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala @@ -53,6 +53,14 @@ object EmailSet { Try(messageIdFactory.fromString(unparsed.value)) } +case class ClientPartId(id: Id) + +case class ClientHtmlBody(partId: ClientPartId, `type`: Type) + +case class ClientEmailBodyValue(value: String, + isEncodingProblem: Option[IsEncodingProblem], + isTruncated: Option[IsTruncated]) + case class EmailCreationRequest(mailboxIds: MailboxIds, messageId: Option[MessageIdsHeaderValue], references: Option[MessageIdsHeaderValue], @@ -66,22 +74,32 @@ case class EmailCreationRequest(mailboxIds: MailboxIds, subject: Option[Subject], sentAt: Option[UTCDate], keywords: Option[Keywords], - receivedAt: Option[UTCDate]) { - def toMime4JMessage: Message = { - val builder = Message.Builder.of - references.flatMap(_.asString).map(new RawField("References", _)).foreach(builder.setField) - inReplyTo.flatMap(_.asString).map(new RawField("In-Reply-To", _)).foreach(builder.setField) - messageId.flatMap(_.asString).map(new RawField(FieldName.MESSAGE_ID, _)).foreach(builder.setField) - subject.foreach(value => builder.setSubject(value.value)) - from.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setFrom) - to.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setTo) - cc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setCc) - bcc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setBcc) - sender.flatMap(_.asMime4JMailboxList).map(_.asJava).map(Fields.addressList(FieldName.SENDER, _)).foreach(builder.setField) - replyTo.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setReplyTo) - sentAt.map(_.asUTC).map(_.toInstant).map(Date.from).foreach(builder.setDate) - builder.setBody("", StandardCharsets.UTF_8) - builder.build() + receivedAt: Option[UTCDate], + htmlBody: Option[List[ClientHtmlBody]], + bodyValues: Option[Map[ClientPartId, ClientEmailBodyValue]]) { + def toMime4JMessage: Either[IllegalArgumentException, Message] = + validateHtmlBody.map(maybeHtmlBody => { + val builder = Message.Builder.of + references.flatMap(_.asString).map(new RawField("References", _)).foreach(builder.setField) + inReplyTo.flatMap(_.asString).map(new RawField("In-Reply-To", _)).foreach(builder.setField) + messageId.flatMap(_.asString).map(new RawField(FieldName.MESSAGE_ID, _)).foreach(builder.setField) + subject.foreach(value => builder.setSubject(value.value)) + from.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setFrom) + to.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setTo) + cc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setCc) + bcc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setBcc) + sender.flatMap(_.asMime4JMailboxList).map(_.asJava).map(Fields.addressList(FieldName.SENDER, _)).foreach(builder.setField) + replyTo.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setReplyTo) + sentAt.map(_.asUTC).map(_.toInstant).map(Date.from).foreach(builder.setDate) + builder.setBody(maybeHtmlBody.getOrElse(""), "html", StandardCharsets.UTF_8) + builder.build() + }) + + def validateHtmlBody: Either[IllegalArgumentException, Option[String]] = htmlBody match { + case None => Right(None) + case Some(html :: Nil) => bodyValues.getOrElse(Map()).get(html.partId) + .map(part => Right(Some(part.value))) + .getOrElse(Left(new IllegalArgumentException("todo"))) } } diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala index 8686751..cf36efc 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala @@ -59,17 +59,17 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, case class DestroyResults(results: Seq[DestroyResult]) { def destroyed: Option[DestroyIds] = Option(results.flatMap{ - case result: DestroySuccess => Some(result.messageId) - case _ => None - }.map(EmailSet.asUnparsed)) + case result: DestroySuccess => Some(result.messageId) + case _ => None + }.map(EmailSet.asUnparsed)) .filter(_.nonEmpty) .map(DestroyIds) def notDestroyed: Option[Map[UnparsedMessageId, SetError]] = Option(results.flatMap{ - case failure: DestroyFailure => Some((failure.unparsedMessageId, failure.asMessageSetError)) - case _ => None - }.toMap) + case failure: DestroyFailure => Some((failure.unparsedMessageId, failure.asMessageSetError)) + case _ => None + }.toMap) .filter(_.nonEmpty) } @@ -78,7 +78,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, val success: Seq[DestroySuccess] = deleteResult.getDestroyed.asScala.toSeq .map(DestroySuccess) val notFound: Seq[DestroyResult] = deleteResult.getNotFound.asScala.toSeq - .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundExeception(id))) + .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundExeception(id))) success ++ notFound } @@ -97,16 +97,16 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, case class CreationResults(results: Seq[CreationResult]) { def created: Option[Map[EmailCreationId, EmailCreationResponse]] = Option(results.flatMap{ - case result: CreationSuccess => Some((result.clientId, result.response)) - case _ => None - }.toMap) + case result: CreationSuccess => Some((result.clientId, result.response)) + case _ => None + }.toMap) .filter(_.nonEmpty) def notCreated: Option[Map[EmailCreationId, SetError]] = { Option(results.flatMap{ - case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError)) - case _ => None - } + case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError)) + case _ => None + } .toMap) .filter(_.nonEmpty) } @@ -134,16 +134,16 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, case class UpdateResults(results: Seq[UpdateResult]) { def updated: Option[Map[MessageId, Unit]] = Option(results.flatMap{ - case result: UpdateSuccess => Some(result.messageId, ()) - case _ => None - }.toMap) + case result: UpdateSuccess => Some(result.messageId, ()) + case _ => None + }.toMap) .filter(_.nonEmpty) def notUpdated: Option[Map[UnparsedMessageId, SetError]] = Option(results.flatMap{ - case failure: UpdateFailure => Some((failure.unparsedMessageId, failure.asMessageSetError)) - case _ => None - }.toMap) + case failure: UpdateFailure => Some((failure.unparsedMessageId, failure.asMessageSetError)) + case _ => None + }.toMap) .filter(_.nonEmpty) } @@ -169,12 +169,12 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, notDestroyed = destroyResults.notDestroyed))), methodCallId = invocation.invocation.methodCallId), processingContext = created.created.getOrElse(Map()) - .foldLeft(invocation.processingContext)({ - case (processingContext, (clientId, response)) => - Id.validate(response.id.serialize) - .fold(_ => processingContext, - serverId => processingContext.recordCreatedId(ClientId(clientId), ServerId(serverId))) - })) + .foldLeft(invocation.processingContext)({ + case (processingContext, (clientId, response)) => + Id.validate(response.id.serialize) + .fold(_ => processingContext, + serverId => processingContext.recordCreatedId(ClientId(clientId), ServerId(serverId))) + })) } override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSetRequest] = asEmailSetRequest(invocation.arguments) @@ -212,27 +212,32 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, private def create(request: EmailSetRequest, mailboxSession: MailboxSession): SMono[CreationResults] = SFlux.fromIterable(request.create.getOrElse(Map())) - .concatMap { - case (clientId, json) => serializer.deserializeCreationRequest(json) - .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new IllegalArgumentException(e.toString))), - creationRequest => create(clientId, creationRequest, mailboxSession)) - }.collectSeq() - .map(CreationResults) + .concatMap { + case (clientId, json) => serializer.deserializeCreationRequest(json) + .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new IllegalArgumentException(e.toString))), + creationRequest => create(clientId, creationRequest, mailboxSession)) + }.collectSeq() + .map(CreationResults) private def create(clientId: EmailCreationId, request: EmailCreationRequest, mailboxSession: MailboxSession): SMono[CreationResult] = { if (request.mailboxIds.value.size != 1) { SMono.just(CreationFailure(clientId, new IllegalArgumentException("mailboxIds need to have size 1"))) } else { SMono.fromCallable[CreationResult](() => { - val mailboxId: MailboxId = request.mailboxIds.value.headOption.get - val appendResult = mailboxManager.getMailbox(mailboxId, mailboxSession) - .appendMessage(AppendCommand.builder() - .recent() - .withFlags(request.keywords.map(_.asFlags).getOrElse(new Flags())) - .withInternalDate(Date.from(request.receivedAt.getOrElse(UTCDate(ZonedDateTime.now())).asUTC.toInstant)) - .build(request.toMime4JMessage), - mailboxSession) - CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId)) + request.toMime4JMessage + .fold(e => CreationFailure(clientId, e), + message => { + val mailboxId: MailboxId = request.mailboxIds.value.headOption.get + val appendResult = mailboxManager.getMailbox(mailboxId, mailboxSession) + .appendMessage(AppendCommand.builder() + .recent() + .withFlags(request.keywords.map(_.asFlags).getOrElse(new Flags())) + .withInternalDate(Date.from(request.receivedAt.getOrElse(UTCDate(ZonedDateTime.now())).asUTC.toInstant)) + .build(message), + mailboxSession) + CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId)) + } + ) }) .subscribeOn(Schedulers.elastic()) .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e))) @@ -405,7 +410,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer, if (newFlags.equals(originalFlags)) { SMono.just[UpdateResult](UpdateSuccess(messageId)) } else { - SMono.fromCallable(() => + SMono.fromCallable(() => messageIdManager.setFlags(newFlags, FlagsUpdateMode.REPLACE, messageId, ImmutableList.copyOf(mailboxIds.value.asJavaCollection), session)) .subscribeOn(Schedulers.elastic()) .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId))) --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org