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/10/30 07:15:51 UTC
[james-project] 01/02: JAMES-3438 Email/set create htmlBody
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 <dl...@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-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org