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/10/26 05:20:13 UTC

[james-project] branch master updated (920e575 -> 7d94885)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 920e575  JAMES-3433 Ensure CachedBlobStore  is only queried for headers
     new 7d3624a  JAMES-3411 [REFACTORING] Validate EmailSetUpdates upfront
     new 0b05fa3  JAMES-3277 Optimization: JMAP Rely on MessageManager::moveMessage (Draft)
     new 2bbde61  JAMES-3277 Optimize range message updates for RFC-8621
     new eed9ea5  JAMES-3410 Optimisation Group destroys together
     new 7f6003f  JAMES-3432 Upload: Attachment
     new 8335aa3  JAMES-3432 Add test case downloadShouldRejectWhenDownloadFromOther
     new 41065ee  JAMES-3432 fix indent issue for Licenses
     new 7d94885  JAMES-3412 Keywords are case insentive, lower cased

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/james/mailbox/MailboxManager.java   |   2 +
 .../james/mailbox/store/StoreMailboxManager.java   |   8 +
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   5 +-
 .../methods/integration/SetMessagesMethodTest.java |  74 +++++
 .../draft/methods/SetMessagesUpdateProcessor.java  |  60 ++++
 .../james/jmap/draft/model/UpdateMessagePatch.java |   6 +
 ...sioningTest.java => DistributedUploadTest.java} |   4 +-
 .../jmap/rfc8621/contract/DownloadContract.scala   |  20 +-
 .../rfc8621/contract/EmailGetMethodContract.scala  |  16 +-
 .../rfc8621/contract/EmailSetMethodContract.scala  | 314 ++++++++++++++++++++-
 .../james/jmap/rfc8621/contract/Fixture.scala      |   2 +
 .../jmap/rfc8621/contract/UploadContract.scala     | 164 +++++++++++
 ...isioningTest.java => MemoryUploadContract.java} |   4 +-
 .../apache/james/jmap/http/SessionSupplier.scala   |  10 +-
 .../apache/james/jmap/json/UploadSerializer.scala  |  24 +-
 .../org/apache/james/jmap/mail/EmailAddress.scala  |   2 +-
 .../org/apache/james/jmap/mail/EmailBodyPart.scala |   2 +-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  19 +-
 .../scala/org/apache/james/jmap/mail/Mailbox.scala |   2 +-
 .../scala/org/apache/james/jmap/mail/Quotas.scala  |   2 +-
 .../scala/org/apache/james/jmap/mail/Rights.scala  |   2 +-
 .../apache/james/jmap/method/CoreEchoMethod.scala  |  10 +-
 .../apache/james/jmap/method/EmailGetMethod.scala  |  10 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  | 183 +++++++++---
 .../org/apache/james/jmap/method/Method.scala      |   8 +-
 .../org/apache/james/jmap/model/Capabilities.scala |  10 +-
 .../org/apache/james/jmap/model/Capability.scala   |  10 +-
 .../scala/org/apache/james/jmap/model/Id.scala     |   2 +-
 .../org/apache/james/jmap/model/Invocation.scala   |  10 +-
 .../org/apache/james/jmap/model/Keyword.scala      |  18 +-
 .../apache/james/jmap/model/RequestObject.scala    |  10 +-
 .../apache/james/jmap/model/ResponseObject.scala   |  10 +-
 .../org/apache/james/jmap/model/Session.scala      |  10 +-
 .../org/apache/james/jmap/model/UnsignedInt.scala  |  10 +-
 .../apache/james/jmap/routes/DownloadRoutes.scala  |  88 ++++--
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  10 +-
 .../apache/james/jmap/routes/UploadRoutes.scala    | 146 ++++++++++
 37 files changed, 1115 insertions(+), 172 deletions(-)
 copy server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/{DistributedProvisioningTest.java => DistributedUploadTest.java} (94%)
 create mode 100644 server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/UploadContract.scala
 copy server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/{MemoryProvisioningTest.java => MemoryUploadContract.java} (93%)
 copy backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/utils/LightweightTransactionException.java => server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala (60%)
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala


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


[james-project] 05/08: JAMES-3432 Upload: Attachment

Posted by bt...@apache.org.
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 7f6003f5ddc5540c56622f99579fb59f6af5106b
Author: duc91 <vd...@linagora.com>
AuthorDate: Thu Oct 22 18:11:47 2020 +0700

    JAMES-3432 Upload: Attachment
---
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   5 +-
 .../rfc8621/distributed/DistributedUploadTest.java |  53 ++++++++
 .../james/jmap/rfc8621/contract/Fixture.scala      |   2 +
 .../jmap/rfc8621/contract/UploadContract.scala     | 131 ++++++++++++++++++++
 .../apache/james/jmap/json/UploadSerializer.scala  |  15 +++
 .../apache/james/jmap/routes/DownloadRoutes.scala  |  39 ++++--
 .../apache/james/jmap/routes/UploadRoutes.scala    | 133 +++++++++++++++++++++
 7 files changed, 368 insertions(+), 10 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 535f1d5..13eeaed 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -47,6 +47,7 @@ import org.apache.james.jmap.method.VacationResponseSetMethod;
 import org.apache.james.jmap.method.ZoneIdProvider;
 import org.apache.james.jmap.model.JmapRfc8621Configuration;
 import org.apache.james.jmap.routes.DownloadRoutes;
+import org.apache.james.jmap.routes.UploadRoutes;
 import org.apache.james.jmap.routes.JMAPApiRoutes;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.utils.PropertiesProvider;
@@ -80,8 +81,8 @@ public class RFC8621MethodsModule extends AbstractModule {
     }
 
     @ProvidesIntoSet
