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 2020/09/08 03:42:56 UTC

[james-project] 02/19: JAMES-3369 EmailBodyPart traversal

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 7f60fecdb24975e603de6eaf8c81fd53b196fd45
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Sep 4 11:49:02 2020 +0700

    JAMES-3369 EmailBodyPart traversal
---
 server/protocols/jmap-rfc-8621/pom.xml             |  5 ++
 .../org/apache/james/jmap/mail/EmailBodyPart.scala | 99 +++++++++++++++++++++-
 .../org/apache/james/jmap/mail/EmailHeader.scala   |  6 ++
 3 files changed, 107 insertions(+), 3 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index bfa2e7f..617bee6 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -135,6 +135,11 @@
             <groupId>org.slf4j</groupId>
             <artifactId>jcl-over-slf4j</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.typelevel</groupId>
+            <artifactId>cats-core_${scala.base}</artifactId>
+            <version>2.2.0</version>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
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 767d7db..323da6e 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
@@ -19,11 +19,102 @@
 
 package org.apache.james.jmap.mail
 
+import java.io.OutputStream
+
+import cats.implicits._
+import com.google.common.io.CountingOutputStream
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.NonNegative
+import eu.timepit.refined.refineV
 import org.apache.james.jmap.mail.Email.Size
+import org.apache.james.jmap.mail.PartId.PartIdValue
+import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mime4j.dom.{Entity, Message, Multipart}
+import org.apache.james.mime4j.message.DefaultMessageWriter
+
+import scala.jdk.CollectionConverters._
+import scala.util.{Failure, Success, Try}
+
+object PartId {
+  type PartIdValue = Int Refined NonNegative
+}
+
+case class PartId(value: PartIdValue) {
+  def serialize: String = value.toString
+
+  def next: PartId = refineV[NonNegative](value + 1) match {
+    case scala.Right(incrementedValue) => PartId(incrementedValue)
+    case Left(e) => throw new IllegalArgumentException(e)
+  }
+}
+
+object EmailBodyPart {
+  def of(messageId: MessageId, message: Message): Try[EmailBodyPart] =
+    of(messageId, PartId(1), message).map(_._1)
 
-case class PartId(value: NonNegative) {
-  def serialize(): String = value.toString
+  def of(messageId: MessageId, partId: PartId, entity: Entity): Try[(EmailBodyPart, PartId)] =
+    entity.getBody match {
+      case multipart: Multipart =>
+        val scanResults: Try[List[(Option[EmailBodyPart], PartId)]] = multipart.getBodyParts
+          .asScala.toList
+          .scanLeft[Try[(Option[EmailBodyPart], PartId)]](Success((None, partId)))(traverse(messageId))
+          .sequence
+        val highestPartIdValidation: Try[PartId] = scanResults.map(list => list.map(_._2).reverse.headOption.getOrElse(partId))
+        val childrenValidation: Try[List[EmailBodyPart]] = scanResults.map(list => list.flatMap(_._1))
+
+        zip(childrenValidation, highestPartIdValidation)
+            .flatMap({
+              case (children, highestPartId) => of(messageId, partId, entity, Some(children))
+                .map(part => (part, highestPartId))
+            })
+      case _ => of(messageId, partId, entity, None).map(part => (part, partId))
+    }
+
+  def traverse(messageId: MessageId)(acc: Try[(Option[EmailBodyPart], PartId)], entity: Entity): Try[(Option[EmailBodyPart], PartId)] = {
+    acc.flatMap({
+      case (_, previousPartId) =>
+        val partId = previousPartId.next
+
+        of(messageId, partId, entity)
+          .map({
+            case (part, partId) => (Some(part), partId)
+          })
+    })
+  }
+
+  private def of(messageId: MessageId,
+                 partId: PartId,
+                 entity: Entity,
+                 subParts: Option[List[EmailBodyPart]]): Try[EmailBodyPart] =
+    zip(BlobId.of(messageId, partId), size(entity))
+      .map({
+        case (blobId, size) => EmailBodyPart(
+          partId = partId,
+          blobId = blobId,
+          headers = entity.getHeader.getFields.asScala.toList.map(EmailHeader(_)),
+          size = size,
+          name = Option(entity.getFilename),
+          `type` = entity.getMimeType,
+          charset = Option(entity.getCharset),
+          disposition = Option(entity.getDispositionType),
+          cid = entity.getHeader.getFields("Content-Id").asScala.headOption.map(field => field.getBody),
+          language = entity.getHeader.getFields("Content-Language").asScala.headOption.map(field => field.getBody),
+          location = entity.getHeader.getFields("Content-Location").asScala.headOption.map(field => field.getBody),
+          subParts = subParts)
+      })
+
+  def size(entity: Entity): Try[Size] = {
+    val countingOutputStream: CountingOutputStream = new CountingOutputStream(OutputStream.nullOutputStream())
+    val writer = new DefaultMessageWriter
+    writer.writeEntity(entity, countingOutputStream)
+    refineV[NonNegative](countingOutputStream.getCount) match {
+      case scala.Right(size) => Success(size)
+      case Left(e) => Failure(new IllegalArgumentException(e))
+    }
+  }
+
+  def zip[A, B](a: Try[A], b: Try[B]): Try[(A, B)] = a.flatMap(aValue => b.map(bValue => (aValue, bValue)))
 }
 
 case class EmailBodyPart(partId: PartId,
@@ -37,4 +128,6 @@ case class EmailBodyPart(partId: PartId,
                          cid: Option[String],
                          language: Option[String],
                          location: Option[String],
-                         subParts: Option[List[EmailBodyPart]])
+                         subParts: Option[List[EmailBodyPart]]) {
+
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala
index 4747e55..f5ea5ae 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailHeader.scala
@@ -19,4 +19,10 @@
 
 package org.apache.james.jmap.mail
 
+import org.apache.james.mime4j.stream.Field
+
+object EmailHeader {
+  def apply(field: Field): EmailHeader = EmailHeader(field.getName, field.getBody)
+}
+
 case class EmailHeader(name: String, value: String)


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