JAMES-3379 Email/get specific parsed headers: asText
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 9a03780..c8783c8 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
@@ -5729,4 +5729,109 @@
| "header:Sender:asRaw": " andre@domain.tld"
|}""".stripMargin)
}
+
+ @Test
+ def emailGetShouldReturnSpecificHeadersAsText(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val alicePath = MailboxPath.inbox(ALICE)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(alicePath)
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setSender(ANDRE.asString())
+ .setFrom(ANDRE.asString())
+ .setSubject(" World domination\r\n" +
+ " and this is also part of the header\r\n")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, bobPath, AppendCommand.from(message))
+ .getMessageId
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties": ["header:Subject:asText"]
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].list[0]")
+ .isEqualTo(
+ s"""{
+ | "id": "${messageId.serialize}",
+ | "header:Subject:asText": "World domination and this is also part of the header"
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailGetShouldSupportDifferentSpecificHeadersTypeOnSameMessage(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val alicePath = MailboxPath.inbox(ALICE)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(alicePath)
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setSender(ANDRE.asString())
+ .setFrom(ANDRE.asString())
+ .setSubject(" World domination\r\n" +
+ " and this is also part of the header\r\n")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, bobPath, AppendCommand.from(message))
+ .getMessageId
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties": ["header:Subject:asText", "header:Subject:asRaw"]
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].list[0]")
+ .isEqualTo(
+ s"""{
+ | "id": "${messageId.serialize}",
+ | "header:Subject:asRaw": " =?US-ASCII?Q?___World_domination=0D=0A_and_thi?=\\r\\n =?US-ASCII?Q?s_is_also_part_of_the_header=0D=0A?=",
+ | "header:Subject:asText": "World domination and this is also part of the header"
+ |}""".stripMargin)
+ }
}
\ No newline at end of file
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 693a8fa..34ca387 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
@@ -20,7 +20,7 @@
package org.apache.james.jmap.json
import org.apache.james.jmap.api.model.Preview
-import org.apache.james.jmap.mail.{Address, BlobId, Charset, Disposition, EmailAddress, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, HasAttachment, HeaderMessageId, IsEncodingProblem, IsTruncated, Language, Location, MailboxIds, Name, PartId, RawHeaderValue, Subject, ThreadId, Type}
+import org.apache.james.jmap.mail.{Address, BlobId, Charset, Disposition, EmailAddress, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, HasAttachment, HeaderMessageId, IsEncodingProblem, IsTruncated, Language, Location, MailboxIds, Name, PartId, RawHeaderValue, Subject, TextHeaderValue, ThreadId, Type}
import org.apache.james.jmap.model._
import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
import play.api.libs.functional.syntax._
@@ -55,8 +55,10 @@
private implicit val hasAttachmentWrites: Writes[HasAttachment] = Json.valueWrites[HasAttachment]
private implicit val headerNameWrites: Writes[EmailHeaderName] = Json.valueWrites[EmailHeaderName]
private implicit val rawHeaderWrites: Writes[RawHeaderValue] = Json.valueWrites[RawHeaderValue]
+ private implicit val textHeaderWrites: Writes[TextHeaderValue] = Json.valueWrites[TextHeaderValue]
private implicit val emailHeaderWrites: Writes[EmailHeaderValue] = {
case headerValue: RawHeaderValue => Json.toJson[RawHeaderValue](headerValue)
+ case headerValue: TextHeaderValue => Json.toJson[TextHeaderValue](headerValue)
}
private implicit val headersWrites: Writes[EmailHeader] = Json.writes[EmailHeader]
private implicit val bodyValueWrites: Writes[EmailBodyValue] = Json.writes[EmailBodyValue]
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 a6872c7..3261372 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
@@ -39,7 +39,7 @@
import org.apache.james.jmap.model.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
import org.apache.james.jmap.model.{Keywords, Properties, UTCDate}
import org.apache.james.mailbox.model.FetchGroup.{FULL_CONTENT, HEADERS, MINIMAL}
-import org.apache.james.mailbox.model.{FetchGroup, MailboxId, MessageId, MessageResult}
+import org.apache.james.mailbox.model.{FetchGroup, MessageResult}
import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
import org.apache.james.mime4j.codec.DecodeMonitor
import org.apache.james.mime4j.dom.field.{AddressListField, DateTimeField, MailboxField, MailboxListField}
@@ -47,6 +47,8 @@
import org.apache.james.mime4j.message.DefaultMessageBuilder
import org.apache.james.mime4j.stream.{Field, MimeConfig}
import org.apache.james.mime4j.util.MimeUtil
+import org.apache.james.mailbox.model.{MailboxId, MessageId}
+import org.apache.james.mime4j.stream.Field
import org.slf4j.{Logger, LoggerFactory}
import reactor.core.scala.publisher.{SFlux, SMono}
import reactor.core.scheduler.Schedulers
@@ -159,23 +161,29 @@
object ParseOptions {
- val allowedParseOption: Properties = Properties("asRaw", "asText", "asAddresses", "asGroupedAddresses", "asMessageIds", "asDate", "asURLs")
+ val allowedParseOption: Set[String] = Set("asRaw", "asText", "asAddresses", "asGroupedAddresses", "asMessageIds", "asDate", "asURLs")
- def validate(value: String): Boolean = {
- from(value).isDefined
- }
+ def validate(parseOption: String): Boolean = from(parseOption).isDefined
def from(value: String): Option[ParseOption] = {
- allowedParseOption.value
- .find(_.value.equals(value))
- .map({parseOption => parseOption.value match {
+ allowedParseOption
+ .find(_.equals(value))
+ .map({
case "asRaw" => AsRaw
- }})
+ case "asText" => AsText
+ })
}
}
-sealed trait ParseOption
-case object AsRaw extends ParseOption
+sealed trait ParseOption {
+ def extractHeaderValue(field: Field): Option[EmailHeaderValue]
+}
+case object AsRaw extends ParseOption {
+ override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(RawHeaderValue.from(field))
+}
+case object AsText extends ParseOption {
+ override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(TextHeaderValue.from(field))
+}
case class HeaderMessageId(value: String) extends AnyVal
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 0eef2da..cd1dcea 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
@@ -49,9 +49,10 @@
case property if property.startsWith(SPECIFIC_HEADER_PREFIX) =>
val headerName = property.substring(SPECIFIC_HEADER_PREFIX.length)
if (headerName.contains(":")) {
- val parseOption = headerName.substring(headerName.indexOf(":") + 1)
+ val indexOfFirstColon = headerName.indexOf(":")
+ val parseOption = headerName.substring(indexOfFirstColon + 1)
if (ParseOptions.validate(parseOption)) {
- scala.Right(SpecificHeaderRequest(property, headerName.substring(0, headerName.indexOf(":")), ParseOptions.from(parseOption)))
+ scala.Right(SpecificHeaderRequest(property, headerName.substring(0, indexOfFirstColon), ParseOptions.from(parseOption)))
} else {
Left(property)
}
@@ -84,9 +85,6 @@
def retrieveHeader(message: Message): (String, Option[EmailHeaderValue]) = {
val field: Option[Field] = Option(message.getHeader.getField(property))
- parseOption.getOrElse(AsRaw) match {
- case AsRaw => (headerName, field.map(RawHeaderValue.from))
- case _ => (headerName, field.map(RawHeaderValue.from))
- }
+ (headerName, field.flatMap(parseOption.getOrElse(AsRaw).extractHeaderValue(_)))
}
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala
index 4eb0575..282813b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala
@@ -21,7 +21,9 @@
import java.nio.charset.StandardCharsets.US_ASCII
+import org.apache.james.mime4j.codec.{DecodeMonitor, DecoderUtil}
import org.apache.james.mime4j.stream.Field
+import org.apache.james.mime4j.util.MimeUtil
object EmailHeader {
def apply(field: Field): EmailHeader = EmailHeader(EmailHeaderName(field.getName), RawHeaderValue.from(field))
@@ -31,9 +33,14 @@
def from(field: Field): RawHeaderValue = RawHeaderValue(new String(field.getRaw.toByteArray, US_ASCII).substring(field.getName.length + 1))
}
+object TextHeaderValue extends EmailHeaderValue {
+ def from(field: Field): TextHeaderValue = TextHeaderValue(MimeUtil.unfold(DecoderUtil.decodeEncodedWords(field.getBody, DecodeMonitor.SILENT)).stripLeading())
+}
+
case class EmailHeaderName(value: String) extends AnyVal
sealed trait EmailHeaderValue
case class RawHeaderValue(value: String) extends EmailHeaderValue
+case class TextHeaderValue(value: String) extends EmailHeaderValue
case class EmailHeader(name: EmailHeaderName, value: EmailHeaderValue)