-    JMAPRoutesHandler routesHandler(SessionRoutes sessionRoutes, JMAPApiRoutes jmapApiRoutes, DownloadRoutes downloadRoutes) {
-        return new JMAPRoutesHandler(Version.RFC8621, jmapApiRoutes, sessionRoutes, downloadRoutes);
+    JMAPRoutesHandler routesHandler(SessionRoutes sessionRoutes, JMAPApiRoutes jmapApiRoutes, DownloadRoutes downloadRoutes, UploadRoutes uploadRoutes) {
+        return new JMAPRoutesHandler(Version.RFC8621, jmapApiRoutes, sessionRoutes, downloadRoutes, uploadRoutes);
     }
 
     @Provides
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedUploadTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedUploadTest.java
new file mode 100644
index 0000000..9d432db
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedUploadTest.java
@@ -0,0 +1,53 @@
+/****************************************************************
+ * 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.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.UploadContract;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedUploadTest implements UploadContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+        CassandraRabbitMQJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .blobStore(BlobStoreConfiguration.builder()
+                .s3()
+                .disableCache()
+                .deduplication())
+            .build())
+        .extension(new DockerElasticSearchExtension())
+        .extension(new CassandraExtension())
+        .extension(new RabbitMQExtension())
+        .extension(new AwsS3BlobStoreExtension())
+        .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+}
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/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
index 4b70b04..6b2a9fb 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
@@ -38,6 +38,7 @@ import org.apache.james.mime4j.dom.Message
 
 object Fixture {
   val ACCOUNT_ID: String = "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
+  val ALICE_ACCOUNT_ID: String = "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90"
 
   def createTestMessage: Message = Message.Builder
       .of
@@ -140,6 +141,7 @@ object Fixture {
       |}""".stripMargin
 
   val ACCEPT_RFC8621_VERSION_HEADER: String = "application/json; jmapVersion=rfc-8621"
+  val RFC8621_VERSION_HEADER: String = "jmapVersion=rfc-8621"
 
   val USER: Username = Username.fromLocalPartWithDomain("user", DOMAIN)
   val USER_PASSWORD: String = "user"
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
new file mode 100644
index 0000000..9605f44
--- /dev/null
+++ 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
@@ -0,0 +1,131 @@
+package org.apache.james.jmap.rfc8621.contract
+
+import java.io.{ByteArrayInputStream, InputStream}
+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 net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.commons.io.IOUtils
+import org.apache.http.HttpStatus.{SC_CREATED, SC_NOT_FOUND, SC_OK, SC_UNAUTHORIZED}
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, BOB, BOB_PASSWORD, DOMAIN, RFC8621_VERSION_HEADER, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.UploadContract.{BIG_INPUT_STREAM, VALID_INPUT_STREAM}
+import org.apache.james.utils.DataProbeImpl
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.{BeforeEach, Test}
+import play.api.libs.json.{JsString, Json}
+
+object UploadContract {
+  private val BIG_INPUT_STREAM: InputStream = new ByteArrayInputStream("123456789\r\n".repeat(10025).getBytes)
+  private val VALID_INPUT_STREAM: InputStream = new ByteArrayInputStream("123456789\r\n".repeat(1).getBytes)
+}
+
+trait UploadContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def shouldUploadFileAndOnlyOwnerCanAccess(): Unit = {
+    val uploadResponse: String = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(VALID_INPUT_STREAM)
+    .when
+      .post(s"/upload/$ACCOUNT_ID/")
+    .`then`
+      .statusCode(SC_CREATED)
+      .extract
+      .body
+      .asString
+
+    val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+    val downloadResponse: String = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/$ACCOUNT_ID/$blobId")
+    .`then`
+      .statusCode(SC_OK)
+      .extract
+      .body
+      .asString
+
+    val expectedResponse: String = IOUtils.toString(VALID_INPUT_STREAM, StandardCharsets.UTF_8)
+
+    assertThat(new ByteArrayInputStream(downloadResponse.getBytes(StandardCharsets.UTF_8)))
+      .hasContent(expectedResponse)
+  }
+
+  @Test
+  def shouldRejectWhenUploadFileTooBig(): Unit = {
+    val response: String = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .contentType(ContentType.BINARY)
+      .body(BIG_INPUT_STREAM)
+    .when
+      .post(s"/upload/$ACCOUNT_ID/")
+    .`then`
+      .statusCode(SC_OK)
+      .extract
+      .body
+      .asString
+
+    // fixme: dont know we limit size or not?
+    assertThatJson(response)
+      .isEqualTo("Should be error")
+  }
+
+  @Test
+  def uploadShouldRejectWhenUnauthenticated(): Unit = {
+    `given`
+      .auth()
+      .none()
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .contentType(ContentType.BINARY)
+      .body(VALID_INPUT_STREAM)
+    .when
+      .post(s"/upload/$ACCOUNT_ID/")
+    .`then`
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
+  def uploadShouldSucceedButExpiredWhenDownload(): Unit = {
+    val uploadResponse: String = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(VALID_INPUT_STREAM)
+    .when
+      .post(s"/upload/$ACCOUNT_ID/")
+    .`then`
+      .statusCode(SC_CREATED)
+      .extract
+      .body
+      .asString
+
+    val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+    // fixme: dont know how to delete file with existing attachment api
+    `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/$ACCOUNT_ID/$blobId")
+    .`then`
+      .statusCode(SC_NOT_FOUND)
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala
new file mode 100644
index 0000000..a7f8b25
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala
@@ -0,0 +1,15 @@
+package org.apache.james.jmap.json
+
+import org.apache.james.jmap.mail.BlobId
+import org.apache.james.jmap.routes.UploadResponse
+import org.apache.james.mailbox.model.ContentType
+import play.api.libs.json.{JsString, JsValue, Json, Writes}
+
+class UploadSerializer {
+
+  private implicit val blobIdWrites: Writes[BlobId] = Json.valueWrites[BlobId]
+  private implicit val contentTypeWrites: Writes[ContentType] = contentType => JsString(contentType.asString())
+  private implicit val uploadResponseWrites: Writes[UploadResponse] = Json.writes[UploadResponse]
+
+  def serialize(uploadResponse: UploadResponse): JsValue = Json.toJson(uploadResponse)(uploadResponseWrites)
+}
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 03af576..f7c580e 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
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ *  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.routes
 
 import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream}
@@ -38,8 +38,8 @@ import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.{BlobId, EmailBodyPart, PartId}
 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, FetchGroup, MessageId, MessageResult}
-import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
+import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, ContentType, FetchGroup, MessageId, MessageResult}
+import org.apache.james.mailbox.{AttachmentManager, MailboxSession, MessageIdManager}
 import org.apache.james.mime4j.codec.EncoderUtil
 import org.apache.james.mime4j.codec.EncoderUtil.Usage
 import org.apache.james.mime4j.message.DefaultMessageWriter
@@ -94,6 +94,16 @@ case class MessageBlob(blobId: BlobId, message: MessageResult) extends Blob {
   override def content: InputStream = message.getFullContent.getInputStream
 }
 
+case class AttachmentBlob(attachmentMetadata: AttachmentMetadata, fileContent: InputStream) extends Blob {
+  override def size: Try[Size] = Success(UploadRoutes.sanitizeSize(attachmentMetadata.getSize))
+
+  override def contentType: ContentType = attachmentMetadata.getType
+
+  override def content: InputStream = fileContent
+
+  override def blobId: BlobId = BlobId.of(attachmentMetadata.getAttachmentId.getId).get
+}
+
 case class EmailBodyPartBlob(blobId: BlobId, part: EmailBodyPart) extends Blob {
   override def size: Try[Size] = Success(part.size)
 
@@ -120,6 +130,17 @@ class MessageBlobResolver @Inject()(val messageIdFactory: MessageId.Factory,
   }
 }
 
+class AttachmentBlobResolver @Inject()(val attachmentManager: AttachmentManager) extends BlobResolver {
+  override def resolve(blobId: BlobId, mailboxSession: MailboxSession): BlobResolutionResult =
+    AttachmentId.from(org.apache.james.mailbox.model.BlobId.fromString(blobId.value.value)) match {
+      case attachmentId: AttachmentId => Applicable(
+        SMono.fromCallable(() => attachmentManager.getAttachment(attachmentId, mailboxSession))
+          .map((attachmentMetadata: AttachmentMetadata) => AttachmentBlob(attachmentMetadata, attachmentManager.load(attachmentMetadata, mailboxSession)))
+      )
+      case _ => NonApplicable()
+    }
+}
+
 class MessagePartBlobResolver @Inject()(val messageIdFactory: MessageId.Factory,
                                         val messageIdManager: MessageIdManager) extends BlobResolver {
   private def asMessageAndPartId(blobId: BlobId): Try[(MessageId, PartId)] = {
@@ -153,11 +174,13 @@ class MessagePartBlobResolver @Inject()(val messageIdFactory: MessageId.Factory,
 }
 
 class BlobResolvers @Inject()(val messageBlobResolver: MessageBlobResolver,
-                    val messagePartBlobResolver: MessagePartBlobResolver) {
+                              val messagePartBlobResolver: MessagePartBlobResolver,
+                              val attachmentBlobResolver: AttachmentBlobResolver) {
   def resolve(blobId: BlobId, mailboxSession: MailboxSession): SMono[Blob] =
     messageBlobResolver
       .resolve(blobId, mailboxSession).asOption
       .orElse(messagePartBlobResolver.resolve(blobId, mailboxSession).asOption)
+      .orElse(attachmentBlobResolver.resolve(blobId, mailboxSession).asOption)
       .getOrElse(SMono.raiseError(BlobNotFoundException(blobId)))
 }
 
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
new file mode 100644
index 0000000..f06fa51
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -0,0 +1,133 @@
+/****************************************************************
+ * 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.routes
+
+import java.io.InputStream
+import java.time.ZonedDateTime
+import java.util.stream
+import java.util.stream.Stream
+
+import eu.timepit.refined.api.Refined
+import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
+import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, CREATED}
+import io.netty.handler.codec.http.HttpMethod
+import javax.inject.{Inject, Named}
+import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
+import org.apache.james.jmap.http.Authenticator
+import org.apache.james.jmap.http.rfc8621.InjectionKeys
+import org.apache.james.jmap.mail.Email.Size
+import org.apache.james.jmap.routes.UploadRoutes.{LOGGER, fromAttachment}
+import org.apache.james.mailbox.{AttachmentManager, MailboxSession}
+import org.apache.james.mailbox.model.{AttachmentMetadata, ContentType}
+import org.apache.james.util.ReactorUtils
+import org.slf4j.{Logger, LoggerFactory}
+import reactor.core.publisher.Mono
+import reactor.core.scala.publisher.SMono
+import reactor.core.scheduler.Schedulers
+import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
+import eu.timepit.refined.auto._
+import eu.timepit.refined.numeric.NonNegative
+import eu.timepit.refined.refineV
+import org.apache.james.jmap.exceptions.UnauthorizedException
+import org.apache.james.jmap.json.UploadSerializer
+import org.apache.james.jmap.mail.BlobId
+
+object UploadRoutes {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[DownloadRoutes])
+
+  type Size = Long Refined NonNegative
+  val Zero: Size = 0L
+
+  def sanitizeSize(value: Long): Size = {
+    val size: Either[String, Size] = refineV[NonNegative](value)
+    size.fold(e => {
+      LOGGER.error(s"Encountered an invalid Email size: $e")
+      Zero
+    },
+      refinedValue => refinedValue)
+  }
+
+  def fromAttachment(attachmentMetadata: AttachmentMetadata): UploadResponse =
+    UploadResponse(
+        blobId = BlobId.of(attachmentMetadata.getAttachmentId.getId).get,
+        `type` = ContentType.of(attachmentMetadata.getType.asString),
+        size = sanitizeSize(attachmentMetadata.getSize),
+        expires = None)
+}
+
+case class UploadResponse(blobId: BlobId,
+                          `type`: ContentType,
+                          size: Size,
+                          expires: Option[ZonedDateTime])
+
+class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
+                             val attachmentManager: AttachmentManager,
+                             val serializer: UploadSerializer) extends JMAPRoutes {
+
+  class CancelledUploadException extends RuntimeException {
+
+  }
+
+  private val accountIdParam: String = "accountId"
+  private val uploadURI = s"/upload/{$accountIdParam}/"
+
+  override def routes(): stream.Stream[JMAPRoute] = Stream.of(
+    JMAPRoute.builder
+      .endpoint(new Endpoint(HttpMethod.POST, uploadURI))
+      .action(this.post)
+      .corsHeaders,
+    JMAPRoute.builder
+      .endpoint(new Endpoint(HttpMethod.OPTIONS, uploadURI))
+      .action(JMAPRoutes.CORS_CONTROL)
+      .noCorsHeaders)
+
+  def post(request: HttpServerRequest, response: HttpServerResponse): Mono[Void] = {
+    request.requestHeaders.get(CONTENT_TYPE) match {
+      case contentType => SMono.fromPublisher(
+          authenticator.authenticate(request))
+          .flatMap(session => post(request, response, ContentType.of(contentType), session))
+          .onErrorResume {
+            case e: UnauthorizedException => SMono.fromPublisher(handleAuthenticationFailure(response, LOGGER, e))
+            case e: Throwable => SMono.fromPublisher(handleInternalError(response, LOGGER, e))
+          }
+          .asJava().`then`()
+      case _ => response.status(BAD_REQUEST).send
+    }
+  }
+
+  def post(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, session: MailboxSession): SMono[Void] = {
+    SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive.asByteBuffer))
+      .flatMap(content => handle(contentType, content, session, response))
+      .subscribeOn(Schedulers.elastic())
+  }
+
+  def handle(contentType: ContentType, content: InputStream, mailboxSession: MailboxSession, response: HttpServerResponse): SMono[Void] =
+    uploadContent(contentType, content, mailboxSession)
+      .flatMap(uploadResponse => SMono.fromPublisher(response
+            .header(CONTENT_TYPE, uploadResponse.`type`.asString())
+            .status(CREATED)
+            .sendString(SMono.just(serializer.serialize(uploadResponse).toString()))))
+
+  def uploadContent(contentType: ContentType, inputStream: InputStream, session: MailboxSession): SMono[UploadResponse] =
+    SMono
+      .fromPublisher(attachmentManager.storeAttachment(contentType, inputStream, session))
+      .map(fromAttachment)
+
+}


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


[james-project] 02/08: JAMES-3277 Optimization: JMAP Rely on MessageManager::moveMessage (Draft)

Posted by bt...@apache.org.
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 0b05fa355e1d9c85e0f16f3ecfd241f7dbb823e3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 22 11:42:48 2020 +0700

    JAMES-3277 Optimization: JMAP Rely on MessageManager::moveMessage (Draft)
    
    We notice that many slow traces at the JMAP level are message moves updates, generating a high count of Cassandra queries.
    
    Query count before: 22m+12
      - 6 message => 146 queries
      - 9 messages => 210 queries
      - 12 messages => 276 queries
    
    Query count after: 16m+15
      - 6 message => 111 queries
      - 9 messages => 159 queries
      - 12 messages => 207 queries
---
 .../org/apache/james/mailbox/MailboxManager.java   |  2 +
 .../james/mailbox/store/StoreMailboxManager.java   |  8 +++
 .../methods/integration/SetMessagesMethodTest.java | 74 ++++++++++++++++++++++
 .../draft/methods/SetMessagesUpdateProcessor.java  | 60 ++++++++++++++++++
 .../james/jmap/draft/model/UpdateMessagePatch.java |  6 ++
 5 files changed, 150 insertions(+)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
index 01740b8..a175127 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
@@ -289,6 +289,8 @@ public interface MailboxManager extends RequestAware, RightManager, MailboxAnnot
      */
     List<MessageRange> moveMessages(MessageRange set, MailboxPath from, MailboxPath to, MailboxSession session) throws MailboxException;
 
+    List<MessageRange> moveMessages(MessageRange set, MailboxId from, MailboxId to, MailboxSession session) throws MailboxException;
+
     enum MailboxSearchFetchType {
         Minimal,
         Counters
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index 2ca6eb0..97c1972 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -623,6 +623,14 @@ public class StoreMailboxManager implements MailboxManager {
     }
 
     @Override
+    public List<MessageRange> moveMessages(MessageRange set, MailboxId from, MailboxId to, MailboxSession session) throws MailboxException {
+        StoreMessageManager toMailbox = (StoreMessageManager) getMailbox(to, session);
+        StoreMessageManager fromMailbox = (StoreMessageManager) getMailbox(from, session);
+
+        return configuration.getMoveBatcher().batchMessages(set, messageRange -> fromMailbox.moveTo(messageRange, toMailbox, session));
+    }
+
+    @Override
     public Flux<MailboxMetaData> search(MailboxQuery expression, MailboxSearchFetchType fetchType, MailboxSession session) {
         Mono<List<Mailbox>> mailboxesMono = searchMailboxes(expression, session, Right.Lookup).collectList();
 
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
index 2fbed4fff..fc5d16a 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
@@ -1130,6 +1130,80 @@ public abstract class SetMessagesMethodTest {
             .body(FIRST_MAILBOX + ".unreadMessages", equalTo(0));
     }
 
+    @Test
+    public void massiveMessageMoveShouldBeApplied() throws MailboxException {
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME.asString(), "mailbox");
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME.asString(), DefaultMailboxes.DRAFTS);
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME.asString(), DefaultMailboxes.OUTBOX);
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME.asString(), DefaultMailboxes.SENT);
+        MailboxId mailboxId = mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME.asString(), DefaultMailboxes.TRASH);
+        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME.asString(), DefaultMailboxes.SPAM);
+
+        ComposedMessageId message1 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message2 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message3 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.SEEN));
+        ComposedMessageId message4 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message5 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.ANSWERED));
+        ComposedMessageId message6 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message7 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message8 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message9 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.SEEN));
+        ComposedMessageId message10 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+        ComposedMessageId message11 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.ANSWERED));
+        ComposedMessageId message12 = mailboxProbe.appendMessage(USERNAME.asString(), USER_MAILBOX,
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)), new Date(), false, new Flags());
+
+        String serializedMessageId1 = message1.getMessageId().serialize();
+        String serializedMessageId2 = message2.getMessageId().serialize();
+        String serializedMessageId3 = message3.getMessageId().serialize();
+        String serializedMessageId4 = message4.getMessageId().serialize();
+        String serializedMessageId5 = message5.getMessageId().serialize();
+        String serializedMessageId6 = message6.getMessageId().serialize();
+        String serializedMessageId7 = message7.getMessageId().serialize();
+        String serializedMessageId8 = message8.getMessageId().serialize();
+        String serializedMessageId9 = message9.getMessageId().serialize();
+        String serializedMessageId10 = message10.getMessageId().serialize();
+        String serializedMessageId11 = message11.getMessageId().serialize();
+        String serializedMessageId12 = message12.getMessageId().serialize();
+
+        // When
+        given()
+            .header("Authorization", accessToken.asString())
+            .body(String.format("[[\"setMessages\", {\"update\": {" +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]}, " +
+                    "  \"%s\" : { \"mailboxIds\": [" + mailboxId.serialize() + "]} " +
+                    "} }, \"#0\"]]", serializedMessageId1, serializedMessageId2, serializedMessageId3,
+                serializedMessageId4, serializedMessageId5, serializedMessageId6,
+                serializedMessageId7, serializedMessageId8, serializedMessageId9,
+                serializedMessageId10, serializedMessageId11, serializedMessageId12))
+            .when()
+            .post("/jmap")
+            // Then
+            .then()
+            .log().ifValidationFails().body(ARGUMENTS + ".updated", hasSize(12));
+    }
+
     @Category(BasicFeature.class)
     @Test
     public void sendingAMailShouldLeadToAppropriateMailboxCountersOnSent() {
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesUpdateProcessor.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesUpdateProcessor.java
index 84d31e5..9f4c176 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesUpdateProcessor.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesUpdateProcessor.java
@@ -153,6 +153,8 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
 
         if (isAMassiveFlagUpdate(patches, messages)) {
             applyRangedFlagUpdate(patches, messages, responseBuilder, mailboxSession);
+        } else if (isAMassiveMove(patches, messages)) {
+            applyMove(patches, messages, responseBuilder, mailboxSession);
         } else {
             patches.forEach((id, patch) -> {
                 if (patch.isValid()) {
@@ -172,6 +174,14 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
             && messages.size() > 3;
     }
 
+    private boolean isAMassiveMove(Map<MessageId, UpdateMessagePatch> patches, Multimap<MessageId, ComposedMessageIdWithMetaData> messages) {
+        // The same patch, that only represents a flag update, is applied to messages within a single mailbox
+        return StreamUtils.isSingleValued(patches.values().stream())
+            && StreamUtils.isSingleValued(messages.values().stream().map(metaData -> metaData.getComposedMessageId().getMailboxId()))
+            && patches.values().iterator().next().isOnlyAMove()
+            && messages.size() > 3;
+    }
+
     private void applyRangedFlagUpdate(Map<MessageId, UpdateMessagePatch> patches, Multimap<MessageId, ComposedMessageIdWithMetaData> messages, SetMessagesResponse.Builder responseBuilder, MailboxSession mailboxSession) {
         MailboxId mailboxId = messages.values()
             .iterator()
@@ -198,6 +208,56 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
                 } catch (MailboxException e) {
                     messageIds
                         .forEach(messageId -> handleMessageUpdateException(messageId, responseBuilder, e));
+                } catch (IllegalArgumentException e) {
+                    ValidationResult invalidPropertyKeywords = ValidationResult.builder()
+                        .property(MessageProperties.MessageProperty.keywords.asFieldName())
+                        .message(e.getMessage())
+                        .build();
+
+                    messageIds
+                        .forEach(messageId -> handleInvalidRequest(responseBuilder, messageId, ImmutableList.of(invalidPropertyKeywords), patch));
+                }
+            });
+        } else {
+            messages.keySet()
+                .forEach(messageId -> handleInvalidRequest(responseBuilder, messageId, patch.getValidationErrors(), patch));
+        }
+    }
+
+    private void applyMove(Map<MessageId, UpdateMessagePatch> patches, Multimap<MessageId, ComposedMessageIdWithMetaData> messages, SetMessagesResponse.Builder responseBuilder, MailboxSession mailboxSession) {
+        MailboxId mailboxId = messages.values()
+            .iterator()
+            .next()
+            .getComposedMessageId()
+            .getMailboxId();
+        UpdateMessagePatch patch = patches.values().iterator().next();
+        List<MessageRange> uidRanges = MessageRange.toRanges(messages.values().stream().map(metaData -> metaData.getComposedMessageId().getUid())
+            .distinct()
+            .collect(Guavate.toImmutableList()));
+
+        if (patch.isValid()) {
+            uidRanges.forEach(range -> {
+                ImmutableList<MessageId> messageIds = messages.entries()
+                    .stream()
+                    .filter(entry -> range.includes(entry.getValue().getComposedMessageId().getUid()))
+                    .map(Map.Entry::getKey)
+                    .distinct()
+                    .collect(Guavate.toImmutableList());
+                try {
+                    MailboxId targetId = mailboxIdFactory.fromString(patch.getMailboxIds().get().iterator().next());
+                    mailboxManager.moveMessages(range, mailboxId, targetId, mailboxSession);
+                    responseBuilder.updated(messageIds);
+                } catch (MailboxException e) {
+                    messageIds
+                        .forEach(messageId -> handleMessageUpdateException(messageId, responseBuilder, e));
+                } catch (IllegalArgumentException e) {
+                    ValidationResult invalidPropertyKeywords = ValidationResult.builder()
+                        .property(MessageProperties.MessageProperty.keywords.asFieldName())
+                        .message(e.getMessage())
+                        .build();
+
+                    messageIds
+                        .forEach(messageId -> handleInvalidRequest(responseBuilder, messageId, ImmutableList.of(invalidPropertyKeywords), patch));
                 }
             });
         } else {
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/UpdateMessagePatch.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/UpdateMessagePatch.java
index 4aa3663..519a55a 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/UpdateMessagePatch.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/UpdateMessagePatch.java
@@ -144,6 +144,12 @@ public class UpdateMessagePatch {
         return !mailboxIds.isPresent() && (oldKeywords.isPresent() || keywords.isPresent());
     }
 
+    public boolean isOnlyAMove() {
+        return mailboxIds.map(list -> list.size() == 1).orElse(false)
+            && oldKeywords.isEmpty()
+            && keywords.isEmpty();
+    }
+
     public ImmutableList<ValidationResult> getValidationErrors() {
         return validationErrors;
     }


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


[james-project] 07/08: JAMES-3432 fix indent issue for Licenses

Posted by bt...@apache.org.
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 41065ee11899f42f740e1e94fdcc92db5e69fcf0
Author: duc91 <vd...@linagora.com>
AuthorDate: Fri Oct 23 15:09:10 2020 +0700

    JAMES-3432 fix indent issue for Licenses
---
 .../scala/org/apache/james/jmap/http/SessionSupplier.scala     | 10 +++++-----
 .../main/scala/org/apache/james/jmap/mail/EmailAddress.scala   |  2 +-
 .../main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala  |  2 +-
 .../src/main/scala/org/apache/james/jmap/mail/Mailbox.scala    |  2 +-
 .../src/main/scala/org/apache/james/jmap/mail/Quotas.scala     |  2 +-
 .../src/main/scala/org/apache/james/jmap/mail/Rights.scala     |  2 +-
 .../scala/org/apache/james/jmap/method/CoreEchoMethod.scala    | 10 +++++-----
 .../scala/org/apache/james/jmap/method/EmailGetMethod.scala    | 10 +++++-----
 .../src/main/scala/org/apache/james/jmap/method/Method.scala   |  8 ++++----
 .../main/scala/org/apache/james/jmap/model/Capabilities.scala  | 10 +++++-----
 .../main/scala/org/apache/james/jmap/model/Capability.scala    | 10 +++++-----
 .../src/main/scala/org/apache/james/jmap/model/Id.scala        |  2 +-
 .../main/scala/org/apache/james/jmap/model/Invocation.scala    | 10 +++++-----
 .../main/scala/org/apache/james/jmap/model/RequestObject.scala | 10 +++++-----
 .../scala/org/apache/james/jmap/model/ResponseObject.scala     | 10 +++++-----
 .../src/main/scala/org/apache/james/jmap/model/Session.scala   | 10 +++++-----
 .../main/scala/org/apache/james/jmap/model/UnsignedInt.scala   | 10 +++++-----
 .../scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala     | 10 +++++-----
 18 files changed, 65 insertions(+), 65 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionSupplier.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionSupplier.scala
index 40fc252..3e625b5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionSupplier.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionSupplier.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.http
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala
index d995610..59f3a6c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailAddress.scala
@@ -15,7 +15,7 @@
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- * ***************************************************************/
+ ****************************************************************/
 
 package org.apache.james.jmap.mail
 
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 9876379..72adc2b 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
@@ -15,7 +15,7 @@
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- * ***************************************************************/
+ ****************************************************************/
 
 package org.apache.james.jmap.mail
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala
index e9f0985..29396ee 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Mailbox.scala
@@ -15,7 +15,7 @@
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- * ***************************************************************/
+ ****************************************************************/
 
 package org.apache.james.jmap.mail
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala
index e107efe..1f41ec3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Quotas.scala
@@ -15,7 +15,7 @@
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- * ***************************************************************/
+ ****************************************************************/
 
 package org.apache.james.jmap.mail
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Rights.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Rights.scala
index dac0ea1..b274f0b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Rights.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Rights.scala
@@ -15,7 +15,7 @@
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- * ***************************************************************/
+ ****************************************************************/
 
 package org.apache.james.jmap.mail
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala
index dfe2aa5..7bba42e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.method
 
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
index 36d4e93..296b836 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailGetMethod.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.method
 
 import java.time.ZoneId
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
index 1322f1e..824e70b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.method
 
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capabilities.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capabilities.scala
index 7dd1788..48f9954 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capabilities.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capabilities.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
 import eu.timepit.refined.auto._
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capability.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capability.scala
index 0d6b828..fbabaf9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capability.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Capability.scala
@@ -1,4 +1,4 @@
-/** *************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala
index f532fd8..0dc7167 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala
@@ -15,7 +15,7 @@
  * KIND, either express or implied.  See the License for the    *
  * specific language governing permissions and limitations      *
  * under the License.                                           *
- * ***************************************************************/
+ ****************************************************************/
 
 package org.apache.james.jmap.model
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
index 0676d8c..8faf272 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
 import eu.timepit.refined.auto._
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala
index f356663..3f40af6 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ResponseObject.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ResponseObject.scala
index 783801b..f36f512 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ResponseObject.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ResponseObject.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Session.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Session.scala
index d8594ad..c76c5b3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Session.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Session.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/UnsignedInt.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/UnsignedInt.scala
index 0cda8fa..606b631 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/UnsignedInt.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/UnsignedInt.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.model
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 626528f..790cecb 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
  * 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        *
@@ -6,16 +6,16 @@
  * 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                 *
- * *
+ *                                                              *
+ * 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.routes
 
 import java.io.InputStream


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


[james-project] 04/08: JAMES-3410 Optimisation Group destroys together

Posted by bt...@apache.org.
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 eed9ea5b8cae2c5c74d2cad78f2ade8bd399b069
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 22 15:01:24 2020 +0700

    JAMES-3410 Optimisation Group destroys together
    
    This enables several enhancements, like grouping mailbox context and access checks, grouping quota updates.
---
 .../apache/james/jmap/method/EmailSetMethod.scala  | 48 ++++++++++++++--------
 1 file changed, 30 insertions(+), 18 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 8c83e61..1d1b519 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -81,13 +81,13 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   }
 
   object DestroyResult {
-    def from(deleteResult: DeleteResult): DestroyResult = {
-      val notFound = deleteResult.getNotFound.asScala
-
-      deleteResult.getDestroyed.asScala
-        .headOption
+    def from(deleteResult: DeleteResult): Seq[DestroyResult] = {
+      val success: Seq[DestroySuccess] = deleteResult.getDestroyed.asScala.toSeq
         .map(DestroySuccess)
-        .getOrElse(DestroyFailure(EmailSet.asUnparsed(notFound.head), MessageNotFoundExeception(notFound.head)))
+      val notFound: Seq[DestroyResult] = deleteResult.getNotFound.asScala.toSeq
+          .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundExeception(id)))
+
+      success ++ notFound
     }
   }
 
