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 2023/01/12 08:11:43 UTC

[james-project] branch master updated: JAMES-3756 Jmap download/upload endpoint should support being called with accountIds of delegated accounts (#1382)

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 130f7aca89 JAMES-3756 Jmap download/upload endpoint should support being called with accountIds of delegated accounts (#1382)
130f7aca89 is described below

commit 130f7aca89284a970c698b15ba7906df5d1ca79d
Author: vttran <vt...@linagora.com>
AuthorDate: Thu Jan 12 15:11:37 2023 +0700

    JAMES-3756 Jmap download/upload endpoint should support being called with accountIds of delegated accounts (#1382)
---
 .../jmap/rfc8621/contract/DownloadContract.scala   | 95 +++++++++++++++++++++-
 .../jmap/rfc8621/contract/UploadContract.scala     | 46 ++++++++++-
 .../apache/james/jmap/routes/DownloadRoutes.scala  | 31 +++----
 .../apache/james/jmap/routes/UploadRoutes.scala    | 36 ++++----
 4 files changed, 163 insertions(+), 45 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/DownloadContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
index c4ddb5b961..650aad6e25 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
@@ -21,7 +21,6 @@ package org.apache.james.jmap.rfc8621.contract
 
 import java.io.ByteArrayInputStream
 import java.nio.charset.StandardCharsets
-
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import org.apache.commons.io.IOUtils
@@ -29,7 +28,7 @@ import org.apache.http.HttpStatus.{SC_FORBIDDEN, SC_NOT_FOUND, SC_OK, SC_UNAUTHO
 import org.apache.james.GuiceJamesServer
 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, ALICE_ACCOUNT_ID, ANDRE, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ALICE_ACCOUNT_ID, ANDRE, BOB, BOB_PASSWORD, CEDRIC, DOMAIN, authScheme, baseRequestSpecBuilder}
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.Right
 import org.apache.james.mailbox.model.{MailboxACL, MailboxPath, MessageId}
@@ -51,6 +50,8 @@ trait DownloadContract {
       .fluent
       .addDomain(DOMAIN.asString)
       .addUser(BOB.asString, BOB_PASSWORD)
+      .addUser(ANDRE.asString, BOB_PASSWORD)
+      .addUser(CEDRIC.asString, BOB_PASSWORD)
 
     requestSpecification = baseRequestSpecBuilder(server)
       .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
@@ -111,7 +112,7 @@ trait DownloadContract {
   }
 
   @Test
-  def downloadMessageShouldSucceedWhenDelegated(server: GuiceJamesServer): Unit = {
+  def downloadMessageShouldSucceedWhenAddedRightACL(server: GuiceJamesServer): Unit = {
     val path = MailboxPath.inbox(ANDRE)
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
     val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
@@ -182,7 +183,7 @@ trait DownloadContract {
   }
 
   @Test
-  def downloadPartShouldSucceedWhenDelegated(server: GuiceJamesServer): Unit = {
+  def downloadPartShouldSucceedWhenAddedRightACL(server: GuiceJamesServer): Unit = {
     val path = MailboxPath.inbox(ANDRE)
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
     val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
@@ -533,4 +534,90 @@ trait DownloadContract {
       .body("type", equalTo("about:blank"))
       .body("detail", equalTo("The resource could not be found"))
   }
+
+  @Test
+  def downloadMessageShouldSucceedWhenDelegatedAccount(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(ANDRE)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString, path, AppendCommand.from(
+        ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    server.getProbe(classOf[DataProbeImpl]).addAuthorizedUser(ANDRE, BOB)
+
+    val response = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/${Fixture.ANDRE_ACCOUNT_ID}/${messageId.serialize()}")
+    .`then`
+      .statusCode(SC_OK)
+      .contentType("message/rfc822")
+      .extract
+      .body
+      .asString
+
+    val expectedResponse: String = IOUtils.toString(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml"),
+      StandardCharsets.UTF_8)
+    assertThat(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)))
+      .hasContent(expectedResponse)
+  }
+
+  @Test
+  def downloadMessageShouldFailWhenNotDelegatedAccount(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(ANDRE)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString, path, AppendCommand.from(
+        ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    server.getProbe(classOf[DataProbeImpl]).addAuthorizedUser(ANDRE, CEDRIC)
+
+    `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/${Fixture.ANDRE_ACCOUNT_ID}/${messageId.serialize()}")
+    .`then`
+      .statusCode(SC_FORBIDDEN)
+      .body("status", equalTo(403))
+      .body("type", equalTo("about:blank"))
+      .body("detail", equalTo("You cannot download in others accounts"))
+  }
+
+  @Test
+  def downloadPartMessageShouldSucceedWhenDelegatedAccount(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(ANDRE)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString, path, AppendCommand.from(
+        ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    server.getProbe(classOf[DataProbeImpl]).addAuthorizedUser(ANDRE, BOB)
+
+    val response = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/${Fixture.ANDRE_ACCOUNT_ID}/${messageId.serialize()}_3")
+    .`then`
+      .statusCode(SC_OK)
+      .contentType("text/plain")
+    .extract
+      .body
+      .asString
+
+    val expectedResponse: String =
+      """-----BEGIN RSA PRIVATE KEY-----
+        |MIIEogIBAAKCAQEAx7PG0+E//EMpm7IgI5Q9TMDSFya/1hE+vvTJrk0iGFllPeHL
+        |A5/VlTM0YWgG6X50qiMfE3VLazf2c19iXrT0mq/21PZ1wFnogv4zxUNaih+Bng62
+        |F0SyruE/O/Njqxh/Ccq6K/e05TV4T643USxAeG0KppmYW9x8HA/GvV832apZuxkV
+        |i6NVkDBrfzaUCwu4zH+HwOv/pI87E7KccHYC++Biaj3
+        |""".stripMargin
+    assertThat(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)))
+      .hasContent(expectedResponse)
+  }
 }
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/UploadContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
index da9a95ef0b..25e9ed1a5d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
@@ -20,17 +20,18 @@ package org.apache.james.jmap.rfc8621.contract
 
 import java.io.ByteArrayInputStream
 import java.nio.charset.StandardCharsets
-
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType
 import org.apache.http.HttpStatus.{SC_BAD_REQUEST, SC_CREATED, SC_FORBIDDEN, SC_OK, SC_UNAUTHORIZED}
 import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.core.AccountId
 import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ALICE, ALICE_ACCOUNT_ID, ALICE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, _2_DOT_DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ALICE, ALICE_ACCOUNT_ID, ALICE_PASSWORD, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, _2_DOT_DOMAIN, authScheme, baseRequestSpecBuilder}
 import org.apache.james.jmap.rfc8621.contract.UploadContract.{BIG_INPUT, VALID_INPUT}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
+import org.hamcrest.Matchers
 import org.hamcrest.Matchers.equalTo
 import org.junit.jupiter.api.{BeforeEach, RepeatedTest, Test}
 import play.api.libs.json.{JsString, Json}
@@ -49,6 +50,7 @@ trait UploadContract {
       .addUser(BOB.asString, BOB_PASSWORD)
       .addDomain(_2_DOT_DOMAIN.asString())
       .addUser(ALICE.asString(), ALICE_PASSWORD)
+      .addUser(ANDRE.asString(), ANDRE_PASSWORD)
 
     requestSpecification = baseRequestSpecBuilder(server)
       .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
@@ -166,4 +168,44 @@ trait UploadContract {
       .body("type", equalTo("about:blank"))
       .body("detail", equalTo("No valid authentication methods provided"))
   }
+
+  @Test
+  def bobShouldBeAllowedToUploadInAliceAccountWhenDelegated(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl]).addAuthorizedUser(ALICE, BOB)
+
+    val aliceAccountId: String = AccountId.from(ALICE).toOption.get.id.value
+
+    `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(VALID_INPUT)
+    .when
+      .post(s"/upload/$aliceAccountId")
+    .`then`
+      .statusCode(SC_CREATED)
+      .body("size", equalTo(11534336))
+      .body("type", equalTo("application/json; charset=UTF-8"))
+      .body("blobId", Matchers.notNullValue())
+      .body("accountId", equalTo(aliceAccountId))
+  }
+
+  @Test
+  def bobShouldBeNotAllowedToUploadInAliceAccountWhenNotDelegated(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl]).addAuthorizedUser(ALICE, ANDRE)
+
+    val aliceAccountId: String = AccountId.from(ALICE).toOption.get.id.value
+
+    `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(VALID_INPUT)
+    .when
+      .post(s"/upload/$aliceAccountId")
+    .`then`
+      .statusCode(SC_FORBIDDEN)
+      .body("status", equalTo(403))
+      .body("type", equalTo("about:blank"))
+      .body("detail", equalTo("Upload to other accounts is forbidden"))
+  }
+
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
index 305578a241..0b54980f25 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
@@ -18,11 +18,6 @@
  ****************************************************************/
 package org.apache.james.jmap.routes
 
-import java.io.InputStream
-import java.nio.charset.StandardCharsets
-import java.util.stream
-import java.util.stream.Stream
-
 import com.google.common.base.CharMatcher
 import eu.timepit.refined.numeric.NonNegative
 import eu.timepit.refined.refineV
@@ -30,19 +25,19 @@ import io.netty.buffer.Unpooled
 import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE}
 import io.netty.handler.codec.http.HttpResponseStatus._
 import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus, QueryStringDecoder}
-import javax.inject.{Inject, Named}
 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
 import org.apache.james.jmap.api.model.{Upload, UploadId, UploadNotFoundException}
 import org.apache.james.jmap.api.upload.UploadRepository
 import org.apache.james.jmap.core.Id.Id
-import org.apache.james.jmap.core.{AccountId, Id, ProblemDetails}
+import org.apache.james.jmap.core.{AccountId, Id, ProblemDetails, SessionTranslator}
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.ResponseSerializer
 import org.apache.james.jmap.mail.{BlobId, EmailBodyPart, PartId}
+import org.apache.james.jmap.method.AccountNotFoundException
 import org.apache.james.jmap.routes.DownloadRoutes.{BUFFER_SIZE, LOGGER}
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.model.ContentType.{MediaType, MimeType, SubType}
@@ -60,6 +55,11 @@ import reactor.core.scala.publisher.SMono
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
+import java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.util.stream
+import java.util.stream.Stream
+import javax.inject.{Inject, Named}
 import scala.compat.java8.FunctionConverters._
 import scala.jdk.CollectionConverters._
 import scala.util.{Failure, Success, Try}
@@ -235,7 +235,8 @@ class BlobResolvers(blobResolvers: Set[BlobResolver]) {
 }
 
 class DownloadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
-                               val blobResolvers: BlobResolvers) extends JMAPRoutes {
+                               val blobResolvers: BlobResolvers,
+                               val sessionTranslator: SessionTranslator) extends JMAPRoutes {
 
   private val accountIdParam: String = "accountId"
   private val blobIdParam: String = "blobId"
@@ -257,7 +258,7 @@ class DownloadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
     SMono(authenticator.authenticate(request))
       .flatMap(mailboxSession => getIfOwner(request, response, mailboxSession))
       .onErrorResume {
-        case e: ForbiddenException =>
+        case _: ForbiddenException | _: AccountNotFoundException =>
           respondDetails(response,
             ProblemDetails(status = FORBIDDEN, detail = "You cannot download in others accounts"),
             FORBIDDEN)
@@ -295,16 +296,8 @@ class DownloadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
 
   private def getIfOwner(request: HttpServerRequest, response: HttpServerResponse, mailboxSession: MailboxSession): SMono[Unit] =
     Id.validate(request.param(accountIdParam)) match {
-      case Right(id: Id) =>
-        val targetAccountId: AccountId = AccountId(id)
-        AccountId.from(mailboxSession.getUser).map(accountId => accountId.equals(targetAccountId))
-          .fold[SMono[Unit]](
-            e => SMono.error(e),
-            value => if (value) {
-              get(request, response, mailboxSession)
-            } else {
-              SMono.error(ForbiddenException())
-            })
+      case Right(id: Id) => sessionTranslator.delegateIfNeeded(mailboxSession, AccountId(id))
+          .flatMap(session => get(request, response, session))
       case Left(throwable: Throwable) => SMono.error(throwable)
     }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
index 4dd37081f8..971246e809 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -24,10 +24,10 @@ import java.nio.ByteBuffer
 import java.nio.charset.StandardCharsets
 import java.util.stream
 import java.util.stream.Stream
-
 import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE}
 import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, CREATED, FORBIDDEN, INTERNAL_SERVER_ERROR, UNAUTHORIZED}
 import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
+
 import javax.inject.{Inject, Named}
 import org.apache.commons.fileupload.util.LimitedInputStream
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
@@ -35,12 +35,13 @@ import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.api.model.{UploadId, UploadMetaData}
 import org.apache.james.jmap.api.upload.UploadRepository
 import org.apache.james.jmap.core.Id.Id
-import org.apache.james.jmap.core.{AccountId, Id, JmapRfc8621Configuration, ProblemDetails}
+import org.apache.james.jmap.core.{AccountId, Id, JmapRfc8621Configuration, ProblemDetails, SessionTranslator}
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.{ResponseSerializer, UploadSerializer}
 import org.apache.james.jmap.mail.BlobId
+import org.apache.james.jmap.method.AccountNotFoundException
 import org.apache.james.jmap.routes.UploadRoutes.LOGGER
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.MailboxSession
@@ -66,10 +67,8 @@ case class UploadResponse(accountId: AccountId,
 class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
                              val configuration: JmapRfc8621Configuration,
                              val uploadRepository: UploadRepository,
-                             val serializer: UploadSerializer) extends JMAPRoutes {
-
-  class CancelledUploadException extends RuntimeException
-  class MaxFileSizeUploadException extends RuntimeException
+                             val serializer: UploadSerializer,
+                             val sessionTranslator: SessionTranslator) extends JMAPRoutes {
 
   private val accountIdParam: String = "accountId"
   private val uploadURI = s"/upload/{$accountIdParam}"
@@ -99,7 +98,7 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
             respondDetails(response,
               ProblemDetails(status = BAD_REQUEST, detail = "Attempt to upload exceed max size"),
               BAD_REQUEST)
-          case _: ForbiddenException =>
+          case _: ForbiddenException | _: AccountNotFoundException =>
             respondDetails(response,
               ProblemDetails(status = FORBIDDEN, detail = "Upload to other accounts is forbidden"),
               FORBIDDEN)
@@ -116,27 +115,24 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
     }
   }
 
-  def post(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, session: MailboxSession): SMono[Void] = {
+  def post(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, mailboxSession: MailboxSession): SMono[Void] = {
     Id.validate(request.param(accountIdParam)) match {
       case Right(id: Id) =>
         val targetAccountId: AccountId = AccountId(id)
-        AccountId.from(session.getUser).map(accountId => accountId.equals(targetAccountId))
-          .fold[SMono[Void]](
-            e => SMono.error(e),
-            value => if (value) {
-              SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive
-                // Unwrapping to byte array needed to solve data races and buffer reordering when using .asByteBuffer()
-                .asByteArray()
-                .map(array => ByteBuffer.wrap(array))))
-              .flatMap(content => handle(targetAccountId, contentType, content, session, response))
-            } else {
-              SMono.error(ForbiddenException())
-            })
+        sessionTranslator.delegateIfNeeded(mailboxSession, targetAccountId)
+          .flatMap(session => handle(request, response, contentType, session, targetAccountId))
 
       case Left(throwable: Throwable) => SMono.error(throwable)
     }
   }
 
+  private def handle(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, session: MailboxSession, targetAccountId: AccountId): SMono[Void] =
+    SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive
+      // Unwrapping to byte array needed to solve data races and buffer reordering when using .asByteBuffer()
+      .asByteArray()
+      .map(array => ByteBuffer.wrap(array))))
+      .flatMap(content => handle(targetAccountId, contentType, content, session, response))
+
   def handle(accountId: AccountId, contentType: ContentType, content: InputStream, mailboxSession: MailboxSession, response: HttpServerResponse): SMono[Void] = {
     val maxSize: Long = configuration.maxUploadSize.value.value
 


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