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 7b6d4c2534 JAMES-3434 EmailSubmission/set should forbid sending email without Mi… (#2624) 7b6d4c2534 is described below commit 7b6d4c253485ee096206551505f29fa13fa21ce6 Author: Rene Cordier <rcord...@linagora.com> AuthorDate: Thu Feb 6 21:41:40 2025 +0700 JAMES-3434 EmailSubmission/set should forbid sending email without Mi… (#2624) --- .../EmailSubmissionSetMethodContract.scala | 55 ++++++++++++++++++++++ .../jmap/method/EmailSubmissionSetMethod.scala | 29 ++++++++---- 2 files changed, 75 insertions(+), 9 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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala index 3903deec50..85515ba42f 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala @@ -1877,6 +1877,61 @@ trait EmailSubmissionSetMethodContract { |}""".stripMargin) } + @Test + def setShouldRejectWhenMissingFromMimeField(server: GuiceJamesServer): Unit = { + val message: Message = Message.Builder + .of + .setSubject("test") + .setTo(ANDRE.asString) + .setBody("testmail", StandardCharsets.UTF_8) + .build + + val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS) + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath) + val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder() + .build(message)) + .getMessageId + + val requestBob = + s"""{ + | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"], + | "methodCalls": [ + | ["EmailSubmission/set", { + | "accountId": "$ACCOUNT_ID", + | "create": { + | "k1490": { + | "emailId": "${messageId.serialize}", + | "envelope": { + | "mailFrom": {"email": "${BOB.asString}"}, + | "rcptTo": [{"email": "${ANDRE.asString}"}] + | } + | } + | } + | }, "c1"]] + |}""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(requestBob) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0][1].notCreated") + .isEqualTo("""{ + | "k1490": { + | "type": "forbiddenFrom", + | "description": "Attempt to send a mail whose MimeMessage From is missing" + | } + |}""".stripMargin) + } + @Test def setShouldRejectOtherUserUsageInFromEnvelopeField(server: GuiceJamesServer): Unit = { val message: Message = Message.Builder 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 31e1c287fc..55c76d6449 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 @@ -98,6 +98,10 @@ object EmailSubmissionSetMethod { SetError(EmailSubmissionSetMethod.forbiddenFrom, SetErrorDescription(s"Attempt to send a mail whose envelope From not allowed for connected user: ${e.from}"), Some(Properties("envelope.mailFrom"))) + case e: ForbiddenHeaderFromException => + LOGGER.warn(s"Attempt to send a mail whose MimeMessage From is missing") + SetError(EmailSubmissionSetMethod.forbiddenFrom, + SetErrorDescription(s"Attempt to send a mail whose MimeMessage From is missing"), None) case _: MessageNotFoundException => LOGGER.info(" EmailSubmission/set failed as the underlying email could not be found") SetError(SetError.invalidArgumentValue, @@ -157,6 +161,7 @@ case class EmailSubmissionCreationParseException(setError: SetError) extends Exc case class NoRecipientException() extends Exception case class ForbiddenFromException(from: String) extends Exception case class ForbiddenMailFromException(from: List[String]) extends Exception +case class ForbiddenHeaderFromException() extends Exception case class MessageMimeMessageSource(id: String, message: MessageResult) extends MimeMessageSource { override def getSourceId: String = id @@ -354,15 +359,21 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize 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)) + Option(mimeMessage.getFrom) match { + case Some(from) if from.nonEmpty => 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" -> from.toList, + "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)) + + case _ => SMono.error(ForbiddenHeaderFromException()) + } + private def validateMailAddress(headName: String, address: Address): MailAddress = Try(new MailAddress(asString(address))) match { --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org