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/11 04:42:55 UTC

[james-project] 03/07: JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)

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 78ce27cd5d548768bfebddd25dfb0b2c38258248
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 10:57:12 2020 +0700

    JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 54 ++++------------
 .../james/jmap/json/EmailGetSerializer.scala       |  5 +-
 .../james/jmap/json/EmailSetSerializer.scala       |  8 ++-
 .../org/apache/james/jmap/mail/EmailBodyPart.scala | 54 ++++++++++------
 .../org/apache/james/jmap/mail/EmailSet.scala      | 71 ++++++++++++----------
 .../jmap/method/EmailSetCreatePerformer.scala      |  5 +-
 6 files changed, 97 insertions(+), 100 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 47b7a64..32322e7 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
@@ -1932,14 +1932,12 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].created.aaaaaa")
       .isEqualTo("{}".stripMargin)
 
-    val blobIdToDownload = Json.parse(response)
+    val messageId = Json.parse(response)
       .\("methodResponses")
       .\(1).\(1)
       .\("list")
       .\(0)
-      .\("attachments")
-      .\(0)
-      .\("blobId")
+      .\("id")
       .get.asInstanceOf[JsString].value
 
     assertThatJson(response)
@@ -1954,7 +1952,7 @@ trait EmailSetMethodContract {
            |  "attachments": [
            |    {
            |      "partId": "3",
-           |      "blobId": "$blobIdToDownload",
+           |      "blobId": "${messageId}_3",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -1964,7 +1962,7 @@ trait EmailSetMethodContract {
            |  "htmlBody": [
            |    {
            |      "partId": "2",
-           |      "blobId": "1_2",
+           |      "blobId": "${messageId}_2",
            |      "size": 166,
            |      "type": "text/html",
            |      "charset": "UTF-8"
@@ -1978,21 +1976,6 @@ trait EmailSetMethodContract {
            |    }
            |  }
            |}]""".stripMargin)
-
-    val downloadResponse = `given`
-      .basePath("")
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-    .when
-      .get(s"/download/$accountId/$blobIdToDownload")
-    .`then`
-      .statusCode(SC_OK)
-      .contentType("text/plain")
-      .extract
-      .body
-      .asInputStream()
-
-    assertThat(downloadResponse)
-      .hasSameContentAs(new ByteArrayInputStream(payload))
   }
 
   @Test
@@ -2006,9 +1989,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
@@ -2057,7 +2040,7 @@ trait EmailSetMethodContract {
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
     .when
-      .post.prettyPeek()
+      .post
     .`then`
       .statusCode(SC_OK)
       .contentType(JSON)
@@ -2103,21 +2086,6 @@ trait EmailSetMethodContract {
            |    }
            |  ]
            |}]""".stripMargin)
-
-    val downloadResponse = `given`
-      .basePath("")
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-    .when
-      .get(s"/download/$accountId/$blobIdToDownload")
-    .`then`
-      .statusCode(SC_OK)
-      .contentType("text/plain")
-      .extract
-      .body
-      .asInputStream()
-
-    assertThat(downloadResponse)
-      .hasSameContentAs(new ByteArrayInputStream(payload))
   }
 
   @Test
@@ -2167,7 +2135,8 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notCreated.aaaaaa")
       .isEqualTo(s"""{
         |  "type": "invalidArguments",
-        |  "description": "Attachment not found: 123"
+        |  "description": "Attachment not found: 123",
+        |  "properties": ["attachments"]
         |}""".stripMargin)
   }
 
@@ -2224,7 +2193,7 @@ trait EmailSetMethodContract {
         .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
         .setBody(request)
         .build, new ResponseSpecBuilder().build)
-    .post
+      .post
     .`then`
       .statusCode(SC_OK)
       .contentType(JSON)
