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/14 14:14:44 UTC

[james-project] branch 3.6.x updated: JAMES-3537 (Email/set create should allow to attach mails) to 3.6.x branch

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

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


The following commit(s) were added to refs/heads/3.6.x by this push:
     new b02976b  JAMES-3537 (Email/set create should allow to attach mails) to 3.6.x branch
b02976b is described below

commit b02976be4eb18faeea3f990e4108180f79ceef93
Author: Juhan Aasaru <Ju...@nortal.com>
AuthorDate: Mon Apr 12 10:34:52 2021 +0300

    JAMES-3537 (Email/set create should allow to attach mails) to 3.6.x branch
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 109 +++++++++++++++++++++
 .../org/apache/james/jmap/mail/EmailSet.scala      | 101 +++++++++----------
 .../jmap/method/EmailSetCreatePerformer.scala      |  43 ++++----
 3 files changed, 179 insertions(+), 74 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 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)
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..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
@@ -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,20 +176,16 @@ 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[LoadedAttachment]] =
       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 {
+      (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)
@@ -198,54 +193,65 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     })
   }
 
+  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 => LoadedAttachment(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])],
-                                     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, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         acc
     }
 
     mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
 
     normalAttachments.foldLeft(mixedMultipartBuilder) {
-      case (acc, (attachment, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         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[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, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         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[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, storedMetadata, content)) =>
-        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+      case (acc, loadedAttachment) =>
+        acc.addBodyPart(toBodypartBuilder(loadedAttachment))
         acc
     }
     relatedMultipartBuilder
   }
 
-  private def toBodypartBuilder(attachment: Attachment, storedMetadata: AttachmentMetadata, content: Array[Byte]) = {
+  private def toBodypartBuilder(loadedAttachment: LoadedAttachment) = {
     val bodypartBuilder = BodyPartBuilder.create()
-    bodypartBuilder.setBody(content, attachment.`type`.value)
-      .setField(contentTypeField(attachment, storedMetadata))
+    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)
     attachment.location.map(_.asField).foreach(bodypartBuilder.addField)
@@ -253,8 +259,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 +279,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"))
@@ -330,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,
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..a50d139 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](() => {
+      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.builder()
+      .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),
-                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))))
-    }
-  }
+      .build(message)
 }

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