@@ -163,11 +163,30 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
       case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
 
-  private def destroy(emailSetRequest: EmailSetRequest, mailboxSession: MailboxSession): SMono[DestroyResults] =
-    SFlux.fromIterable(emailSetRequest.destroy.getOrElse(DestroyIds(Seq())).value)
-      .flatMap(id => deleteMessage(id, mailboxSession))
-      .collectSeq()
-      .map(DestroyResults)
+  private def destroy(emailSetRequest: EmailSetRequest, mailboxSession: MailboxSession): SMono[DestroyResults] = {
+    if (emailSetRequest.destroy.isDefined) {
+      val messageIdsValidation: Seq[Either[DestroyFailure, MessageId]] = emailSetRequest.destroy.get.value
+        .map(unparsedId => EmailSet.parse(messageIdFactory)(unparsedId).toEither
+          .left.map(e => DestroyFailure(unparsedId, e)))
+      val messageIds: Seq[MessageId] = messageIdsValidation.flatMap {
+        case Right(messageId) => Some(messageId)
+        case _ => None
+      }
+      val parsingErrors: Seq[DestroyFailure] = messageIdsValidation.flatMap {
+        case Left(e) => Some(e)
+        case _ => None
+      }
+
+      SMono.fromCallable(() => messageIdManager.delete(messageIds.toList.asJava, mailboxSession))
+        .map(DestroyResult.from)
+        .subscribeOn(Schedulers.elastic())
+        .onErrorResume(e => SMono.just(messageIds.map(id => DestroyFailure(EmailSet.asUnparsed(id), e))))
+        .map(_ ++ parsingErrors)
+        .map(DestroyResults)
+    } else {
+      SMono.just(DestroyResults(Seq()))
+    }
+  }
 
   private def update(emailSetRequest: EmailSetRequest, mailboxSession: MailboxSession): SMono[UpdateResults] = {
     emailSetRequest.update
@@ -341,11 +360,4 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
         .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId)))
     }
   }
