blob: 110ab5e6547f2a48f7aa56b34d83e82c893742d0 [file] [log] [blame]
/****************************************************************
* 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")
}
}