@@ -2237,7 +2206,8 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notCreated.aaaaaa")
       .isEqualTo(s"""{
                     |  "type": "invalidArguments",
-                    |  "description": "Attachment not found: $blobId"
+                    |  "description": "Attachment not found: $blobId",
+                    |  "properties": ["attachments"]
                     |}""".stripMargin)
   }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index fb89158..4d34d5b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -21,7 +21,7 @@ package org.apache.james.jmap.json
 
 import org.apache.james.jmap.api.model.Preview
 import org.apache.james.jmap.core.Properties
-import org.apache.james.jmap.mail.{AddressesHeaderValue, BlobId, Charset, DateHeaderValue, Disposition, EmailAddress, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, Group [...]
+import org.apache.james.jmap.mail._
 import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
@@ -42,7 +42,8 @@ object EmailGetSerializer {
   private implicit val typeWrites: Writes[Type] = Json.valueWrites[Type]
   private implicit val charsetWrites: Writes[Charset] = Json.valueWrites[Charset]
   private implicit val dispositionWrites: Writes[Disposition] = Json.valueWrites[Disposition]
-  private implicit val languageWrites: Writes[Language] = Json.valueWrites[Language]
+  private implicit val languageWrites: Format[Language] = Json.valueFormat[Language]
+  private implicit val languagesFormat: Format[Languages] = Json.valueFormat[Languages]
   private implicit val locationWrites: Writes[Location] = Json.valueWrites[Location]
   private implicit val emailerNameWrites: Writes[EmailerName] = Json.valueWrites[EmailerName]
   private implicit val emailAddressWrites: Writes[EmailAddress] = Json.writes[EmailAddress]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index d7f467c..fa97676 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -28,9 +28,9 @@ import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.{Id, SetError, UTCDate}
 import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId, UnparsedMessageIdConstraint}
 import org.apache.james.jmap.mail.KeywordsFactory.STRICT_KEYWORDS_FACTORY
-import org.apache.james.jmap.mail.{AddressesHeaderValue, AsAddresses, AsDate, AsGroupedAddresses, AsMessageIds, AsRaw, AsText, AsURLs, Attachment, BlobId, Charset, ClientEmailBodyValue, ClientHtmlBody, ClientPartId, DateHeaderValue, DestroyIds, Disposition, EmailAddress, EmailAddressGroup, EmailCreationRequest, EmailCreationResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailSetRequest, EmailSetResponse, EmailSetUpdate, EmailerName, GroupName, GroupedAddressesHeaderValue, Head [...]
+import org.apache.james.jmap.mail.{AddressesHeaderValue, AsAddresses, AsDate, AsGroupedAddresses, AsMessageIds, AsRaw, AsText, AsURLs, Attachment, BlobId, Charset, ClientCid, ClientEmailBodyValue, ClientHtmlBody, ClientPartId, DateHeaderValue, DestroyIds, Disposition, EmailAddress, EmailAddressGroup, EmailCreationRequest, EmailCreationResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailSetRequest, EmailSetResponse, EmailSetUpdate, EmailerName, GroupName, GroupedAddressesHeader [...]
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
-import play.api.libs.json.{JsArray, JsBoolean, JsError, JsNull, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
+import play.api.libs.json.{Format, JsArray, JsBoolean, JsError, JsNull, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
 
 import scala.util.Try
 
@@ -353,8 +353,10 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   private implicit val nameReads: Reads[Name] = Json.valueReads[Name]
   private implicit val charsetReads: Reads[Charset] = Json.valueReads[Charset]
   private implicit val dispositionReads: Reads[Disposition] = Json.valueReads[Disposition]
-  private implicit val languageReads: Reads[Language] = Json.valueReads[Language]
+  private implicit val languageReads: Format[Language] = Json.valueFormat[Language]
+  private implicit val languagesWrites: Format[Languages] = Json.valueFormat[Languages]
   private implicit val locationReads: Reads[Location] = Json.valueReads[Location]
+  private implicit val cidFormat: Format[ClientCid] = Json.valueFormat[ClientCid]
   private implicit val attachmentReads: Reads[Attachment] = Json.reads[Attachment]
   private implicit val emailCreationRequestWithoutHeadersReads: Reads[EmailCreationRequestWithoutHeaders] = Json.reads[EmailCreationRequestWithoutHeaders]
   private implicit val emailCreationRequestReads: Reads[EmailCreationRequest] = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index 0c08138..348fe37 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -30,16 +30,16 @@ import eu.timepit.refined.refineV
 import org.apache.commons.io.IOUtils
 import org.apache.james.jmap.core.Properties
 import org.apache.james.jmap.mail.Email.Size
-import org.apache.james.jmap.mail.EmailBodyPart.{MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN}
+import org.apache.james.jmap.mail.EmailBodyPart.{FILENAME_PREFIX, MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN}
 import org.apache.james.jmap.mail.PartId.PartIdValue
 import org.apache.james.mailbox.model.{Cid, MessageId, MessageResult}
 import org.apache.james.mime4j.codec.{DecodeMonitor, DecoderUtil}
+import org.apache.james.mime4j.dom.field.{ContentLanguageField, ContentTypeField, FieldName}
 import org.apache.james.mime4j.dom.{Entity, Message, Multipart, TextBody => Mime4JTextBody}
 import org.apache.james.mime4j.message.{DefaultMessageBuilder, DefaultMessageWriter}
-import org.apache.james.mime4j.stream.MimeConfig
+import org.apache.james.mime4j.stream.{Field, MimeConfig, RawField}
 
 import scala.jdk.CollectionConverters._
-import scala.jdk.OptionConverters._
 import scala.util.{Failure, Success, Try}
 
 object PartId {
@@ -124,24 +124,12 @@ object EmailBodyPart {
           blobId = blobId,
           headers = entity.getHeader.getFields.asScala.toList.map(EmailHeader(_)),
           size = size,
-          name = Option(entity.getFilename).map(Name)
-              .orElse({
-                headerValue(entity, "Content-Type")
-                  .map(value => DecoderUtil.decodeEncodedWords(value, DecodeMonitor.SILENT))
-                  .filter(_.contains(FILENAME_PREFIX))
-                  .map(v => Name(v.substring(v.indexOf(FILENAME_PREFIX) + FILENAME_PREFIX.length + 1).replace("\"", "")))
-              }),
+          name = Name.of(entity),
           `type` = Type(entity.getMimeType),
           charset = Option(entity.getCharset).map(Charset),
           disposition = Option(entity.getDispositionType).map(Disposition(_)),
-          cid = headerValue(entity, "Content-Id")
-            .flatMap(Cid.parser()
-              .relaxed()
-              .unwrap()
-              .parse(_)
-              .toScala),
-          language = headerValue(entity, "Content-Language")
-            .map(_.split("; ").toList.map(Language)),
+          cid = ClientCid.of(entity),
+          language = Languages.of(entity),
           location = headerValue(entity, "Content-Location")
             .map(Location),
           subParts = subParts,
@@ -169,6 +157,15 @@ object EmailBodyPart {
   } yield (aValue, bValue)
 }
 
+object Name {
+  def of(entity: Entity): Option[Name] = Option(entity.getHeader.getField(FieldName.CONTENT_TYPE))
+    .flatMap {
+      case contentTypeField: ContentTypeField => Option(contentTypeField.getParameter(FILENAME_PREFIX))
+          .map(DecoderUtil.decodeEncodedWords(_, DecodeMonitor.SILENT))
+      case _ => None
+    }.map(Name(_))
+}
+
 case class Name(value: String)
 case class Type(value: String)
 case class Charset(value: String)
@@ -179,8 +176,25 @@ object Disposition {
 }
 
 case class Disposition(value: String)
+
+object Languages {
+  def of(entity: Entity): Option[Languages] =
+    Option(entity.getHeader.getField(FieldName.CONTENT_LANGUAGE))
+      .flatMap {
+        case contentLanguageField: ContentLanguageField => Some(Languages(contentLanguageField.getLanguages.asScala.toList.map(Language)))
+        case _ => None
+      }
+}
+
+case class Languages(value: List[Language]) {
+  def asField: Field = new RawField("Content-Language", value.map(_.value).mkString(", "))
+}
+
 case class Language(value: String)
-case class Location(value: String)
+
+case class Location(value: String) {
+  def asField: Field = new RawField("Content-Location", value)
+}
 
 case class EmailBodyPart(partId: PartId,
                          blobId: Option[BlobId],
@@ -191,7 +205,7 @@ case class EmailBodyPart(partId: PartId,
                          charset: Option[Charset],
                          disposition: Option[Disposition],
                          cid: Option[Cid],
-                         language: Option[List[Language]],
+                         language: Option[Languages],
                          location: Option[Location],
                          subParts: Option[List[EmailBodyPart]],
                          entity: Entity) {
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 585017e..eb06e54 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
@@ -1,4 +1,4 @@
-/****************************************************************
+/** **************************************************************
  * Licensed to the Apache Software Foundation (ASF) under one   *
  * or more contributor license agreements.  See the NOTICE file *
  * distributed with this work for additional information        *
@@ -6,16 +6,16 @@
  * to you under the Apache License, Version 2.0 (the            *
  * "License"); you may not use this file except in compliance   *
  * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0                 *
+ * *
  * Unless required by applicable law or agreed to in writing,   *
  * software distributed under the License is distributed on an  *
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- ****************************************************************/
+ * ***************************************************************/
 package org.apache.james.jmap.mail
 
 import java.io.IOException
@@ -32,18 +32,19 @@ import org.apache.james.jmap.core.{AccountId, SetError, UTCDate}
 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}
+import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, MessageId, Cid => MailboxCid}
 import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxSession}
 import org.apache.james.mime4j.codec.EncoderUtil
 import org.apache.james.mime4j.codec.EncoderUtil.Usage
-import org.apache.james.mime4j.dom.Message
-import org.apache.james.mime4j.dom.field.{ContentTypeField, FieldName}
+import org.apache.james.mime4j.dom.field.{ContentIdField, ContentTypeField, FieldName}
+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.RawField
+import org.apache.james.mime4j.stream.{Field, RawField}
 import play.api.libs.json.JsObject
 
 import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
 import scala.util.{Right, Try}
 
 object EmailSet {
@@ -73,14 +74,27 @@ case class ClientEmailBodyValue(value: String,
                                 isEncodingProblem: Option[IsEncodingProblem],
                                 isTruncated: Option[IsTruncated])
 
+object ClientCid {
+  def of(entity: Entity): Option[MailboxCid] =
+    Option(entity.getHeader.getField(FieldName.CONTENT_ID))
+      .flatMap {
+        case contentIdField: ContentIdField => MailboxCid.parser().relaxed().unwrap().parse(contentIdField.getId).toScala
+        case _ => None
+      }
+}
+
+case class ClientCid(value: String) {
+  def asField: Field = new RawField("Content-ID", value)
+}
+
 case class Attachment(blobId: BlobId,
                       `type`: Type,
                       name: Option[Name],
                       charset: Option[Charset],
                       disposition: Option[Disposition],
-                      language: Option[List[Language]],
+                      language: Option[Languages],
                       location: Option[Location],
-                      cid: Option[String]) {
+                      cid: Option[ClientCid]) {
 
   def isInline: Boolean = disposition.contains("inline")
 }
@@ -137,7 +151,6 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
                                              attachmentContentLoader: AttachmentContentLoader,
                                              mailboxSession: MailboxSession): Either[Exception, MultipartBuilder] = {
     val multipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
-    val bodypartBuilder = BodyPartBuilder.create()
 
     val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
       attachments
@@ -147,26 +160,20 @@ 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) {
-        case (builder, (attachment, attachmentMetadata, content)) =>
+        case (acc, (attachment, storedMetadata, content)) =>
+          val bodypartBuilder = BodyPartBuilder.create()
           bodypartBuilder.setBody(content, attachment.`type`.value)
-            .setField(contentTypeField(attachment, attachmentMetadata))
+            .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)
 
-          if (attachment.cid.isDefined) {
-            bodypartBuilder.setField(new RawField("Content-ID", attachment.cid.get))
-          }
-          if(attachment.location.isDefined) {
-            bodypartBuilder.setField(new RawField("Content-Location", attachment.location.map(_.value).get))
-          }
-          if(attachment.language.isDefined) {
-            bodypartBuilder.setField(new RawField("Content-Language", attachment.language.get.map(_.value).mkString("; ")))
-          }
-
-          builder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
-          builder.addBodyPart(bodypartBuilder)
-          builder
+          acc.addBodyPart(bodypartBuilder)
+          acc
       }
     })
   }
@@ -264,7 +271,7 @@ case class EmailSetUpdate(keywords: Option[Keywords],
       Left(new IllegalArgumentException("Partial update and reset specified for mailboxIds"))
     } else if (keywords.isDefined && (keywordsToAdd.isDefined || keywordsToRemove.isDefined)) {
       Left(new IllegalArgumentException("Partial update and reset specified for keywords"))
-    } else  {
+    } else {
       val mailboxIdsIdentity: Function[MailboxIds, MailboxIds] = ids => ids
       val mailboxIdsAddition: Function[MailboxIds, MailboxIds] = mailboxIdsToAdd
         .map(toBeAdded => (ids: MailboxIds) => ids ++ toBeAdded)
@@ -307,12 +314,14 @@ case class EmailSetUpdate(keywords: Option[Keywords],
     mailboxIdsToAdd.isEmpty && mailboxIdsToRemove.isEmpty
 }
 
-case class ValidatedEmailSetUpdate private (keywordsTransformation: Function[Keywords, Keywords],
-                                            mailboxIdsTransformation: Function[MailboxIds, MailboxIds],
-                                            update: EmailSetUpdate)
+case class ValidatedEmailSetUpdate private(keywordsTransformation: Function[Keywords, Keywords],
+                                           mailboxIdsTransformation: Function[MailboxIds, MailboxIds],
+                                           update: EmailSetUpdate)
 
 class EmailUpdateValidationException() extends IllegalArgumentException
+
 case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
+
 case class InvalidEmailUpdateException(property: String, cause: String) extends EmailUpdateValidationException
 
 case class EmailCreationResponse(id: MessageId)
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 3008d70..1fcac8c 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
@@ -22,10 +22,11 @@ package org.apache.james.jmap.method
 import java.time.ZonedDateTime
 import java.util.Date
 
+import eu.timepit.refined.auto._
 import javax.inject.Inject
 import javax.mail.Flags
 import org.apache.james.jmap.core.SetError.SetErrorDescription
-import org.apache.james.jmap.core.{SetError, UTCDate}
+import org.apache.james.jmap.core.{Properties, SetError, UTCDate}
 import org.apache.james.jmap.json.EmailSetSerializer
 import org.apache.james.jmap.mail.EmailSet.EmailCreationId
 import org.apache.james.jmap.mail.{EmailCreationRequest, EmailCreationResponse, EmailSetRequest}
@@ -60,7 +61,7 @@ object EmailSetCreatePerformer {
   case class CreationFailure(clientId: EmailCreationId, e: Throwable) extends CreationResult {
     def asMessageSetError: SetError = e match {
       case e: MailboxNotFoundException => SetError.notFound(SetErrorDescription("Mailbox " + e.getMessage))
-      case e: AttachmentNotFoundException => SetError.invalidArguments(SetErrorDescription(s"${e.getMessage}"))
+      case e: AttachmentNotFoundException => SetError.invalidArguments(SetErrorDescription(s"${e.getMessage}"), Some(Properties("attachments")))
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
       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