-
-  private def deleteMessage(destroyId: UnparsedMessageId, mailboxSession: MailboxSession): SMono[DestroyResult] =
-    EmailSet.parse(messageIdFactory)(destroyId)
-      .fold(e => SMono.just(DestroyFailure(destroyId, e)),
-        parsedId => SMono.fromCallable(() => DestroyResult.from(messageIdManager.delete(parsedId, mailboxSession)))
-          .subscribeOn(Schedulers.elastic)
-          .onErrorRecover(e => DestroyFailure(EmailSet.asUnparsed(parsedId), e)))
 }
\ No newline at end of file


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


[james-project] 01/08: JAMES-3411 [REFACTORING] Validate EmailSetUpdates upfront

Posted by bt...@apache.org.
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 7d3624a9ba169e75889ca7aabf81e31003a2e8dd
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 22 13:03:10 2020 +0700

    JAMES-3411 [REFACTORING] Validate EmailSetUpdates upfront
---
 .../apache/james/jmap/method/EmailSetMethod.scala  | 33 ++++++++++------------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 8455658..051d435 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -25,7 +25,7 @@ import javax.mail.Flags
 import org.apache.james.jmap.http.SessionSupplier
 import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
-import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds, ValidatedEmailSetUpdate}
+import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, EmailSetResponse, MailboxIds, ValidatedEmailSetUpdate}
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CAPABILITY}
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
@@ -174,22 +174,23 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   }
 
   private def update(updates: Map[UnparsedMessageId, JsObject], session: MailboxSession): SMono[UpdateResults] = {
-    val validatedUpdates: List[Either[UpdateFailure, (MessageId, EmailSetUpdate)]] = updates
+    val validatedUpdates: List[Either[UpdateFailure, (MessageId, ValidatedEmailSetUpdate)]] = updates
       .map({
         case (unparsedMessageId, json) => EmailSet.parse(messageIdFactory)(unparsedMessageId)
           .toEither
           .left.map(e => UpdateFailure(unparsedMessageId, e))
           .flatMap(id => serializer.deserializeEmailSetUpdate(json)
-            .asEither
-            .fold(e => Left(UpdateFailure(unparsedMessageId, new IllegalArgumentException(e.toString()))),
-              (emailSetUpdate: EmailSetUpdate) => Right((id, emailSetUpdate))))
+            .asEither.left.map(e => new IllegalArgumentException(e.toString))
+            .flatMap(_.validate)
+            .fold(e => Left(UpdateFailure(unparsedMessageId, e)),
+              emailSetUpdate => Right((id, emailSetUpdate))))
       })
       .toList
     val failures: List[UpdateFailure] = validatedUpdates.flatMap({
       case Left(e) => Some(e)
       case _ => None
     })
