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 a36760613d1df34f0133ae962526a13357272f16 Author: LanKhuat <[email protected]> AuthorDate: Thu Sep 17 11:27:05 2020 +0700 JAMES-3379 Email/get specific parsed headers: asAddresses --- .../rfc8621/contract/EmailGetMethodContract.scala | 161 +++++++++++++++++++++ .../james/jmap/json/EmailGetSerializer.scala | 4 +- .../scala/org/apache/james/jmap/mail/Email.scala | 42 +++--- .../org/apache/james/jmap/mail/EmailAddress.scala | 15 +- .../org/apache/james/jmap/mail/EmailGet.scala | 5 +- .../org/apache/james/jmap/mail/EmailHeader.scala | 6 + 6 files changed, 205 insertions(+), 28 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 c8783c8..6f71695 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 @@ -5834,4 +5834,165 @@ trait EmailGetMethodContract { | "header:Subject:asText": "World domination and this is also part of the header" |}""".stripMargin) } + + @Test + def emailGetShouldReturnSpecificHeadersAsAddresses(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]>, \"user2\" <[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:To:asAddresses"] + | }, + | "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:To:asAddresses": [ + | { "name": "user1", "email": "[email protected]" }, + | { "name": "user2", "email": "[email protected]" } + | ] + |}""".stripMargin) + } + + @Test + def emailGetShouldReturnSpecificHeadersAsAddressesAndIgnoresGroup(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]>, Friends: \"user2\" <[email protected]>, \"user3\" <[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:To:asAddresses"] + | }, + | "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:To:asAddresses": [ + | { "name": "user1", "email": "[email protected]" }, + | { "name": "user2", "email": "[email protected]" }, + | { "name": "user3", "email": "[email protected]" } + | ] + |}""".stripMargin) + } + + @Test + def emailGetShouldReturnEmptyWhenCannotParseAsAdresses(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", "blahblah")) + .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:To:asAddresses"] + | }, + | "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:To:asAddresses": [] + }""".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 34ca387..475e1df 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, IsT [...] +import org.apache.james.jmap.mail.{Address, AddressesHeaderValue, 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, [...] import org.apache.james.jmap.model._ import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId} import play.api.libs.functional.syntax._ @@ -56,9 +56,11 @@ object EmailGetSerializer { 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 addressesHeaderWrites: Writes[AddressesHeaderValue] = Json.valueWrites[AddressesHeaderValue] 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) } 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 3261372..560e509 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.method.ZoneIdProvider 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, MessageResult} +import org.apache.james.mailbox.model.{FetchGroup, MailboxId, MessageId, 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,8 +47,6 @@ import org.apache.james.mime4j.dom.{Header, Message} 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,19 +157,16 @@ object HeaderMessageId { } } - object ParseOptions { val allowedParseOption: Set[String] = Set("asRaw", "asText", "asAddresses", "asGroupedAddresses", "asMessageIds", "asDate", "asURLs") def validate(parseOption: String): Boolean = from(parseOption).isDefined - def from(value: String): Option[ParseOption] = { - allowedParseOption - .find(_.equals(value)) - .map({ - case "asRaw" => AsRaw - case "asText" => AsText - }) + def from(value: String): Option[ParseOption] = value match { + case "asRaw" => Some(AsRaw) + case "asText" => Some(AsText) + case "asAddresses" => Some(AsAddresses) + case _ => None } } @@ -184,6 +179,9 @@ case object AsRaw extends ParseOption { case object AsText extends ParseOption { override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(TextHeaderValue.from(field)) } +case object AsAddresses extends ParseOption { + override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(AddressesHeaderValue.from(field)) +} case class HeaderMessageId(value: String) extends AnyVal @@ -252,15 +250,15 @@ object EmailHeaders { .toList) .filter(_.nonEmpty) - private def extractAddresses(mime4JMessage: Message, fieldName: String): Option[List[EmailAddress]] = + private def extractAddresses(mime4JMessage: Message, fieldName: String): Option[AddressesHeaderValue] = extractLastField(mime4JMessage, fieldName) .flatMap { - case f: AddressListField => Some(EmailAddress.from(f.getAddressList)) - case f: MailboxListField => Some(EmailAddress.from(f.getMailboxList)) - case f: MailboxField => Some(List(EmailAddress.from(f.getMailbox))) + case f: AddressListField => Some(AddressesHeaderValue(EmailAddress.from(f.getAddressList))) + case f: MailboxListField => Some(AddressesHeaderValue(EmailAddress.from(f.getMailboxList))) + case f: MailboxField => Some(AddressesHeaderValue(List(EmailAddress.from(f.getMailbox)))) case _ => None } - .filter(_.nonEmpty) + .filter(_.value.nonEmpty) private def extractDate(mime4JMessage: Message, fieldName: String): Option[Date] = extractLastField(mime4JMessage, fieldName) @@ -280,12 +278,12 @@ case class EmailHeaders(headers: List[EmailHeader], messageId: Option[List[HeaderMessageId]], inReplyTo: Option[List[HeaderMessageId]], references: Option[List[HeaderMessageId]], - to: Option[List[EmailAddress]], - cc: Option[List[EmailAddress]], - bcc: Option[List[EmailAddress]], - from: Option[List[EmailAddress]], - sender: Option[List[EmailAddress]], - replyTo: Option[List[EmailAddress]], + to: Option[AddressesHeaderValue], + cc: Option[AddressesHeaderValue], + bcc: Option[AddressesHeaderValue], + from: Option[AddressesHeaderValue], + sender: Option[AddressesHeaderValue], + replyTo: Option[AddressesHeaderValue], subject: Option[Subject], sentAt: Option[UTCDate]) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala index 5fa00bb..d995610 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala @@ -23,22 +23,29 @@ import org.apache.james.mime4j.dom.address.{AddressList, MailboxList, Mailbox => import scala.jdk.CollectionConverters._ +object EmailerName { + def from(value: String): EmailerName = EmailerName(value.strip()) +} + case class EmailerName(value: String) extends AnyVal + case class Address(value: String) extends AnyVal object EmailAddress { - def from(addressList: AddressList): List[EmailAddress] = - from(addressList.flatten()) + def from(addressList: AddressList): List[EmailAddress] = Option(addressList) + .map(addressList => from(addressList.flatten())) + .getOrElse(List()) def from(addressList: MailboxList): List[EmailAddress] = addressList.asScala .toList .map(from) - def from(mailbox: Mime4jMailbox): EmailAddress = + def from(mailbox: Mime4jMailbox): EmailAddress = { EmailAddress( - name = Option(mailbox.getName).map(EmailerName), + name = Option(mailbox.getName).map(EmailerName.from), email = Address(mailbox.getAddress)) + } } case class EmailAddress(name: Option[EmailerName], email: Address) 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 cd1dcea..43e8c84 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 @@ -30,6 +30,7 @@ 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 +import scala.jdk.CollectionConverters._ case class EmailIds(value: List[UnparsedEmailId]) @@ -83,7 +84,9 @@ case class EmailGetResponse(accountId: AccountId, 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)) + val field: Option[Field] = Option(message.getHeader.getFields(property)) + .map(_.asScala) + .flatMap(fields => fields.reverse.headOption) (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 282813b..98133ec 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 @@ -22,6 +22,7 @@ package org.apache.james.jmap.mail import java.nio.charset.StandardCharsets.US_ASCII import org.apache.james.mime4j.codec.{DecodeMonitor, DecoderUtil} +import org.apache.james.mime4j.field.AddressListFieldImpl import org.apache.james.mime4j.stream.Field import org.apache.james.mime4j.util.MimeUtil @@ -37,10 +38,15 @@ object TextHeaderValue extends EmailHeaderValue { def from(field: Field): TextHeaderValue = TextHeaderValue(MimeUtil.unfold(DecoderUtil.decodeEncodedWords(field.getBody, DecodeMonitor.SILENT)).stripLeading()) } +object AddressesHeaderValue extends EmailHeaderValue { + def from(field: Field): AddressesHeaderValue = AddressesHeaderValue(EmailAddress.from(AddressListFieldImpl.PARSER.parse(field, DecodeMonitor.SILENT).getAddressList)) +} + 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 AddressesHeaderValue(value: List[EmailAddress]) extends EmailHeaderValue case class EmailHeader(name: EmailHeaderName, value: EmailHeaderValue) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
