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/12/05 07:10:12 UTC

[james-project] 05/17: JAMES-2884 Extract text from HTML for fetchTextBodyValues

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 4f0700c779ba9424152409d30dc8d954a1e63efb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 26 11:57:33 2020 +0700

    JAMES-2884 Extract text from HTML for fetchTextBodyValues
    
    This is required for text/plain clients
---
 .../src/main/resources/eml/alternative.eml         |  28 +++++
 .../src/main/resources/eml/html.eml                |  64 ++++++++++
 .../rfc8621/contract/EmailGetMethodContract.scala  | 130 +++++++++++++++++++++
 .../scala/org/apache/james/jmap/mail/Email.scala   |  40 +++++--
 .../org/apache/james/jmap/mail/EmailBodyPart.scala |  25 ++++
 5 files changed, 275 insertions(+), 12 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/alternative.eml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/alternative.eml
new file mode 100644
index 0000000..99357a0
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/alternative.eml
@@ -0,0 +1,28 @@
+Return-Path: <fr...@linagora.com>
+To: to@linagora.com
+From: Lina <fr...@linagora.com>
+Subject: MultiAttachment
+Message-ID: <13...@linagora.com>
+Date: Mon, 27 Feb 2017 11:24:48 +0700
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101
+ Thunderbird/45.2.0
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------64D8D789FC30153D6ED18258"
+
+This is a multi-part message in MIME format.
+--------------64D8D789FC30153D6ED18258
+Content-Type: text/html; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+<p>Send<br/>concerted from html</p>
+
+
+--------------64D8D789FC30153D6ED18258
+Content-Type: text/plain; charset=UTF-8;
+ name="text1"
+Content-Transfer-Encoding: 7bit
+
+I am the text plain part!
+
+--------------64D8D789FC30153D6ED18258
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/html.eml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/html.eml
new file mode 100644
index 0000000..64d64fd
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/html.eml
@@ -0,0 +1,64 @@
+Return-Path: <fr...@linagora.com>
+To: to@linagora.com
+From: Lina <fr...@linagora.com>
+Subject: MultiAttachment
+Message-ID: <13...@linagora.com>
+Date: Mon, 27 Feb 2017 11:24:48 +0700
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101
+ Thunderbird/45.2.0
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------64D8D789FC30153D6ED18258"
+
+This is a multi-part message in MIME format.
+--------------64D8D789FC30153D6ED18258
+Content-Type: text/html; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+<p>Send<br/>concerted from html</p>
+
+
+--------------64D8D789FC30153D6ED18258
+Content-Type: text/plain; charset=UTF-8;
+ name="text1"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="text1"
+
+LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeDdQRzAr
+RS8vRU1wbTdJZ0k1UTlUTURTRnlhLzFoRSt2dlRKcmswaUdGbGxQZUhMCkE1L1ZsVE0wWVdn
+RzZYNTBxaU1mRTNWTGF6ZjJjMTlpWHJUMG1xLzIxUFoxd0Zub2d2NHp4VU5haWgrQm5nNjIK
+RjBTeXJ1RS9PL05qcXhoL0NjcTZLL2UwNVRWNFQ2NDNVU3hBZUcwS3BwbVlXOXg4SEEvR3ZW
+ODMyYXBadXhrVgppNk5Wa0RCcmZ6YVVDd3U0ekgrSHdPdi9wSTg3RTdLY2NIWUMrK0JpYWoz
+Cg==
+--------------64D8D789FC30153D6ED18258
+Content-Type: application/vnd.ms-publisher;
+ name="text2"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="text2"
+
+c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFESHM4YlQ0VC84UXltYnNp
+QWpsRDFNd05JWEpyL1dFVDYrOU1tdVRTSVlXV1U5NGNzRG45V1ZNelJoYUFicGZuU3FJeDhU
+ZFV0ck4vWnpYMkpldFBTYXIvYlU5blhBV2VpQy9qUEZRMXFLSDRHZURyWVhSTEt1NFQ4Nzgy
+T3JHSDhKeXJvcjk3VGxOWGhQcmpkUkxFQjRiUXFtbVpoYjNId2NEOGE5WHpmWnFsbTdHUldM
+bzFXUU1HdC9OcFFMQzdqTWY0ZkE2LytranpzVHNweHdkZ0w3NEdKcVBmT1hPaXdnTEhYOENa
+Ni81UnlUcWhUNnBEM01rdFNOV2F6L3pJSFBORXFmNUJZOUNCTTFURlI1dys2TURIbzBnbWlJ
+c1hGRUpUUG5maEJ2SERoU2pCMVJJMEt4VUNseVlySjRmQmxVVmVLZm5hd29WY3U3WXZDcUY0
+RjUgcXV5bmhubkBsaW5hZ29yYQo=
+--------------64D8D789FC30153D6ED18258
+Content-Type: text/plain; charset=UTF-8;
+ name="text3"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="text3"
+
+fDF8b1M3NU9nTDN2RjJHZGw5OUNKRGJFcGFKM3lFPXxJTkdxbGpDVzFYTWY0Z2dPUW0yNi9C
+Tm5LR2M9IHNzaC1yc2EgQUFBQUIzTnphQzF5YzJFQUFBQUJJd0FBQVFFQXEyQTdoUkdtZG5t
+OXRVRGJPOUlEU3dCSzZUYlFhK1BYWVBDUHk2cmJUclR0dzdQSGtjY0tycHAweVZocDVIZEVJ
+Y0tyNnBMbFZEQmZPTFg5UVVzeUNPVjB3emZqSUpObEdFWXNkbExKaXpIaGJuMm1VanZTQUhR
+cVpFVFlQODFlRnpMUU5uUEh0NEVWVlVoN1ZmREVTVTg0S2V6bUQ1UWxXcFhMbXZVMzEveU1m
+K1NlOHhoSFR2S1NDWklGSW1Xd29HNm1iVW9XZjluenBJb2FTakIrd2VxcVVVbXBhYWFzWFZh
+bDcySitVWDJCKzJSUFczUmNUMGVPelFncWxKTDNSS3JUSnZkc2pFM0pFQXZHcTNsR0hTWlh5
+TjZtNVU0aHBwaDl1T3Y1NGFIYzRYcjhqaEFhL1NYNU1KCg==
+--------------64D8D789FC30153D6ED18258--
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/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index e2469db..ff1eaf8 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -4214,6 +4214,136 @@ trait EmailGetMethodContract {
   }
 
   @Test
