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/12/15 01:52:56 UTC
[james-project] 02/07: JAMES-3461 Mailbox/changes method & contract
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 e6053bd935ba8ac26494f199ebaf02974338945c
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Fri Dec 4 18:05:41 2020 +0700
JAMES-3461 Mailbox/changes method & contract
---
.../james/modules/mailbox/MemoryMailboxModule.java | 4 +
.../apache/james/jmap/draft/JmapGuiceProbe.java | 10 +-
.../rfc8621/contract/MailboxChangesContract.scala | 172 ----
.../contract/MailboxChangesMethodContract.scala | 862 +++++++++++++++++++++
.../memory/MemoryMailboxChangesMethodTest.java | 7 +-
.../src/test/resources/listeners.xml | 3 +
.../apache/james/jmap/core/ResponseObject.scala | 2 +
.../scala/org/apache/james/jmap/core/Session.scala | 10 +-
.../james/jmap/json/EmailGetSerializer.scala | 1 +
.../apache/james/jmap/json/MailboxSerializer.scala | 18 +-
.../james/jmap/json/ResponseSerializer.scala | 6 -
.../apache/james/jmap/json/ThreadSerializer.scala | 4 +-
.../scala/org/apache/james/jmap/json/package.scala | 4 +-
.../org/apache/james/jmap/mail/MailboxGet.scala | 20 +-
.../scala/org/apache/james/jmap/mail/Thread.scala | 5 +-
.../james/jmap/method/MailboxChangesMethod.scala | 41 +-
.../org/apache/james/jmap/method/Method.scala | 4 +-
17 files changed, 957 insertions(+), 216 deletions(-)
diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
index 6f42336..211efe6 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
+++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
@@ -25,6 +25,8 @@ import javax.inject.Singleton;
import org.apache.james.adapter.mailbox.UserRepositoryAuthenticator;
import org.apache.james.adapter.mailbox.UserRepositoryAuthorizator;
+import org.apache.james.jmap.api.change.MailboxChangeRepository;
+import org.apache.james.jmap.memory.change.MemoryMailboxChangeRepository;
import org.apache.james.mailbox.AttachmentContentLoader;
import org.apache.james.mailbox.AttachmentManager;
import org.apache.james.mailbox.Authenticator;
@@ -103,6 +105,7 @@ public class MemoryMailboxModule extends AbstractModule {
bind(Authorizator.class).to(UserRepositoryAuthorizator.class);
bind(MailboxManager.class).to(InMemoryMailboxManager.class);
bind(StoreMailboxManager.class).to(InMemoryMailboxManager.class);
+ bind(MailboxChangeRepository.class).to(MemoryMailboxChangeRepository.class);
bind(MessageIdManager.class).to(StoreMessageIdManager.class);
bind(AttachmentManager.class).to(StoreAttachmentManager.class);
bind(SessionProvider.class).to(SessionProviderImpl.class);
@@ -123,6 +126,7 @@ public class MemoryMailboxModule extends AbstractModule {
bind(UserRepositoryAuthenticator.class).in(Scopes.SINGLETON);
bind(UserRepositoryAuthorizator.class).in(Scopes.SINGLETON);
bind(InMemoryMailboxManager.class).in(Scopes.SINGLETON);
+ bind(MemoryMailboxChangeRepository.class).in(Scopes.SINGLETON);
bind(InMemoryMessageId.Factory.class).in(Scopes.SINGLETON);
bind(StoreMessageIdManager.class).in(Scopes.SINGLETON);
bind(StoreAttachmentManager.class).in(Scopes.SINGLETON);
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java
index 6347e36..c6d19fb 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JmapGuiceProbe.java
@@ -25,6 +25,8 @@ import javax.inject.Inject;
import org.apache.james.core.Username;
import org.apache.james.jmap.JMAPServer;
+import org.apache.james.jmap.api.change.MailboxChange;
+import org.apache.james.jmap.api.change.MailboxChangeRepository;
import org.apache.james.jmap.api.model.AccountId;
import org.apache.james.jmap.api.projections.MessageFastViewProjection;
import org.apache.james.jmap.api.vacation.Vacation;
@@ -46,6 +48,7 @@ import reactor.core.publisher.Mono;
public class JmapGuiceProbe implements GuiceProbe {
private final VacationRepository vacationRepository;
+ private final MailboxChangeRepository mailboxChangeRepository;
private final JMAPServer jmapServer;
private final MessageIdManager messageIdManager;
private final MailboxManager mailboxManager;
@@ -53,8 +56,9 @@ public class JmapGuiceProbe implements GuiceProbe {
private final MessageFastViewProjection messageFastViewProjection;
@Inject
- private JmapGuiceProbe(VacationRepository vacationRepository, JMAPServer jmapServer, MessageIdManager messageIdManager, MailboxManager mailboxManager, EventBus eventBus, MessageFastViewProjection messageFastViewProjection) {
+ private JmapGuiceProbe(VacationRepository vacationRepository, MailboxChangeRepository mailboxChangeRepository, JMAPServer jmapServer, MessageIdManager messageIdManager, MailboxManager mailboxManager, EventBus eventBus, MessageFastViewProjection messageFastViewProjection) {
this.vacationRepository = vacationRepository;
+ this.mailboxChangeRepository = mailboxChangeRepository;
this.jmapServer = jmapServer;
this.messageIdManager = messageIdManager;
this.mailboxManager = mailboxManager;
@@ -86,4 +90,8 @@ public class JmapGuiceProbe implements GuiceProbe {
public void clearMessageFastViewProjection() {
Mono.from(messageFastViewProjection.clear()).block();
}
+
+ public void saveMailboxChange(MailboxChange change) {
+ mailboxChangeRepository.save(change).block();
+ }
}
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/MailboxChangesContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesContract.scala
deleted file mode 100644
index b38a3ce..0000000
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesContract.scala
+++ /dev/null
@@ -1,172 +0,0 @@
-/****************************************************************
- * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
-import io.restassured.RestAssured.{`given`, requestSpecification}
-import io.restassured.http.ContentType.JSON
-import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
-import org.apache.http.HttpStatus.SC_OK
-import org.apache.james.GuiceJamesServer
-import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
-import org.apache.james.utils.DataProbeImpl
-import org.junit.jupiter.api.{BeforeEach, Test}
-
-trait MailboxChangesContract {
- @BeforeEach
- def setUp(server: GuiceJamesServer): Unit = {
- server.getProbe(classOf[DataProbeImpl])
- .fluent
- .addDomain(DOMAIN.asString)
- .addDomain("domain-alias.tld")
- .addUser(BOB.asString, BOB_PASSWORD)
-
- requestSpecification = baseRequestSpecBuilder(server)
- .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
- .build
- }
-
- @Test
- def shouldReturnCannotCalculateChanges(): Unit = {
- val request =
- s"""{
- | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
- | "methodCalls": [[
- | "Mailbox/changes",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "sinceState": "any-state"
- | },
- | "c1"]]
- |}""".stripMargin
-
- val response = `given`
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(request)
- .when
- .post
- .`then`
- .statusCode(SC_OK)
- .contentType(JSON)
- .extract
- .body
- .asString
-
- assertThatJson(response)
- .isEqualTo(
- """{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "error",
- | {
- | "type": "cannotCalculateChanges",
- | "description": "Naive implementation for Mailbox/changes"
- | },
- | "c1"
- | ]
- | ]
- |}""".stripMargin)
- }
-
- @Test
- def badAccountIdShouldBeRejected(): Unit = {
- val request =
- s"""{
- | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
- | "methodCalls": [[
- | "Mailbox/changes",
- | {
- | "accountId": "bad",
- | "sinceState": "any-state"
- | },
- | "c1"]]
- |}""".stripMargin
-
- val response = `given`
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(request)
- .when
- .post
- .`then`
- .statusCode(SC_OK)
- .contentType(JSON)
- .extract
- .body
- .asString
-
- assertThatJson(response)
- .isEqualTo(
- """{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "error",
- | {
- | "type": "accountNotFound"
- | },
- | "c1"
- | ]
- | ]
- |}""".stripMargin)
- }
-
- @Test
- def shouldReturnEmptyWhenKnownState(): Unit = {
- val request =
- s"""{
- | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
- | "methodCalls": [[
- | "Mailbox/changes",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "sinceState": "000001"
- | },
- | "c1"]]
- |}""".stripMargin
-
- val response = `given`
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(request)
- .when
- .post
- .`then`
- .statusCode(SC_OK)
- .contentType(JSON)
- .extract
- .body
- .asString
-
- assertThatJson(response)
- .inPath("methodResponses[0][1]")
- .isEqualTo(
- """{
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "oldState": "000001",
- | "newState": "000001",
- | "hasMoreChanges": false,
- | "updatedProperties": [],
- | "created": [],
- | "updated": [],
- | "destroyed": []
- |}""".stripMargin)
- }
-}
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/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
new file mode 100644
index 0000000..e7de455
--- /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/MailboxChangesMethodContract.scala
@@ -0,0 +1,862 @@
+/****************************************************************
+ * 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.nio.charset.StandardCharsets
+import java.time.ZonedDateTime
+import java.util.UUID
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+import io.restassured.builder.ResponseSpecBuilder
+import io.restassured.http.ContentType.JSON
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
+import net.javacrumbs.jsonunit.core.internal.Options
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.api.change.MailboxChange
+import org.apache.james.jmap.api.change.MailboxChange.State
+import org.apache.james.jmap.api.model.AccountId
+import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.draft.JmapGuiceProbe
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_ACCOUNT_ID, ANDRE_PASSWORD, 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, MailboxId, MailboxPath}
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
+import org.apache.james.utils.DataProbeImpl
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
+import play.api.libs.json.{JsString, Json}
+
+import scala.jdk.CollectionConverters._
+
+object TestId {
+ def of(value: Long): MailboxId = TestId(value)
+}
+
+case class TestId(value: Long) extends MailboxId {
+ override def serialize(): String = String.valueOf(value)
+}
+
+trait MailboxChangesMethodContract {
+
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent
+ .addDomain(DOMAIN.asString)
+ .addDomain("domain-alias.tld")
+ .addUser(BOB.asString, BOB_PASSWORD)
+ .addUser(ANDRE.asString, ANDRE_PASSWORD)
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+ .build
+ }
+
+ @Test
+ def mailboxChangesShouldReturnCreatedChanges(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+
+ val mailboxId1: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ .serialize
+
+ val mailboxId2: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
+ .serialize
+
+ val mailboxId3: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox3"))
+ .serialize
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": ["$mailboxId1", "$mailboxId2", "$mailboxId3"],
+ | "updated": [],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnUpdatedChangesWhenRenameMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+ provisionSystemMailboxes(server)
+ val path = MailboxPath.forUser(BOB, "mailbox1")
+ val mailboxId: String = mailboxProbe
+ .createMailbox(path)
+ .serialize
+
+ val oldState: State = storeReferenceState(server)
+
+ renameMailbox(mailboxId, "mailbox11")
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": [],
+ | "updated": ["$mailboxId"],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnUpdatedChangesWhenAppendMessageToMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+ provisionSystemMailboxes(server)
+
+ val path = MailboxPath.forUser(BOB, "mailbox1")
+ val mailboxId: String = mailboxProbe
+ .createMailbox(path)
+ .serialize
+
+ val oldState: State = storeReferenceState(server)
+
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": [],
+ | "updated": ["$mailboxId"],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ @Disabled("Not implemented yet")
+ def mailboxChangesShouldReturnUpdatedChangesWhenAppendMessageToDelegatedMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+
+ val path = MailboxPath.forUser(BOB, "mailbox1")
+ val mailboxId: String = mailboxProbe
+ .createMailbox(path)
+ .serialize
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "$ANDRE_ACCOUNT_ID",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`(
+ baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+ .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .setBody(request)
+ .build, new ResponseSpecBuilder().build)
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "$ANDRE_ACCOUNT_ID",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": [],
+ | "updated": ["$mailboxId"],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnDestroyedChanges(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+ provisionSystemMailboxes(server)
+
+ val path = MailboxPath.forUser(BOB, "mailbox1")
+ val mailboxId: String = mailboxProbe
+ .createMailbox(path)
+ .serialize
+
+ val oldState: State = storeReferenceState(server)
+
+ mailboxProbe
+ .deleteMailbox(path.getNamespace, BOB.asString(), path.getName)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": [],
+ | "updated": [],
+ | "destroyed": ["$mailboxId"]
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnAllTypeOfChanges(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+
+ val path1 = MailboxPath.forUser(BOB, "mailbox1")
+ val mailboxId1: String = mailboxProbe
+ .createMailbox(path1)
+ .serialize
+
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ mailboxProbe.appendMessage(BOB.asString(), path1, AppendCommand.from(message))
+
+ val path2 = MailboxPath.forUser(BOB, "mailbox2")
+ val mailboxId2: String = mailboxProbe
+ .createMailbox(path2)
+ .serialize
+ renameMailbox(mailboxId2, "mailbox22")
+
+ server.getProbe(classOf[MailboxProbeImpl])
+ .deleteMailbox(path1.getNamespace, BOB.asString(), path1.getName)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": ["$mailboxId1", "$mailboxId2"],
+ | "updated": ["$mailboxId1", "$mailboxId2"],
+ | "destroyed": ["$mailboxId1"]
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnHasMoreChangesWhenTrue(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+
+ val mailboxId1: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ .serialize
+
+ val mailboxId2: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
+ .serialize
+
+ val mailboxId3: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox3"))
+ .serialize
+
+ val mailboxId4: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox4"))
+ .serialize
+
+ val mailboxId5: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox5"))
+ .serialize
+
+ val mailboxId6: String = mailboxProbe
+ .createMailbox(MailboxPath.forUser(BOB, "mailbox6"))
+ .serialize
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "hasMoreChanges": true,
+ | "updatedProperties": [],
+ | "created": ["$mailboxId1", "$mailboxId2", "$mailboxId3", "$mailboxId4", "$mailboxId5"],
+ | "updated": [],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldFailWhenAccountIdNotFound(server: GuiceJamesServer): Unit = {
+ val oldState: State = storeReferenceState(server)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "bad",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [[
+ | "error", {
+ | "type": "accountNotFound"
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldFailWhenStateNotFound(server: GuiceJamesServer): Unit = {
+ provisionSystemMailboxes(server)
+
+ val state: String = UUID.randomUUID().toString
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "$state"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].newState")
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [[
+ | "error", {
+ | "type": "cannotCalculateChanges",
+ | "description": "State '$state' could not be found"
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnNoChangesWhenNoNewerState(server: GuiceJamesServer): Unit = {
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "${oldState.getValue}",
+ | "newState": "${oldState.getValue}",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": [],
+ | "updated": [],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def mailboxChangesShouldReturnDifferentStateThanOldState(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+ mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ val newState = Json.parse(response)
+ .\("methodResponses")
+ .\(0).\(1)
+ .\("newState")
+ .get.asInstanceOf[JsString].value
+
+ assertThat(oldState.getValue.toString).isNotEqualTo(newState)
+ }
+
+ @Test
+ def mailboxChangesShouldEventuallyReturnNoChanges(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+ provisionSystemMailboxes(server)
+
+ val oldState: State = storeReferenceState(server)
+ mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
+
+ val request1 =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "${oldState.getValue}"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response1 = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request1)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ val newState = Json.parse(response1)
+ .\("methodResponses")
+ .\(0).\(1)
+ .\("newState")
+ .get.asInstanceOf[JsString].value
+
+ val request2 =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/changes",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "sinceState": "$newState"
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response2 = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request2)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response2)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [ "Mailbox/changes", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "oldState": "$newState",
+ | "newState": "$newState",
+ | "hasMoreChanges": false,
+ | "updatedProperties": [],
+ | "created": [],
+ | "updated": [],
+ | "destroyed": []
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ private def renameMailbox(mailboxId: String, name: String): Unit = {
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [[
+ | "Mailbox/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "$mailboxId": {
+ | "name": "$name"
+ | }
+ | }
+ | }, "c1"]
+ | ]
+ |}
+ |""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ }
+
+ private def storeReferenceState(server: GuiceJamesServer): State = {
+ val state: State = State.of(UUID.randomUUID())
+ val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
+ jmapGuiceProbe.saveMailboxChange(MailboxChange.of(AccountId.fromUsername(BOB), state, ZonedDateTime.now(), List().asJava, List(TestId.of(0)).asJava, List().asJava))
+
+ state
+ }
+
+ private def provisionSystemMailboxes(server: GuiceJamesServer): Unit = {
+ val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Mailbox/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["$mailboxId"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ }
+}
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/MemoryMailboxChangesMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
index 9b8cd71..eadfa66 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryMailboxChangesMethodTest.java
@@ -24,16 +24,15 @@ import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_
import org.apache.james.GuiceJamesServer;
import org.apache.james.JamesServerBuilder;
import org.apache.james.JamesServerExtension;
-import org.apache.james.jmap.rfc8621.contract.EmailChangesContract;
-import org.apache.james.jmap.rfc8621.contract.MailboxChangesContract;
+import org.apache.james.jmap.rfc8621.contract.MailboxChangesMethodContract;
import org.apache.james.modules.TestJMAPServerModule;
import org.junit.jupiter.api.extension.RegisterExtension;
-public class MemoryMailboxChangesMethodTest implements MailboxChangesContract {
+public class MemoryMailboxChangesMethodTest implements MailboxChangesMethodContract {
@RegisterExtension
static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
.server(configuration -> GuiceJamesServer.forConfiguration(configuration)
.combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
.overrideWith(new TestJMAPServerModule()))
.build();
-}
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
index a1a139d..af44f35 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/listeners.xml
@@ -47,4 +47,7 @@
<class>org.apache.james.jmap.event.PopulateEmailQueryViewListener</class>
<async>true</async>
</listener>
+ <listener>
+ <class>org.apache.james.jmap.change.MailboxChangeListener</class>
+ </listener>
</listeners>
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ResponseObject.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ResponseObject.scala
index ec05f82..634594a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ResponseObject.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ResponseObject.scala
@@ -19,6 +19,8 @@
package org.apache.james.jmap.core
+import eu.timepit.refined.auto._
+
case class ResponseObject(sessionState: State, methodResponses: Seq[Invocation])
object ResponseObject {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
index 2189de3..123aa3d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Session.scala
@@ -24,8 +24,12 @@ import java.nio.charset.StandardCharsets
import java.util.UUID
import com.google.common.hash.Hashing
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.auto._
import eu.timepit.refined.refineV
+import eu.timepit.refined.string.Uuid
import org.apache.james.core.Username
+import org.apache.james.jmap.api.change.MailboxChanges
import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.State.INSTANCE
@@ -73,9 +77,13 @@ final case class Account private(accountId: AccountId,
accountCapabilities: Set[_ <: Capability])
object State {
+ type UUIDString = String Refined Uuid
+
val INSTANCE: State = fromString("2c9f1b12-b35a-43e6-9af2-0106fb53a943")
- def fromString(value: String): State = State(UUID.fromString(value))
+ def fromString(value: UUIDString): State = State(UUID.fromString(value.value))
+
+ def fromMailboxChanges(mailboxChanges: MailboxChanges): State = State(mailboxChanges.getNewState.getValue)
}
case class State(value: UUID)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index a0062a1..edc3392 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -110,6 +110,7 @@ object EmailGetSerializer {
private implicit val bodyValueWrites: Writes[EmailBodyValue] = Json.writes[EmailBodyValue]
private implicit val emailIdsReads: Reads[EmailIds] = Json.valueReads[EmailIds]
private implicit val emailGetRequestReads: Reads[EmailGetRequest] = Json.reads[EmailGetRequest]
+
private implicit val emailChangesRequestReads: Reads[EmailChangesRequest] = Json.reads[EmailChangesRequest]
private implicit val subjectWrites: Writes[Subject] = Json.valueWrites[Subject]
private implicit val emailNotFoundWrites: Writes[EmailNotFound] = Json.valueWrites[EmailNotFound]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
index 0064179..f85c08e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/MailboxSerializer.scala
@@ -19,12 +19,11 @@
package org.apache.james.jmap.json
-import java.util.UUID
-
import eu.timepit.refined._
import eu.timepit.refined.collection.NonEmpty
import javax.inject.Inject
import org.apache.james.core.{Domain, Username}
+import org.apache.james.jmap.api.change.MailboxChange.Limit
import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.core.{ClientId, Properties, SetError, State}
import org.apache.james.jmap.mail.MailboxGet.{UnparsedMailboxId, UnparsedMailboxIdConstraint}
@@ -114,6 +113,13 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
private implicit val idsRead: Reads[Ids] = Json.valueReads[Ids]
private implicit val mailboxGetRequest: Reads[MailboxGetRequest] = Json.reads[MailboxGetRequest]
+
+ private implicit val limitReads: Reads[Limit] = {
+ case JsNumber(underlying) if underlying > 0 => JsSuccess(Limit.of(underlying.intValue))
+ case JsNumber(underlying) if underlying <= 0 => JsError("Expecting a positive integer as Limit")
+ case _ => JsError("Expecting a number as Limit")
+ }
+
private implicit val mailboxChangesRequest: Reads[MailboxChangesRequest] = Json.reads[MailboxChangesRequest]
private implicit val mailboxRemoveEmailsOnDestroy: Reads[RemoveEmailsOnDestroy] = Json.valueFormat[RemoveEmailsOnDestroy]
@@ -126,10 +132,6 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
private implicit val mapCreationRequestByMailBoxCreationId: Reads[Map[MailboxCreationId, JsObject]] =
Reads.mapReads[MailboxCreationId, JsObject] {string => refineV[NonEmpty](string).fold(JsError(_), id => JsSuccess(id)) }
- private implicit val stateReads: Reads[State] = {
- case JsString(underlying) => Try(UUID.fromString(underlying))
- .fold(e => JsError(e.getMessage), value => JsSuccess(State(value)))
- }
private implicit val mailboxSetRequestReads: Reads[MailboxSetRequest] = Json.reads[MailboxSetRequest]
private implicit val notFoundWrites: Writes[NotFound] = Json.valueWrites[NotFound]
@@ -188,10 +190,10 @@ class MailboxSerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
def deserializeMailboxGetRequest(input: String): JsResult[MailboxGetRequest] = Json.parse(input).validate[MailboxGetRequest]
- def deserializeMailboxGetRequest(input: JsValue): JsResult[MailboxGetRequest] = Json.fromJson[MailboxGetRequest](input)
-
def deserializeMailboxChangesRequest(input: JsValue): JsResult[MailboxChangesRequest] = Json.fromJson[MailboxChangesRequest](input)
+ def deserializeMailboxGetRequest(input: JsValue): JsResult[MailboxGetRequest] = Json.fromJson[MailboxGetRequest](input)
+
def deserializeMailboxSetRequest(input: JsValue): JsResult[MailboxSetRequest] = Json.fromJson[MailboxSetRequest](input)
def deserializeRights(input: JsValue): JsResult[Rights] = Json.fromJson[Rights](input)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index b75aca5..8bdbe4a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -21,7 +21,6 @@ package org.apache.james.jmap.json
import java.io.InputStream
import java.net.URL
-import java.util.UUID
import eu.timepit.refined.refineV
import io.netty.handler.codec.http.HttpResponseStatus
@@ -37,7 +36,6 @@ import play.api.libs.json._
import scala.collection.{Seq => LegacySeq}
import scala.language.implicitConversions
-import scala.util.Try
object ResponseSerializer {
// CreateIds
@@ -68,10 +66,6 @@ object ResponseSerializer {
// RequestObject
private implicit val requestObjectRead: Format[RequestObject] = Json.format[RequestObject]
- private implicit val stateReads: Reads[State] = {
- case JsString(underlying) => Try(UUID.fromString(underlying))
- .fold(e => JsError(e.getMessage), value => JsSuccess(State(value)))
- }
private implicit val stateWrites: Writes[State] = Json.valueWrites[State]
// ResponseObject
private implicit val responseObjectFormat: Format[ResponseObject] = Json.format[ResponseObject]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
index 235a817..4e4622d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
@@ -19,8 +19,9 @@
package org.apache.james.jmap.json
+import org.apache.james.jmap.core.State
import org.apache.james.jmap.mail.{Thread, ThreadChangesRequest, ThreadChangesResponse, ThreadGetRequest, ThreadGetResponse}
-import play.api.libs.json.{JsObject, JsResult, JsValue, Json, OWrites, Reads}
+import play.api.libs.json.{JsObject, JsResult, JsValue, Json, OWrites, Reads, Writes}
import scala.language.implicitConversions
@@ -28,6 +29,7 @@ object ThreadSerializer {
private implicit val threadGetReads: Reads[ThreadGetRequest] = Json.reads[ThreadGetRequest]
private implicit val threadChangesReads: Reads[ThreadChangesRequest] = Json.reads[ThreadChangesRequest]
private implicit val threadWrites: OWrites[Thread] = Json.writes[Thread]
+ private implicit val stateWrites: Writes[State] = Json.valueWrites[State]
private implicit val threadGetWrites: OWrites[ThreadGetResponse] = Json.writes[ThreadGetResponse]
private implicit val changesResponseWrites: OWrites[ThreadChangesResponse] = Json.writes[ThreadChangesResponse]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala
index 3a8d44f..e3160cf 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/package.scala
@@ -25,7 +25,7 @@ import java.time.format.DateTimeFormatter
import eu.timepit.refined.api.{RefType, Validate}
import org.apache.james.core.MailAddress
import org.apache.james.jmap.core.SetError.SetErrorDescription
-import org.apache.james.jmap.core.{AccountId, Properties, SetError, UTCDate}
+import org.apache.james.jmap.core.{AccountId, Properties, SetError, State, UTCDate}
import org.apache.james.jmap.mail.HasMoreChanges
import play.api.libs.json._
@@ -79,6 +79,8 @@ package object json {
}
case _ => JsError("Expecting js string to represent UTC Date")
}
+
+ private[json] implicit val stateReads: Reads[State] = Json.valueReads[State]
private[json] implicit val accountIdWrites: Format[AccountId] = Json.valueFormat[AccountId]
private[json] implicit val propertiesFormat: Format[Properties] = Json.valueFormat[Properties]
private[json] implicit val setErrorDescriptionWrites: Writes[SetErrorDescription] = Json.valueWrites[SetErrorDescription]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
index 3e10abe..add1c6e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
@@ -22,8 +22,8 @@ package org.apache.james.jmap.mail
import eu.timepit.refined
import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.NonEmpty
-import org.apache.james.jmap.core.Id.Id
-import org.apache.james.jmap.core.UnsignedInt.UnsignedInt
+import org.apache.james.jmap.api.change.MailboxChange.Limit
+import org.apache.james.jmap.api.change.MailboxChanges
import org.apache.james.jmap.core.{AccountId, Properties, State}
import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
import org.apache.james.jmap.method.WithAccountId
@@ -66,15 +66,21 @@ case class MailboxGetResponse(accountId: AccountId,
list: List[Mailbox],
notFound: NotFound)
+object HasMoreChanges {
+ def fromMailboxChanges(mailboxChanges: MailboxChanges): HasMoreChanges = HasMoreChanges(mailboxChanges.hasMoreChanges)
+}
+
+case class HasMoreChanges(value: Boolean) extends AnyVal
+
case class MailboxChangesRequest(accountId: AccountId,
- sinceState: State,
- maxChanged: Option[UnsignedInt]) extends WithAccountId
+ sinceState: State,
+ maxChanged: Option[Limit]) extends WithAccountId
case class MailboxChangesResponse(accountId: AccountId,
oldState: State,
newState: State,
hasMoreChanges: HasMoreChanges,
updatedProperties: Option[Properties],
- created: List[Id],
- updated: List[Id],
- destroyed: List[Id])
\ No newline at end of file
+ created: Set[MailboxId],
+ updated: Set[MailboxId],
+ destroyed: Set[MailboxId])
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
index b85954c..a51e16c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
@@ -19,10 +19,9 @@
package org.apache.james.jmap.mail
-import org.apache.james.jmap.core.AccountId
import org.apache.james.jmap.core.Id.Id
-import org.apache.james.jmap.core.State.State
import org.apache.james.jmap.core.UnsignedInt.UnsignedInt
+import org.apache.james.jmap.core.{AccountId, State}
import org.apache.james.jmap.method.WithAccountId
case class Thread(id: Id, emailIds: List[Id])
@@ -38,8 +37,6 @@ case class ThreadChangesRequest(accountId: AccountId,
sinceState: State,
maxChanged: Option[UnsignedInt]) extends WithAccountId
-case class HasMoreChanges(value: Boolean) extends AnyVal
-
case class ThreadChangesResponse(accountId: AccountId,
oldState: State,
newState: State,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
index 09b5ccb..438704e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxChangesMethod.scala
@@ -21,9 +21,12 @@ package org.apache.james.jmap.method
import eu.timepit.refined.auto._
import javax.inject.Inject
+import org.apache.james.jmap.api.change.MailboxChange.{State => JavaState}
+import org.apache.james.jmap.api.change.MailboxChangeRepository
+import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_MAIL}
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.core.{ErrorCode, Invocation, Properties, State}
+import org.apache.james.jmap.core.{Invocation, Properties, State}
import org.apache.james.jmap.json.{MailboxSerializer, ResponseSerializer}
import org.apache.james.jmap.mail.{HasMoreChanges, MailboxChangesRequest, MailboxChangesResponse}
import org.apache.james.jmap.routes.SessionSupplier
@@ -32,13 +35,19 @@ import org.apache.james.metrics.api.MetricFactory
import play.api.libs.json.{JsError, JsSuccess}
import reactor.core.scala.publisher.SMono
+import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
+
class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
- val metricFactory: MetricFactory,
- val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[MailboxChangesRequest] {
+ val metricFactory: MetricFactory,
+ val sessionSupplier: SessionSupplier,
+ val mailboxChangeRepository: MailboxChangeRepository) extends MethodRequiringAccountId[MailboxChangesRequest] {
override val methodName: MethodName = MethodName("Mailbox/changes")
override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_MAIL)
override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: MailboxChangesRequest): SMono[InvocationWithContext] =
+
+ // Support for LTT.RS. This should be removed as soon as Mailbox/get returns the correct state
if (request.sinceState.equals(State.INSTANCE)) {
val response: MailboxChangesResponse = MailboxChangesResponse(
accountId = request.accountId,
@@ -46,19 +55,31 @@ class MailboxChangesMethod @Inject()(mailboxSerializer: MailboxSerializer,
newState = State.INSTANCE,
hasMoreChanges = HasMoreChanges(false),
updatedProperties = Some(Properties()),
- created = List(),
- updated = List(),
- destroyed = List())
+ created = Set(),
+ updated = Set(),
+ destroyed = Set())
SMono.just(InvocationWithContext(invocation = Invocation(
methodName = methodName,
arguments = Arguments(mailboxSerializer.serializeChanges(response)),
methodCallId = invocation.invocation.methodCallId
), processingContext = invocation.processingContext))
} else {
- SMono.just(InvocationWithContext(invocation = Invocation.error(ErrorCode.CannotCalculateChanges,
- "Naive implementation for Mailbox/changes",
- invocation.invocation.methodCallId),
- processingContext = invocation.processingContext))
+ SMono.fromPublisher(mailboxChangeRepository.getSinceState(JavaAccountId.fromUsername(mailboxSession.getUser), JavaState.of(request.sinceState.value), request.maxChanged.toJava))
+ .map(mailboxChanges => MailboxChangesResponse(
+ accountId = request.accountId,
+ oldState = request.sinceState,
+ newState = State.fromMailboxChanges(mailboxChanges),
+ hasMoreChanges = HasMoreChanges.fromMailboxChanges(mailboxChanges),
+ updatedProperties = Some(Properties()),
+ created = mailboxChanges.getCreated.asScala.toSet,
+ updated = mailboxChanges.getUpdated.asScala.toSet,
+ destroyed = mailboxChanges.getDestroyed.asScala.toSet))
+ .map(response => InvocationWithContext(
+ invocation = Invocation(
+ methodName = methodName,
+ arguments = Arguments(mailboxSerializer.serializeChanges(response)),
+ methodCallId = invocation.invocation.methodCallId),
+ processingContext = invocation.processingContext))
}
override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, MailboxChangesRequest] =
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 c2663a4..a4acfe1 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
@@ -18,6 +18,7 @@
****************************************************************/
package org.apache.james.jmap.method
+import org.apache.james.jmap.api.exception.ChangeNotFoundException
import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.core.Invocation.MethodName
import org.apache.james.jmap.core.{AccountId, ErrorCode, Invocation, Session}
@@ -27,7 +28,7 @@ import org.apache.james.mailbox.MailboxSession
import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.metrics.api.MetricFactory
import org.reactivestreams.Publisher
-import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scala.publisher.SFlux
case class AccountNotFoundException(invocation: Invocation) extends IllegalArgumentException
@@ -81,6 +82,7 @@ trait MethodRequiringAccountId[REQUEST <: WithAccountId] extends Method {
invocation.invocation.methodCallId), invocation.processingContext))
case e: IllegalArgumentException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
case e: MailboxNotFoundException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.InvalidArguments, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
+ case e: ChangeNotFoundException => SFlux.just[InvocationWithContext] (InvocationWithContext(Invocation.error(ErrorCode.CannotCalculateChanges, e.getMessage, invocation.invocation.methodCallId), invocation.processingContext))
case e: Throwable => SFlux.raiseError[InvocationWithContext] (e)
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org