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 d5717e42cb12fd249eeaf5743b67fc5e3c5f798a Author: LanKhuat <[email protected]> AuthorDate: Tue Sep 22 15:30:31 2020 +0700 JAMES-3379 Email/get specific parsed headers: asMessageIds --- .../rfc8621/contract/EmailGetMethodContract.scala | 224 ++++++++++++++++++++- .../james/jmap/json/EmailGetSerializer.scala | 4 +- .../scala/org/apache/james/jmap/mail/Email.scala | 25 ++- .../org/apache/james/jmap/mail/EmailHeader.scala | 22 +- 4 files changed, 260 insertions(+), 15 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 601ba30..f266760 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 @@ -2105,7 +2105,8 @@ trait EmailGetMethodContract { .inPath("methodResponses[0][1].list[0]") .isEqualTo( s"""{ - | "id": "${messageId.serialize}" + | "id": "${messageId.serialize}", + | "messageId":null |}""".stripMargin) } @@ -2148,7 +2149,8 @@ trait EmailGetMethodContract { .inPath("methodResponses[0][1].list[0]") .isEqualTo( s"""{ - | "id": "${messageId.serialize}" + | "id": "${messageId.serialize}", + | "inReplyTo":null |}""".stripMargin) } @@ -2191,7 +2193,8 @@ trait EmailGetMethodContract { .inPath("methodResponses[0][1].list[0]") .isEqualTo( s"""{ - | "id": "${messageId.serialize}" + | "id": "${messageId.serialize}", + | "references":null |}""".stripMargin) } @@ -6197,4 +6200,219 @@ trait EmailGetMethodContract { | "header:To:asGroupedAddresses": [] }""".stripMargin) } + + @Test + def emailGetShouldReturnSpecificHeaderAsMessageIds(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()) + .setMessageId("<[email protected]>") + .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:Message-Id:asMessageIds"] + | }, + | "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:Message-Id:asMessageIds": [ + | "[email protected]" + | ] + }""".stripMargin) + } + + @Test + def emailGetSpecificHeaderAsMessageIdsShouldSupportMultipleIds(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("References", "<[email protected]> \r\n <[email protected]>")) + .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:References:asMessageIds"] + | }, + | "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:References:asMessageIds": [ + | "[email protected]", + | "[email protected]" + | ] + }""".stripMargin) + } + + @Test + def emailGetAsMessageIdsHeaderShouldReturnNullWhenInvalidMessageIds(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()) + .setMessageId("invalid") + .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:Message-Id:asMessageIds"] + | }, + | "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:Message-Id:asMessageIds": null + }""".stripMargin) + } + + @Test + def emailGetShouldReturnSpecificHeaderWhenPartialInvalidMessageIds(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("References", "invalid bloblah \r\n <[email protected]>")) + .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:References:asMessageIds"] + | }, + | "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:References:asMessageIds": [ + | "[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 ae33dc1..f937c44 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, AddressesHeaderValue, BlobId, Charset, Disposition, EmailAddress, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, GroupName, Gr [...] +import org.apache.james.jmap.mail.{Address, AddressesHeaderValue, BlobId, Charset, Disposition, EmailAddress, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, GroupName, Gr [...] import org.apache.james.jmap.model._ import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId} import play.api.libs.functional.syntax._ @@ -63,11 +63,13 @@ object EmailGetSerializer { "name" -> Json.toJson(o.name), "addresses" -> Json.toJson(o.addresses)) private implicit val groupedAddressesHeaderWrites: Writes[GroupedAddressesHeaderValue] = Json.valueWrites[GroupedAddressesHeaderValue] + private implicit val messageIdsHeaderWrites: Writes[MessageIdsHeaderValue] = Json.valueWrites[MessageIdsHeaderValue] private implicit val emailHeaderWrites: Writes[EmailHeaderValue] = { case headerValue: RawHeaderValue => Json.toJson[RawHeaderValue](headerValue) case headerValue: TextHeaderValue => Json.toJson[TextHeaderValue](headerValue) case headerValue: AddressesHeaderValue => Json.toJson[AddressesHeaderValue](headerValue) case headerValue: GroupedAddressesHeaderValue => Json.toJson[GroupedAddressesHeaderValue](headerValue) + case headerValue: MessageIdsHeaderValue => Json.toJson[MessageIdsHeaderValue](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 ece0fe3..de92da1 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 @@ -167,6 +167,7 @@ object ParseOptions { case "asText" => Some(AsText) case "asAddresses" => Some(AsAddresses) case "asGroupedAddresses" => Some(AsGroupedAddresses) + case "asMessageIds" => Some(AsMessageIds) case _ => None } } @@ -186,6 +187,9 @@ case object AsAddresses extends ParseOption { case object AsGroupedAddresses extends ParseOption { override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(GroupedAddressesHeaderValue.from(field)) } +case object AsMessageIds extends ParseOption { + override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(MessageIdsHeaderValue.from(field)) +} case class HeaderMessageId(value: String) extends AnyVal @@ -246,13 +250,14 @@ object EmailHeaders { .map(MimeUtil.unscrambleHeaderValue) .map(Subject) - private def extractMessageId(mime4JMessage: Message, fieldName: String): Option[List[HeaderMessageId]] = - Option(mime4JMessage.getHeader.getFields(fieldName)) - .map(_.asScala - .map(_.getBody) - .map(HeaderMessageId.from) - .toList) - .filter(_.nonEmpty) + private def extractMessageId(mime4JMessage: Message, fieldName: String): MessageIdsHeaderValue = + MessageIdsHeaderValue( + Option(mime4JMessage.getHeader.getFields(fieldName)) + .map(_.asScala + .map(_.getBody) + .map(HeaderMessageId.from) + .toList) + .filter(_.nonEmpty)) private def extractAddresses(mime4JMessage: Message, fieldName: String): Option[AddressesHeaderValue] = extractLastField(mime4JMessage, fieldName) @@ -279,9 +284,9 @@ object EmailHeaders { } case class EmailHeaders(headers: List[EmailHeader], - messageId: Option[List[HeaderMessageId]], - inReplyTo: Option[List[HeaderMessageId]], - references: Option[List[HeaderMessageId]], + messageId: MessageIdsHeaderValue, + inReplyTo: MessageIdsHeaderValue, + references: MessageIdsHeaderValue, to: Option[AddressesHeaderValue], cc: Option[AddressesHeaderValue], bcc: Option[AddressesHeaderValue], 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 e1da31b..58db534 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 @@ -20,10 +20,12 @@ package org.apache.james.jmap.mail import java.nio.charset.StandardCharsets.US_ASCII +import java.util.Date +import org.apache.commons.lang3.StringUtils import org.apache.james.mime4j.codec.{DecodeMonitor, DecoderUtil} import org.apache.james.mime4j.dom.address.{AddressList, Group, Address => Mime4jAddress, Mailbox => Mime4jMailbox} -import org.apache.james.mime4j.field.AddressListFieldImpl +import org.apache.james.mime4j.field.{AddressListFieldImpl, DateTimeFieldImpl} import org.apache.james.mime4j.stream.Field import org.apache.james.mime4j.util.MimeUtil @@ -75,6 +77,23 @@ object GroupedAddressesHeaderValue extends EmailHeaderValue { } } +object MessageIdsHeaderValue { + def from(field: Field): MessageIdsHeaderValue = { + val messageIds: List[HeaderMessageId] = MimeUtil.unfold(StringUtils.normalizeSpace(field.getBody)) + .split(' ') + .flatMap(body => { + if(body.startsWith("<") && body.endsWith(">") && body.contains("@")) { + scala.Right(HeaderMessageId.from(body)) + } else { + Left() + } + }.toOption) + .toList + + MessageIdsHeaderValue(Option(messageIds).filter(_.nonEmpty)) + } +} + case class EmailHeaderName(value: String) extends AnyVal sealed trait EmailHeaderValue @@ -82,5 +101,6 @@ case class RawHeaderValue(value: String) extends EmailHeaderValue case class TextHeaderValue(value: String) extends EmailHeaderValue case class AddressesHeaderValue(value: List[EmailAddress]) extends EmailHeaderValue case class GroupedAddressesHeaderValue(value: List[EmailAddressGroup]) extends EmailHeaderValue +case class MessageIdsHeaderValue(value: Option[List[HeaderMessageId]]) extends EmailHeaderValue case class EmailHeader(name: EmailHeaderName, value: EmailHeaderValue) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
