You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/04/08 03:57:26 UTC

[james-project] branch master updated (4619763 -> 5ca0f79)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 4619763  JAMES-3435 test for mailbox.read.strong.cst
     new dc9633d  JAMES-3537 Email/set create should be backed by the blob resolver
     new d2912ec  JAMES-3537 Refactoring: use a case class to get rid of a tuple3
     new 5ca0f79  JAMES-3537 Email/set create should allow to attach emails

The 3 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:
 .../rfc8621/contract/EmailSetMethodContract.scala  | 109 +++++++++++++++++++++
 .../org/apache/james/jmap/mail/EmailSet.scala      | 101 +++++++++----------
 .../jmap/method/EmailSetCreatePerformer.scala      |  47 +++++----
 3 files changed, 181 insertions(+), 76 deletions(-)

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 01/03: JAMES-3537 Email/set create should be backed by the blob resolver

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit dc9633d8a486f0878b12c824865a5cc2d2531616
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Apr 5 11:57:55 2021 +0700

    JAMES-3537 Email/set create should be backed by the blob resolver
---
 .../org/apache/james/jmap/mail/EmailSet.scala      | 89 +++++++++-------------
 .../jmap/method/EmailSetCreatePerformer.scala      | 47 +++++++-----
 2 files changed, 63 insertions(+), 73 deletions(-)

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 a9fb1e9..bebbb0a 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
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.james.jmap.mail
 
-import java.io.IOException
 import java.nio.charset.{StandardCharsets, Charset => NioCharset}
 import java.util.Date
 
@@ -34,9 +33,9 @@ import org.apache.james.jmap.mail.Disposition.INLINE
 import org.apache.james.jmap.mail.Email.Size
 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, Cid, MessageId}
-import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxSession}
+import org.apache.james.jmap.routes.{Blob, BlobResolvers}
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.mailbox.model.{Cid, MessageId}
 import org.apache.james.mime4j.codec.EncoderUtil
 import org.apache.james.mime4j.codec.EncoderUtil.Usage
 import org.apache.james.mime4j.dom.field.{ContentIdField, ContentTypeField, FieldName}
@@ -46,10 +45,11 @@ import org.apache.james.mime4j.message.{BodyPartBuilder, MultipartBuilder}
 import org.apache.james.mime4j.stream.{Field, NameValuePair, RawField}
 import org.apache.james.util.html.HtmlTextExtractor
 import play.api.libs.json.JsObject
+import reactor.core.scheduler.Schedulers
 
 import scala.jdk.CollectionConverters._
 import scala.jdk.OptionConverters._
