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>