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 546550f1cd65bb03538bf9515ed7995b21909561 Author: LanKhuat <[email protected]> AuthorDate: Mon Sep 21 18:05:55 2020 +0700 JAMES-3379 Email/get specific parsed headers: asGroupedAddresses --- .../rfc8621/contract/EmailGetMethodContract.scala | 206 ++++++++++++++++++++- .../james/jmap/json/EmailGetSerializer.scala | 9 +- .../scala/org/apache/james/jmap/mail/Email.scala | 4 + .../apache/james/jmap/mail/EmailAddressGroup.scala | 24 +++ .../org/apache/james/jmap/mail/EmailHeader.scala | 34 ++++ 5 files changed, 274 insertions(+), 3 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 6f71695..601ba30 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 @@ -28,7 +28,6 @@ import io.restassured.RestAssured.{`given`, requestSpecification} import io.restassured.http.ContentType.JSON import javax.mail.Flags import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson -import net.javacrumbs.jsonunit.core.Option import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER import net.javacrumbs.jsonunit.core.internal.Options import org.apache.http.HttpStatus.SC_OK @@ -5945,7 +5944,7 @@ trait EmailGetMethodContract { } @Test - def emailGetShouldReturnEmptyWhenCannotParseAsAdresses(server: GuiceJamesServer): Unit = { + def emailGetShouldReturnEmptyWhenCannotParseAsAddresses(server: GuiceJamesServer): Unit = { val bobPath = MailboxPath.inbox(BOB) server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath) val alicePath = MailboxPath.inbox(ALICE) @@ -5995,4 +5994,207 @@ trait EmailGetMethodContract { | "header:To:asAddresses": [] }""".stripMargin) } + + @Test + def emailGetShouldReturnGroupedAddresses(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]>, Friends: <[email protected]>, \"user4\" <[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:asGroupedAddresses"] + | }, + | "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:asGroupedAddresses": [ + | { + | "name": null, + | "addresses": [ + | { + | "name": "user1", + | "email": "[email protected]" + | }, + | { + | "name": "user2", + | "email": "[email protected]" + | } + | ] + | }, + | { + | "name": "Friends", + | "addresses": [ + | { + | "email": "[email protected]" + | }, + | { + | "name": "user4", + | "email": "[email protected]" + | } + | ] + | } + | ] + |}""".stripMargin) + } + + @Test + def emailGetShouldReturnAsGroupedAddressesWithoutGroupInfo(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]>, <[email protected]>, \"user4\" <[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:asGroupedAddresses"] + | }, + | "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:asGroupedAddresses": [ + | { + | "name": null, + | "addresses": [ + | { + | "name": "user1", + | "email": "[email protected]" + | }, + | { + | "name": "user2", + | "email": "[email protected]" + | }, + | { + | "email": "[email protected]" + | }, + | { + | "name": "user4", + | "email": "[email protected]" + | } + | ] + | } + | ] + |}""".stripMargin) + } + + @Test + def emailGetShouldReturnEmptyWhenCannotParseAsGroupedAddresses(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:asGroupedAddresses"] + | }, + | "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:asGroupedAddresses": [] + }""".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 475e1df..ae33dc1 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, 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.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._ @@ -57,10 +57,17 @@ object EmailGetSerializer { 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 GroupNameWrites: Writes[GroupName] = Json.valueWrites[GroupName] + private implicit val emailAddressGroupWrites: Writes[EmailAddressGroup] = (o: EmailAddressGroup) => + Json.obj( + "name" -> Json.toJson(o.name), + "addresses" -> Json.toJson(o.addresses)) + private implicit val groupedAddressesHeaderWrites: Writes[GroupedAddressesHeaderValue] = Json.valueWrites[GroupedAddressesHeaderValue] 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) } 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 560e509..ece0fe3 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 @@ -166,6 +166,7 @@ object ParseOptions { case "asRaw" => Some(AsRaw) case "asText" => Some(AsText) case "asAddresses" => Some(AsAddresses) + case "asGroupedAddresses" => Some(AsGroupedAddresses) case _ => None } } @@ -182,6 +183,9 @@ case object AsText extends ParseOption { case object AsAddresses extends ParseOption { override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(AddressesHeaderValue.from(field)) } +case object AsGroupedAddresses extends ParseOption { + override def extractHeaderValue(field: Field): Option[EmailHeaderValue] = Some(GroupedAddressesHeaderValue.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/EmailAddressGroup.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddressGroup.scala new file mode 100644 index 0000000..b423996 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddressGroup.scala @@ -0,0 +1,24 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.mail + +case class GroupName(value: String) extends AnyVal + +case class EmailAddressGroup(name: Option[GroupName], addresses: List[EmailAddress]) 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 98133ec..e1da31b 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,10 +22,13 @@ 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.dom.address.{AddressList, Group, Address => Mime4jAddress, Mailbox => Mime4jMailbox} import org.apache.james.mime4j.field.AddressListFieldImpl import org.apache.james.mime4j.stream.Field import org.apache.james.mime4j.util.MimeUtil +import scala.jdk.CollectionConverters._ + object EmailHeader { def apply(field: Field): EmailHeader = EmailHeader(EmailHeaderName(field.getName), RawHeaderValue.from(field)) } @@ -42,11 +45,42 @@ object AddressesHeaderValue extends EmailHeaderValue { def from(field: Field): AddressesHeaderValue = AddressesHeaderValue(EmailAddress.from(AddressListFieldImpl.PARSER.parse(field, DecodeMonitor.SILENT).getAddressList)) } +object GroupedAddressesHeaderValue extends EmailHeaderValue { + def from(field: Field): GroupedAddressesHeaderValue = { + val addresses: List[Mime4jAddress] = + Option(AddressListFieldImpl.PARSER.parse(field, DecodeMonitor.SILENT).getAddressList) + .getOrElse(new AddressList()) + .asScala + .toList + + val groups: List[EmailAddressGroup] = addresses + .flatMap({ + case group: Group => Some(group) + case _ => None + }) + .map(group => EmailAddressGroup(Some(GroupName(group.getName)), EmailAddress.from(group.getMailboxes))) + + val addressesWithoutGroup: List[EmailAddress] = addresses + .flatMap({ + case mailbox: Mime4jMailbox => Some(mailbox) + case _ => None + }) + .map(EmailAddress.from(_)) + + if (addresses.isEmpty) { + GroupedAddressesHeaderValue(List()) + } else { + GroupedAddressesHeaderValue(List(EmailAddressGroup(None, addressesWithoutGroup)) ++ groups) + } + } +} + 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 GroupedAddressesHeaderValue(value: List[EmailAddressGroup]) extends EmailHeaderValue case class EmailHeader(name: EmailHeaderName, value: EmailHeaderValue) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