-import scala.util.{Right, Try}
+import scala.util.{Right, Try, Using}
 
 object EmailSet {
   type EmailCreationId = Id
@@ -124,10 +124,9 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
                                 bodyValues: Option[Map[ClientPartId, ClientEmailBodyValue]],
                                 specificHeaders: List[EmailHeader],
                                 attachments: Option[List[Attachment]]) {
-  def toMime4JMessage(attachmentManager: AttachmentManager,
-                      attachmentContentLoader: AttachmentContentLoader,
+  def toMime4JMessage(blobResolvers: BlobResolvers,
                       htmlTextExtractor: HtmlTextExtractor,
-                      mailboxSession: MailboxSession): Either[Exception, Message] =
+                      mailboxSession: MailboxSession): Either[Throwable, Message] =
     validateHtmlBody
       .flatMap(maybeHtmlBody => validateTextBody.map((maybeHtmlBody, _)))
       .flatMap {
@@ -148,7 +147,7 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
             .flatMap(_ => {
               specificHeaders.map(_.asField).foreach(builder.addField)
               attachments.filter(_.nonEmpty).map(attachments =>
-                createMultipartWithAttachments(maybeHtmlBody, maybeTextBody, attachments, attachmentManager, attachmentContentLoader, htmlTextExtractor, mailboxSession)
+                createMultipartWithAttachments(maybeHtmlBody, maybeTextBody, attachments, blobResolvers, htmlTextExtractor, mailboxSession)
                   .map(multipartBuilder => {
                     builder.setBody(multipartBuilder)
                     builder.build
@@ -177,19 +176,15 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
   private def createMultipartWithAttachments(maybeHtmlBody: Option[String],
                                              maybeTextBody: Option[String],
                                              attachments: List[Attachment],
-                                             attachmentManager: AttachmentManager,
-                                             attachmentContentLoader: AttachmentContentLoader,
+                                             blobResolvers: BlobResolvers,
                                              htmlTextExtractor: HtmlTextExtractor,
-                                             mailboxSession: MailboxSession): Either[Exception, MultipartBuilder] = {
-    val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
+                                             mailboxSession: MailboxSession): Either[Throwable, MultipartBuilder] = {
+    val maybeAttachments: Either[Throwable, List[(Attachment, Blob, Array[Byte])]] =
       attachments
-        .map(attachment => getAttachmentMetadata(attachment, attachmentManager, mailboxSession))
-        .map(attachmentMetadataList => attachmentMetadataList
-          .flatMap(attachmentAndMetadata => loadAttachment(attachmentAndMetadata._1, attachmentAndMetadata._2, attachmentContentLoader, mailboxSession)))
+        .map(loadWithMetadata(blobResolvers, mailboxSession))
         .sequence
 
     maybeAttachments.map(list => {
-
       (list.filter(_._1.isInline), list.filter(!_._1.isInline)) match {
         case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, maybeTextBody, normalAttachments, htmlTextExtractor)
         case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, maybeTextBody, inlineAttachments, htmlTextExtractor)
@@ -198,54 +193,63 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     })
   }
 
+  private def loadWithMetadata(blobResolvers: BlobResolvers, mailboxSession: MailboxSession)(attachment: Attachment): Either[Throwable, (Attachment, Blob, Array[Byte])] =
+    Try(blobResolvers.resolve(attachment.blobId, mailboxSession).subscribeOn(Schedulers.elastic()).block())
+      .toEither.flatMap(blob => load(blob).map(content => (attachment, blob, content)))
+
+  private def load(blob: Blob): Either[Throwable, Array[Byte]] =
+    Using(blob.content) {
+      _.readAllBytes()
+    }.toEither
+
   private def createMixedRelatedBody(maybeHtmlBody: Option[String],
                                      maybeTextBody: Option[String],
-                                     inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])],
-                                     normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])],
+                                     inlineAttachments: List[(Attachment, Blob, Array[Byte])],
+                                     normalAttachments: List[(Attachment, Blob, Array[Byte])],
                                      htmlTextExtractor: HtmlTextExtractor) = {
     val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
     val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
     relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, maybeTextBody, htmlTextExtractor).build))
     inlineAttachments.foldLeft(relatedMultipartBuilder) {
-      case (acc, (attachment, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, (attachment, blob, content)) =>
+        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
         acc
     }
 
     mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
 
     normalAttachments.foldLeft(mixedMultipartBuilder) {
-      case (acc, (attachment, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, (attachment, blob, content)) =>
+        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
         acc
     }
   }
 
-  private def createMixedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
+  private def createMixedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], normalAttachments: List[(Attachment, Blob, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
     val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
     mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, maybeTextBody, htmlTextExtractor).build))
     normalAttachments.foldLeft(mixedMultipartBuilder) {
-      case (acc, (attachment, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, (attachment, blob, content)) =>
+        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
         acc
     }
   }
 
-  private def createRelatedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
+  private def createRelatedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], inlineAttachments: List[(Attachment, Blob, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
     val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
     relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, maybeTextBody, htmlTextExtractor).build))
     inlineAttachments.foldLeft(relatedMultipartBuilder) {
-      case (acc, (attachment, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, (attachment, blob, content)) =>
+        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
         acc
     }
     relatedMultipartBuilder
   }
 