-    val validUpdates: List[(MessageId, EmailSetUpdate)] = validatedUpdates.flatMap({
+    val validUpdates: List[(MessageId, ValidatedEmailSetUpdate)] = validatedUpdates.flatMap({
       case Right(pair) => Some(pair)
       case _ => None
     })
@@ -210,7 +211,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     }
   }
 
-  private def doUpdate(messageId: MessageId, update: EmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
+  private def doUpdate(messageId: MessageId, update: ValidatedEmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
     val mailboxIds: MailboxIds = MailboxIds(storedMetaData.map(metaData => metaData.getComposedMessageId.getMailboxId))
     val originFlags: Flags = storedMetaData
       .foldLeft[Flags](new Flags())((flags: Flags, m: ComposedMessageIdWithMetaData) => {
@@ -221,17 +222,13 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     if (mailboxIds.value.isEmpty) {
       SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), MessageNotFoundExeception(messageId)))
     } else {
-      update.validate
-        .fold(
-          e => SMono.just(UpdateFailure(EmailSet.asUnparsed(messageId), e)),
-          validatedUpdate =>
-            updateFlags(messageId, validatedUpdate, mailboxIds, originFlags, session)
-              .flatMap {
-                case failure: UpdateFailure => SMono.just[UpdateResult](failure)
-                case _: UpdateSuccess => updateMailboxIds(messageId, validatedUpdate, mailboxIds, session)
-              }
-              .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
-              .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+      updateFlags(messageId, update, mailboxIds, originFlags, session)
+        .flatMap {
+          case failure: UpdateFailure => SMono.just[UpdateResult](failure)
+          case _: UpdateSuccess => updateMailboxIds(messageId, update, mailboxIds, session)
+        }
+        .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
+        .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId)))
     }
   }
 


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


[james-project] 08/08: JAMES-3412 Keywords are case insentive, lower cased

Posted by bt...@apache.org.
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 7d948857608db296efb457e1b8631297101bc7fd
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Oct 23 09:57:08 2020 +0700

    JAMES-3412 Keywords are case insentive, lower cased
---
 .../jmap/rfc8621/contract/EmailGetMethodContract.scala | 16 ++++++++--------
 .../jmap/rfc8621/contract/EmailSetMethodContract.scala | 18 +++++++++---------
 .../scala/org/apache/james/jmap/model/Keyword.scala    | 18 ++++++++++--------
 3 files changed, 27 insertions(+), 25 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/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 0821e63..a6ca953 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
@@ -5475,7 +5475,7 @@ trait EmailGetMethodContract {
           |  {
           |     "id":"%s",
           |    "keywords": {
-          |      "$Answered": true
+          |      "$answered": true
           |    }
           |  }
       """.stripMargin, messageId.serialize)
@@ -5530,10 +5530,10 @@ trait EmailGetMethodContract {
           |  {
           |     "id":"%s",
           |    "keywords": {
-          |      "$Answered": true,
-          |      "$Seen":  true,
-          |      "$Draft":  true,
-          |      "$Flagged": true
+          |      "$answered": true,
+          |      "$seen":  true,
+          |      "$draft":  true,
+          |      "$flagged": true
           |    }
           |  }
       """.stripMargin, messageId.serialize)
@@ -5588,10 +5588,10 @@ trait EmailGetMethodContract {
           |  {
           |     "id":"%s",
           |    "keywords": {
-          |      "$Answered": true,
+          |      "$answered": true,
           |      "custom_flag":  true,
-          |      "$Draft":  true,
-          |      "$Flagged": true
+          |      "$draft":  true,
+          |      "$flagged": true
           |    }
           |  }
       """.stripMargin, messageId.serialize)
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 4029127..2898b92 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
@@ -662,7 +662,7 @@ trait EmailSetMethodContract {
         """{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true,
+          |       "$answered": true,
           |       "music": true
           |    }
           |}
@@ -746,28 +746,28 @@ trait EmailSetMethodContract {
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true,
+          |       "$answered": true,
           |       "music": true
           |    }
           |},
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true,
+          |       "$answered": true,
           |       "music": true
           |    }
           |},
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true,
+          |       "$answered": true,
           |       "music": true
           |    }
           |},
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true,
+          |       "$answered": true,
           |       "music": true
           |    }
           |}
@@ -853,25 +853,25 @@ trait EmailSetMethodContract {
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true
+          |       "$answered": true
           |    }
           |},
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true
+          |       "$answered": true
           |    }
           |},
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true
+          |       "$answered": true
           |    }
           |},
           |{
           |   "id":"%s",
           |   "keywords": {
-          |       "$Answered": true
+          |       "$answered": true
           |    }
           |}
           |]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala
index 704caeb..f63a424 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Keyword.scala
@@ -18,6 +18,8 @@
  * **************************************************************/
 package org.apache.james.jmap.model
 
+import java.util.Locale
+
 import com.ibm.icu.text.UnicodeSet
 import javax.mail.Flags
 import org.apache.commons.lang3.StringUtils
