JAMES-4025 ImapSetMessagesMailboxesUpdatesCompatibility cucumber test for JMAP RFC-8621 in plain scala
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/DistributedIMAPSetMessagesCompatibilityTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIMAPSetMessagesCompatibilityTest.java
new file mode 100644
index 0000000..19ee97e
--- /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/DistributedIMAPSetMessagesCompatibilityTest.java
@@ -0,0 +1,34 @@
+/****************************************************************
+ * 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.jmap.rfc8621.contract.ImapSetMessagesMailboxesUpdatesCompatibilityContract;
+import org.apache.james.utils.TestIMAPClient;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedIMAPSetMessagesCompatibilityTest extends DistributedBase implements ImapSetMessagesMailboxesUpdatesCompatibilityContract {
+ @RegisterExtension
+ TestIMAPClient testIMAPClient = new TestIMAPClient();
+
+ @Override
+ public TestIMAPClient imapClient() {
+ return testIMAPClient;
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml
index ead2b34..f7429d1 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml
@@ -21,4 +21,21 @@
<imapservers>
+ <imapserver enabled="true">
+ <jmxName>imapserver</jmxName>
+ <bind>0.0.0.0:0</bind>
+ <connectionBacklog>200</connectionBacklog>
+ <tls socketTLS="false" startTLS="false">
+ <!-- To create a new keystore execute:
+ keytool -genkey -alias james -keyalg RSA -keystore /path/to/james/conf/keystore
+ -->
+ <keystore>file://conf/keystore</keystore>
+ <secret>james72laBalle</secret>
+ <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+ </tls>
+ <connectionLimit>0</connectionLimit>
+ <connectionLimitPerIP>0</connectionLimitPerIP>
+ <plainAuthDisallowed>false</plainAuthDisallowed>
+ <gracefulShutdown>false</gracefulShutdown>
+ </imapserver>
</imapservers>
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
index 173ede4..2766eb7 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
@@ -48,6 +48,10 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-imap</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-jmap</artifactId>
</dependency>
<dependency>
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/ImapSetMessagesMailboxesUpdatesCompatibilityContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ImapSetMessagesMailboxesUpdatesCompatibilityContract.scala
new file mode 100644
index 0000000..110ab5e
--- /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/ImapSetMessagesMailboxesUpdatesCompatibilityContract.scala
@@ -0,0 +1,306 @@
+/****************************************************************
+ * 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.util
+import java.util.concurrent.TimeUnit
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured
+import io.restassured.RestAssured.`given`
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.JMAPTestingConstants.{DOMAIN, LOCALHOST_IP}
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, BOB, BOB_PASSWORD, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.ImapSetMessagesMailboxesUpdatesCompatibilityContract.bobInboxPath
+import org.apache.james.mailbox.DefaultMailboxes
+import org.apache.james.mailbox.MessageManager.AppendCommand
+import org.apache.james.mailbox.model.{MailboxConstants, MailboxId, MailboxPath, MessageId}
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.modules.MailboxProbeImpl
+import org.apache.james.modules.protocols.ImapGuiceProbe
+import org.apache.james.utils.{DataProbeImpl, TestIMAPClient}
+import org.assertj.core.api.Assertions.assertThat
+import org.awaitility.Awaitility
+import org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+object ImapSetMessagesMailboxesUpdatesCompatibilityContract {
+ private val bobInboxPath = MailboxPath.forUser(BOB, DefaultMailboxes.INBOX)
+}
+
+trait ImapSetMessagesMailboxesUpdatesCompatibilityContract {
+ private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS
+ private lazy val calmlyAwait = Awaitility.`with`
+ .pollInterval(slowPacedPollInterval)
+ .and.`with`.pollDelay(slowPacedPollInterval)
+ .await
+ private lazy val awaitAtMostOneMinute = calmlyAwait.atMost(1, TimeUnit.MINUTES)
+
+ def imapClient: TestIMAPClient
+
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent
+ .addDomain(DOMAIN)
+ .addUser(BOB.asString, BOB_PASSWORD)
+
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.INBOX)
+ mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.ARCHIVE)
+ mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.TRASH)
+
+ RestAssured.requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+ .build
+ }
+
+ @Test
+ def messageMovedByJmapIsSeenMovedByImap(server: GuiceJamesServer): Unit = {
+ // Given the user has a message "m1" in "inbox" with subject "My awesome subject", content "This is the content"
+ val messageId: MessageId = appendMessageToInbox(server)
+
+ // When the user moves "m1" to user mailbox "archive"
+ moveMessageFromInboxToArchive(server, messageId)
+
+ // Then the user has a IMAP message in mailbox "archive"
+ imapClient.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB, BOB_PASSWORD)
+ .select(DefaultMailboxes.ARCHIVE)
+ .awaitMessageCount(awaitAtMostOneMinute, 1)
+
+ // And the user does not have a IMAP message in mailbox "inbox"
+ imapClient.select(DefaultMailboxes.INBOX)
+ .awaitMessageCount(awaitAtMostOneMinute, 0)
+ }
+
+ @Test
+ def messageCopiedByJmapIsSeenAsCopiedByImap(server: GuiceJamesServer): Unit = {
+ // Given the user has a message "m1" in "inbox" with subject "My awesome subject", content "This is the content"
+ val messageId: MessageId = appendMessageToInbox(server)
+
+ // When the user copies "m1" from mailbox "inbox" to mailbox "archive"
+ copyMessageFromInboxToArchive(server, messageId)
+
+ // Then the user has a IMAP message in mailbox "archive"
+ imapClient.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB, BOB_PASSWORD)
+ .select(DefaultMailboxes.ARCHIVE)
+ .awaitMessageCount(awaitAtMostOneMinute, 1)
+
+ // And the user has a IMAP message in mailbox "inbox"
+ imapClient.select(DefaultMailboxes.INBOX)
+ .awaitMessageCount(awaitAtMostOneMinute, 1)
+ }
+
+ @Test
+ def imapClientShouldBeNotifiedWhenSelectingMailboxWhereMessageMovedByJmap(server: GuiceJamesServer): Unit = {
+ // Given the user has a message "m1" in "inbox" mailbox with subject "My awesome subject", content "This is the content"
+ val messageId: MessageId = appendMessageToInbox(server)
+
+ // When the user moves "m1" to user mailbox "archive"
+ moveMessageFromInboxToArchive(server, messageId)
+
+ // Then the user has a IMAP notification about 1 new message when selecting mailbox "archive"
+ imapClient.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB, BOB_PASSWORD)
+ .select(DefaultMailboxes.ARCHIVE)
+
+ assertThat(imapClient.userGetNotifiedForNewMessagesWhenSelectingMailbox(1))
+ .isTrue
+ }
+
+ @Test
+ def imapClientShouldBeNotifiedOnCurrentMailboxWhenMessageMovedByJmapToTheSameMailbox(server: GuiceJamesServer): Unit = {
+ // Given the user has a message "m1" in "inbox" mailbox with subject "My awesome subject", content "This is the content"
+ val messageId: MessageId = appendMessageToInbox(server)
+
+ // Given the user has an open IMAP connection with mailbox "archive" selected
+ imapClient.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB, BOB_PASSWORD)
+ .select(DefaultMailboxes.ARCHIVE)
+
+ // When the user moves "m1" to user mailbox "archive"
+ moveMessageFromInboxToArchive(server, messageId)
+
+ // Then mailbox "archive" contains 1 messages
+ awaitAtMostOneMinute.until(() => listMessageIdsArchive(server).size == 1)
+
+ // Then the user has a IMAP RECENT and a notification about 1 new messages on connection for mailbox "archive"
+ assertThat(imapClient.userGetNotifiedForNewMessages(1))
+ .isTrue
+ }
+
+ @Test
+ def whenMessageCopiedByImapShouldBeSeenByJmapAndNotifiedOnImapWithDestinationMailboxAlreadySelected(server: GuiceJamesServer): Unit = {
+ // Given the user has a message "m1" in "inbox" mailbox with subject "My awesome subject", content "This is the content"
+ appendMessageToInbox(server)
+
+ // Given the user has an open IMAP connection with mailbox "archive" selected
+ imapClient.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB, BOB_PASSWORD)
+ .select(DefaultMailboxes.ARCHIVE)
+
+ // When the user copy by IMAP first message of "inbox" to mailbox "archive"
+ val imapClientCopy: TestIMAPClient = new TestIMAPClient
+ imapClientCopy.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB.asString, BOB_PASSWORD)
+ .select(DefaultMailboxes.INBOX)
+ .copyFirstMessage(DefaultMailboxes.ARCHIVE)
+ imapClientCopy.close()
+
+ // Then mailbox "archive" contains 1 messages
+ awaitAtMostOneMinute.until(() => listMessageIdsArchive(server).size == 1)
+
+ // Then the user has a IMAP RECENT and a notification about 1 new messages on connection for mailbox "mailbox"
+ assertThat(imapClient.userGetNotifiedForNewMessages(1))
+ .isTrue
+ }
+
+ @Test
+ def whenMessageMovedByJmapThemImapClientWithSourceMailboxSelectedWillNotBeNotified(server: GuiceJamesServer): Unit = {
+ // Given the user has a message "m1" in "inbox" mailbox with subject "My awesome subject", content "This is the content"
+ val messageId: MessageId = appendMessageToInbox(server)
+
+ // Given the user has an open IMAP connection with mailbox "inbox" selected
+ imapClient.connect(LOCALHOST_IP, server.getProbe(classOf[ImapGuiceProbe]).getImapPort)
+ .login(BOB, BOB_PASSWORD)
+ .select(DefaultMailboxes.INBOX)
+
+ // When the user moves "m1" to user mailbox "archive"
+ moveMessageFromInboxToArchive(server, messageId)
+ awaitAtMostOneMinute.until(() => listMessageIdsArchive(server).size == 1)
+
+ // Then the user has IMAP EXPUNGE and a notification for 1 message sequence number on connection for mailbox "inbox"
+ assertThat(imapClient.userGetNotifiedForDeletion(1)).isTrue
+ }
+
+ private def appendMessageToInbox(server: GuiceJamesServer): MessageId = {
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setSender(ANDRE.asString)
+ .setFrom("ANDRE <" + ANDRE.asString + ">")
+ .setTo(BOB.asString)
+ .setSubject("My awesome subject")
+ .setBody("This is the content", StandardCharsets.UTF_8)
+ .build
+
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString(), bobInboxPath, AppendCommand.builder().build(message))
+ .getMessageId
+ }
+
+ def moveMessageFromInboxToArchive(server: GuiceJamesServer, messageId: MessageId): Unit = {
+ val archiveMailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+ .getMailboxId(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.ARCHIVE)
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${archiveMailboxId.serialize}": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ }
+
+ def copyMessageFromInboxToArchive(server: GuiceJamesServer, messageId: MessageId): Unit = {
+ val inboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+ .getMailboxId(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.INBOX)
+ val archiveMailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+ .getMailboxId(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.ARCHIVE)
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${inboxId.serialize}": true,
+ | "${archiveMailboxId.serialize}": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ }
+
+ def listMessageIdsArchive(server: GuiceJamesServer): util.ArrayList[String] = {
+ val archiveMailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+ .getMailboxId(MailboxConstants.USER_NAMESPACE, BOB.asString, DefaultMailboxes.ARCHIVE)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/query",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "filter": {"inMailbox": "${archiveMailboxId.serialize}"}
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post()
+ .`then`
+ .statusCode(SC_OK)
+ .extract
+ .body
+ .path("methodResponses[0][1].ids")
+ }
+}
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/MemoryIMAPSetMessagesCompatibilityTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIMAPSetMessagesCompatibilityTest.java
new file mode 100644
index 0000000..73932e9
--- /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/MemoryIMAPSetMessagesCompatibilityTest.java
@@ -0,0 +1,34 @@
+/****************************************************************
+ * 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 org.apache.james.jmap.rfc8621.contract.ImapSetMessagesMailboxesUpdatesCompatibilityContract;
+import org.apache.james.utils.TestIMAPClient;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryIMAPSetMessagesCompatibilityTest extends MemoryBase implements ImapSetMessagesMailboxesUpdatesCompatibilityContract {
+ @RegisterExtension
+ TestIMAPClient testIMAPClient = new TestIMAPClient();
+
+ @Override
+ public TestIMAPClient imapClient() {
+ return testIMAPClient;
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml
index ead2b34..f7429d1 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/resources/imapserver.xml
@@ -21,4 +21,21 @@
<imapservers>
+ <imapserver enabled="true">
+ <jmxName>imapserver</jmxName>
+ <bind>0.0.0.0:0</bind>
+ <connectionBacklog>200</connectionBacklog>
+ <tls socketTLS="false" startTLS="false">
+ <!-- To create a new keystore execute:
+ keytool -genkey -alias james -keyalg RSA -keystore /path/to/james/conf/keystore
+ -->
+ <keystore>file://conf/keystore</keystore>
+ <secret>james72laBalle</secret>
+ <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+ </tls>
+ <connectionLimit>0</connectionLimit>
+ <connectionLimitPerIP>0</connectionLimitPerIP>
+ <plainAuthDisallowed>false</plainAuthDisallowed>
+ <gracefulShutdown>false</gracefulShutdown>
+ </imapserver>
</imapservers>