-  private def toBodypartBuilder(attachment: Attachment, storedMetadata: AttachmentMetadata, content: Array[Byte]) = {
+  private def toBodypartBuilder(attachment: Attachment, blob: Blob, content: Array[Byte]) = {
     val bodypartBuilder = BodyPartBuilder.create()
     bodypartBuilder.setBody(content, attachment.`type`.value)
-      .setField(contentTypeField(attachment, storedMetadata))
+      .setField(contentTypeField(attachment, blob))
       .setContentDisposition(attachment.disposition.getOrElse(Disposition.ATTACHMENT).value)
     attachment.cid.map(_.asField).foreach(bodypartBuilder.addField)
     attachment.location.map(_.asField).foreach(bodypartBuilder.addField)
@@ -253,8 +257,8 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     bodypartBuilder
   }
 
-  private def contentTypeField(attachment: Attachment, attachmentMetadata: AttachmentMetadata): ContentTypeField = {
-    val typeAsField: ContentTypeField = attachmentMetadata.getType.asMime4J
+  private def contentTypeField(attachment: Attachment, blob: Blob): ContentTypeField = {
+    val typeAsField: ContentTypeField = blob.contentType.asMime4J
     if (attachment.name.isDefined) {
       Fields.contentType(typeAsField.getMimeType,
         Map.newBuilder[String, String]
@@ -273,25 +277,6 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
       .filter(!_._1.equals("name"))
       .toMap
 
-  private def getAttachmentMetadata(attachment: Attachment,
-                                    attachmentManager: AttachmentManager,
-                                    mailboxSession: MailboxSession): Either[AttachmentNotFoundException, (Attachment, AttachmentMetadata)] =
-    Try(attachmentManager.getAttachment(AttachmentId.from(attachment.blobId.value.toString), mailboxSession))
-      .fold(e => Left(new AttachmentNotFoundException(attachment.blobId.value.value, s"Attachment not found: ${attachment.blobId.value}", e)),
-        attachmentMetadata => Right((attachment, attachmentMetadata)))
-
-  private def loadAttachment(attachment: Attachment,
-                             attachmentMetadata: AttachmentMetadata,
-                             attachmentContentLoader: AttachmentContentLoader,
-                             mailboxSession: MailboxSession): Either[Exception, (Attachment, AttachmentMetadata, Array[Byte])] =
-    Try(attachmentContentLoader.load(attachmentMetadata, mailboxSession))
-      .toEither
-      .fold(e => e match {
-        case e: AttachmentNotFoundException => Left(new AttachmentNotFoundException(attachment.blobId.value.value, s"Attachment not found: ${attachment.blobId.value}", e))
-        case e: IOException => Left(e)
-      },
-        inputStream => scala.Right((attachment, attachmentMetadata, inputStream.readAllBytes())))
-
   def validateHtmlBody: Either[IllegalArgumentException, Option[String]] = htmlBody match {
     case None => Right(None)
     case Some(html :: Nil) if !html.`type`.value.equals("text/html") => Left(new IllegalArgumentException("Expecting htmlBody type to be text/html"))
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 0bc684f..b05cdb5 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
@@ -31,10 +31,12 @@ import org.apache.james.jmap.json.EmailSetSerializer
 import org.apache.james.jmap.mail.EmailSet.EmailCreationId
 import org.apache.james.jmap.mail.{BlobId, Email, EmailCreationRequest, EmailCreationResponse, EmailSetRequest}
 import org.apache.james.jmap.method.EmailSetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
+import org.apache.james.jmap.routes.{BlobNotFoundException, BlobResolvers}
 import org.apache.james.mailbox.MessageManager.AppendCommand
-import org.apache.james.mailbox.exception.{AttachmentNotFoundException, MailboxNotFoundException}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
 import org.apache.james.mailbox.model.MailboxId
-import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxManager, MailboxSession}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
+import org.apache.james.mime4j.dom.Message
 import org.apache.james.util.html.HtmlTextExtractor
 import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
@@ -62,7 +64,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}"), Some(Properties("attachments")))
+      case e: BlobNotFoundException => SetError.invalidArguments(SetErrorDescription(s"Attachment not found: ${e.blobId.value}"), Some(Properties("attachments")))
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
       case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
     }
@@ -70,8 +72,7 @@ object EmailSetCreatePerformer {
 }
 
 class EmailSetCreatePerformer @Inject()(serializer: EmailSetSerializer,
-                                        attachmentManager: AttachmentManager,
-                                        attachmentContentLoader: AttachmentContentLoader,
+                                        blobResolvers: BlobResolvers,
                                         htmlTextExtractor: HtmlTextExtractor,
                                         mailboxManager: MailboxManager) {
 
@@ -89,22 +90,26 @@ 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, htmlTextExtractor, mailboxSession)
-        .fold(e => SMono.just(CreationFailure(clientId, e)),
-          message => SMono.fromCallable[CreationResult](() => {
-            val appendResult = mailboxManager.getMailbox(mailboxIds.head, mailboxSession)
-              .appendMessage(AppendCommand.builder()
-                .recent()
-                .withFlags(request.keywords.map(_.asFlags).getOrElse(new Flags()))
-                .withInternalDate(Date.from(request.receivedAt.getOrElse(UTCDate(ZonedDateTime.now())).asUTC.toInstant))
-                .build(message),
-                mailboxSession)
-
-            val blobId: Option[BlobId] = BlobId.of(appendResult.getId.getMessageId).toOption
-            CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId, blobId, blobId, Email.sanitizeSize(appendResult.getSize)))
-          })
-            .subscribeOn(Schedulers.elastic())
-            .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e))))
+      SMono.fromCallable(() => request.toMime4JMessage(blobResolvers, htmlTextExtractor, mailboxSession))
+        .flatMap(either => either.fold(e => SMono.just(CreationFailure(clientId, e)),
+          message => SMono.fromCallable[CreationResult](() => append(clientId, asAppendCommand(request, message), mailboxSession, mailboxIds))))
+        .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e)))
+        .subscribeOn(Schedulers.elastic())
     }
   }
