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


The following commit(s) were added to refs/heads/master by this push:
     new a2583d4e8a JAMES-4008 JMAP - Email/set - Should be able to save a 
draft with invalid email address (#2040)
a2583d4e8a is described below

commit a2583d4e8afe69b0974085004a92eff15445445b
Author: vttran <vtt...@linagora.com>
AuthorDate: Mon Feb 26 14:35:39 2024 +0700

    JAMES-4008 JMAP - Email/set - Should be able to save a draft with invalid 
email address (#2040)
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 238 ++++++++++++++++++++-
 .../james/jmap/json/EmailGetSerializer.scala       |   3 +
 .../james/jmap/json/EmailSetSerializer.scala       |  18 +-
 .../scala/org/apache/james/jmap/mail/Email.scala   |  20 +-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  91 +++++++-
 .../jmap/method/EmailSetCreatePerformer.scala      |   4 +-
 .../jmap/method/EmailSubmissionSetMethod.scala     |  19 ++
 .../james/jmap/json/EmailSetSerializerTest.scala   |  87 ++++++++
 8 files changed, 451 insertions(+), 29 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 32beadf08b..25163a8e8c 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
@@ -45,11 +45,11 @@ import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.DownloadContract.accountId
 import 
org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, 
ACCOUNT_ID, ANDRE, ANDRE_ACCOUNT_ID, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, 
authScheme, baseRequestSpecBuilder}
 import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbe
-import org.apache.james.mailbox.FlagsBuilder
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.Right
 import org.apache.james.mailbox.model.{ComposedMessageId, MailboxACL, 
MailboxConstants, MailboxId, MailboxPath, MessageId}
 import org.apache.james.mailbox.probe.MailboxProbe
+import org.apache.james.mailbox.{DefaultMailboxes, FlagsBuilder}
 import org.apache.james.mime4j.dom.Message
 import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl, 
QuotaProbesImpl}
 import org.apache.james.util.ClassLoaderUtils
@@ -59,7 +59,7 @@ import org.awaitility.Awaitility
 import org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS
 import org.hamcrest.Matchers
 import org.hamcrest.Matchers.{equalTo, not}
-import org.junit.jupiter.api.{BeforeEach, Test}
+import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.ValueSource
 import play.api.libs.json.{JsNumber, JsString, Json}
