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 10f8fd38822490212308f537b1439a91c32a66d4 Author: LanKhuat <[email protected]> AuthorDate: Tue Sep 15 17:47:55 2020 +0700 JAMES-3379 Email/get specific parsed headers: asRaw --- .../rfc8621/contract/EmailGetMethodContract.scala | 208 ++++++++++++++++----- .../james/jmap/json/EmailGetSerializer.scala | 8 +- .../scala/org/apache/james/jmap/mail/Email.scala | 21 ++- .../org/apache/james/jmap/mail/EmailGet.scala | 23 ++- .../org/apache/james/jmap/mail/EmailHeader.scala | 10 +- 5 files changed, 213 insertions(+), 57 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 a2ed6b4..9a03780 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 @@ -38,8 +38,7 @@ import org.apache.james.jmap.rfc8621.contract.EmailGetMethodContract.createTestM import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ALICE, ANDRE, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder} import org.apache.james.mailbox.MessageManager.AppendCommand import org.apache.james.mailbox.model.MailboxACL.Right -import org.apache.james.mailbox.model.MailboxId -import org.apache.james.mailbox.model.{MailboxACL, MailboxPath, MessageId} +import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageId} import org.apache.james.mime4j.dom.Message import org.apache.james.mime4j.message.MultipartBuilder import org.apache.james.mime4j.stream.RawField @@ -3387,15 +3386,15 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "MIME-Version", - | "value": "1.0" + | "value": " 1.0" | }, | { | "name": "Subject", - | "value": "test" + | "value": " test" | }, | { | "name": "Content-Type", - | "value": "text/plain; charset=UTF-8" + | "value": " text/plain; charset=UTF-8" | } | ], | "size": 8, @@ -3450,7 +3449,7 @@ trait EmailGetMethodContract { .body .asString - val contentType = "multipart/mixed; boundary=\\\"------------64D8D789FC30153D6ED18258\\\"" + val contentType = " multipart/mixed;\\r\\n boundary=\\\"------------64D8D789FC30153D6ED18258\\\"" assertThatJson(response).isEqualTo( s"""{ | "sessionState": "75128aab4b1b", @@ -3468,35 +3467,35 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Return-Path", - | "value": "<[email protected]>" + | "value": " <[email protected]>" | }, | { | "name": "To", - | "value": "[email protected]" + | "value": " [email protected]" | }, | { | "name": "From", - | "value": "Lina <[email protected]>" + | "value": " Lina <[email protected]>" | }, | { | "name": "Subject", - | "value": "MultiAttachment" + | "value": " MultiAttachment" | }, | { | "name": "Message-ID", - | "value": "<[email protected]>" + | "value": " <[email protected]>" | }, | { | "name": "Date", - | "value": "Mon, 27 Feb 2017 11:24:48 +0700" + | "value": " Mon, 27 Feb 2017 11:24:48 +0700" | }, | { | "name": "User-Agent", - | "value": "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Thunderbird/45.2.0" + | "value": " Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101\\r\\n Thunderbird/45.2.0" | }, | { | "name": "MIME-Version", - | "value": "1.0" + | "value": " 1.0" | }, | { | "name": "Content-Type", @@ -3513,11 +3512,11 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "text/plain; charset=utf-8; format=flowed" + | "value": " text/plain; charset=utf-8; format=flowed" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "7bit" + | "value": " 7bit" | } | ], | "size": 8, @@ -3530,15 +3529,15 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "text/plain; charset=UTF-8; name=\\\"text1\\\"" + | "value": " text/plain; charset=UTF-8;\\r\\n name=\\\"text1\\\"" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "base64" + | "value": " base64" | }, | { | "name": "Content-Disposition", - | "value": "attachment; filename=\\\"text1\\\"" + | "value": " attachment;\\r\\n filename=\\\"text1\\\"" | } | ], | "size": 271, @@ -3553,15 +3552,15 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "application/vnd.ms-publisher; name=\\\"text2\\\"" + | "value": " application/vnd.ms-publisher;\\r\\n name=\\\"text2\\\"" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "base64" + | "value": " base64" | }, | { | "name": "Content-Disposition", - | "value": "attachment; filename=\\\"text2\\\"" + | "value": " attachment;\\r\\n filename=\\\"text2\\\"" | } | ], | "size": 398, @@ -3576,15 +3575,15 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "text/plain; charset=UTF-8; name=\\\"text3\\\"" + | "value": " text/plain; charset=UTF-8;\\r\\n name=\\\"text3\\\"" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "base64" + | "value": " base64" | }, | { | "name": "Content-Disposition", - | "value": "attachment; filename=\\\"text3\\\"" + | "value": " attachment;\\r\\n filename=\\\"text3\\\"" | } | ], | "size": 412, @@ -3658,35 +3657,35 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Date", - | "value": "Tue, 03 Jan 2017 16:05:01 +0100" + | "value": " Tue, 03 Jan 2017 16:05:01 +0100" | }, | { | "name": "From", - | "value": "sender <[email protected]>" + | "value": " sender <[email protected]>" | }, | { | "name": "MIME-Version", - | "value": "1.0" + | "value": " 1.0" | }, | { | "name": "To", - | "value": "David DOLCIMASCOLO <[email protected]>" + | "value": " David DOLCIMASCOLO <[email protected]>" | }, | { | "name": "Subject", - | "value": "Re: [Internet] Rendez-vous" + | "value": " Re: [Internet] Rendez-vous" | }, | { | "name": "References", - | "value": "<[email protected]>" + | "value": " <[email protected]>" | }, | { | "name": "In-Reply-To", - | "value": "<[email protected]>" + | "value": " <[email protected]>" | }, | { | "name": "X-Gie-Attachments", - | "value": "none" + | "value": " none" | }, | { | "name": "Cc", @@ -3694,7 +3693,7 @@ trait EmailGetMethodContract { | }, | { | "name": "Content-type", - | "value": "multipart/mixed; boundary=\\"----------=_1483455916-7086-3\\"" + | "value": " multipart/mixed; boundary=\\"----------=_1483455916-7086-3\\"" | } | ], | "size": 891, @@ -3706,7 +3705,7 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "multipart/alternative; boundary=\\\"------------060506070600060108040700\\\"" + | "value": " multipart/alternative; boundary=\\\"------------060506070600060108040700\\\"" | } | ], | "size": 398, @@ -3719,11 +3718,11 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "text/plain; charset=ISO-8859-1; format=flowed" + | "value": " text/plain; charset=ISO-8859-1; format=flowed" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "8bit" + | "value": " 8bit" | } | ], | "size": 20, @@ -3736,11 +3735,11 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-Type", - | "value": "text/html; charset=ISO-8859-1" + | "value": " text/html; charset=ISO-8859-1" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "7bit" + | "value": " 7bit" | } | ], | "size": 30, @@ -3755,19 +3754,19 @@ trait EmailGetMethodContract { | "headers": [ | { | "name": "Content-ID", - | "value": "<[email protected]>" + | "value": " <[email protected]>" | }, | { | "name": "Content-Type", - | "value": "text/plain; charset=\\\"iso-8859-1\\\"; name=\\\"avertissement.txt\\\"" + | "value": " text/plain; charset=\\\"iso-8859-1\\\"; name=\\\"avertissement.txt\\\"" | }, | { | "name": "Content-Disposition", - | "value": "inline; filename=\\\"avertissement.txt\\\"" + | "value": " inline; filename=\\\"avertissement.txt\\\"" | }, | { | "name": "Content-Transfer-Encoding", - | "value": "binary" + | "value": " binary" | } | ], | "size": 19, @@ -5607,4 +5606,127 @@ trait EmailGetMethodContract { | } """.stripMargin) } + + @Test + def emailGetShouldReturnSpecificHeadersAsRaw(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") + .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: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?=" + |}""".stripMargin) + } + + @Test + def asRawShouldSupportSeveralHeaders(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()) + .addField(new RawField("To", + "\"user1\" [email protected]")) + .addField(new RawField("Cc", + "\"user2\" [email protected]")) + .addField(new RawField("Bcc", + "\"user3\" [email protected]")) + .addField(new RawField("ReplyTo", + "\"user1\" [email protected]")) + .setSubject("World domination \r\n" + + " and this is also part of the header") + .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:asRaw", "header:Sender:asRaw", "header:From:asRaw", "header:To:asRaw", "header:Cc:asRaw", "header:Bcc:asRaw", + | "header:ReplyTo:asRaw", "header:InReplyTo:asRaw", "header:References:asRaw", "header:MessageId:asRaw", "header:sentAt: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": "1", + | "header:Bcc:asRaw": " \\"user3\\" [email protected]", + | "header:MessageId:asRaw": null, + | "header:ReplyTo:asRaw": " \\"user1\\" [email protected]", + | "header:From:asRaw": " [email protected]", + | "header:Cc:asRaw": " \\"user2\\" [email protected]", + | "header:Subject:asRaw": " =?US-ASCII?Q?World_domination_=0D=0A_and_thi?=\\r\\n =?US-ASCII?Q?s_is_also_part_of_the_header?=", + | "header:InReplyTo:asRaw": null, + | "header:sentAt:asRaw": null, + | "header:To:asRaw": " \\"user1\\" [email protected]", + | "header:References:asRaw": null, + | "header:Sender:asRaw": " [email protected]" + |}""".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 6a654a0..693a8fa 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,8 +20,7 @@ package org.apache.james.jmap.json import org.apache.james.jmap.api.model.Preview -import org.apache.james.jmap.mail.Email.Size -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, IsT [...] +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, IsT [...] import org.apache.james.jmap.model._ import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId} import play.api.libs.functional.syntax._ @@ -55,7 +54,10 @@ object EmailGetSerializer { private implicit val previewWrites: Writes[Preview] = preview => JsString(preview.getValue) private implicit val hasAttachmentWrites: Writes[HasAttachment] = Json.valueWrites[HasAttachment] private implicit val headerNameWrites: Writes[EmailHeaderName] = Json.valueWrites[EmailHeaderName] - private implicit val headerValueWrites: Writes[EmailHeaderValue] = Json.valueWrites[EmailHeaderValue] + private implicit val rawHeaderWrites: Writes[RawHeaderValue] = Json.valueWrites[RawHeaderValue] + private implicit val emailHeaderWrites: Writes[EmailHeaderValue] = { + case headerValue: RawHeaderValue => Json.toJson[RawHeaderValue](headerValue) + } private implicit val headersWrites: Writes[EmailHeader] = Json.writes[EmailHeader] private implicit val bodyValueWrites: Writes[EmailBodyValue] = Json.writes[EmailBodyValue] private implicit val emailIdsReads: Reads[EmailIds] = Json.valueReads[EmailIds] 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 65f6e80..a6872c7 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 @@ -158,6 +158,25 @@ object HeaderMessageId { } +object ParseOptions { + val allowedParseOption: Properties = Properties("asRaw", "asText", "asAddresses", "asGroupedAddresses", "asMessageIds", "asDate", "asURLs") + + def validate(value: String): Boolean = { + from(value).isDefined + } + + def from(value: String): Option[ParseOption] = { + allowedParseOption.value + .find(_.value.equals(value)) + .map({parseOption => parseOption.value match { + case "asRaw" => AsRaw + }}) + } +} + +sealed trait ParseOption +case object AsRaw extends ParseOption + case class HeaderMessageId(value: String) extends AnyVal case class Subject(value: String) extends AnyVal @@ -207,7 +226,7 @@ object EmailHeaders { .asScala .map(header => EmailHeader( EmailHeaderName(header.getName), - EmailHeaderValue(new String(header.getRaw.toByteArray, US_ASCII) + RawHeaderValue(new String(header.getRaw.toByteArray, US_ASCII) .substring(header.getName.length + 1)))) .toList 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 847c7b8..0eef2da 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 @@ -29,6 +29,7 @@ import org.apache.james.jmap.mail.EmailHeaders.SPECIFIC_HEADER_PREFIX import org.apache.james.jmap.model.State.State import org.apache.james.jmap.model.{AccountId, Properties} import org.apache.james.mime4j.dom.Message +import org.apache.james.mime4j.stream.Field case class EmailIds(value: List[UnparsedEmailId]) @@ -48,9 +49,14 @@ object SpecificHeaderRequest { case property if property.startsWith(SPECIFIC_HEADER_PREFIX) => val headerName = property.substring(SPECIFIC_HEADER_PREFIX.length) if (headerName.contains(":")) { - Left(property) + val parseOption = headerName.substring(headerName.indexOf(":") + 1) + if (ParseOptions.validate(parseOption)) { + scala.Right(SpecificHeaderRequest(property, headerName.substring(0, headerName.indexOf(":")), ParseOptions.from(parseOption))) + } else { + Left(property) + } } else { - scala.Right(SpecificHeaderRequest(property, headerName)) + scala.Right(SpecificHeaderRequest(property, headerName, None)) } case _ => Left(property) } @@ -74,8 +80,13 @@ case class EmailGetResponse(accountId: AccountId, list: List[EmailView], notFound: EmailNotFound) -case class SpecificHeaderRequest(headerName: NonEmptyString, property: String) { - def retrieveHeader(message: Message): (String, Option[EmailHeaderValue]) = (headerName, - Option(message.getHeader.getField(property)) - .map(field => EmailHeaderValue.from(field))) +case class SpecificHeaderRequest(headerName: NonEmptyString, property: String, parseOption: Option[ParseOption]) { + 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)) + } + } } 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 dbb4623..4eb0575 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 @@ -24,14 +24,16 @@ import java.nio.charset.StandardCharsets.US_ASCII import org.apache.james.mime4j.stream.Field object EmailHeader { - def apply(field: Field): EmailHeader = EmailHeader(EmailHeaderName(field.getName), EmailHeaderValue(field.getBody)) + def apply(field: Field): EmailHeader = EmailHeader(EmailHeaderName(field.getName), RawHeaderValue.from(field)) } -object EmailHeaderValue { - def from(field: Field): EmailHeaderValue = EmailHeaderValue(new String(field.getRaw.toByteArray, US_ASCII).substring(field.getName.length + 1)) +object RawHeaderValue extends EmailHeaderValue { + def from(field: Field): RawHeaderValue = RawHeaderValue(new String(field.getRaw.toByteArray, US_ASCII).substring(field.getName.length + 1)) } case class EmailHeaderName(value: String) extends AnyVal -case class EmailHeaderValue(value: String) extends AnyVal + +sealed trait EmailHeaderValue +case class RawHeaderValue(value: String) extends EmailHeaderValue case class EmailHeader(name: EmailHeaderName, value: EmailHeaderValue) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