+
+  private def append(clientId: EmailCreationId, appendCommand: AppendCommand, mailboxSession: MailboxSession, mailboxIds: List[MailboxId]): CreationSuccess = {
+    val appendResult = mailboxManager.getMailbox(mailboxIds.head, mailboxSession)
+      .appendMessage(appendCommand, mailboxSession)
+
+    val blobId: Option[BlobId] = BlobId.of(appendResult.getId.getMessageId).toOption
+    CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId, blobId, blobId, Email.sanitizeSize(appendResult.getSize)))
+  }
+
+  private def asAppendCommand(request: EmailCreationRequest, message: Message): AppendCommand =
+    AppendCommand.builder()
+      .recent()
+      .withFlags(request.keywords.map(_.asFlags).getOrElse(new Flags()))
+      .withInternalDate(Date.from(request.receivedAt.getOrElse(UTCDate(ZonedDateTime.now())).asUTC.toInstant))
+      .build(message)
 }

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 02/03: JAMES-3537 Refactoring: use a case class to get rid of a tuple3

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit d2912ece655de976db8287466bf70b7b4d4965a1
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Apr 5 13:26:29 2021 +0700

    JAMES-3537 Refactoring: use a case class to get rid of a tuple3
---
 .../org/apache/james/jmap/mail/EmailSet.scala      | 44 ++++++++++++----------
 1 file changed, 25 insertions(+), 19 deletions(-)

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 bebbb0a..7505f72 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
@@ -179,13 +179,13 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
                                              blobResolvers: BlobResolvers,
                                              htmlTextExtractor: HtmlTextExtractor,
                                              mailboxSession: MailboxSession): Either[Throwable, MultipartBuilder] = {
-    val maybeAttachments: Either[Throwable, List[(Attachment, Blob, Array[Byte])]] =
+    val maybeAttachments: Either[Throwable, List[LoadedAttachment]] =
       attachments
         .map(loadWithMetadata(blobResolvers, mailboxSession))
         .sequence
 
     maybeAttachments.map(list => {
-      (list.filter(_._1.isInline), list.filter(!_._1.isInline)) match {
+      (list.filter(_.isInline), list.filter(!_.isInline)) match {
         case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, maybeTextBody, normalAttachments, htmlTextExtractor)
         case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, maybeTextBody, inlineAttachments, htmlTextExtractor)
         case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, maybeTextBody, inlineAttachments, normalAttachments, htmlTextExtractor)
@@ -193,9 +193,9 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     })
   }
 
-  private def loadWithMetadata(blobResolvers: BlobResolvers, mailboxSession: MailboxSession)(attachment: Attachment): Either[Throwable, (Attachment, Blob, Array[Byte])] =
+  private def loadWithMetadata(blobResolvers: BlobResolvers, mailboxSession: MailboxSession)(attachment: Attachment): Either[Throwable, LoadedAttachment] =
     Try(blobResolvers.resolve(attachment.blobId, mailboxSession).subscribeOn(Schedulers.elastic()).block())
