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