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:27 UTC

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

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