You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/11/12 04:42:17 UTC
[james-project] branch master updated (74b7682 -> a44eac6)
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.
from 74b7682 JAMES-3359 Add tests to demystify Mailbox/set update parentId data races
new 0faf47a JAMES-3439 Include inline attachments
new c1a920d JAMES-3439 Wrap inline attachments in multipart/related
new 4ebd13d JAMES-3439 Email/set create: Simplify mime structure when possible
new 71e12dd JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)
new 900bc9c JAMES-3442 Email/set create position multipart/alternative for text/html
new d2f466c JAMES-3171 Enhance MailboxGetMethod reactive management
new 7fa59de JAMES-3171 Limit concurrency in JMAP methods
new c9305e3 JAMES-3378 Email/query keywords should be case incentive
new 87b3678 JAMES-3373 Avoid using hamcrest
new 433162e JAMES-3413 Email/set updateValidation message should pass on distributed james
new 6736651 JAMES-3432 UploadRoutes error handling and schedulers
new a44eac6 JAMES-3377 Split case sensitivity test case for subject
The 12 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../elasticsearch/MailboxMappingFactory.java | 1 +
.../search/AbstractMessageSearchIndexTest.java | 8 +
.../distributed/DistributedEmailSetMethodTest.java | 5 +
.../jmap/rfc8621/contract/DownloadContract.scala | 15 +-
.../rfc8621/contract/EmailGetMethodContract.scala | 4 +-
.../contract/EmailQueryMethodContract.scala | 61 +-
.../rfc8621/contract/EmailSetMethodContract.scala | 1355 +++++++++++++++++---
.../rfc8621/memory/MemoryEmailSetMethodTest.java | 5 +
.../org/apache/james/jmap/mail/EmailSet.scala | 122 +-
.../jmap/method/EmailSetCreatePerformer.scala | 4 +-
.../jmap/method/EmailSetUpdatePerformer.scala | 2 +-
.../james/jmap/method/MailboxGetMethod.scala | 30 +-
.../jmap/method/MailboxSetDeletePerformer.scala | 3 +-
.../jmap/method/MailboxSetUpdatePerformer.scala | 7 +-
.../apache/james/jmap/routes/UploadRoutes.scala | 33 +-
15 files changed, 1430 insertions(+), 225 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 02/12: JAMES-3439 Wrap inline attachments in
multipart/related
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit c1a920dcd83ce0638b9c26ef1a46a49f347ab8ce
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 11:39:36 2020 +0700
JAMES-3439 Wrap inline attachments in multipart/related
---
.../rfc8621/contract/EmailSetMethodContract.scala | 205 +++++++++++++++++++--
.../org/apache/james/jmap/mail/EmailSet.scala | 41 +++--
2 files changed, 216 insertions(+), 30 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 78b89ec..248680f 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -1818,7 +1818,7 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "3",
+ | "partId": "4",
| "blobId": "$blobIdToDownload",
| "size": 11,
| "type": "text/plain",
@@ -1951,8 +1951,8 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "3",
- | "blobId": "${messageId}_3",
+ | "partId": "4",
+ | "blobId": "${messageId}_4",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -1961,15 +1961,15 @@ trait EmailSetMethodContract {
| ],
| "htmlBody": [
| {
- | "partId": "2",
- | "blobId": "${messageId}_2",
+ | "partId": "3",
+ | "blobId": "${messageId}_3",
| "size": 166,
| "type": "text/html",
| "charset": "UTF-8"
| }
| ],
| "bodyValues": {
- | "2": {
+ | "3": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -2065,7 +2065,7 @@ trait EmailSetMethodContract {
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(request)
.when
- .post.prettyPeek()
+ .post
.`then`
.statusCode(SC_OK)
.contentType(JSON)
@@ -2097,8 +2097,8 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "3",
- | "blobId": "${messageId}_3",
+ | "partId": "4",
+ | "blobId": "${messageId}_4",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -2106,8 +2106,8 @@ trait EmailSetMethodContract {
| "cid": "abc"
| },
| {
- | "partId": "4",
- | "blobId": "${messageId}_4",
+ | "partId": "5",
+ | "blobId": "${messageId}_5",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -2115,8 +2115,8 @@ trait EmailSetMethodContract {
| "cid": "def"
| },
| {
- | "partId": "5",
- | "blobId": "${messageId}_5",
+ | "partId": "6",
+ | "blobId": "${messageId}_6",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -2125,15 +2125,15 @@ trait EmailSetMethodContract {
| ],
| "htmlBody": [
| {
- | "partId": "2",
- | "blobId": "${messageId}_2",
+ | "partId": "3",
+ | "blobId": "${messageId}_3",
| "size": 166,
| "type": "text/html",
| "charset": "UTF-8"
| }
| ],
| "bodyValues": {
- | "2": {
+ | "3": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -2143,6 +2143,177 @@ trait EmailSetMethodContract {
}
@Test
+ def inlinedAttachmentsShouldBeWrappedInRelatedMultipart(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val uploadResponse: String = `given`
+ .basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .contentType("text/plain")
+ .body(payload)
+ .when
+ .post(s"/upload/$ACCOUNT_ID/")
+ .`then`
+ .statusCode(SC_CREATED)
+ .extract
+ .body
+ .asString
+
+ val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "def"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "attachment"
+ | }
+ | ],
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["bodyStructure"],
+ | "bodyProperties": ["type", "disposition", "cid", "subParts"]
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Email/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | },
+ | "c1"
+ | ],
+ | [
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/mixed",
+ | "subParts": [
+ | {
+ | "type": "multipart/related",
+ | "subParts": [
+ | {
+ | "type": "text/html"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "def"
+ | }
+ | ]
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "attachment"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | },
+ | "c2"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
def createShouldSupportAttachmentWithName(server: GuiceJamesServer): Unit = {
val bobPath = MailboxPath.inbox(BOB)
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
@@ -2239,7 +2410,7 @@ trait EmailSetMethodContract {
| "attachments": [
| {
| "name": "myAttachment",
- | "partId": "3",
+ | "partId": "4",
| "blobId": "$blobIdToDownload",
| "size": 11,
| "type": "text/plain",
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index 6533819..c607f8b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -65,6 +65,7 @@ object EmailSet {
object SubType {
val HTML_SUBTYPE = "html"
val MIXED_SUBTYPE = "mixed"
+ val RELATED_SUBTYPE = "related"
}
case class ClientPartId(id: Id)
@@ -151,8 +152,6 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
attachmentManager: AttachmentManager,
attachmentContentLoader: AttachmentContentLoader,
mailboxSession: MailboxSession): Either[Exception, MultipartBuilder] = {
- val multipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
-
val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
attachments
.map(attachment => getAttachmentMetadata(attachment, attachmentManager, mailboxSession))
@@ -160,24 +159,40 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
.flatMap(attachmentAndMetadata => loadAttachment(attachmentAndMetadata._1, attachmentAndMetadata._2, attachmentContentLoader, mailboxSession)))
.sequence
- multipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
maybeAttachments.map(list => {
- list.foldLeft(multipartBuilder) {
+ val inlineAttachments = list.filter(_._1.isInline)
+ val normalAttachments = list.filter(!_._1.isInline)
+
+ val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
+ val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
+ relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ inlineAttachments.foldLeft(relatedMultipartBuilder) {
case (acc, (attachment, storedMetadata, content)) =>
- val bodypartBuilder = BodyPartBuilder.create()
- bodypartBuilder.setBody(content, attachment.`type`.value)
- .setField(contentTypeField(attachment, storedMetadata))
- .setContentDisposition(attachment.disposition.getOrElse(Disposition.ATTACHMENT).value)
- attachment.cid.map(_.asField).foreach(bodypartBuilder.addField)
- attachment.location.map(_.asField).foreach(bodypartBuilder.addField)
- attachment.language.map(_.asField).foreach(bodypartBuilder.addField)
-
- acc.addBodyPart(bodypartBuilder)
+ acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+ acc
+ }
+
+ mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
+
+ normalAttachments.foldLeft(mixedMultipartBuilder) {
+ case (acc, (attachment, storedMetadata, content)) =>
+ acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
acc
}
})
}
+ private def toBodypartBuilder(attachment: Attachment, storedMetadata: AttachmentMetadata, content: Array[Byte]) = {
+ val bodypartBuilder = BodyPartBuilder.create()
+ bodypartBuilder.setBody(content, attachment.`type`.value)
+ .setField(contentTypeField(attachment, storedMetadata))
+ .setContentDisposition(attachment.disposition.getOrElse(Disposition.ATTACHMENT).value)
+ attachment.cid.map(_.asField).foreach(bodypartBuilder.addField)
+ attachment.location.map(_.asField).foreach(bodypartBuilder.addField)
+ attachment.language.map(_.asField).foreach(bodypartBuilder.addField)
+ bodypartBuilder
+ }
+
private def contentTypeField(attachment: Attachment, attachmentMetadata: AttachmentMetadata): ContentTypeField = {
val typeAsField: ContentTypeField = attachmentMetadata.getType.asMime4J
if (attachment.name.isDefined) {
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 08/12: JAMES-3378 Email/query keywords should be
case incentive
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit c9305e37b26079e765ec61ad1b5d3651d3c7174b
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:36:25 2020 +0700
JAMES-3378 Email/query keywords should be case incentive
---
.../org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java b/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java
index aeb6782..944c4a7 100644
--- a/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java
+++ b/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java
@@ -147,6 +147,7 @@ public class MailboxMappingFactory {
.startObject(USER_FLAGS)
.field(TYPE, KEYWORD)
+ .field(NORMALIZER, CASE_INSENSITIVE)
.endObject()
.startObject(MEDIA_TYPE)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 01/12: JAMES-3439 Include inline attachments
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 0faf47a3cd0f8b25ce680795f13cb1aec85a7839
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 11:14:06 2020 +0700
JAMES-3439 Include inline attachments
---
.../rfc8621/contract/EmailSetMethodContract.scala | 164 +++++++++++++++++++++
.../org/apache/james/jmap/mail/EmailSet.scala | 4 +-
2 files changed, 166 insertions(+), 2 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 32322e7..78b89ec 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -1979,6 +1979,170 @@ trait EmailSetMethodContract {
}
@Test
+ def createShouldSupportInlinedAttachmentsMixedWithRegularAttachmentsAndHtmlBody(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val uploadResponse: String = `given`
+ .basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .contentType("text/plain")
+ .body(payload)
+ .when
+ .post(s"/upload/$ACCOUNT_ID/")
+ .`then`
+ .statusCode(SC_CREATED)
+ .extract
+ .body
+ .asString
+
+ val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "def"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "attachment"
+ | }
+ | ],
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["mailboxIds", "subject", "attachments", "htmlBody", "bodyValues"],
+ | "fetchHTMLBodyValues": true
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post.prettyPeek()
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[1][1].list[0].id")
+ .inPath(s"methodResponses[1][1].list")
+ .isEqualTo(
+ s"""[{
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [
+ | {
+ | "partId": "3",
+ | "blobId": "${messageId}_3",
+ | "size": 11,
+ | "type": "text/plain",
+ | "charset": "UTF-8",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "partId": "4",
+ | "blobId": "${messageId}_4",
+ | "size": 11,
+ | "type": "text/plain",
+ | "charset": "UTF-8",
+ | "disposition": "inline",
+ | "cid": "def"
+ | },
+ | {
+ | "partId": "5",
+ | "blobId": "${messageId}_5",
+ | "size": 11,
+ | "type": "text/plain",
+ | "charset": "UTF-8",
+ | "disposition": "attachment"
+ | }
+ | ],
+ | "htmlBody": [
+ | {
+ | "partId": "2",
+ | "blobId": "${messageId}_2",
+ | "size": 166,
+ | "type": "text/html",
+ | "charset": "UTF-8"
+ | }
+ | ],
+ | "bodyValues": {
+ | "2": {
+ | "value": "$htmlBody",
+ | "isEncodingProblem": false,
+ | "isTruncated": false
+ | }
+ | }
+ |}]""".stripMargin)
+ }
+
+ @Test
def createShouldSupportAttachmentWithName(server: GuiceJamesServer): Unit = {
val bobPath = MailboxPath.inbox(BOB)
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index e156674..6533819 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -29,6 +29,7 @@ import eu.timepit.refined.collection.NonEmpty
import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.State.State
import org.apache.james.jmap.core.{AccountId, SetError, UTCDate}
+import org.apache.james.jmap.mail.Disposition.INLINE
import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
import org.apache.james.jmap.method.WithAccountId
import org.apache.james.mailbox.exception.AttachmentNotFoundException
@@ -96,7 +97,7 @@ case class Attachment(blobId: BlobId,
location: Option[Location],
cid: Option[ClientCid]) {
- def isInline: Boolean = disposition.contains("inline")
+ def isInline: Boolean = disposition.contains(INLINE)
}
case class EmailCreationRequest(mailboxIds: MailboxIds,
@@ -154,7 +155,6 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
attachments
- .filter(!_.isInline)
.map(attachment => getAttachmentMetadata(attachment, attachmentManager, mailboxSession))
.map(attachmentMetadataList => attachmentMetadataList
.flatMap(attachmentAndMetadata => loadAttachment(attachmentAndMetadata._1, attachmentAndMetadata._2, attachmentContentLoader, mailboxSession)))
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 03/12: JAMES-3439 Email/set create: Simplify mime
structure when possible
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 4ebd13dba7ad1773f76f06275f529cdf179f4756
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 12:02:22 2020 +0700
JAMES-3439 Email/set create: Simplify mime structure when possible
---
.../rfc8621/contract/EmailSetMethodContract.scala | 446 ++++++++++++++++++++-
.../org/apache/james/jmap/mail/EmailSet.scala | 63 ++-
2 files changed, 480 insertions(+), 29 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 248680f..bdf41cc 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -1818,7 +1818,7 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "4",
+ | "partId": "3",
| "blobId": "$blobIdToDownload",
| "size": 11,
| "type": "text/plain",
@@ -1951,8 +1951,8 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "4",
- | "blobId": "${messageId}_4",
+ | "partId": "3",
+ | "blobId": "${messageId}_3",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -1961,15 +1961,15 @@ trait EmailSetMethodContract {
| ],
| "htmlBody": [
| {
- | "partId": "3",
- | "blobId": "${messageId}_3",
+ | "partId": "2",
+ | "blobId": "${messageId}_2",
| "size": 166,
| "type": "text/html",
| "charset": "UTF-8"
| }
| ],
| "bodyValues": {
- | "3": {
+ | "2": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -2154,9 +2154,9 @@ trait EmailSetMethodContract {
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.contentType("text/plain")
.body(payload)
- .when
+ .when
.post(s"/upload/$ACCOUNT_ID/")
- .`then`
+ .`then`
.statusCode(SC_CREATED)
.extract
.body
@@ -2228,9 +2228,9 @@ trait EmailSetMethodContract {
val response = `given`
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(request)
- .when
+ .when
.post
- .`then`
+ .`then`
.statusCode(SC_OK)
.contentType(JSON)
.extract
@@ -2314,6 +2314,430 @@ trait EmailSetMethodContract {
}
@Test
+ def htmlBodyPartWithOnlyNormalAttachmentsShouldNotBeWrappedInARelatedMultipart(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val uploadResponse: String = `given`
+ .basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .contentType("text/plain")
+ .body(payload)
+ .when
+ .post(s"/upload/$ACCOUNT_ID/")
+ .`then`
+ .statusCode(SC_CREATED)
+ .extract
+ .body
+ .asString
+
+ val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "attachment"
+ | }
+ | ],
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["bodyStructure"],
+ | "bodyProperties": ["type", "disposition", "cid", "subParts"]
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Email/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | },
+ | "c1"
+ | ],
+ | [
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/mixed",
+ | "subParts": [
+ | {
+ | "type": "text/html"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "attachment"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | },
+ | "c2"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def inlinedAttachmentsOnlyShouldNotBeWrappedInAMixedMultipart(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val uploadResponse: String = `given`
+ .basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .contentType("text/plain")
+ .body(payload)
+ .when
+ .post(s"/upload/$ACCOUNT_ID/")
+ .`then`
+ .statusCode(SC_CREATED)
+ .extract
+ .body
+ .asString
+
+ val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "def"
+ | }
+ | ],
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["bodyStructure"],
+ | "bodyProperties": ["type", "disposition", "cid", "subParts"]
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Email/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | },
+ | "c1"
+ | ],
+ | [
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/related",
+ | "subParts": [
+ | {
+ | "type": "text/html"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "def"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | },
+ | "c2"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def htmlBodyOnlyShouldNotBeWrappedInMultiparts(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val uploadResponse: String = `given`
+ .basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .contentType("text/plain")
+ .body(payload)
+ .when
+ .post(s"/upload/$ACCOUNT_ID/")
+ .`then`
+ .statusCode(SC_CREATED)
+ .extract
+ .body
+ .asString
+
+ val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [],
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["bodyStructure"],
+ | "bodyProperties": ["type", "disposition", "cid", "subParts"]
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Email/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | },
+ | "c1"
+ | ],
+ | [
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "text/html"
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | },
+ | "c2"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
def createShouldSupportAttachmentWithName(server: GuiceJamesServer): Unit = {
val bobPath = MailboxPath.inbox(BOB)
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
@@ -2410,7 +2834,7 @@ trait EmailSetMethodContract {
| "attachments": [
| {
| "name": "myAttachment",
- | "partId": "4",
+ | "partId": "3",
| "blobId": "$blobIdToDownload",
| "size": 11,
| "type": "text/plain",
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index c607f8b..a1a72a8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -137,7 +137,7 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
validateSpecificHeaders(builder)
.flatMap(_ => {
specificHeaders.map(_.asField).foreach(builder.addField)
- attachments.map(attachments =>
+ attachments.filter(_.nonEmpty).map(attachments =>
createMultipartWithAttachments(maybeHtmlBody, attachments, attachmentManager, attachmentContentLoader, mailboxSession)
.map(multipartBuilder => {
builder.setBody(multipartBuilder)
@@ -160,28 +160,55 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
.sequence
maybeAttachments.map(list => {
- val inlineAttachments = list.filter(_._1.isInline)
- val normalAttachments = list.filter(!_._1.isInline)
-
- val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
- val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
- relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
- inlineAttachments.foldLeft(relatedMultipartBuilder) {
- case (acc, (attachment, storedMetadata, content)) =>
- acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
- acc
- }
-
- mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
- normalAttachments.foldLeft(mixedMultipartBuilder) {
- case (acc, (attachment, storedMetadata, content)) =>
- acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
- acc
+ (list.filter(_._1.isInline), list.filter(!_._1.isInline)) match {
+ case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, normalAttachments)
+ case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, inlineAttachments)
+ case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, inlineAttachments, normalAttachments)
}
})
}
+ private def createMixedRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+ val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
+ val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
+ relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ inlineAttachments.foldLeft(relatedMultipartBuilder) {
+ case (acc, (attachment, storedMetadata, content)) =>
+ acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+ acc
+ }
+
+ mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
+
+ normalAttachments.foldLeft(mixedMultipartBuilder) {
+ case (acc, (attachment, storedMetadata, content)) =>
+ acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+ acc
+ }
+ }
+
+ private def createMixedBody(maybeHtmlBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+ val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
+ mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ normalAttachments.foldLeft(mixedMultipartBuilder) {
+ case (acc, (attachment, storedMetadata, content)) =>
+ acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+ acc
+ }
+ }
+
+ private def createRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+ val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
+ relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ inlineAttachments.foldLeft(relatedMultipartBuilder) {
+ case (acc, (attachment, storedMetadata, content)) =>
+ acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+ acc
+ }
+ relatedMultipartBuilder
+ }
+
private def toBodypartBuilder(attachment: Attachment, storedMetadata: AttachmentMetadata, content: Array[Byte]) = {
val bodypartBuilder = BodyPartBuilder.create()
bodypartBuilder.setBody(content, attachment.`type`.value)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 11/12: JAMES-3432 UploadRoutes error handling and
schedulers
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 6736651799a5dd6350770753cd853057c9c2ed8c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:58:27 2020 +0700
JAMES-3432 UploadRoutes error handling and schedulers
Required to pass on Distributed James
---
.../apache/james/jmap/routes/UploadRoutes.scala | 33 ++++++++++------------
1 file changed, 15 insertions(+), 18 deletions(-)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
index 9bb9f78..2f084c6 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -19,7 +19,7 @@
package org.apache.james.jmap.routes
-import java.io.{IOException, InputStream, UncheckedIOException}
+import java.io.InputStream
import java.util.stream
import java.util.stream.Stream
@@ -51,6 +51,8 @@ import reactor.core.scala.publisher.SMono
import reactor.core.scheduler.Schedulers
import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
+case class TooBigUploadException() extends RuntimeException
+
object UploadRoutes {
val LOGGER: Logger = LoggerFactory.getLogger(classOf[DownloadRoutes])
@@ -96,23 +98,23 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
def post(request: HttpServerRequest, response: HttpServerResponse): Mono[Void] = {
request.requestHeaders.get(CONTENT_TYPE) match {
case contentType => SMono.fromPublisher(
- authenticator.authenticate(request))
- .flatMap(session => post(request, response, ContentType.of(contentType), session))
- .onErrorResume {
- case e: UnauthorizedException => SMono.fromPublisher(handleAuthenticationFailure(response, LOGGER, e))
- case e: Throwable => SMono.fromPublisher(handleInternalError(response, LOGGER, e))
- }
- .asJava().`then`()
+ authenticator.authenticate(request))
+ .flatMap(session => post(request, response, ContentType.of(contentType), session))
+ .onErrorResume {
+ case e: TooBigUploadException => SMono.fromPublisher(response.status(BAD_REQUEST).sendString(SMono.just("Attempt to upload exceed max size")).`then`())
+ case e: UnauthorizedException => SMono.fromPublisher(handleAuthenticationFailure(response, LOGGER, e))
+ case e: Throwable => SMono.fromPublisher(handleInternalError(response, LOGGER, e))
+ }
+ .asJava()
+ .subscribeOn(Schedulers.elastic())
+ .`then`()
case _ => response.status(BAD_REQUEST).send
}
}
- private def handleUncheckedIOException(response: HttpServerResponse, e: UncheckedIOException) =
- response.status(BAD_REQUEST).sendString(SMono.just(e.getMessage))
-
def post(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, session: MailboxSession): SMono[Void] = {
Id.validate(request.param(accountIdParam)) match {
- case Right(id: Id) => {
+ case Right(id: Id) =>
val targetAccountId: AccountId = AccountId(id)
AccountId.from(session.getUser).map(accountId => accountId.equals(targetAccountId))
.fold[SMono[Void]](
@@ -120,11 +122,9 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
value => if (value) {
SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive.asByteBuffer))
.flatMap(content => handle(targetAccountId, contentType, content, session, response))
- .subscribeOn(Schedulers.elastic())
} else {
SMono.raiseError(new UnauthorizedException("Attempt to upload in another account"))
})
- }
case Left(throwable: Throwable) => SMono.raiseError(throwable)
}
@@ -135,16 +135,13 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
SMono.fromCallable(() => new LimitedInputStream(content, maxSize) {
override def raiseError(max: Long, count: Long): Unit = if (count > max) {
- throw new IOException("Attempt to upload exceed max size")
+ throw TooBigUploadException()
}})
.flatMap(uploadContent(accountId, contentType, _, mailboxSession))
.flatMap(uploadResponse => SMono.fromPublisher(response
.header(CONTENT_TYPE, uploadResponse.`type`.asString())
.status(CREATED)
.sendString(SMono.just(serializer.serialize(uploadResponse).toString()))))
- .onErrorResume {
- case e: UncheckedIOException => SMono.fromPublisher(handleUncheckedIOException(response, e))
- }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 04/12: JAMES-3439 Various refactorings regarding
Email/set create attachments (mixed)
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 71e12dd04df9c3342a429ecc71921aa13e424b1c
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 17:35:20 2020 +0700
JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)
---
.../james/jmap/rfc8621/contract/EmailSetMethodContract.scala | 8 ++++----
.../src/main/scala/org/apache/james/jmap/mail/EmailSet.scala | 6 +++---
2 files changed, 7 insertions(+), 7 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index bdf41cc..378fbbb 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -2154,9 +2154,9 @@ trait EmailSetMethodContract {
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.contentType("text/plain")
.body(payload)
- .when
+ .when
.post(s"/upload/$ACCOUNT_ID/")
- .`then`
+ .`then`
.statusCode(SC_CREATED)
.extract
.body
@@ -2228,9 +2228,9 @@ trait EmailSetMethodContract {
val response = `given`
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(request)
- .when
+ .when
.post
- .`then`
+ .`then`
.statusCode(SC_OK)
.contentType(JSON)
.extract
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index a1a72a8..464be6d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -33,7 +33,7 @@ import org.apache.james.jmap.mail.Disposition.INLINE
import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
import org.apache.james.jmap.method.WithAccountId
import org.apache.james.mailbox.exception.AttachmentNotFoundException
-import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, MessageId, Cid => MailboxCid}
+import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, Cid, MessageId}
import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxSession}
import org.apache.james.mime4j.codec.EncoderUtil
import org.apache.james.mime4j.codec.EncoderUtil.Usage
@@ -77,10 +77,10 @@ case class ClientEmailBodyValue(value: String,
isTruncated: Option[IsTruncated])
object ClientCid {
- def of(entity: Entity): Option[MailboxCid] =
+ def of(entity: Entity): Option[Cid] =
Option(entity.getHeader.getField(FieldName.CONTENT_ID))
.flatMap {
- case contentIdField: ContentIdField => MailboxCid.parser().relaxed().unwrap().parse(contentIdField.getId).toScala
+ case contentIdField: ContentIdField => Cid.parser().relaxed().unwrap().parse(contentIdField.getId).toScala
case _ => None
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 07/12: JAMES-3171 Limit concurrency in JMAP methods
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 7fa59ded0afb5621195683de91bdccbda1cdd971
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 14:13:53 2020 +0700
JAMES-3171 Limit concurrency in JMAP methods
---
.../main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala | 3 ++-
.../org/apache/james/jmap/method/MailboxSetDeletePerformer.scala | 3 ++-
.../org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala | 7 +++----
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index c13bf3a..a7adf77 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -108,7 +108,8 @@ class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
case Some(ids) => SFlux.fromIterable(ids.value)
.flatMap(id => Try(mailboxIdFactory.fromString(id.value))
.fold(e => SMono.just(MailboxGetResults.notFound(id)),
- mailboxId => getMailboxResultById(capabilities, mailboxId, mailboxSession)))
+ mailboxId => getMailboxResultById(capabilities, mailboxId, mailboxSession)),
+ maxConcurrency = 5)
}
private def getMailboxResultById(capabilities: Set[CapabilityIdentifier],
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
index 3ccd8f4..de5d4ea 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
@@ -67,7 +67,8 @@ class MailboxSetDeletePerformer @Inject()(mailboxManager: MailboxManager,
def deleteMailboxes(mailboxSession: MailboxSession, mailboxSetRequest: MailboxSetRequest): SMono[MailboxDeletionResults] = {
SFlux.fromIterable(mailboxSetRequest.destroy.getOrElse(Seq()))
.flatMap(id => delete(mailboxSession, id, mailboxSetRequest.onDestroyRemoveEmails.getOrElse(RemoveEmailsOnDestroy(false)))
- .onErrorRecover(e => MailboxDeletionFailure(id, e)))
+ .onErrorRecover(e => MailboxDeletionFailure(id, e)),
+ maxConcurrency = 5)
.collectSeq()
.map(MailboxDeletionResults)
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
index f208989..c82e5c3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
@@ -92,7 +92,7 @@ class MailboxSetUpdatePerformer @Inject()(serializer: MailboxSerializer,
e => SMono.just(MailboxUpdateFailure(unparsedMailboxId, e, None)),
mailboxId => updateMailbox(mailboxSession, mailboxId, unparsedMailboxId, patch, capabilities))
.onErrorResume(e => SMono.just(MailboxUpdateFailure(unparsedMailboxId, e, None)))
- })
+ }, maxConcurrency = 5)
.collectSeq()
.map(MailboxUpdateResults)
}
@@ -216,9 +216,8 @@ class MailboxSetUpdatePerformer @Inject()(serializer: MailboxSerializer,
}).getOrElse(SMono.empty)
val partialUpdatesOperation: SMono[Unit] = SFlux.fromIterable(validatedPatch.rightsPartialUpdates)
- .flatMap(partialUpdate => SMono.fromCallable(() => {
- mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)
- }))
+ .flatMap(partialUpdate => SMono.fromCallable(() => mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)),
+ maxConcurrency = 5)
.`then`()
SFlux.merge(Seq(resetOperation, partialUpdatesOperation))
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 12/12: JAMES-3377 Split case sensitivity test case
for subject
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit a44eac63c2aa3939c0ce6c47997451eb3b81842a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 12:03:42 2020 +0700
JAMES-3377 Split case sensitivity test case for subject
---
.../search/AbstractMessageSearchIndexTest.java | 8 +++
.../contract/EmailQueryMethodContract.scala | 61 +++++++++++++++++++++-
2 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
index 5e3d2e9..b0500ef 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
@@ -827,6 +827,14 @@ public abstract class AbstractMessageSearchIndexTest {
}
@Test
+ void headerContainsShouldBeCaseInsensitive() throws Exception {
+ SearchQuery searchQuery = SearchQuery.of(SearchQuery.headerContains("Precedence", "LiSt"));
+
+ assertThat(messageSearchIndex.search(session, mailbox, searchQuery).toStream())
+ .containsOnly(m1.getUid(), m6.getUid(), m8.getUid(), m9.getUid());
+ }
+
+ @Test
void headerExistsShouldReturnUidsOfMessageHavingThisHeader() throws Exception {
SearchQuery searchQuery = SearchQuery.of(SearchQuery.headerExists("Precedence"));
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/EmailQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
index 9007184..9f9078a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
@@ -4682,10 +4682,69 @@ trait EmailQueryMethodContract {
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "filter" : {
+ | "subject": "paradise"
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .inPath("$.methodResponses[0][1].ids")
+ .isEqualTo(s"""["${messageId1.serialize}"]""")
+ }
+ }
+
+ @Test
+ def subjectShouldBeCaseInsensitive(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ def messageBuilder = Message.Builder
+ .of
+ .setBody("testmail", StandardCharsets.UTF_8)
+ val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
+ messageBuilder
+ .setSubject("Yet another day in paradise")
+ .build))
+ .getMessageId
+ val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
+ messageBuilder
+ .setSubject("Welcome to hell")
+ .build))
+ .getMessageId
+ val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
+ messageBuilder.build))
+ .getMessageId
val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
- .setSubject("Yet another day in PaRaDiSeS")
+ .setSubject("Yet another day in PaRaDiSe")
.build))
.getMessageId
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 06/12: JAMES-3171 Enhance MailboxGetMethod reactive
management
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit d2f466c443c61d62e569e4abe4b4d5719cd999a1
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 14:06:12 2020 +0700
JAMES-3171 Enhance MailboxGetMethod reactive management
- Chain SMonos with `.zip`
- Remove unnecessary intermediate flatMaps
---
.../james/jmap/method/MailboxGetMethod.scala | 27 ++++++++++------------
1 file changed, 12 insertions(+), 15 deletions(-)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index c433d17..c13bf3a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -29,7 +29,7 @@ import org.apache.james.jmap.json.{MailboxSerializer, ResponseSerializer}
import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
import org.apache.james.jmap.mail.{Mailbox, MailboxFactory, MailboxGet, MailboxGetRequest, MailboxGetResponse, NotFound, PersonalNamespace, Subscriptions}
import org.apache.james.jmap.routes.SessionSupplier
-import org.apache.james.jmap.utils.quotas.{QuotaLoader, QuotaLoaderWithPreloadedDefaultFactory}
+import org.apache.james.jmap.utils.quotas.{QuotaLoaderWithPreloadedDefault, QuotaLoaderWithPreloadedDefaultFactory}
import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.mailbox.model.search.MailboxQuery
import org.apache.james.mailbox.model.{MailboxId, MailboxMetaData}
@@ -138,23 +138,20 @@ class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
val subscriptions: SMono[Subscriptions] = SMono.fromCallable(() =>
Subscriptions(subscriptionManager.subscriptions(mailboxSession).asScala.toSet))
- quotaFactory.loadFor(mailboxSession)
- .flatMap(quotaLoader => subscriptions.map[(QuotaLoader, Subscriptions)](subscriptions => (quotaLoader, subscriptions)))
+ SMono.zip(array => (array(0).asInstanceOf[Seq[MailboxMetaData]],
+ array(1).asInstanceOf[QuotaLoaderWithPreloadedDefault],
+ array(2).asInstanceOf[Subscriptions]),
+ getAllMailboxesMetaData(capabilities, mailboxSession),
+ quotaFactory.loadFor(mailboxSession),
+ subscriptions)
.subscribeOn(Schedulers.elastic)
- .flatMap {
- case (quotaLoader, subscriptions) => getAllMailboxesMetaData(capabilities, mailboxSession)
- .map((_, quotaLoader, subscriptions))
- }
.flatMapMany {
case (mailboxes, quotaLoader, subscriptions) => SFlux.fromIterable(mailboxes)
- .map(mailbox => (mailboxes, mailbox, quotaLoader, subscriptions))
- }
- .flatMap {
- case (mailboxes, mailbox, quotaLoader, subs) => mailboxFactory.create(mailboxMetaData = mailbox,
- mailboxSession = mailboxSession,
- subscriptions = subs,
- allMailboxesMetadata = mailboxes,
- quotaLoader = quotaLoader)
+ .flatMap(mailbox => mailboxFactory.create(mailboxMetaData = mailbox,
+ mailboxSession = mailboxSession,
+ subscriptions = subscriptions,
+ allMailboxesMetadata = mailboxes,
+ quotaLoader = quotaLoader))
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 10/12: JAMES-3413 Email/set updateValidation
message should pass on distributed james
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 433162e9405555d85f45f7a93c51652ccf8c1bc7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:54:56 2020 +0700
JAMES-3413 Email/set updateValidation message should pass on distributed james
---
.../distributed/DistributedEmailSetMethodTest.java | 5 ++
.../rfc8621/contract/EmailGetMethodContract.scala | 4 +-
.../rfc8621/contract/EmailSetMethodContract.scala | 60 +++++++++++-----------
.../rfc8621/memory/MemoryEmailSetMethodTest.java | 5 ++
.../jmap/method/EmailSetUpdatePerformer.scala | 2 +-
5 files changed, 44 insertions(+), 32 deletions(-)
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java
index 20a16f1..ec07ea6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java
@@ -62,4 +62,9 @@ public class DistributedEmailSetMethodTest implements EmailSetMethodContract {
public MessageId randomMessageId() {
return MESSAGE_ID_FACTORY.of(UUIDs.timeBased());
}
+
+ @Override
+ public String invalidMessageIdMessage(String invalid) {
+ return String.format("Invalid UUID string: %s", invalid);
+ }
}
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 20a1800..347c4ea 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
@@ -2417,7 +2417,7 @@ trait EmailGetMethodContract {
| "threadId": "${messageId.serialize}",
| "size": 2695,
| "keywords": {},
- | "blobId": "1",
+ | "blobId": "${messageId.serialize}",
| "mailboxIds": {"${mailboxId.serialize}": true},
| "id": "${messageId.serialize}",
| "receivedAt": "2014-10-30T14:12:00Z",
@@ -5885,7 +5885,7 @@ trait EmailGetMethodContract {
.inPath("methodResponses[0][1].list[0]")
.isEqualTo(
s"""{
- | "id": "1",
+ | "id": "${messageId.serialize}",
| "header:Bcc:asRaw": " \\"user3\\" user3@domain.tld",
| "header:MessageId:asRaw": null,
| "header:ReplyTo:asRaw": " \\"user1\\" user1@domain.tld",
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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index af77d0b..8bd5a9e 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -71,6 +71,8 @@ trait EmailSetMethodContract {
def randomMessageId: MessageId
+ def invalidMessageIdMessage(invalid: String): String
+
@Test
def shouldResetKeywords(server: GuiceJamesServer): Unit = {
val message: Message = Fixture.createTestMessage
@@ -426,9 +428,9 @@ trait EmailSetMethodContract {
assertThatJson(response)
.inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
.isEqualTo(
- """|{
+ s"""|{
| "type":"invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
|}""".stripMargin)
}
@@ -962,7 +964,7 @@ trait EmailSetMethodContract {
.inPath("methodResponses[0][1].notCreated.aaaaaa")
.isEqualTo(
s"""{
- | "description": "List((/mailboxIds,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))",
+ | "description": "List((/mailboxIds,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))",
| "type": "invalidArguments"
|}""".stripMargin)
}
@@ -3310,7 +3312,7 @@ trait EmailSetMethodContract {
.isEqualTo(
"""|{
| "type":"invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
|}""".stripMargin)
}
@@ -3366,7 +3368,7 @@ trait EmailSetMethodContract {
s"""{
| "${messageId.serialize}":{
| "type":"invalidPatch",
- | "description":"Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))),ArraySeq()))))"}
+ | "description":"Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))),ArraySeq()))))"}
| }
|}"""
.stripMargin)
@@ -3507,10 +3509,10 @@ trait EmailSetMethodContract {
assertThatJson(response)
.inPath("methodResponses[0][1].notUpdated")
- .isEqualTo("""{
+ .isEqualTo(s"""{
| "invalid": {
| "type":"invalidPatch",
- | "description":"Message invalid update is invalid: For input string: \"invalid\""
+ | "description":"Message update is invalid: ${invalidMessageIdMessage("invalid")}"
| }
|}""".stripMargin)
}
@@ -4122,7 +4124,7 @@ trait EmailSetMethodContract {
.isEqualTo(s"""{
| "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message 1 update is invalid: Partial update and reset specified for keywords"
+ | "description": "Message update is invalid: Partial update and reset specified for keywords"
| }
|}
""".stripMargin)
@@ -4172,7 +4174,7 @@ trait EmailSetMethodContract {
.isEqualTo(
"""|{
| "type":"invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(keywords/mus*c is an invalid entry in an Email/set update patch: FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))"}"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(keywords/mus*c is an invalid entry in an Email/set update patch: FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))"}"
|}""".stripMargin)
}
@@ -4219,9 +4221,9 @@ trait EmailSetMethodContract {
assertThatJson(response)
.inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
.isEqualTo(
- """|{
+ s"""|{
| "type":"invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords/movie is invalid: Keywords partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords/movie is invalid: Keywords partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
|}""".stripMargin)
}
@@ -4275,7 +4277,7 @@ trait EmailSetMethodContract {
s"""{
| "${messageId.serialize}":{
| "type":"invalidPatch",
- | "description":"Message 1 update is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))"}
+ | "description":"Message update is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))"}
| }
|}"""
.stripMargin)
@@ -4375,7 +4377,7 @@ trait EmailSetMethodContract {
| "notDestroyed": {
| "invalid": {
| "type": "invalidArguments",
- | "description": "invalid is not a messageId: For input string: \\"invalid\\""
+ | "description": "invalid is not a messageId: ${invalidMessageIdMessage("invalid")}"
| }
| }
| }, "c1"]]
@@ -4782,9 +4784,9 @@ trait EmailSetMethodContract {
.inPath("methodResponses[0][1].notUpdated")
.isEqualTo(
s"""{
- | "1": {
+ | "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(invalid is an invalid entry in an Email/set update patch),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(invalid is an invalid entry in an Email/set update patch),ArraySeq()))))"
| }
|}""".stripMargin)
}
@@ -4831,9 +4833,9 @@ trait EmailSetMethodContract {
.inPath("methodResponses[0][1].notUpdated")
.isEqualTo(
s"""{
- | "1": {
+ | "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(mailboxIds/invalid is an invalid entry in an Email/set update patch: For input string: \\"invalid\\"),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(mailboxIds/invalid is an invalid entry in an Email/set update patch: ${invalidMessageIdMessage("invalid")}),ArraySeq()))))"
| }
|}""".stripMargin)
}
@@ -4880,9 +4882,9 @@ trait EmailSetMethodContract {
.inPath("methodResponses[0][1].notUpdated")
.isEqualTo(
s"""{
- | "1": {
+ | "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds/1 is invalid: MailboxId partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds/${mailboxId1.serialize} is invalid: MailboxId partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
| }
|}""".stripMargin)
}
@@ -4932,9 +4934,9 @@ trait EmailSetMethodContract {
.inPath("methodResponses[0][1].notUpdated")
.isEqualTo(
s"""{
- | "1": {
+ | "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message 1 update is invalid: Partial update and reset specified for mailboxIds"
+ | "description": "Message update is invalid: Partial update and reset specified for mailboxIds"
| }
|}""".stripMargin)
}
@@ -4989,7 +4991,7 @@ trait EmailSetMethodContract {
| "notDestroyed": {
| "invalid": {
| "type": "invalidArguments",
- | "description": "invalid is not a messageId: For input string: \\"invalid\\""
+ | "description": "invalid is not a messageId: ${invalidMessageIdMessage("invalid")}"
| }
| }
| }, "c1"]
@@ -5224,9 +5226,9 @@ trait EmailSetMethodContract {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "newState": "000001",
| "notUpdated": {
- | "1": {
+ | "${messageId.serialize}": {
| "type": "notFound",
- | "description": "Cannot find message with messageId: 1"
+ | "description": "Cannot find message with messageId: ${messageId.serialize}"
| }
| }
| }, "c1"]
@@ -5381,7 +5383,7 @@ trait EmailSetMethodContract {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "newState": "000001",
| "notUpdated": {
- | "1": {
+ | "${messageId.serialize}": {
| "type": "notFound",
| "description": "Mailbox not found"
| }
@@ -5692,7 +5694,7 @@ trait EmailSetMethodContract {
| "notUpdated": {
| "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
| }
| }
| }, "c1"]
@@ -5753,7 +5755,7 @@ trait EmailSetMethodContract {
| "notUpdated": {
| "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
| }
| }
| }, "c1"]
@@ -5814,7 +5816,7 @@ trait EmailSetMethodContract {
| "notUpdated": {
| "${messageId.serialize}": {
| "type": "invalidPatch",
- | "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
| }
| }
| }, "c1"]
@@ -5895,7 +5897,7 @@ trait EmailSetMethodContract {
| "notUpdated": {
| "${messageId2.serialize}": {
| "type": "invalidPatch",
- | "description": "Message ${messageId2.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))),ArraySeq()))))"
+ | "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
| }
| }
| }, "c1"]
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
index cdf72f4..b67b80c 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
@@ -45,4 +45,9 @@ public class MemoryEmailSetMethodTest implements EmailSetMethodContract {
public MessageId randomMessageId() {
return InMemoryMessageId.of(ThreadLocalRandom.current().nextInt(100000) + 100);
}
+
+ @Override
+ public String invalidMessageIdMessage(String invalid) {
+ return String.format("For input string: \\\"%s\\\"", invalid);
+ }
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
index 094f823..c5a71b9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
@@ -46,7 +46,7 @@ object EmailSetUpdatePerformer {
case class EmailUpdateSuccess(messageId: MessageId) extends EmailUpdateResult
case class EmailUpdateFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends EmailUpdateResult {
def asMessageSetError: SetError = e match {
- case e: IllegalArgumentException => SetError.invalidPatch(SetErrorDescription(s"Message $unparsedMessageId update is invalid: ${e.getMessage}"))
+ case e: IllegalArgumentException => SetError.invalidPatch(SetErrorDescription(s"Message update is invalid: ${e.getMessage}"))
case _: MailboxNotFoundException => SetError.notFound(SetErrorDescription(s"Mailbox not found"))
case e: MessageNotFoundException => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 05/12: JAMES-3442 Email/set create position
multipart/alternative for text/html
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 900bc9c3311eb86f7527286864399d3dcc46958d
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Nov 11 16:10:16 2020 +0700
JAMES-3442 Email/set create position multipart/alternative for text/html
---
.../rfc8621/contract/EmailSetMethodContract.scala | 682 +++++++++++++++------
.../org/apache/james/jmap/mail/EmailSet.scala | 56 +-
.../jmap/method/EmailSetCreatePerformer.scala | 4 +-
3 files changed, 533 insertions(+), 209 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 378fbbb..af77d0b 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -1175,7 +1175,7 @@ trait EmailSetMethodContract {
| },
| "subject": "World domination",
| "bodyValues": {
- | "1": {
+ | "2": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -1254,7 +1254,7 @@ trait EmailSetMethodContract {
| },
| "subject": "World domination",
| "bodyValues": {
- | "1": {
+ | "2": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -1818,7 +1818,7 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "3",
+ | "partId": "5",
| "blobId": "$blobIdToDownload",
| "size": 11,
| "type": "text/plain",
@@ -1951,8 +1951,8 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "3",
- | "blobId": "${messageId}_3",
+ | "partId": "5",
+ | "blobId": "${messageId}_5",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -1961,15 +1961,15 @@ trait EmailSetMethodContract {
| ],
| "htmlBody": [
| {
- | "partId": "2",
- | "blobId": "${messageId}_2",
+ | "partId": "3",
+ | "blobId": "${messageId}_3",
| "size": 166,
| "type": "text/html",
| "charset": "UTF-8"
| }
| ],
| "bodyValues": {
- | "2": {
+ | "3": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -2097,8 +2097,8 @@ trait EmailSetMethodContract {
| "subject": "World domination",
| "attachments": [
| {
- | "partId": "4",
- | "blobId": "${messageId}_4",
+ | "partId": "6",
+ | "blobId": "${messageId}_6",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -2106,8 +2106,8 @@ trait EmailSetMethodContract {
| "cid": "abc"
| },
| {
- | "partId": "5",
- | "blobId": "${messageId}_5",
+ | "partId": "7",
+ | "blobId": "${messageId}_7",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -2115,8 +2115,8 @@ trait EmailSetMethodContract {
| "cid": "def"
| },
| {
- | "partId": "6",
- | "blobId": "${messageId}_6",
+ | "partId": "8",
+ | "blobId": "${messageId}_8",
| "size": 11,
| "type": "text/plain",
| "charset": "UTF-8",
@@ -2125,15 +2125,15 @@ trait EmailSetMethodContract {
| ],
| "htmlBody": [
| {
- | "partId": "3",
- | "blobId": "${messageId}_3",
+ | "partId": "4",
+ | "blobId": "${messageId}_4",
| "size": 166,
| "type": "text/html",
| "charset": "UTF-8"
| }
| ],
| "bodyValues": {
- | "3": {
+ | "4": {
| "value": "$htmlBody",
| "isEncodingProblem": false,
| "isTruncated": false
@@ -2253,63 +2253,63 @@ trait EmailSetMethodContract {
assertThatJson(response)
.isEqualTo(
s"""{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "Email/set",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "newState": "000001",
- | "created": {
- | "aaaaaa": {
- | "id": "$messageId"
- | }
- | }
- | },
- | "c1"
- | ],
- | [
- | "Email/get",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "000001",
- | "list": [
- | {
- | "id": "$messageId",
- | "bodyStructure": {
- | "type": "multipart/mixed",
- | "subParts": [
- | {
- | "type": "multipart/related",
- | "subParts": [
- | {
- | "type": "text/html"
- | },
- | {
- | "type": "text/plain",
- | "disposition": "inline",
- | "cid": "abc"
- | },
- | {
- | "type": "text/plain",
- | "disposition": "inline",
- | "cid": "def"
- | }
- | ]
- | },
- | {
- | "type": "text/plain",
- | "disposition": "attachment"
- | }
- | ]
- | }
- | }
- | ],
- | "notFound": []
- | },
- | "c2"
- | ]
- | ]
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/mixed",
+ | "subParts": [
+ | {
+ | "type": "multipart/related",
+ | "subParts": [
+ | {
+ | "type": "multipart/alternative",
+ | "subParts": [
+ | {
+ | "type": "text/html"
+ | },
+ | {
+ | "type": "text/plain"
+ | }
+ | ]
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "def"
+ | }
+ | ]
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "attachment"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | }, "c2"]
+ | ]
|}""".stripMargin)
}
@@ -2410,48 +2410,48 @@ trait EmailSetMethodContract {
assertThatJson(response)
.isEqualTo(
s"""{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "Email/set",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "newState": "000001",
- | "created": {
- | "aaaaaa": {
- | "id": "$messageId"
- | }
- | }
- | },
- | "c1"
- | ],
- | [
- | "Email/get",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "000001",
- | "list": [
- | {
- | "id": "$messageId",
- | "bodyStructure": {
- | "type": "multipart/mixed",
- | "subParts": [
- | {
- | "type": "text/html"
- | },
- | {
- | "type": "text/plain",
- | "disposition": "attachment"
- | }
- | ]
- | }
- | }
- | ],
- | "notFound": []
- | },
- | "c2"
- | ]
- | ]
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/mixed",
+ | "subParts": [
+ | {
+ | "type":"multipart/alternative",
+ | "subParts": [
+ | {
+ | "type":"text/html"
+ | },
+ | {
+ | "type":"text/plain"
+ | }
+ | ]
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "attachment"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | }, "c2"]
+ | ]
|}""".stripMargin)
}
@@ -2560,54 +2560,54 @@ trait EmailSetMethodContract {
assertThatJson(response)
.isEqualTo(
s"""{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "Email/set",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "newState": "000001",
- | "created": {
- | "aaaaaa": {
- | "id": "$messageId"
- | }
- | }
- | },
- | "c1"
- | ],
- | [
- | "Email/get",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "000001",
- | "list": [
- | {
- | "id": "$messageId",
- | "bodyStructure": {
- | "type": "multipart/related",
- | "subParts": [
- | {
- | "type": "text/html"
- | },
- | {
- | "type": "text/plain",
- | "disposition": "inline",
- | "cid": "abc"
- | },
- | {
- | "type": "text/plain",
- | "disposition": "inline",
- | "cid": "def"
- | }
- | ]
- | }
- | }
- | ],
- | "notFound": []
- | },
- | "c2"
- | ]
- | ]
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/related",
+ | "subParts": [
+ | {
+ | "type":"multipart/alternative",
+ | "subParts": [
+ | {
+ | "type":"text/html"
+ | },
+ | {
+ | "type":"text/plain"
+ | }
+ | ]
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "def"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | }, "c2"]
+ | ]
|}""".stripMargin)
}
@@ -2631,6 +2631,254 @@ trait EmailSetMethodContract {
.body
.asString
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "attachments": [],
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["bodyStructure"],
+ | "bodyProperties": ["type", "disposition", "cid", "subParts"]
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type":"multipart/alternative",
+ | "subParts": [
+ | {
+ | "type":"text/html"
+ | },
+ | {
+ | "type":"text/plain"
+ | }
+ | ]
+ | }
+ | }
+ | ], "notFound": []
+ | }, "c2"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def createShouldSupportHtmlAndTextBody(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "create": {
+ | "aaaaaa": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": true
+ | },
+ | "subject": "World domination",
+ | "htmlBody": [
+ | {
+ | "partId": "a49d",
+ | "type": "text/html"
+ | }
+ | ],
+ | "bodyValues": {
+ | "a49d": {
+ | "value": "$htmlBody",
+ | "isTruncated": false,
+ | "isEncodingProblem": false
+ | }
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["#aaaaaa"],
+ | "properties": ["bodyStructure", "bodyValues"],
+ | "bodyProperties": ["type", "disposition", "cid", "subParts", "charset"],
+ | "fetchAllBodyValues": true
+ | },
+ | "c2"]
+ | ]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+ .inPath("methodResponses[0][1].created.aaaaaa")
+ .isEqualTo("{}".stripMargin)
+
+ val messageId = Json.parse(response)
+ .\("methodResponses")
+ .\(1).\(1)
+ .\("list")
+ .\(0)
+ .\("id")
+ .get.asInstanceOf[JsString].value
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/alternative",
+ | "charset": "us-ascii",
+ | "subParts": [
+ | {
+ | "type": "text/html",
+ | "charset": "UTF-8"
+ | },
+ | {
+ | "type": "text/plain",
+ | "charset": "UTF-8"
+ | }
+ | ]
+ | },
+ | "bodyValues": {
+ | "2": {
+ | "value": "$htmlBody",
+ | "isEncodingProblem": false,
+ | "isTruncated": false
+ | },
+ | "3": {
+ | "value": "I have the most brilliant plan. Let me tell you all about it. What we do is, we",
+ | "isEncodingProblem": false,
+ | "isTruncated": false
+ | }
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | }, "c2"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def createShouldWrapInlineBodyWithAlternativeMultipart(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+ val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+ val uploadResponse: String = `given`
+ .basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .contentType("text/plain")
+ .body(payload)
+ .when
+ .post(s"/upload/$ACCOUNT_ID/")
+ .`then`
+ .statusCode(SC_CREATED)
+ .extract
+ .body
+ .asString
+
val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
val request =
@@ -2645,7 +2893,28 @@ trait EmailSetMethodContract {
| "${mailboxId.serialize}": true
| },
| "subject": "World domination",
- | "attachments": [],
+ | "attachments": [
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "inline",
+ | "cid": "def"
+ | },
+ | {
+ | "blobId": "$blobId",
+ | "type":"text/plain",
+ | "charset":"UTF-8",
+ | "disposition": "attachment"
+ | }
+ | ],
| "htmlBody": [
| {
| "partId": "a49d",
@@ -2699,41 +2968,66 @@ trait EmailSetMethodContract {
.get.asInstanceOf[JsString].value
assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
.isEqualTo(
s"""{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "Email/set",
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "created": {
+ | "aaaaaa": {
+ | "id": "$messageId"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "$messageId",
+ | "bodyStructure": {
+ | "type": "multipart/mixed",
+ | "subParts": [
| {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "newState": "000001",
- | "created": {
- | "aaaaaa": {
- | "id": "$messageId"
+ | "type": "multipart/related",
+ | "subParts": [
+ | {
+ | "type": "multipart/alternative",
+ | "subParts": [
+ | {
+ | "type": "text/html"
+ | },
+ | {
+ | "type": "text/plain"
| }
+ | ]
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "abc"
+ | },
+ | {
+ | "type": "text/plain",
+ | "disposition": "inline",
+ | "cid": "def"
| }
+ | ]
| },
- | "c1"
- | ],
- | [
- | "Email/get",
| {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "000001",
- | "list": [
- | {
- | "id": "$messageId",
- | "bodyStructure": {
- | "type": "text/html"
- | }
- | }
- | ],
- | "notFound": []
- | },
- | "c2"
- | ]
- | ]
+ | "type": "text/plain",
+ | "disposition": "attachment"
+ | }
+ | ]
+ | }
+ | }
+ | ],
+ | "notFound": []
+ | }, "c2"]
+ | ]
|}""".stripMargin)
}
@@ -2834,7 +3128,7 @@ trait EmailSetMethodContract {
| "attachments": [
| {
| "name": "myAttachment",
- | "partId": "3",
+ | "partId": "5",
| "blobId": "$blobIdToDownload",
| "size": 11,
| "type": "text/plain",
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index 464be6d..ca1af42 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -19,10 +19,12 @@
package org.apache.james.jmap.mail
import java.io.IOException
-import java.nio.charset.StandardCharsets
+import java.nio.charset.{StandardCharsets, Charset => NioCharset}
import java.util.Date
import cats.implicits._
+import com.google.common.net.MediaType
+import com.google.common.net.MediaType.{HTML_UTF_8, PLAIN_TEXT_UTF_8}
import eu.timepit.refined
import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.NonEmpty
@@ -41,7 +43,8 @@ import org.apache.james.mime4j.dom.field.{ContentIdField, ContentTypeField, Fiel
import org.apache.james.mime4j.dom.{Entity, Message}
import org.apache.james.mime4j.field.Fields
import org.apache.james.mime4j.message.{BodyPartBuilder, MultipartBuilder}
-import org.apache.james.mime4j.stream.{Field, RawField}
+import org.apache.james.mime4j.stream.{Field, NameValuePair, RawField}
+import org.apache.james.util.html.HtmlTextExtractor
import play.api.libs.json.JsObject
import scala.jdk.CollectionConverters._
@@ -66,6 +69,7 @@ object SubType {
val HTML_SUBTYPE = "html"
val MIXED_SUBTYPE = "mixed"
val RELATED_SUBTYPE = "related"
+ val ALTERNATIVE_SUBTYPE = "alternative"
}
case class ClientPartId(id: Id)
@@ -119,7 +123,10 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
bodyValues: Option[Map[ClientPartId, ClientEmailBodyValue]],
specificHeaders: List[EmailHeader],
attachments: Option[List[Attachment]]) {
- def toMime4JMessage(attachmentManager: AttachmentManager, attachmentContentLoader: AttachmentContentLoader, mailboxSession: MailboxSession): Either[Exception, Message] =
+ def toMime4JMessage(attachmentManager: AttachmentManager,
+ attachmentContentLoader: AttachmentContentLoader,
+ htmlTextExtractor: HtmlTextExtractor,
+ mailboxSession: MailboxSession): Either[Exception, Message] =
validateHtmlBody
.flatMap(maybeHtmlBody => {
val builder = Message.Builder.of
@@ -138,19 +145,37 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
.flatMap(_ => {
specificHeaders.map(_.asField).foreach(builder.addField)
attachments.filter(_.nonEmpty).map(attachments =>
- createMultipartWithAttachments(maybeHtmlBody, attachments, attachmentManager, attachmentContentLoader, mailboxSession)
+ createMultipartWithAttachments(maybeHtmlBody, attachments, attachmentManager, attachmentContentLoader, htmlTextExtractor, mailboxSession)
.map(multipartBuilder => {
builder.setBody(multipartBuilder)
builder.build
}))
- .getOrElse(Right(builder.setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build))
+ .getOrElse({
+ builder.setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor))
+ Right(builder.build)
+ })
})
})
+ private def createAlternativeBody(htmlBody: Option[String], htmlTextExtractor: HtmlTextExtractor): MultipartBuilder = {
+ val alternativeBuilder: MultipartBuilder = MultipartBuilder.create(SubType.ALTERNATIVE_SUBTYPE)
+ addBodypart(alternativeBuilder, htmlBody.getOrElse(""), HTML_UTF_8, StandardCharsets.UTF_8)
+ addBodypart(alternativeBuilder, htmlTextExtractor.toPlainText(htmlBody.getOrElse("")), PLAIN_TEXT_UTF_8, StandardCharsets.UTF_8)
+
+ alternativeBuilder
+ }
+
+ private def addBodypart(multipartBuilder: MultipartBuilder, body: String, mediaType: MediaType, charset: NioCharset): MultipartBuilder =
+ multipartBuilder.addBodyPart(
+ BodyPartBuilder.create.setBody(body, charset)
+ .setContentType(mediaType.withoutParameters().toString, new NameValuePair("charset", charset.name))
+ .setContentTransferEncoding("quoted-printable"))
+
private def createMultipartWithAttachments(maybeHtmlBody: Option[String],
attachments: List[Attachment],
attachmentManager: AttachmentManager,
attachmentContentLoader: AttachmentContentLoader,
+ htmlTextExtractor: HtmlTextExtractor,
mailboxSession: MailboxSession): Either[Exception, MultipartBuilder] = {
val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
attachments
@@ -162,17 +187,20 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
maybeAttachments.map(list => {
(list.filter(_._1.isInline), list.filter(!_._1.isInline)) match {
- case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, normalAttachments)
- case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, inlineAttachments)
- case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, inlineAttachments, normalAttachments)
+ case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, normalAttachments, htmlTextExtractor)
+ case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, inlineAttachments, htmlTextExtractor)
+ case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, inlineAttachments, normalAttachments, htmlTextExtractor)
}
})
}
- private def createMixedRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+ private def createMixedRelatedBody(maybeHtmlBody: Option[String],
+ inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])],
+ normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])],
+ htmlTextExtractor: HtmlTextExtractor) = {
val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
- relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor).build))
inlineAttachments.foldLeft(relatedMultipartBuilder) {
case (acc, (attachment, storedMetadata, content)) =>
acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
@@ -188,9 +216,9 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
}
}
- private def createMixedBody(maybeHtmlBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+ private def createMixedBody(maybeHtmlBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
- mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor).build))
normalAttachments.foldLeft(mixedMultipartBuilder) {
case (acc, (attachment, storedMetadata, content)) =>
acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
@@ -198,9 +226,9 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
}
}
- private def createRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+ private def createRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
- relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+ relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor).build))
inlineAttachments.foldLeft(relatedMultipartBuilder) {
case (acc, (attachment, storedMetadata, content)) =>
acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
index 1fcac8c..662c750 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
@@ -35,6 +35,7 @@ import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.exception.{AttachmentNotFoundException, MailboxNotFoundException}
import org.apache.james.mailbox.model.MailboxId
import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxManager, MailboxSession}
+import org.apache.james.util.html.HtmlTextExtractor
import reactor.core.scala.publisher.{SFlux, SMono}
import reactor.core.scheduler.Schedulers
@@ -71,6 +72,7 @@ object EmailSetCreatePerformer {
class EmailSetCreatePerformer @Inject()(serializer: EmailSetSerializer,
attachmentManager: AttachmentManager,
attachmentContentLoader: AttachmentContentLoader,
+ htmlTextExtractor: HtmlTextExtractor,
mailboxManager: MailboxManager) {
def create(request: EmailSetRequest, mailboxSession: MailboxSession): SMono[CreationResults] =
@@ -87,7 +89,7 @@ class EmailSetCreatePerformer @Inject()(serializer: EmailSetSerializer,
if (mailboxIds.size != 1) {
SMono.just(CreationFailure(clientId, new IllegalArgumentException("mailboxIds need to have size 1")))
} else {
- request.toMime4JMessage(attachmentManager, attachmentContentLoader, mailboxSession)
+ request.toMime4JMessage(attachmentManager, attachmentContentLoader, htmlTextExtractor, mailboxSession)
.fold(e => SMono.just(CreationFailure(clientId, e)),
message => SMono.fromCallable[CreationResult](() => {
val appendResult = mailboxManager.getMailbox(mailboxIds.head, mailboxSession)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 09/12: JAMES-3373 Avoid using hamcrest
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 87b3678f88f424d29eb91da91e0b009eae29beaa
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:39:05 2020 +0700
JAMES-3373 Avoid using hamcrest
---
.../james/jmap/rfc8621/contract/DownloadContract.scala | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 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/DownloadContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
index 20579b9..f10ef4d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
@@ -36,7 +36,7 @@ import org.apache.james.mailbox.model.{MailboxACL, MailboxPath, MessageId}
import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
import org.apache.james.utils.DataProbeImpl
import org.assertj.core.api.Assertions.assertThat
-import org.hamcrest.Matchers.{containsString, emptyOrNullString}
+import org.hamcrest.Matchers.containsString
import org.junit.jupiter.api.{BeforeEach, Test}
object DownloadContract {
@@ -303,14 +303,17 @@ trait DownloadContract {
ClassLoader.getSystemResourceAsStream("eml/multipart_simple.eml")))
.getMessageId
- `given`
+ val contentDisposition = `given`
.basePath("")
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.when
.get(s"/download/$accountId/${messageId.serialize()}_3")
.`then`
.statusCode(SC_OK)
- .header("Content-Disposition", emptyOrNullString())
+ .extract()
+ .header("Content-Disposition")
+
+ assertThat(contentDisposition).isNullOrEmpty()
}
@Test
@@ -342,14 +345,16 @@ trait DownloadContract {
ClassLoader.getSystemResourceAsStream("eml/multipart_simple.eml")))
.getMessageId
- `given`
+ val contentDisposition = `given`
.basePath("")
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.when
.get(s"/download/$accountId/${messageId.serialize()}")
.`then`
.statusCode(SC_OK)
- .header("Content-Disposition", emptyOrNullString())
+ .extract().header("Content-Disposition")
+
+ assertThat(contentDisposition).isNullOrEmpty()
}
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org