@@ -32,13 +34,13 @@ object Keyword {
       " or {'(' ')' '{' ']' '%' '*' '\"' '\\'} "
 
   private val FLAG_NAME_PATTERN = new UnicodeSet("[[a-z][A-Z][0-9]$_-]").freeze
-  val DRAFT = Keyword.of("$Draft").get
-  val SEEN = Keyword.of("$Seen").get
-  val FLAGGED = Keyword.of("$Flagged").get
-  val ANSWERED = Keyword.of("$Answered").get
-  val DELETED = Keyword.of("$Deleted").get
-  val RECENT = Keyword.of("$Recent").get
-  val FORWARDED = Keyword.of("$Forwarded").get
+  val DRAFT = Keyword.of("$draft").get
+  val SEEN = Keyword.of("$seen").get
+  val FLAGGED = Keyword.of("$flagged").get
+  val ANSWERED = Keyword.of("$answered").get
+  val DELETED = Keyword.of("$deleted").get
+  val RECENT = Keyword.of("$recent").get
+  val FORWARDED = Keyword.of("$forwarded").get
   val FLAG_VALUE: Boolean = true
   private val NON_EXPOSED_IMAP_KEYWORDS = List(Keyword.RECENT, Keyword.DELETED)
   private val IMAP_SYSTEM_FLAGS: Map[Flags.Flag, Keyword] =
@@ -50,7 +52,7 @@ object Keyword {
       Flags.Flag.RECENT -> RECENT,
       Flags.Flag.DELETED -> DELETED)
 
-  def parse(flagName: String): Either[String, Keyword] = Either.cond(isValid(flagName), Keyword(flagName), VALIDATION_MESSAGE)
+  def parse(flagName: String): Either[String, Keyword] = Either.cond(isValid(flagName), Keyword(flagName.toLowerCase(Locale.US)), VALIDATION_MESSAGE)
 
   def of(flagName: String): Try[Keyword] = parse(flagName) match {
     case Left(errorMessage: String) => Failure(new IllegalArgumentException(errorMessage))


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


[james-project] 03/08: JAMES-3277 Optimize range message updates for RFC-8621

Posted by bt...@apache.org.
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 2bbde614cd5b6c8463038a492167eb18312c1967
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 22 14:34:53 2020 +0700

    JAMES-3277 Optimize range message updates for RFC-8621
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 312 +++++++++++++++++++++
 .../org/apache/james/jmap/mail/EmailSet.scala      |  19 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  | 104 ++++++-
 3 files changed, 419 insertions(+), 16 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 8a92b53..4029127 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
@@ -670,6 +670,318 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def rangeFlagsAdditionShouldUpdateStoredFlags(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = FlagsBuilder.builder()
+      .add(Flags.Flag.ANSWERED)
+      .build()
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId3: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId4: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId1.serialize}":{
+         |          "keywords/music": true
+         |        },
+         |        "${messageId2.serialize}":{
+         |          "keywords/music": true
+         |        },
+         |        "${messageId3.serialize}":{
+         |          "keywords/music": true
+         |        },
+         |        "${messageId4.serialize}":{
+         |          "keywords/music": true
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId1.serialize}", "${messageId2.serialize}", "${messageId3.serialize}", "${messageId4.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin, "$Seen")
+
+    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].updated")
+      .isEqualTo(s"""{
+          |  "${messageId1.serialize}": null,
+          |  "${messageId2.serialize}": null,
+          |  "${messageId3.serialize}": null,
+          |  "${messageId4.serialize}": null
+          |}
+      """.stripMargin)
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list")
+      .isEqualTo(String.format(
+        """[
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true,
+          |       "music": true
+          |    }
+          |},
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true,
+          |       "music": true
+          |    }
+          |},
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true,
+          |       "music": true
+          |    }
+          |},
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true,
+          |       "music": true
+          |    }
+          |}
+          |]
+      """.stripMargin, messageId1.serialize, messageId2.serialize, messageId3.serialize, messageId4.serialize))
+  }
+
+  @Test
+  def rangeFlagsRemovalShouldUpdateStoredFlags(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = FlagsBuilder.builder()
+      .add(Flags.Flag.ANSWERED)
+      .add("music")
+      .build()
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId3: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId4: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId1.serialize}":{
+         |          "keywords/music": null
+         |        },
+         |        "${messageId2.serialize}":{
+         |          "keywords/music": null
+         |        },
+         |        "${messageId3.serialize}":{
+         |          "keywords/music": null
+         |        },
+         |        "${messageId4.serialize}":{
+         |          "keywords/music": null
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId1.serialize}", "${messageId2.serialize}", "${messageId3.serialize}", "${messageId4.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin, "$Seen")
+
+    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].updated")
+      .isEqualTo(s"""{
+          |  "${messageId1.serialize}": null,
+          |  "${messageId2.serialize}": null,
+          |  "${messageId3.serialize}": null,
+          |  "${messageId4.serialize}": null
+          |}
+      """.stripMargin)
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list")
+      .isEqualTo(String.format(
+        """[
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true
+          |    }
+          |},
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true
+          |    }
+          |},
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true
+          |    }
+          |},
+          |{
+          |   "id":"%s",
+          |   "keywords": {
+          |       "$Answered": true
+          |    }
+          |}
+          |]
+      """.stripMargin, messageId1.serialize, messageId2.serialize, messageId3.serialize, messageId4.serialize))
+  }
+
+  @Test
+  def rangeMoveShouldUpdateMailboxId(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = FlagsBuilder.builder()
+      .add(Flags.Flag.ANSWERED)
+      .add("music")
+      .build()
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val newId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "other"))
+    val messageId1: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId3: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+    val messageId4: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags).build(message)).getMessageId
+
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId1.serialize}":{
+         |          "mailboxIds": { "${newId.serialize()}" : true}
+         |        },
+         |        "${messageId2.serialize}":{
+         |          "mailboxIds": { "${newId.serialize()}" : true}
+         |        },
+         |        "${messageId3.serialize}":{
+         |          "mailboxIds": { "${newId.serialize()}" : true}
+         |        },
+         |        "${messageId4.serialize}":{
+         |          "mailboxIds": { "${newId.serialize()}" : true}
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId1.serialize}", "${messageId2.serialize}", "${messageId3.serialize}", "${messageId4.serialize}"],
+         |       "properties": ["mailboxIds"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin, "$Seen")
+
+    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].updated")
+      .isEqualTo(s"""{
+          |  "${messageId1.serialize}": null,
+          |  "${messageId2.serialize}": null,
+          |  "${messageId3.serialize}": null,
+          |  "${messageId4.serialize}": null
+          |}
+      """.stripMargin)
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list")
+      .isEqualTo(s"""[
+          |{
+          |   "id":"${messageId1.serialize}",
+          |   "mailboxIds": {
+          |       "${newId.serialize}": true
+          |    }
+          |},
+          |{
+          |   "id":"${messageId2.serialize}",
+          |   "mailboxIds": {
+          |       "${newId.serialize}": true
+          |    }
+          |},
+          |{
+          |   "id":"${messageId3.serialize}",
+          |   "mailboxIds": {
+          |       "${newId.serialize}": true
+          |    }
+          |},
+          |{
+          |   "id":"${messageId4.serialize}",
+          |   "mailboxIds": {
+          |       "${newId.serialize}": true
+          |    }
+          |}
+          |]
+      """.stripMargin)
+  }
+
+  @Test
   def emailSetShouldRejectPartiallyUpdateAndResetKeywordsAtTheSameTime(server: GuiceJamesServer): Unit = {
     val message: Message = Fixture.createTestMessage
 
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 8e36b33..ddeac39 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
@@ -23,13 +23,12 @@ import eu.timepit.refined.api.Refined
 import eu.timepit.refined.collection.NonEmpty
 import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
 import org.apache.james.jmap.method.WithAccountId
-import org.apache.james.jmap.model.KeywordsFactory.STRICT_KEYWORDS_FACTORY
 import org.apache.james.jmap.model.State.State
 import org.apache.james.jmap.model.{AccountId, Keywords, SetError}
 import org.apache.james.mailbox.model.MessageId
 import play.api.libs.json.JsObject
 
-import scala.util.{Failure, Right, Success, Try}
+import scala.util.{Right, Try}
 
 object EmailSet {
   type UnparsedMessageIdConstraint = NonEmpty
@@ -97,13 +96,23 @@ case class EmailSetUpdate(keywords: Option[Keywords],
         .compose(keywordsRemoval)
         .compose(keywordsReset)
 
-      Right(ValidatedEmailSetUpdate(keywordsTransformation, mailboxIdsTransformation))
+      Right(ValidatedEmailSetUpdate(keywordsTransformation, mailboxIdsTransformation, this))
     }
   }
+
+  def isOnlyMove: Boolean = mailboxIds.isDefined && mailboxIds.get.value.size == 1 &&
+    keywords.isEmpty && keywordsToAdd.isEmpty && keywordsToRemove.isEmpty
+
+  def isOnlyFlagAddition: Boolean = keywordsToAdd.isDefined && keywordsToRemove.isEmpty && mailboxIds.isEmpty &&
+    mailboxIdsToAdd.isEmpty && mailboxIdsToRemove.isEmpty
+
+  def isOnlyFlagRemoval: Boolean = keywordsToRemove.isDefined && keywordsToAdd.isEmpty && mailboxIds.isEmpty &&
+    mailboxIdsToAdd.isEmpty && mailboxIdsToRemove.isEmpty
 }
 
-case class ValidatedEmailSetUpdate private (keywords: Function[Keywords, Keywords],
-                                            mailboxIdsTransformation: Function[MailboxIds, MailboxIds])
+case class ValidatedEmailSetUpdate private (keywordsTransformation: Function[Keywords, Keywords],
+                                            mailboxIdsTransformation: Function[MailboxIds, MailboxIds],
+                                            update: EmailSetUpdate)
 
 class EmailUpdateValidationException() extends IllegalArgumentException
 case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 051d435..8c83e61 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.jmap.method
 
+import java.util.function.Consumer
+
 import com.google.common.collect.ImmutableList
 import eu.timepit.refined.auto._
 import javax.inject.Inject
@@ -34,8 +36,8 @@ import org.apache.james.jmap.model.SetError.SetErrorDescription
 import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
 import org.apache.james.mailbox.MessageManager.FlagsUpdateMode
 import org.apache.james.mailbox.exception.MailboxNotFoundException
-import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MessageId}
-import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
+import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MailboxId, MessageId, MessageRange}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageIdManager, MessageManager}
 import org.apache.james.metrics.api.MetricFactory
 import play.api.libs.json.{JsError, JsObject, JsSuccess}
 import reactor.core.scala.publisher.{SFlux, SMono}
@@ -47,6 +49,7 @@ case class MessageNotFoundExeception(messageId: MessageId) extends Exception
 
 class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
                                messageIdManager: MessageIdManager,