@@ -7455,6 +7455,240 @@ trait EmailSetMethodContract {
           |}""".stripMargin)
   }
 
+  @Test
+  def emailSetShouldSucceedWhenInvalidToMailAddressAndHaveDraftKeyword(server: 
GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    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
+         |          },
+         |          "keywords":{ "$$draft": true },
+         |          "to": [{"email": "invalid1"}],
+         |          "from": [{"email": "${BOB.asString}"}]
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["#aaaaaa"],
+         |       "properties": ["sentAt", "messageId"]
+         |     },
+         |     "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
+
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list.[0].sentAt")
+      .isEqualTo("\"${json-unit.ignore}\"")
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list.[0].messageId")
+      .isEqualTo("[\"${json-unit.ignore}\"]")
+
+  }
+
+  @Test
+  def emailGetShouldReturnUncheckedMailAddressValueWhenDraftEmail(server: 
GuiceJamesServer): Unit = {
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    val draftId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "create": {
+         |        "e1526":{
+         |          "mailboxIds": {
+         |             "${draftId.serialize}": true
+         |          },
+         |          "keywords":{ "$$draft": true },
+         |          "to": [{"email": "invalid1", "name" : "name1"}],
+         |          "from": [{"email": "${BOB.asString}"}]
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["#e1526"],
+         |       "properties": ["to", "from" ]
+         |     },
+         |     "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
+
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list")
+      .isEqualTo("""[{
+                   |    "to": [{
+                   |        "name": "name1",
+                   |        "email": "invalid1"
+                   |      }],
+                   |    "id": "1",
+                   |    "from": [{
+                   |        "email": "b...@domain.tld"
+                   |      }
+                   |    ]}]""".stripMargin)
+
+  }
+
+  @Test
+  def emailSubmissionSetShouldFailWhenInvalidEmailAddressHeader(server: 
GuiceJamesServer): Unit = {
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val draftId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "create": {
+         |        "e1526":{
+         |          "mailboxIds": {
+         |             "${draftId.serialize}": true
+         |          },
+         |          "keywords":{ "$$draft": true },
+         |          "to": [{"email": "invalid1"}],
+         |          "from": [{"email": "${BOB.asString}"}]
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["#e1526"],
+         |       "properties": ["sentAt"]
+         |     },
+         |     "c2"],
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "#e1526",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c3"]]
+         |}""".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)
+      .inPath("methodResponses[2]")
+      .isEqualTo(s"""[
+                   |  "EmailSubmission/set",
+                   |  {
+                   |    "accountId": "$${json-unit.ignore}",
+                   |    "newState": "$${json-unit.ignore}",
+                   |    "notCreated": {
+                   |      "k1490": {
+                   |        "type": "invalidArguments",
+                   |        "description": "Invalid mail address: invalid1 in 
to header"
+                   |      }
+                   |    }
+                   |  },
+                   |  "c3"
+                   |]""".stripMargin)
+
+  }
+
+  @Test
+  def 
emailSetShouldFailWhenInvalidToEmailAddressAndHaveNotDraftKeyword(server: 
GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    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
+         |          },
+         |          "keywords":{ },
+         |          "to": [{"email": "invalid1"}],
+         |          "from": [{"email": "${BOB.asString}"}]
+         |        }
+         |      }
+         |    }, "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)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo(
+        """{
+          |    "aaaaaa": {
+          |        "type": "invalidArguments",
+          |        "description": "/to: Invalid email address `invalid1`"
+          |    }
+          |}""".stripMargin)
+  }
+
   private def buildTestMessage = {
     Message.Builder
       .of
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index 3da03d5c39..001a3874c8 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -81,6 +81,8 @@ object EmailGetSerializer {
   private implicit val locationWrites: Writes[Location] = 
Json.valueWrites[Location]
   private implicit val emailerNameWrites: Writes[EmailerName] = 
Json.valueWrites[EmailerName]
   private implicit val emailAddressWrites: Writes[EmailAddress] = 
Json.writes[EmailAddress]
+  private implicit val uncheckedEmailWrites: Writes[UncheckedEmail] = 
Json.valueWrites[UncheckedEmail]
+  private implicit val uncheckedEmailAddressWrites: 
Writes[UncheckedEmailAddress] = Json.writes[UncheckedEmailAddress]
   private implicit val headerMessageIdWrites: Writes[HeaderMessageId] = 
Json.valueWrites[HeaderMessageId]
   private implicit val isEncodingProblemWrites: Writes[IsEncodingProblem] = 
Json.valueWrites[IsEncodingProblem]
   private implicit val isTruncatedWrites: Writes[IsTruncated] = 
Json.valueWrites[IsTruncated]
@@ -91,6 +93,7 @@ object EmailGetSerializer {
   private implicit val rawHeaderWrites: Writes[RawHeaderValue] = 
Json.valueWrites[RawHeaderValue]
   private implicit val textHeaderWrites: Writes[TextHeaderValue] = 
Json.valueWrites[TextHeaderValue]
   private implicit val addressesHeaderWrites: Writes[AddressesHeaderValue] = 
Json.valueWrites[AddressesHeaderValue]
+  private implicit val uncheckedAddressesHeaderValueWrites: 
Writes[UncheckedAddressesHeaderValue] = 
Json.valueWrites[UncheckedAddressesHeaderValue]
   private implicit val GroupNameWrites: Writes[GroupName] = 
Json.valueWrites[GroupName]
   private implicit val emailAddressGroupWrites: Writes[EmailAddressGroup] = 
(o: EmailAddressGroup) =>
     Json.obj(
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 53523f7307..f966a5e34b 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -29,7 +29,7 @@ import org.apache.james.jmap.api.model.{EmailAddress, 
EmailerName}
 import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.{Id, SetError, UTCDate, UuidState}
 import org.apache.james.jmap.mail.KeywordsFactory.STRICT_KEYWORDS_FACTORY
-import org.apache.james.jmap.mail.{AddressesHeaderValue, AllHeaderValues, 
AsAddresses, AsDate, AsGroupedAddresses, AsMessageIds, AsRaw, AsText, AsURLs, 
Attachment, BlobId, Charset, ClientBody, ClientCid, ClientEmailBodyValue, 
ClientEmailBodyValueWithoutHeaders, ClientPartId, DateHeaderValue, DestroyIds, 
Disposition, EmailAddressGroup, EmailCreationId, EmailCreationRequest, 
EmailCreationResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, 
EmailImport, EmailImportRequest, EmailImportR [...]
+import org.apache.james.jmap.mail.{AddressesHeaderValue, AllHeaderValues, 
AsAddresses, AsDate, AsGroupedAddresses, AsMessageIds, AsRaw, AsText, AsURLs, 
Attachment, BlobId, Charset, ClientBody, ClientCid, ClientEmailBodyValue, 
ClientEmailBodyValueWithoutHeaders, ClientPartId, DateHeaderValue, DestroyIds, 
Disposition, EmailAddressGroup, EmailCreationId, EmailCreationRequest, 
EmailCreationResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, 
EmailImport, EmailImportRequest, EmailImportR [...]
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
 import play.api.libs.json.{Format, JsArray, JsBoolean, JsError, JsNull, 
JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
 
@@ -241,8 +241,12 @@ class EmailSetSerializer @Inject()(messageIdFactory: 
MessageId.Factory, mailboxI
 
   private implicit val subjectReads: Reads[Subject] = Json.valueReads[Subject]
   private implicit val emailerNameReads: Reads[EmailerName] = 
Json.valueReads[EmailerName]
+  private implicit val unvalidatedEmailReads: Reads[UncheckedEmail] = 
Json.valueReads[UncheckedEmail]
+
   private implicit val headerMessageIdReads: Reads[HeaderMessageId] = 
Json.valueReads[HeaderMessageId]
   private implicit val emailAddressReads: Reads[EmailAddress] = 
Json.reads[EmailAddress]
+  private implicit val unvalidatedEmailAddressReads: 
Reads[UncheckedEmailAddress] = Json.reads[UncheckedEmailAddress]
+  private implicit val unvalidatedAddressesHeaderValueReads: 
Reads[UncheckedAddressesHeaderValue] = 
Json.valueReads[UncheckedAddressesHeaderValue]
   private implicit val addressesHeaderValueReads: Reads[AddressesHeaderValue] 
= Json.valueReads[AddressesHeaderValue]
   private implicit val messageIdsHeaderValueReads: 
Reads[MessageIdsHeaderValue] = {
     case JsArray(value) => value.map(headerMessageIdReads.reads)
@@ -308,12 +312,12 @@ class EmailSetSerializer @Inject()(messageIdFactory: 
MessageId.Factory, mailboxI
                                                 messageId: 
Option[MessageIdsHeaderValue],
                                                 references: 
Option[MessageIdsHeaderValue],
                                                 inReplyTo: 
Option[MessageIdsHeaderValue],
-                                                from: 
Option[AddressesHeaderValue],
-                                                to: 
Option[AddressesHeaderValue],
-                                                cc: 
Option[AddressesHeaderValue],
-                                                bcc: 
Option[AddressesHeaderValue],
-                                                sender: 
Option[AddressesHeaderValue],
-                                                replyTo: 
Option[AddressesHeaderValue],
+                                                from: 
Option[UncheckedAddressesHeaderValue],
+                                                to: 
Option[UncheckedAddressesHeaderValue],
+                                                cc: 
Option[UncheckedAddressesHeaderValue],
+                                                bcc: 
Option[UncheckedAddressesHeaderValue],
+                                                sender: 
Option[UncheckedAddressesHeaderValue],
+                                                replyTo: 
Option[UncheckedAddressesHeaderValue],
                                                 subject: Option[Subject],
                                                 sentAt: Option[UTCDate],
                                                 keywords: Option[Keywords],
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 82c51a7633..50f2c7c096 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
@@ -363,14 +363,14 @@ object EmailHeaders {
           .map(_.flatten))
         .filter(_.nonEmpty))
 
-  private def extractAddresses(mime4JMessage: Message, fieldName: String): 
Option[AddressesHeaderValue] =
+  private def extractAddresses(mime4JMessage: Message, fieldName: String): 
Option[UncheckedAddressesHeaderValue] =
     extractLastField(mime4JMessage, fieldName)
       .flatMap {
-        case f: AddressListField => 
Some(AddressesHeaderValue(EmailAddress.from(f.getAddressList)))
-        case f: MailboxListField => 
Some(AddressesHeaderValue(EmailAddress.from(f.getMailboxList)))
+        case f: AddressListField => 
Some(UncheckedAddressesHeaderValue(UncheckedEmailAddress.from(f.getAddressList)))
+        case f: MailboxListField => 
Some(UncheckedAddressesHeaderValue(UncheckedEmailAddress.from(f.getMailboxList)))
         case f: MailboxField =>
           val asMailboxListField = 
AddressListFieldLenientImpl.PARSER.parse(RawFieldParser.DEFAULT.parseField(f.getRaw),
 DecodeMonitor.SILENT)
-          
Some(AddressesHeaderValue(EmailAddress.from(asMailboxListField.getAddressList)))
+          
Some(UncheckedAddressesHeaderValue(UncheckedEmailAddress.from(asMailboxListField.getAddressList)))
         case _ => None
       }
       .filter(_.value.nonEmpty)
@@ -392,12 +392,12 @@ case class EmailHeaders(headers: List[EmailHeader],
                         messageId: MessageIdsHeaderValue,
                         inReplyTo: MessageIdsHeaderValue,
                         references: MessageIdsHeaderValue,
-                        to: Option[AddressesHeaderValue],
-                        cc: Option[AddressesHeaderValue],
-                        bcc: Option[AddressesHeaderValue],
-                        from: Option[AddressesHeaderValue],
-                        sender: Option[AddressesHeaderValue],
-                        replyTo: Option[AddressesHeaderValue],
+                        to: Option[UncheckedAddressesHeaderValue],
+                        cc: Option[UncheckedAddressesHeaderValue],
+                        bcc: Option[UncheckedAddressesHeaderValue],
+                        from: Option[UncheckedAddressesHeaderValue],
+                        sender: Option[UncheckedAddressesHeaderValue],
+                        replyTo: Option[UncheckedAddressesHeaderValue],
                         subject: Option[Subject],
                         sentAt: Option[UTCDate])
 
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 8999b38e6f..cb919180db 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
@@ -25,17 +25,20 @@ import cats.implicits._
 import com.google.common.net.MediaType
 import com.google.common.net.MediaType.{HTML_UTF_8, PLAIN_TEXT_UTF_8}
 import eu.timepit.refined
+import org.apache.james.core.MailAddress
 import org.apache.james.jmap.api.model.Size.Size
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName}
 import org.apache.james.jmap.core.Id.{Id, IdConstraint}
 import org.apache.james.jmap.core.{AccountId, SetError, UTCDate, UuidState}
 import org.apache.james.jmap.mail.Disposition.INLINE
+import org.apache.james.jmap.mail.EmailCreationRequest.KEYWORD_DRAFT
 import org.apache.james.jmap.method.WithAccountId
 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.Usage
 import org.apache.james.mime4j.codec.{DecodeMonitor, EncoderUtil}
-import org.apache.james.mime4j.dom.address.Mailbox
+import org.apache.james.mime4j.dom.address.{AddressList, MailboxList, Mailbox 
=> Mime4jMailbox}
 import org.apache.james.mime4j.dom.field.{ContentIdField, ContentTypeField, 
FieldName}
 import org.apache.james.mime4j.dom.{Entity, Message}
 import org.apache.james.mime4j.field.{ContentIdFieldImpl, Fields}
@@ -112,16 +115,66 @@ case class Attachment(blobId: BlobId,
   def isInline: Boolean = disposition.contains(INLINE)
 }
 
+case class UncheckedEmail(value: String) extends AnyVal
+
+object UncheckedEmailAddress {
+  def from(addressList: AddressList): List[UncheckedEmailAddress] = 
Option(addressList)
+    .map(addressList => from(addressList.flatten()))
+    .getOrElse(List())
+
+  def from(addressList: MailboxList): List[UncheckedEmailAddress] =
+    addressList.asScala
+      .toList
+      .filter(address => !address.getAddress.equals(">"))  // Temporary fix 
for https://github.com/linagora/james-project/issues/5086
+      .map(mailbox => UncheckedEmailAddress(
+        name = Option(mailbox.getName).map(EmailerName.from),
+        email = UncheckedEmail(mailbox.getAddress)))
+}
+case class UncheckedEmailAddress(name: Option[EmailerName], email: 
UncheckedEmail) {
+  def asMime4JMailbox: Mime4jMailbox = {
+    val parts = email.value.split('@')
+    val domainPart: String = parts match {
+      case Array(_, domain) => domain
+      case _ => ""
+    }
+    Some(email.value.split('@'))
+      .map(parts => new Mime4jMailbox(
+        name.map(_.value).orNull,
+        parts.head,
+        domainPart))
+      .get
+  }
+
+  def validate: Either[IllegalArgumentException, EmailAddress] =
+    Try(new MailAddress(email.value))
+      .map(email => EmailAddress(name, email))
+      .toEither match {
+      case scala.Right(value) => scala.Right(value)
+      case Left(e) => Left(new IllegalArgumentException(s"Invalid email 
address `${email.value}`", e))
+    }
+}
+
+case class UncheckedAddressesHeaderValue(value: List[UncheckedEmailAddress]) {
+  def asMime4JMailboxList: Option[List[Mime4jMailbox]] = 
Some(value.map(_.asMime4JMailbox)).filter(_.nonEmpty)
+
+  def validate: Either[IllegalArgumentException, AddressesHeaderValue] = 
value.map(_.validate)
+    .sequence
+    .map(l => AddressesHeaderValue(l))
+}
+
+object EmailCreationRequest {
+  val KEYWORD_DRAFT: Keyword = org.apache.james.jmap.mail.Keyword("$draft")
+}
 case class EmailCreationRequest(mailboxIds: MailboxIds,
                                 messageId: Option[MessageIdsHeaderValue],
                                 references: Option[MessageIdsHeaderValue],
                                 inReplyTo: Option[MessageIdsHeaderValue],
-                                from: Option[AddressesHeaderValue],
-                                to: Option[AddressesHeaderValue],
-                                cc: Option[AddressesHeaderValue],
-                                bcc: Option[AddressesHeaderValue],
-                                sender: Option[AddressesHeaderValue],
-                                replyTo: Option[AddressesHeaderValue],
+                                from: Option[UncheckedAddressesHeaderValue],
+                                to: Option[UncheckedAddressesHeaderValue],
+                                cc: Option[UncheckedAddressesHeaderValue],
+                                bcc: Option[UncheckedAddressesHeaderValue],
+                                sender: Option[UncheckedAddressesHeaderValue],
+                                replyTo: Option[UncheckedAddressesHeaderValue],
                                 subject: Option[Subject],
                                 sentAt: Option[UTCDate],
                                 keywords: Option[Keywords],
@@ -142,7 +195,7 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
           references.flatMap(_.asString).map(new RawField("References", 
_)).foreach(builder.setField)
           inReplyTo.flatMap(_.asString).map(new RawField("In-Reply-To", 
_)).foreach(builder.setField)
           subject.foreach(value => builder.setSubject(value.value))
-          val maybeFrom: Option[List[Mailbox]] = 
from.flatMap(_.asMime4JMailboxList)
+          val maybeFrom: Option[List[Mime4jMailbox]] = 
from.flatMap(_.asMime4JMailboxList)
           maybeFrom.map(_.asJava).foreach(builder.setFrom)
           
to.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setTo)
           
cc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setCc)
@@ -167,7 +220,7 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
             })
       }
 
-  private def generateUniqueMessageId(fromAddress: Option[List[Mailbox]]): 
String = 
+  private def generateUniqueMessageId(fromAddress: 
Option[List[Mime4jMailbox]]): String =
     
MimeUtil.createUniqueMessageId(fromAddress.flatMap(_.headOption).map(_.getDomain).orNull)
 
   private def createAlternativeBody(htmlBody: Option[ClientBodyPart], 
textBody: Option[ClientBodyPart], htmlTextExtractor: HtmlTextExtractor) = {
@@ -316,6 +369,26 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     case _ => Left(new IllegalArgumentException("Expecting textBody to 
contains only 1 part"))
   }
 
+  def validateRequest: Either[IllegalArgumentException, EmailCreationRequest] 
= validateEmailAddressHeader
+
+  def validateEmailAddressHeader: Either[IllegalArgumentException, 
EmailCreationRequest] = keywords match {
+    case Some(k) if k.keywords.contains(KEYWORD_DRAFT) => scala.Right(this)
+    case _ => doValidateEmailAddressHeader()
+  }
+
+  private def doValidateEmailAddressHeader(): Either[IllegalArgumentException, 
EmailCreationRequest] = {
+    val addressesHeaderInvalid: Map[String, IllegalArgumentException] = 
Map("from" -> from, "to" -> to, "cc" -> cc, "bcc" -> bcc, "sender" -> sender, 
"replyTo" -> replyTo)
+      .map {
+        case (name, maybeAddresses) => (name, maybeAddresses.map(_.validate))
+      }.collect { case (name, Some(addresses)) => (name, addresses) }
+      .collect { case (name, scala.Left(exception)) => (name, exception) }
+
+    addressesHeaderInvalid match {
+      case invalid if invalid.nonEmpty => Left(new 
IllegalArgumentException(s"/${addressesHeaderInvalid.map { case (name, 
exception) => s"$name: ${exception.getMessage}" }.mkString(", ")}"))
+      case _ => scala.Right(this)
+    }
+  }
+
   private def retrieveCorrespondingBody(partId: ClientPartId): 
Option[Either[IllegalArgumentException, Some[ClientBodyPart]]] =
     bodyValues.getOrElse(Map())
       .get(partId)
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 8a8361fd43..d32a474fc7 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
@@ -104,7 +104,9 @@ class EmailSetCreatePerformer @Inject()(serializer: 
EmailSetSerializer,
       .concatMap {
         case (clientId, json) => serializer.deserializeCreationRequest(json)
           .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new 
IllegalArgumentException(e.toString))),
-            creationRequest => create(clientId, creationRequest, 
mailboxSession))
+            creationRequest => creationRequest.validateRequest
+              .fold(e => SMono.just[CreationResult](CreationFailure(clientId, 
e)),
+                _ => create(clientId, creationRequest, mailboxSession)))
       }.collectSeq()
       .map(CreationResults)
 
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index a2be422488..1478c0994b 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -269,6 +269,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
       submissionId = EmailSubmissionId.generate
       message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+      _ <- validateMimeMessages(message)
       envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
       _ <- validate(mailboxSession)(message, envelope)
       _ <- SMono.fromTry(validateFromParameters(envelope.mailFrom.parameters))
@@ -348,6 +349,24 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
       Failure(new IllegalArgumentException("Invalid delayed time!"))
     }
 
+  def validateMimeMessages(mimeMessage: MimeMessage) : SMono[MimeMessage] = 
validateMailAddressHeaderMimeMessage(mimeMessage)
+  private def validateMailAddressHeaderMimeMessage(mimeMessage: MimeMessage): 
SMono[MimeMessage] =
+    SFlux.fromIterable(Map("to" -> 
Option(mimeMessage.getRecipients(RecipientType.TO)).toList.flatten,
+        "cc" -> 
Option(mimeMessage.getRecipients(RecipientType.CC)).toList.flatten,
+        "bcc" -> 
Option(mimeMessage.getRecipients(RecipientType.BCC)).toList.flatten,
+        "from" -> Option(mimeMessage.getFrom).toList.flatten,
+        "sender" -> Option(mimeMessage.getSender).toList,
+        "replyTo" -> Option(mimeMessage.getReplyTo).toList.flatten))
+      .doOnNext { case (headerName, addresses) => (headerName, 
addresses.foreach(address => validateMailAddress(headerName, address))) }
+      .`then`()
+      .`then`(SMono.just(mimeMessage))
+
+  private def validateMailAddress(headName: String, address: Address): 
MailAddress =
+    Try(new MailAddress(address.toString)) match {
+      case Success(mailAddress) => mailAddress
+      case Failure(_) => throw new IllegalArgumentException(s"Invalid mail 
address: $address in $headName header")
+    }
+
   def validateRcptTo(recipients: List[EmailSubmissionAddress]): 
SMono[List[EmailSubmissionAddress]] =
     SFlux.fromIterable(recipients)
       .filter(validateRecipient)
diff --git 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/EmailSetSerializerTest.scala
 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/EmailSetSerializerTest.scala
new file mode 100644
index 0000000000..300a5405a9
--- /dev/null
+++ 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/EmailSetSerializerTest.scala
@@ -0,0 +1,87 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.json
+
+import org.apache.james.jmap.json.EmailSetSerializerTest.SERIALIZER
+import org.apache.james.jmap.mail.EmailCreationRequest
+import org.apache.james.mailbox.model.{TestId, TestMessageId}
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import play.api.libs.json.{JsResult, Json}
+
+object EmailSetSerializerTest {
+  val SERIALIZER: EmailSetSerializer = new EmailSetSerializer(new 
TestMessageId.Factory, new TestId.Factory)
+}
+
+class EmailSetSerializerTest extends AnyWordSpec with Matchers {
+
+  "Deserialize EmailSetRequest" should {
+    "Request should be success" in {
+      val jsResult: JsResult[EmailCreationRequest] = 
SERIALIZER.deserializeCreationRequest(
+        Json.parse(
+          """{
+            |    "mailboxIds": {
+            |        "1": true
+            |    },
+            |    "keywords": {
+            |        "$draft": true,
+            |        "$seen": true
+            |    },
+            |    "subject": "draft 1",
+            |    "from": [
+            |        {
+            |            "name": "Van Tung TRAN",
+            |            "email": "vtt...@linagora.com"
+            |        }
+            |    ],
+            |    "to": [
+            |        {
+            |            "name": null,
+            |            "email": "bt"
+            |        }
+            |    ],
+            |    "cc": [],
+            |    "bcc": [],
+            |    "replyTo": [
+            |        {
+            |            "name": null,
+            |            "email": "vtt...@linagora.com"
+            |        }
+            |    ],
+            |    "htmlBody": [
+            |        {
+            |            "partId": "951c3960-d139-11ee-843e-b70023541167",
+            |            "type": "text/html"
+            |        }
+            |    ],
+            |    "bodyValues": {
+            |        "951c3960-d139-11ee-843e-b70023541167": {
+            |            "value": "<div><br>xin chao</div>",
+            |            "isEncodingProblem": false,
+            |            "isTruncated": false
+            |        }
+            |    },
+            |    "header:User-Agent:asText": "Team-Mail/0.11.3 Mozilla/5.0 
(Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/121.0.0.0 Safari/537.36"
+            |}""".stripMargin))
+
+      assert(jsResult.isSuccess)
+    }
+  }
+}


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


Reply via email to