-      .toEither.flatMap(blob => load(blob).map(content => (attachment, blob, content)))
+      .toEither.flatMap(blob => load(blob).map(content => LoadedAttachment(attachment, blob, content)))
 
   private def load(blob: Blob): Either[Throwable, Array[Byte]] =
     Using(blob.content) {
@@ -204,51 +204,53 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
 
   private def createMixedRelatedBody(maybeHtmlBody: Option[String],
                                      maybeTextBody: Option[String],
-                                     inlineAttachments: List[(Attachment, Blob, Array[Byte])],
-                                     normalAttachments: List[(Attachment, Blob, Array[Byte])],
-                                     htmlTextExtractor: HtmlTextExtractor) = {
+                                     inlineAttachments: List[LoadedAttachment],
+                                     normalAttachments: List[LoadedAttachment],
+                                     htmlTextExtractor: HtmlTextExtractor): MultipartBuilder = {
     val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
     val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
     relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, maybeTextBody, htmlTextExtractor).build))
     inlineAttachments.foldLeft(relatedMultipartBuilder) {
-      case (acc, (attachment, blob, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         acc
     }
 
     mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
 
     normalAttachments.foldLeft(mixedMultipartBuilder) {
-      case (acc, (attachment, blob, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         acc
     }
   }
 
-  private def createMixedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], normalAttachments: List[(Attachment, Blob, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
+  private def createMixedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], normalAttachments: List[LoadedAttachment], htmlTextExtractor: HtmlTextExtractor) = {
     val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
     mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, maybeTextBody, htmlTextExtractor).build))
     normalAttachments.foldLeft(mixedMultipartBuilder) {
-      case (acc, (attachment, blob, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         acc
     }
   }
 
-  private def createRelatedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], inlineAttachments: List[(Attachment, Blob, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
+  private def createRelatedBody(maybeHtmlBody: Option[String], maybeTextBody: Option[String], inlineAttachments: List[LoadedAttachment], htmlTextExtractor: HtmlTextExtractor) = {
     val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
     relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, maybeTextBody, htmlTextExtractor).build))
     inlineAttachments.foldLeft(relatedMultipartBuilder) {
-      case (acc, (attachment, blob, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, blob, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         acc
     }
     relatedMultipartBuilder
   }
 
-  private def toBodypartBuilder(attachment: Attachment, blob: Blob, content: Array[Byte]) = {
+  private def toBodypartBuilder(loadedAttachment: LoadedAttachment) = {
     val bodypartBuilder = BodyPartBuilder.create()
-    bodypartBuilder.setBody(content, attachment.`type`.value)
+    val attachment = loadedAttachment.attachment
+    val blob = loadedAttachment.blob
+    bodypartBuilder.setBody(loadedAttachment.content, attachment.`type`.value)
       .setField(contentTypeField(attachment, blob))
       .setContentDisposition(attachment.disposition.getOrElse(Disposition.ATTACHMENT).value)
     attachment.cid.map(_.asField).foreach(bodypartBuilder.addField)
@@ -315,6 +317,10 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
   }
 }
 
+case class LoadedAttachment(attachment: Attachment, blob: Blob, content: Array[Byte]) {
+  def isInline: Boolean = attachment.isInline
+}
+
 case class DestroyIds(value: Seq[UnparsedMessageId])
 
 case class EmailSetRequest(accountId: AccountId,

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 03/03: JAMES-3537 Email/set create should allow to attach emails

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 5ca0f7958d0a67f936342fceb6ea5ede709ecc25
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Apr 5 15:26:06 2021 +0700

    JAMES-3537 Email/set create should allow to attach emails
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 109 +++++++++++++++++++++
 1 file changed, 109 insertions(+)

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 a7096e6..4c33a7b 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
@@ -3729,6 +3729,115 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def createShouldSupportAttachedMessages(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(ANDRE.asString())
+      .setFrom(ANDRE.asString())
+      .setSubject("I'm happy to be attached")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    val attachedMessageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, bobPath, AppendCommand.from(message))
+      .getMessageId
+
+    val blobId: String = attachedMessageId.serialize()
+
+    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",
+         |              "charset":"us-ascii",
+         |              "disposition": "attachment",
+         |              "type":"message/rfc822"
+         |            }
+         |          ]
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |      {
+         |        "accountId": "$ACCOUNT_ID",
+         |        "ids": ["#aaaaaa"],
+         |        "properties": ["mailboxIds", "subject", "attachments"],
+         |        "bodyProperties": ["partId", "blobId", "size", "type", "charset", "disposition"]
+         |      },
+         |    "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
+
+    val responseAsJson = Json.parse(response)
+      .\("methodResponses")
+      .\(0).\(1)
+      .\("created")
+      .\("aaaaaa")
+
+    val messageId = responseAsJson
+      .\("id")
+      .get.asInstanceOf[JsString].value
+    val size = responseAsJson
+      .\("size")
+      .get.asInstanceOf[JsNumber].value
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].created.aaaaaa")
+      .isEqualTo(
+        s"""{
+           | "id": "$messageId",
+           | "blobId": "$messageId",
+           | "threadId": "$messageId",
+           | "size": $size
+           |}""".stripMargin)
+
+    assertThatJson(response)
+      .inPath(s"methodResponses[1][1].list[0]")
+      .isEqualTo(
+        s"""{
+           |  "id": "$messageId",
+           |  "mailboxIds": {
+           |    "${mailboxId.serialize}": true
+           |  },
+           |  "subject": "World domination",
+           |  "attachments": [
+           |    {
+           |      "partId": "4",
+           |      "blobId": "${messageId}_4",
+           |      "size": 155,
+           |      "type": "message/rfc822",
+           |      "charset": "us-ascii",
+           |      "disposition": "attachment"
+           |    }
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Test
   def createShouldFailWhenAttachmentNotFound(server: GuiceJamesServer): Unit = {
     val bobPath = MailboxPath.inbox(BOB)
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org