+                               mailboxManager: MailboxManager,
                                messageIdFactory: MessageId.Factory,
                                val metricFactory: MetricFactory,
                                val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailSetRequest] {
@@ -137,7 +140,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSetRequest): SMono[InvocationWithContext] = {
     for {
       destroyResults <- destroy(request, mailboxSession)
-      updateResults <- update(request, mailboxSession)
+      updateResults <- update(request, mailboxSession).doOnError(e => e.printStackTrace())
     } yield InvocationWithContext(
       invocation = Invocation(
         methodName = invocation.invocation.methodName,
@@ -199,19 +202,98 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
       updates <- SFlux.fromPublisher(messageIdManager.messagesMetadata(validUpdates.map(_._1).asJavaCollection, session))
         .collectMultimap(metaData => metaData.getComposedMessageId.getMessageId)
         .flatMap(metaData => {
-          SFlux.fromIterable(validUpdates)
-            .concatMap[UpdateResult]({
-              case (messageId, updatePatch) =>
-                doUpdate(messageId, updatePatch, metaData.get(messageId).toList.flatten, session)
-            })
-            .collectSeq()
+          doUpdate(validUpdates, metaData, session)
         })
     } yield {
       UpdateResults(updates ++ failures)
     }
   }
 
-  private def doUpdate(messageId: MessageId, update: ValidatedEmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
+  private def doUpdate(validUpdates: List[(MessageId, ValidatedEmailSetUpdate)],
+                       metaData: Map[MessageId, Traversable[ComposedMessageIdWithMetaData]],
+                       session: MailboxSession): SMono[Seq[UpdateResult]] = {
+    val sameUpdate: Boolean = validUpdates.map(_._2).distinctBy(_.update).size == 1
+    val singleMailbox: Boolean = metaData.values.flatten.map(_.getComposedMessageId.getMailboxId).toSet.size == 1
+
+    if (sameUpdate && singleMailbox && validUpdates.size > 3) {
+      val update: ValidatedEmailSetUpdate = validUpdates.map(_._2).headOption.get
+      val ranges: List[MessageRange] = asRanges(metaData)
+      val mailboxId: MailboxId = metaData.values.flatten.map(_.getComposedMessageId.getMailboxId).headOption.get
+
+      if (update.update.isOnlyFlagAddition) {
+        updateFlagsByRange(mailboxId, update.update.keywordsToAdd.get.asFlags, ranges, metaData, FlagsUpdateMode.ADD, session)
+      } else if (update.update.isOnlyFlagRemoval) {
+        updateFlagsByRange(mailboxId, update.update.keywordsToRemove.get.asFlags, ranges, metaData, FlagsUpdateMode.REMOVE, session)
+      } else if (update.update.isOnlyMove) {
+        moveByRange(mailboxId, update, ranges, metaData, session)
+      } else {
+        updateEachMessage(validUpdates, metaData, session)
+      }
+    } else {
+      updateEachMessage(validUpdates, metaData, session)
+    }
+  }
+
+  private def asRanges(metaData: Map[MessageId, Traversable[ComposedMessageIdWithMetaData]]) =
+    MessageRange.toRanges(metaData.values
+      .flatten.map(_.getComposedMessageId.getUid)
+      .toList.asJava)
+      .asScala.toList
+
+  private def updateFlagsByRange(mailboxId: MailboxId,
+                                 flags: Flags,
+                                 ranges: List[MessageRange],
+                                 metaData: Map[MessageId, Traversable[ComposedMessageIdWithMetaData]],
+                                 updateMode: FlagsUpdateMode,
+                                 session: MailboxSession): SMono[Seq[UpdateResult]] = {
+    val mailboxMono: SMono[MessageManager] = SMono.fromCallable(() => mailboxManager.getMailbox(mailboxId, session))
+
+    mailboxMono.flatMap(mailbox => updateByRange(ranges, metaData,
+      range => mailbox.setFlags(flags, updateMode, range, session)))
+      .subscribeOn(Schedulers.elastic())
+  }
+
+  private def moveByRange(mailboxId: MailboxId,
+                          update: ValidatedEmailSetUpdate,
+                          ranges: List[MessageRange],
+                          metaData: Map[MessageId, Traversable[ComposedMessageIdWithMetaData]],
+                          session: MailboxSession): SMono[Seq[UpdateResult]] = {
+    val targetId: MailboxId = update.update.mailboxIds.get.value.headOption.get
+
+    updateByRange(ranges, metaData,
+      range => mailboxManager.moveMessages(range, mailboxId, targetId, session))
+  }
+
+  private def updateByRange(ranges: List[MessageRange],
+                            metaData: Map[MessageId, Traversable[ComposedMessageIdWithMetaData]],
+                            operation: Consumer[MessageRange]): SMono[Seq[UpdateResult]] = {
+
+    SFlux.fromIterable(ranges)
+      .concatMap(range => {
+        val messageIds = metaData.filter(entry => entry._2.exists(composedId => range.includes(composedId.getComposedMessageId.getUid)))
+          .keys
+          .toSeq
+        SMono.fromCallable[Seq[UpdateResult]](() => {
+          operation.accept(range)
+          messageIds.map(UpdateSuccess)
+        })
+          .onErrorResume(e => SMono.just(messageIds.map(id => UpdateFailure(EmailSet.asUnparsed(id), e))))
+          .subscribeOn(Schedulers.elastic())
+      })
+      .reduce(Seq(), _ ++ _)
+  }
+
+  private def updateEachMessage(validUpdates: List[(MessageId, ValidatedEmailSetUpdate)],
+                                metaData: Map[MessageId, Traversable[ComposedMessageIdWithMetaData]],
+                                session: MailboxSession): SMono[Seq[UpdateResult]] =
+    SFlux.fromIterable(validUpdates)
+      .concatMap[UpdateResult]({
+        case (messageId, updatePatch) =>
+          updateSingleMessage(messageId, updatePatch, metaData.get(messageId).toList.flatten, session)
+      })
+      .collectSeq()
+
+  private def updateSingleMessage(messageId: MessageId, update: ValidatedEmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
     val mailboxIds: MailboxIds = MailboxIds(storedMetaData.map(metaData => metaData.getComposedMessageId.getMailboxId))
     val originFlags: Flags = storedMetaData
       .foldLeft[Flags](new Flags())((flags: Flags, m: ComposedMessageIdWithMetaData) => {
@@ -246,7 +328,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   }
 
   private def updateFlags(messageId: MessageId, update: ValidatedEmailSetUpdate, mailboxIds: MailboxIds, originalFlags: Flags, session: MailboxSession): SMono[UpdateResult] = {
-    val newFlags = update.keywords
+    val newFlags = update.keywordsTransformation
       .apply(LENIENT_KEYWORDS_FACTORY.fromFlags(originalFlags).get)
       .asFlagsWithRecentAndDeletedFrom(originalFlags)
 


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


[james-project] 06/08: JAMES-3432 Add test case downloadShouldRejectWhenDownloadFromOther

Posted by bt...@apache.org.
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 8335aa3962e04210147f4fe043fe06853c982e32
Author: duc91 <vd...@linagora.com>
AuthorDate: Fri Oct 23 15:03:32 2020 +0700

    JAMES-3432 Add test case downloadShouldRejectWhenDownloadFromOther
---
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |  2 +-
 .../jmap/rfc8621/contract/DownloadContract.scala   | 20 ++++-
 .../jmap/rfc8621/contract/UploadContract.scala     | 87 +++++++++++++++-------
 .../jmap/rfc8621/memory/MemoryUploadContract.java  | 38 ++++++++++
 .../apache/james/jmap/json/UploadSerializer.scala  | 18 +++++
 .../apache/james/jmap/routes/DownloadRoutes.scala  | 57 ++++++++++----
 .../apache/james/jmap/routes/UploadRoutes.scala    | 55 ++++++++------
 7 files changed, 212 insertions(+), 65 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 13eeaed..d8c2771 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -47,8 +47,8 @@ import org.apache.james.jmap.method.VacationResponseSetMethod;
 import org.apache.james.jmap.method.ZoneIdProvider;
 import org.apache.james.jmap.model.JmapRfc8621Configuration;
 import org.apache.james.jmap.routes.DownloadRoutes;
-import org.apache.james.jmap.routes.UploadRoutes;
 import org.apache.james.jmap.routes.JMAPApiRoutes;
+import org.apache.james.jmap.routes.UploadRoutes;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.utils.PropertiesProvider;
 import org.slf4j.Logger;
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 5e0e753..20579b9 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
@@ -29,7 +29,7 @@ import org.apache.http.HttpStatus.{SC_NOT_FOUND, SC_OK, SC_UNAUTHORIZED}
 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, 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, 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}
@@ -153,6 +153,24 @@ trait DownloadContract {
   }
 
   @Test
+  def downloadingInOtherAccountsShouldFail(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/multipart_simple.eml")))
+      .getMessageId
+
+    `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/$ALICE_ACCOUNT_ID/${messageId.serialize}")
+    .`then`
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
   def downloadPartShouldSucceedWhenDelegated(server: GuiceJamesServer): Unit = {
     val path = MailboxPath.inbox(ANDRE)
     server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
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 9605f44..770efcd 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
@@ -1,7 +1,26 @@
+/****************************************************************
+ * 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.rfc8621.contract
 
 import java.io.{ByteArrayInputStream, InputStream}
 import java.nio.charset.StandardCharsets
+
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType
@@ -10,11 +29,11 @@ import org.apache.commons.io.IOUtils
 import org.apache.http.HttpStatus.{SC_CREATED, SC_NOT_FOUND, SC_OK, SC_UNAUTHORIZED}
 import org.apache.james.GuiceJamesServer
 import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, BOB, BOB_PASSWORD, DOMAIN, RFC8621_VERSION_HEADER, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ALICE, ALICE_ACCOUNT_ID, ALICE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, RFC8621_VERSION_HEADER, _2_DOT_DOMAIN, authScheme, baseRequestSpecBuilder}
 import org.apache.james.jmap.rfc8621.contract.UploadContract.{BIG_INPUT_STREAM, VALID_INPUT_STREAM}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
-import org.junit.jupiter.api.{BeforeEach, Test}
+import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
 import play.api.libs.json.{JsString, Json}
 
 object UploadContract {
@@ -29,6 +48,8 @@ trait UploadContract {
       .fluent
       .addDomain(DOMAIN.asString)
       .addUser(BOB.asString, BOB_PASSWORD)
+      .addDomain(_2_DOT_DOMAIN.asString())
+      .addUser(ALICE.asString(), ALICE_PASSWORD)
 
     requestSpecification = baseRequestSpecBuilder(server)
       .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
@@ -36,7 +57,7 @@ trait UploadContract {
   }
 
   @Test
-  def shouldUploadFileAndOnlyOwnerCanAccess(): Unit = {
+  def shouldUploadFileAndAllowToDownloadIt(): Unit = {
     val uploadResponse: String = `given`
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
@@ -69,63 +90,75 @@ trait UploadContract {
   }
 
   @Test
-  def shouldRejectWhenUploadFileTooBig(): Unit = {
-    val response: String = `given`
+  def bobShouldNotBeAllowedToUploadInAliceAccount(): Unit = {
+    `given`
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .contentType(ContentType.BINARY)
-      .body(BIG_INPUT_STREAM)
+      .body(VALID_INPUT_STREAM)
+    .when
+      .post(s"/upload/$ALICE_ACCOUNT_ID/")
+    .`then`
+      .statusCode(SC_UNAUTHORIZED)
+  }
+
+  @Test
+  def aliceShouldNotAccessOrDownloadFileUploadedByBob(): Unit = {
+    val uploadResponse: String = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(VALID_INPUT_STREAM)
     .when
       .post(s"/upload/$ACCOUNT_ID/")
     .`then`
-      .statusCode(SC_OK)
+      .statusCode(SC_CREATED)
       .extract
       .body
       .asString
 
-    // fixme: dont know we limit size or not?
-    assertThatJson(response)
-      .isEqualTo("Should be error")
-  }
+    val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
 
-  @Test
-  def uploadShouldRejectWhenUnauthenticated(): Unit = {
     `given`
-      .auth()
-      .none()
+      .auth().basic(ALICE.asString(), ALICE_PASSWORD)
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .contentType(ContentType.BINARY)
-      .body(VALID_INPUT_STREAM)
     .when
-      .post(s"/upload/$ACCOUNT_ID/")
+      .get(s"/download/$ALICE_ACCOUNT_ID/$blobId")
     .`then`
       .statusCode(SC_UNAUTHORIZED)
   }
 
   @Test
-  def uploadShouldSucceedButExpiredWhenDownload(): Unit = {
-    val uploadResponse: String = `given`
+  @Disabled("JAMES-1788 Upload size limitation needs to be contributed")
+  def shouldRejectWhenUploadFileTooBig(): Unit = {
+    val response: String = `given`
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-      .body(VALID_INPUT_STREAM)
+      .contentType(ContentType.BINARY)
+      .body(BIG_INPUT_STREAM)
     .when
       .post(s"/upload/$ACCOUNT_ID/")
     .`then`
-      .statusCode(SC_CREATED)
+      .statusCode(SC_OK)
       .extract
       .body
       .asString
 
-    val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+    assertThatJson(response)
+      .isEqualTo("Should be error")
+  }
 
-    // fixme: dont know how to delete file with existing attachment api
+  @Test
+  def uploadShouldRejectWhenUnauthenticated(): Unit = {
     `given`
+      .auth()
+      .none()
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .contentType(ContentType.BINARY)
+      .body(VALID_INPUT_STREAM)
     .when
-      .get(s"/download/$ACCOUNT_ID/$blobId")
+      .post(s"/upload/$ACCOUNT_ID/")
     .`then`
-      .statusCode(SC_NOT_FOUND)
+      .statusCode(SC_UNAUTHORIZED)
   }
 }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadContract.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadContract.java
new file mode 100644
index 0000000..9f55acd
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadContract.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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.rfc8621.memory;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.UploadContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryUploadContract implements UploadContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+            .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala
index a7f8b25..3014869 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/UploadSerializer.scala
@@ -1,3 +1,21 @@
+/****************************************************************
+ * 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.mail.BlobId
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 f7c580e..702fce3 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
@@ -36,8 +36,11 @@ import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.mail.{BlobId, EmailBodyPart, PartId}
+import org.apache.james.jmap.model.Id.Id
+import org.apache.james.jmap.model.{AccountId, Id}
 import org.apache.james.jmap.routes.DownloadRoutes.{BUFFER_SIZE, LOGGER}
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
+import org.apache.james.mailbox.exception.AttachmentNotFoundException
 import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, ContentType, FetchGroup, MessageId, MessageResult}
 import org.apache.james.mailbox.{AttachmentManager, MailboxSession, MessageIdManager}
 import org.apache.james.mime4j.codec.EncoderUtil
@@ -133,10 +136,13 @@ class MessageBlobResolver @Inject()(val messageIdFactory: MessageId.Factory,
 class AttachmentBlobResolver @Inject()(val attachmentManager: AttachmentManager) extends BlobResolver {
   override def resolve(blobId: BlobId, mailboxSession: MailboxSession): BlobResolutionResult =
     AttachmentId.from(org.apache.james.mailbox.model.BlobId.fromString(blobId.value.value)) match {
-      case attachmentId: AttachmentId => Applicable(
-        SMono.fromCallable(() => attachmentManager.getAttachment(attachmentId, mailboxSession))
-          .map((attachmentMetadata: AttachmentMetadata) => AttachmentBlob(attachmentMetadata, attachmentManager.load(attachmentMetadata, mailboxSession)))
-      )
+      case attachmentId: AttachmentId =>
+        Try(attachmentManager.getAttachment(attachmentId, mailboxSession)) match {
+          case Success(attachmentMetadata) => Applicable(
+            SMono.fromCallable(() => AttachmentBlob(attachmentMetadata, attachmentManager.load(attachmentMetadata, mailboxSession))))
+          case Failure(_) => Applicable(SMono.raiseError(BlobNotFoundException(blobId)))
+        }
+
       case _ => NonApplicable()
     }
 }
@@ -205,17 +211,7 @@ class DownloadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
 
   private def get(request: HttpServerRequest, response: HttpServerResponse): Mono[Void] =
     SMono(authenticator.authenticate(request))
-      .flatMap((mailboxSession: MailboxSession) =>
-        SMono.fromTry(BlobId.of(request.param(blobIdParam)))
-          .flatMap(blobResolvers.resolve(_, mailboxSession))
-          .flatMap(blob => downloadBlob(
-            optionalName = queryParam(request, nameParam),
-            response = response,
-            blobContentType = queryParam(request, contentTypeParam)
-              .map(ContentType.of)
-              .getOrElse(blob.contentType),
-            blob = blob))
-          .`then`)
+      .flatMap(mailboxSession => getIfOwner(request, response, mailboxSession))
       .onErrorResume {
         case e: UnauthorizedException => SMono.fromPublisher(handleAuthenticationFailure(response, LOGGER, e)).`then`
         case _: BlobNotFoundException => SMono.fromPublisher(response.status(SC_NOT_FOUND).send).`then`
@@ -227,6 +223,37 @@ class DownloadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
       .asJava()
       .`then`
 
+  private def get(request: HttpServerRequest, response: HttpServerResponse, mailboxSession: MailboxSession): SMono[Unit] = {
+    SMono.fromTry(BlobId.of(request.param(blobIdParam)))
+      .flatMap(blobResolvers.resolve(_, mailboxSession))
+      .flatMap(blob => downloadBlob(
+        optionalName = queryParam(request, nameParam),
+        response = response,
+        blobContentType = queryParam(request, contentTypeParam)
+          .map(ContentType.of)
+          .getOrElse(blob.contentType),
+        blob = blob)
+        .`then`())
+  }
+
+  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.raiseError(e),
+            value => if (value) {
+              get(request, response, mailboxSession)
+            } else {
+              SMono.raiseError(new UnauthorizedException("You cannot upload to others"))
+            })
+      }
+
+      case Left(throwable: Throwable) => SMono.raiseError(throwable)
+    }
+  }
+
   private def downloadBlob(optionalName: Option[String],
                            response: HttpServerResponse,
                            blobContentType: ContentType,
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 f06fa51..6e37695 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
@@ -33,7 +33,7 @@ import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.mail.Email.Size
-import org.apache.james.jmap.routes.UploadRoutes.{LOGGER, fromAttachment}
+import org.apache.james.jmap.routes.UploadRoutes.{LOGGER, sanitizeSize}
 import org.apache.james.mailbox.{AttachmentManager, MailboxSession}
 import org.apache.james.mailbox.model.{AttachmentMetadata, ContentType}
 import org.apache.james.util.ReactorUtils
@@ -48,6 +48,8 @@ import eu.timepit.refined.refineV
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.json.UploadSerializer
 import org.apache.james.jmap.mail.BlobId
+import org.apache.james.jmap.model.{AccountId, Id}
+import org.apache.james.jmap.model.Id.Id
 
 object UploadRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[DownloadRoutes])
@@ -63,27 +65,18 @@ object UploadRoutes {
     },
       refinedValue => refinedValue)
   }
-
-  def fromAttachment(attachmentMetadata: AttachmentMetadata): UploadResponse =
-    UploadResponse(
-        blobId = BlobId.of(attachmentMetadata.getAttachmentId.getId).get,
-        `type` = ContentType.of(attachmentMetadata.getType.asString),
-        size = sanitizeSize(attachmentMetadata.getSize),
-        expires = None)
 }
 
-case class UploadResponse(blobId: BlobId,
+case class UploadResponse(accountId: AccountId,
+                          blobId: BlobId,
                           `type`: ContentType,
-                          size: Size,
-                          expires: Option[ZonedDateTime])
+                          size: Size)
 
 class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
                              val attachmentManager: AttachmentManager,
                              val serializer: UploadSerializer) extends JMAPRoutes {
 
-  class CancelledUploadException extends RuntimeException {
-
-  }
+  class CancelledUploadException extends RuntimeException
 
   private val accountIdParam: String = "accountId"
   private val uploadURI = s"/upload/{$accountIdParam}/"
@@ -113,21 +106,41 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
   }
 
   def post(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, session: MailboxSession): SMono[Void] = {
-    SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive.asByteBuffer))
-      .flatMap(content => handle(contentType, content, session, response))
-      .subscribeOn(Schedulers.elastic())
+    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.raiseError(e),
+            value => if (value) {
+              SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive.asByteBuffer))
+              .flatMap(content => handle(targetAccountId, contentType, content, session, response))
+              .subscribeOn(Schedulers.elastic())
+            } else {
+              SMono.raiseError(new UnauthorizedException("Attempt to upload in another account"))
+            })
+      }
+
+      case Left(throwable: Throwable) => SMono.raiseError(throwable)
+    }
   }
 
-  def handle(contentType: ContentType, content: InputStream, mailboxSession: MailboxSession, response: HttpServerResponse): SMono[Void] =
-    uploadContent(contentType, content, mailboxSession)
+  def handle(accountId: AccountId, contentType: ContentType, content: InputStream, mailboxSession: MailboxSession, response: HttpServerResponse): SMono[Void] =
+    uploadContent(accountId, contentType, content, mailboxSession)
       .flatMap(uploadResponse => SMono.fromPublisher(response
             .header(CONTENT_TYPE, uploadResponse.`type`.asString())
             .status(CREATED)
             .sendString(SMono.just(serializer.serialize(uploadResponse).toString()))))
 
-  def uploadContent(contentType: ContentType, inputStream: InputStream, session: MailboxSession): SMono[UploadResponse] =
+  def uploadContent(accountId: AccountId, contentType: ContentType, inputStream: InputStream, session: MailboxSession): SMono[UploadResponse] =
     SMono
       .fromPublisher(attachmentManager.storeAttachment(contentType, inputStream, session))
-      .map(fromAttachment)
+      .map(fromAttachment(_, accountId))
 
+  private def fromAttachment(attachmentMetadata: AttachmentMetadata, accountId: AccountId): UploadResponse =
+    UploadResponse(
+        blobId = BlobId.of(attachmentMetadata.getAttachmentId.getId).get,
+        `type` = ContentType.of(attachmentMetadata.getType.asString),
+        size = sanitizeSize(attachmentMetadata.getSize),
+        accountId = accountId)
 }


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