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 2023/02/13 01:43:12 UTC
[james-project] branch master updated: JAMES-3419 JMAP EmailBodyPart individual headers (#1433)
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
The following commit(s) were added to refs/heads/master by this push:
new 3192f76b48 JAMES-3419 JMAP EmailBodyPart individual headers (#1433)
3192f76b48 is described below
commit 3192f76b486f37be6f417103dfe7ab199dc13e4d
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Mon Feb 13 08:43:06 2023 +0700
JAMES-3419 JMAP EmailBodyPart individual headers (#1433)
---
.../rfc8621/contract/EmailGetMethodContract.scala | 57 ++++++++++++++++++++++
.../james/jmap/json/EmailGetSerializer.scala | 9 ++--
.../scala/org/apache/james/jmap/mail/Email.scala | 16 +++---
.../org/apache/james/jmap/mail/EmailBodyPart.scala | 36 ++++++++------
.../org/apache/james/jmap/mail/EmailGet.scala | 14 +++---
.../apache/james/jmap/method/EmailGetMethod.scala | 14 ++++--
.../apache/james/jmap/routes/DownloadRoutes.scala | 10 ++--
7 files changed, 115 insertions(+), 41 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/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index bce366b985..e3d77f3573 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -3615,6 +3615,63 @@ trait EmailGetMethodContract {
|}""".stripMargin)
}
+ @Test
+ def bodyStructureShouldSupportSpecificHeaders(server: GuiceJamesServer): Unit = {
+ val path = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, path, AppendCommand.from(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties":["bodyStructure"],
+ | "bodyProperties":["partId", "blobId", "size", "name", "type", "charset", "disposition", "cid", "header:Subject:asText"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].state")
+ .inPath("methodResponses[0][1].list[0]")
+ .isEqualTo(
+ s"""{
+ | "id": "${messageId.serialize}",
+ | "bodyStructure": {
+ | "header:Subject:asText": "test",
+ | "charset": "UTF-8",
+ | "size": 8,
+ | "partId": "1",
+ | "blobId": "1_1",
+ | "type": "text/plain"
+ | }
+ |}""".stripMargin)
+ }
+
@Test
def bodyStructureForSimpleMultipart(server: GuiceJamesServer): Unit = {
val path = MailboxPath.inbox(BOB)
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 a07953d846..bb8f29ac81 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
@@ -44,7 +44,8 @@ object EmailBodyPartToSerialize {
language = part.language,
location = part.location,
name = part.name,
- subParts = part.subParts.map(list => list.map(EmailBodyPartToSerialize.from)))
+ subParts = part.subParts.map(list => list.map(EmailBodyPartToSerialize.from)),
+ specificHeaders = part.specificHeaders)
}
case class EmailBodyPartToSerialize(partId: PartId,
@@ -58,7 +59,8 @@ case class EmailBodyPartToSerialize(partId: PartId,
cid: Option[Cid],
language: Option[Languages],
location: Option[Location],
- subParts: Option[List[EmailBodyPartToSerialize]])
+ subParts: Option[List[EmailBodyPartToSerialize]],
+ specificHeaders: Map[String, Option[EmailHeaderValue]])
object EmailGetSerializer {
private implicit val mailboxIdWrites: Writes[MailboxId] = mailboxId => JsString(mailboxId.serialize)
@@ -147,7 +149,8 @@ object EmailGetSerializer {
(__ \ "cid").writeNullable[Cid] and
(__ \ "language").writeNullable[Languages] and
(__ \ "location").writeNullable[Location] and
- (__ \ "subParts").lazyWriteNullable(implicitly[Writes[List[EmailBodyPartToSerialize]]])
+ (__ \ "subParts").lazyWriteNullable(implicitly[Writes[List[EmailBodyPartToSerialize]]]) and
+ JsPath.write[Map[String, Option[EmailHeaderValue]]]
)(unlift(EmailBodyPartToSerialize.unapply))
private implicit val bodyPartWrites: Writes[EmailBodyPart] = part => bodyPartWritesToSerializeWrites.writes(EmailBodyPartToSerialize.from(part))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index bc6c039658..decafec662 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -272,10 +272,10 @@ object EmailHeaders {
sentAt = extractDate(mime4JMessage, "Date").map(date => UTCDate.from(date, zoneId)))
}
- def extractSpecificHeaders(properties: Option[Properties])(zoneId: ZoneId, mime4JMessage: Message) = {
+ def extractSpecificHeaders(properties: Option[Properties])(zoneId: ZoneId, header: org.apache.james.mime4j.dom.Header) = {
properties.getOrElse(Properties.empty()).value
.flatMap(property => SpecificHeaderRequest.from(property).toOption)
- .map(_.retrieveHeader(zoneId, mime4JMessage))
+ .map(_.retrieveHeader(zoneId, header))
.toMap
}
@@ -490,7 +490,7 @@ private class EmailHeaderViewFactory @Inject()(zoneIdProvider: ZoneIdProvider) e
size = sanitizeSize(firstMessage.getSize),
keywords = keywords),
header = EmailHeaders.from(zoneIdProvider.get())(mime4JMessage),
- specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage))
+ specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage.getHeader))
}
}
}
@@ -509,7 +509,7 @@ private class EmailFullViewFactory @Inject()(zoneIdProvider: ZoneIdProvider, pre
.map(Success(_))
.getOrElse(Failure(new IllegalArgumentException("No message supplied")))
mime4JMessage <- Email.parseAsMime4JMessage(firstMessage)
- bodyStructure <- EmailBodyPart.of(messageId, mime4JMessage)
+ bodyStructure <- EmailBodyPart.of(request.bodyProperties, zoneIdProvider.get(), messageId, mime4JMessage)
bodyValues <- extractBodyValues(htmlTextExtractor)(bodyStructure, request)
blobId <- BlobId.of(messageId)
preview <- Try(previewFactory.fromMime4JMessage(mime4JMessage))
@@ -534,7 +534,7 @@ private class EmailFullViewFactory @Inject()(zoneIdProvider: ZoneIdProvider, pre
htmlBody = bodyStructure.htmlBody,
attachments = bodyStructure.attachments,
bodyValues = bodyValues),
- specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage))
+ specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage.getHeader))
}
}
@@ -693,7 +693,7 @@ private class EmailFastViewReader @Inject()(messageIdManager: MessageIdManager,
hasAttachment = HasAttachment(fastView.hasAttachment),
preview = fastView.getPreview),
header = EmailHeaders.from(zoneIdProvider.get())(mime4JMessage),
- specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage))
+ specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage.getHeader))
}
}
}
@@ -784,8 +784,8 @@ private class EmailFastViewWithAttachmentsMetadataReader @Inject()(messageIdMana
hasAttachment = HasAttachment(fastView.hasAttachment),
preview = fastView.getPreview),
header = EmailHeaders.from(zoneIdProvider.get())(mime4JMessage),
- specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage),
- attachments = AttachmentsMetadata(firstMessage.getLoadedAttachments.asScala.toList.map(EmailBodyPart.fromAttachment(_, mime4JMessage))))
+ specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage.getHeader),
+ attachments = AttachmentsMetadata(firstMessage.getLoadedAttachments.asScala.toList.map(EmailBodyPart.fromAttachment(request.bodyProperties, zoneIdProvider.get(), _, mime4JMessage))))
}
}
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index f3af9e778f..142c244bb8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -20,6 +20,7 @@
package org.apache.james.jmap.mail
import java.io.OutputStream
+import java.time.ZoneId
import cats.implicits._
import com.google.common.io.CountingOutputStream
@@ -73,16 +74,16 @@ object EmailBodyPart {
val defaultProperties: Properties = Properties("partId", "blobId", "size", "name", "type", "charset", "disposition", "cid", "language", "location")
val allowedProperties: Properties = defaultProperties ++ Properties("subParts", "headers")
- def of(messageId: MessageId, message: MessageResult): Try[EmailBodyPart] = {
+ def of(properties: Option[Properties], zoneId: ZoneId, messageId: MessageId, message: MessageResult): Try[EmailBodyPart] = {
val defaultMessageBuilder = new DefaultMessageBuilder
defaultMessageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE)
defaultMessageBuilder.setDecodeMonitor(DecodeMonitor.SILENT)
val mime4JMessage = Try(defaultMessageBuilder.parseMessage(message.getFullContent.getInputStream))
- mime4JMessage.flatMap(of(messageId, _))
+ mime4JMessage.flatMap(of(properties, zoneId, messageId, _))
}
- def fromAttachment(attachment: MessageAttachmentMetadata, entity: Message): EmailBodyPart = {
+ def fromAttachment(properties: Option[Properties], zoneId: ZoneId, attachment: MessageAttachmentMetadata, entity: Message): EmailBodyPart = {
def parseDisposition(attachment: MessageAttachmentMetadata): Option[Disposition] =
if (attachment.isInline) {
Option(Disposition.INLINE)
@@ -105,45 +106,48 @@ object EmailBodyPart {
language = Option.empty,
location = Option.empty,
subParts = Option.empty,
- entity = entity)
+ entity = entity,
+ specificHeaders = EmailHeaders.extractSpecificHeaders(properties)(zoneId, entity.getHeader))
}
- def of(messageId: MessageId, message: Message): Try[EmailBodyPart] =
- of(messageId, PartId(1), message).map(_._1)
+ def of(properties: Option[Properties], zoneId: ZoneId, messageId: MessageId, message: Message): Try[EmailBodyPart] =
+ of(properties, zoneId, messageId, PartId(1), message).map(_._1)
- private def of(messageId: MessageId, partId: PartId, entity: Entity): Try[(EmailBodyPart, PartId)] =
+ private def of(properties: Option[Properties], zoneId: ZoneId, messageId: MessageId, partId: PartId, entity: Entity): Try[(EmailBodyPart, PartId)] =
entity.getBody match {
case multipart: Multipart =>
val scanResults: Try[List[(Option[EmailBodyPart], PartId)]] = multipart.getBodyParts
.asScala.toList
- .scanLeft[Try[(Option[EmailBodyPart], PartId)]](Success((None, partId)))(traverse(messageId))
+ .scanLeft[Try[(Option[EmailBodyPart], PartId)]](Success((None, partId)))(traverse(properties, zoneId, messageId))
.sequence
val highestPartIdValidation: Try[PartId] = scanResults.map(list => list.map(_._2).reverse.headOption.getOrElse(partId))
val childrenValidation: Try[List[EmailBodyPart]] = scanResults.map(list => list.flatMap(_._1))
zip(childrenValidation, highestPartIdValidation)
.flatMap {
- case (children, highestPartId) => of(None, partId, entity, Some(children))
+ case (children, highestPartId) => of(properties, zoneId, None, partId, entity, Some(children))
.map(part => (part, highestPartId))
}
case _ => BlobId.of(messageId, partId)
- .flatMap(blobId => of(Some(blobId), partId, entity, None))
+ .flatMap(blobId => of(properties, zoneId, Some(blobId), partId, entity, None))
.map(part => (part, partId))
}
- private def traverse(messageId: MessageId)(acc: Try[(Option[EmailBodyPart], PartId)], entity: Entity): Try[(Option[EmailBodyPart], PartId)] = {
+ private def traverse(properties: Option[Properties], zoneId: ZoneId, messageId: MessageId)(acc: Try[(Option[EmailBodyPart], PartId)], entity: Entity): Try[(Option[EmailBodyPart], PartId)] = {
acc.flatMap {
case (_, previousPartId) =>
val partId = previousPartId.next
- of(messageId, partId, entity)
+ of(properties, zoneId, messageId, partId, entity)
.map({
case (part, partId) => (Some(part), partId)
})
}
}
- private def of(blobId: Option[BlobId],
+ private def of(properties: Option[Properties],
+ zoneId: ZoneId,
+ blobId: Option[BlobId],
partId: PartId,
entity: Entity,
subParts: Option[List[EmailBodyPart]]): Try[EmailBodyPart] =
@@ -162,7 +166,8 @@ object EmailBodyPart {
location = headerValue(entity, "Content-Location")
.map(Location),
subParts = subParts,
- entity = entity))
+ entity = entity,
+ specificHeaders = EmailHeaders.extractSpecificHeaders(properties)(zoneId, entity.getHeader)))
private def headerValue(entity: Entity, headerName: String): Option[String] = entity.getHeader
.getFields(headerName)
@@ -259,7 +264,8 @@ case class EmailBodyPart(partId: PartId,
language: Option[Languages],
location: Option[Location],
subParts: Option[List[EmailBodyPart]],
- entity: Entity) {
+ entity: Entity,
+ specificHeaders: Map[String, Option[EmailHeaderValue]]) {
def bodyContent: Try[Option[EmailBodyValue]] = entity.getBody match {
case textBody: Mime4JTextBody =>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailGet.scala
index 60d0dc9cab..a479e1f8f8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailGet.scala
@@ -94,15 +94,15 @@ case class EmailGetResponse(accountId: AccountId,
notFound: EmailNotFound)
case class SpecificHeaderRequest(property: NonEmptyString, headerName: String, parseOption: Option[ParseOption], isAll: Boolean = false) {
- def retrieveHeader(zoneId: ZoneId, message: Message): (String, Option[EmailHeaderValue]) =
+ def retrieveHeader(zoneId: ZoneId, header: org.apache.james.mime4j.dom.Header): (String, Option[EmailHeaderValue]) =
if (isAll) {
- extractAllHeaders(zoneId, message)
+ extractAllHeaders(zoneId, header)
} else {
- extractLastHeader(zoneId, message)
+ extractLastHeader(zoneId, header)
}
- private def extractAllHeaders(zoneId: ZoneId, message: Message) = {
- val fields: List[Field] = Option(message.getHeader.getFields(headerName))
+ private def extractAllHeaders(zoneId: ZoneId, header: org.apache.james.mime4j.dom.Header) = {
+ val fields: List[Field] = Option(header.getFields(headerName))
.map(_.asScala.toList)
.getOrElse(List())
@@ -110,8 +110,8 @@ case class SpecificHeaderRequest(property: NonEmptyString, headerName: String, p
(property.value, Some(AllHeaderValues(fields.map(toHeader(zoneId, option)))))
}
- private def extractLastHeader(zoneId: ZoneId, message: Message) = {
- val field: Option[Field] = Option(message.getHeader.getFields(headerName))
+ private def extractLastHeader(zoneId: ZoneId, header: org.apache.james.mime4j.dom.Header) = {
+ val field: Option[Field] = Option(header.getFields(headerName))
.map(_.asScala)
.flatMap(fields => fields.reverse.headOption)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
index a9cc31b665..568b661e9f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
@@ -130,11 +130,17 @@ class EmailGetMethod @Inject() (readerFactory: EmailViewReaderFactory,
request.bodyProperties match {
case None => Right(EmailBodyPart.defaultProperties)
case Some(properties) =>
- val invalidProperties = properties -- EmailBodyPart.allowedProperties
- if (invalidProperties.isEmpty()) {
- Right(properties)
+ val invalidProperties: Set[NonEmptyString] = properties.value
+ .flatMap(property => SpecificHeaderRequest.from(property)
+ .fold(
+ invalidProperty => Some(invalidProperty),
+ _ => None
+ )) -- EmailBodyPart.allowedProperties.value
+
+ if (invalidProperties.nonEmpty) {
+ Left(new IllegalArgumentException(s"The following bodyProperties [${invalidProperties.map(p => p.value).mkString(", ")}] do not exist."))
} else {
- Left(new IllegalArgumentException(s"The following bodyProperties [${invalidProperties.format()}] do not exist."))
+ Right(properties)
}
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
index 0b54980f25..63317105da 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
@@ -37,7 +37,7 @@ import org.apache.james.jmap.http.Authenticator
import org.apache.james.jmap.http.rfc8621.InjectionKeys
import org.apache.james.jmap.json.ResponseSerializer
import org.apache.james.jmap.mail.{BlobId, EmailBodyPart, PartId}
-import org.apache.james.jmap.method.AccountNotFoundException
+import org.apache.james.jmap.method.{AccountNotFoundException, ZoneIdProvider}
import org.apache.james.jmap.routes.DownloadRoutes.{BUFFER_SIZE, LOGGER}
import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
import org.apache.james.mailbox.model.ContentType.{MediaType, MimeType, SubType}
@@ -54,12 +54,13 @@ import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
import reactor.core.scheduler.Schedulers
import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
-
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.util.stream
import java.util.stream.Stream
+
import javax.inject.{Inject, Named}
+
import scala.compat.java8.FunctionConverters._
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try}
@@ -186,7 +187,8 @@ class AttachmentBlobResolver @Inject()(val attachmentManager: AttachmentManager)
}
class MessagePartBlobResolver @Inject()(val messageIdFactory: MessageId.Factory,
- val messageIdManager: MessageIdManager) extends BlobResolver {
+ val messageIdManager: MessageIdManager,
+ val zoneIdSupplier: ZoneIdProvider) extends BlobResolver {
private def asMessageAndPartId(blobId: BlobId): Try[(MessageId, PartId)] = {
blobId.value.value.split('_').toList match {
case List(messageIdString, partIdString) => for {
@@ -206,7 +208,7 @@ class MessagePartBlobResolver @Inject()(val messageIdFactory: MessageId.Factory,
Applicable(SMono.fromPublisher(
messageIdManager.getMessagesReactive(List(messageId).asJava, FetchGroup.FULL_CONTENT, mailboxSession))
.handle[EmailBodyPart] {
- case (message, sink) => EmailBodyPart.of(messageId, message)
+ case (message, sink) => EmailBodyPart.of(None, zoneIdSupplier.get(), messageId, message)
.fold(sink.error, sink.next)
}
.handle[EmailBodyPart] {
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org