+  def textBodyValuesForHtmlMessage(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoader.getSystemResourceAsStream("eml/html.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize}"],
+         |      "properties":["bodyValues"],
+         |      "fetchTextBodyValues": true
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [[
+         |            "Email/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {
+         |                        "id": "${messageId.serialize}",
+         |                        "bodyValues": {
+         |                            "2": {
+         |                                "value": "Send\\nconcerted from html\\n\\n\\r\\n\\r\\n",
+         |                                "isEncodingProblem": false,
+         |                                "isTruncated": false
+         |                            }
+         |                        }
+         |                    }
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ]]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def textBodyValuesForAlternativeMessage(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoader.getSystemResourceAsStream("eml/alternative.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize}"],
+         |      "properties":["bodyValues"],
+         |      "fetchTextBodyValues": true
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [
+         |        [
+         |            "Email/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "notFound": [
+         |
+         |                ],
+         |                "state": "000001",
+         |                "list": [
+         |                    {
+         |                        "id": "1",
+         |                        "bodyValues": {
+         |                            "3": {
+         |                                "value": "I am the text plain part!\\r\\n",
+         |                                "isEncodingProblem": false,
+         |                                "isTruncated": false
+         |                            }
+         |                        }
+         |                    }
+         |                ]
+         |            },
+         |            "c1"
+         |        ]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
   def textBodyValuesForComplexMultipart(server: GuiceJamesServer): Unit = {
     val path = MailboxPath.inbox(BOB)
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index f923fc7..0c0f868 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -50,6 +50,7 @@ import org.apache.james.mime4j.field.AddressListFieldLenientImpl
 import org.apache.james.mime4j.message.DefaultMessageBuilder
 import org.apache.james.mime4j.stream.{Field, MimeConfig, RawFieldParser}
 import org.apache.james.mime4j.util.MimeUtil
+import org.apache.james.util.html.HtmlTextExtractor
 import org.slf4j.{Logger, LoggerFactory}
 import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
@@ -394,11 +395,12 @@ sealed trait EmailViewReader[+EmailView] {
 }
 
 private sealed trait EmailViewFactory[+EmailView] {
-  def toEmail(request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailView]
+  def toEmail(htmlTextExtractor: HtmlTextExtractor, request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailView]
 }
 
 private class GenericEmailViewReader[+EmailView](messageIdManager: MessageIdManager,
                                      fetchGroup: FetchGroup,
+                                     htmlTextExtractor: HtmlTextExtractor,
                                      metadataViewFactory: EmailViewFactory[EmailView]) extends EmailViewReader[EmailView] {
   override def read[T >: EmailView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
     SFlux.fromPublisher(messageIdManager.getMessagesReactive(
@@ -407,7 +409,7 @@ private class GenericEmailViewReader[+EmailView](messageIdManager: MessageIdMana
         mailboxSession))
       .collectSeq()
       .flatMapIterable(messages => messages.groupBy(_.getMessageId).toSet)
-      .map(metadataViewFactory.toEmail(request))
+      .map(metadataViewFactory.toEmail(htmlTextExtractor, request))
       .handle[T]((aTry, sink) => aTry match {
         case Success(value) => sink.next(value)
         case Failure(e) => sink.error(e)
@@ -415,7 +417,7 @@ private class GenericEmailViewReader[+EmailView](messageIdManager: MessageIdMana
 }
 
 private class EmailMetadataViewFactory @Inject()(zoneIdProvider: ZoneIdProvider) extends EmailViewFactory[EmailMetadataView] {
-  override def toEmail(request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailMetadataView] = {
+  override def toEmail(htmlTextExtractor: HtmlTextExtractor, request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailMetadataView] = {
     val messageId: MessageId = message._1
     val mailboxIds: MailboxIds = MailboxIds(message._2
       .map(_.getMailboxId)
@@ -443,7 +445,7 @@ private class EmailMetadataViewFactory @Inject()(zoneIdProvider: ZoneIdProvider)
 }
 
 private class EmailHeaderViewFactory @Inject()(zoneIdProvider: ZoneIdProvider) extends EmailViewFactory[EmailHeaderView] {
-  override def toEmail(request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailHeaderView] = {
+  override def toEmail(htmlTextExtractor: HtmlTextExtractor, request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailHeaderView] = {
     val messageId: MessageId = message._1
     val mailboxIds: MailboxIds = MailboxIds(message._2
       .map(_.getMailboxId)
@@ -474,7 +476,7 @@ private class EmailHeaderViewFactory @Inject()(zoneIdProvider: ZoneIdProvider) e
 }
 
 private class EmailFullViewFactory @Inject()(zoneIdProvider: ZoneIdProvider, previewFactory: Preview.Factory) extends EmailViewFactory[EmailFullView] {
-  override def toEmail(request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailFullView] = {
+  override def toEmail(htmlTextExtractor: HtmlTextExtractor, request: EmailGetRequest)(message: (MessageId, Seq[MessageResult])): Try[EmailFullView] = {
     val messageId: MessageId = message._1
     val mailboxIds: MailboxIds = MailboxIds(message._2
       .map(_.getMailboxId)
@@ -487,7 +489,7 @@ private class EmailFullViewFactory @Inject()(zoneIdProvider: ZoneIdProvider, pre
         .getOrElse(Failure(new IllegalArgumentException("No message supplied")))
       mime4JMessage <- Email.parseAsMime4JMessage(firstMessage)
       bodyStructure <- EmailBodyPart.of(messageId, mime4JMessage)
-      bodyValues <- extractBodyValues(bodyStructure, request)
+      bodyValues <- extractBodyValues(htmlTextExtractor)(bodyStructure, request)
       blobId <- BlobId.of(messageId)
       preview <- Try(previewFactory.fromMessageResult(firstMessage))
       keywords <- LENIENT_KEYWORDS_FACTORY.fromFlags(firstMessage.getFlags)
@@ -515,8 +517,8 @@ private class EmailFullViewFactory @Inject()(zoneIdProvider: ZoneIdProvider, pre
     }
   }
 
-  private def extractBodyValues(bodyStructure: EmailBodyPart, request: EmailGetRequest): Try[Map[PartId, EmailBodyValue]] = for {
-    textBodyValues <- extractBodyValues(bodyStructure.textBody, request, request.fetchTextBodyValues.exists(_.value))
+  private def extractBodyValues(htmlTextExtractor: HtmlTextExtractor)(bodyStructure: EmailBodyPart, request: EmailGetRequest): Try[Map[PartId, EmailBodyValue]] = for {
+    textBodyValues <- extractTextBodyValues(htmlTextExtractor)(bodyStructure.textBody, request, request.fetchTextBodyValues.exists(_.value))
     htmlBodyValues <- extractBodyValues(bodyStructure.htmlBody, request, request.fetchHTMLBodyValues.exists(_.value))
     allBodyValues <- extractBodyValues(bodyStructure.flatten, request, request.fetchAllBodyValues.exists(_.value))
   } yield {
@@ -534,27 +536,40 @@ private class EmailFullViewFactory @Inject()(zoneIdProvider: ZoneIdProvider, pre
     } else {
       Success(Nil)
     }
+
+  private def extractTextBodyValues(htmlTextExtractor: HtmlTextExtractor)(parts: List[EmailBodyPart], request: EmailGetRequest, shouldFetch: Boolean): Try[List[(PartId, EmailBodyValue)]] =
+    if (shouldFetch) {
+      parts
+        .map(part => part.textBodyContent(htmlTextExtractor).map(bodyValue => bodyValue.map(b => (part.partId, b.truncate(request.maxBodyValueBytes)))))
+        .sequence
+        .map(list => list.flatten)
+    } else {
+      Success(Nil)
+    }
 }
 
 private class EmailMetadataViewReader @Inject()(messageIdManager: MessageIdManager,
+                                                htmlTextExtractor: HtmlTextExtractor,
                                                 metadataViewFactory: EmailMetadataViewFactory) extends EmailViewReader[EmailMetadataView] {
-  private val reader: GenericEmailViewReader[EmailMetadataView] = new GenericEmailViewReader[EmailMetadataView](messageIdManager, MINIMAL, metadataViewFactory)
+  private val reader: GenericEmailViewReader[EmailMetadataView] = new GenericEmailViewReader[EmailMetadataView](messageIdManager, MINIMAL, htmlTextExtractor, metadataViewFactory)
 
   override def read[T >: EmailMetadataView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
     reader.read(ids, request, mailboxSession)
 }
 
 private class EmailHeaderViewReader @Inject()(messageIdManager: MessageIdManager,
+                                              htmlTextExtractor: HtmlTextExtractor,
                                               headerViewFactory: EmailHeaderViewFactory) extends EmailViewReader[EmailHeaderView] {
-  private val reader: GenericEmailViewReader[EmailHeaderView] = new GenericEmailViewReader[EmailHeaderView](messageIdManager, HEADERS, headerViewFactory)
+  private val reader: GenericEmailViewReader[EmailHeaderView] = new GenericEmailViewReader[EmailHeaderView](messageIdManager, HEADERS, htmlTextExtractor, headerViewFactory)
 
   override def read[T >: EmailHeaderView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
     reader.read(ids, request, mailboxSession)
 }
 
 private class EmailFullViewReader @Inject()(messageIdManager: MessageIdManager,
+                                            htmlTextExtractor: HtmlTextExtractor,
                                             fullViewFactory: EmailFullViewFactory) extends EmailViewReader[EmailFullView] {
-  private val reader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, fullViewFactory)
+  private val reader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, htmlTextExtractor, fullViewFactory)
 
 
   override def read[T >: EmailFullView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
@@ -573,9 +588,10 @@ private case class FastViewUnavailable(id: MessageId) extends FastViewResult
 
 private class EmailFastViewReader @Inject()(messageIdManager: MessageIdManager,
                                             messageFastViewProjection: MessageFastViewProjection,
+                                            htmlTextExtractor: HtmlTextExtractor,
                                             zoneIdProvider: ZoneIdProvider,
                                             fullViewFactory: EmailFullViewFactory) extends EmailViewReader[EmailView] {
-  private val fullReader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, fullViewFactory)
+  private val fullReader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, htmlTextExtractor, fullViewFactory)
 
   override def read[T >: EmailView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
     SMono.fromPublisher(messageFastViewProjection.retrieve(ids.asJava))
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 c5eb21f..e3d08a8 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
@@ -38,6 +38,7 @@ import org.apache.james.mime4j.dom.field.{ContentLanguageField, ContentTypeField
 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.{Field, MimeConfig, RawField}
+import org.apache.james.util.html.HtmlTextExtractor
 
 import scala.jdk.CollectionConverters._
 import scala.util.{Failure, Success, Try}
@@ -196,6 +197,22 @@ case class Location(value: String) extends AnyVal {
   def asField: Field = new RawField("Content-Location", value)
 }
 
+object Context {
+  def of(`type`: Type): Context = `type` match {
+    case MULTIPART_ALTERNATIVE => AlternativeContext
+    case _ => NoContext
+  }
+  def of(`type`: Type, previousContext: Context): Context = (`type`, previousContext) match {
+    case (_, AlternativeContext) => AlternativeContext
+    case (MULTIPART_ALTERNATIVE, _) => AlternativeContext
+    case _ => NoContext
+  }
+}
+
+sealed trait Context
+case object NoContext extends Context
+case object AlternativeContext extends Context
+
 case class EmailBodyPart(partId: PartId,
                          blobId: Option[BlobId],
                          headers: List[EmailHeader],
@@ -222,6 +239,14 @@ case class EmailBodyPart(partId: PartId,
     case _ => Success(None)
   }
 
+  def textBodyContent(htmlTextExtractor: HtmlTextExtractor): Try[Option[EmailBodyValue]] = `type` match {
+    case TEXT_HTML => bodyContent.map(maybeContent => maybeContent.map(
+      content => EmailBodyValue(htmlTextExtractor.toPlainText(content.value),
+        content.isEncodingProblem,
+        content.isTruncated)))
+    case _ => bodyContent
+  }
+
   private def charset(charset: Option[String]): java.nio.charset.Charset = charset
     .map(java.nio.charset.Charset.forName)
     .getOrElse(org.apache.james.mime4j.Charsets.DEFAULT_CHARSET)


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