blob: efbdb1ffdd608a5942cbd9993f4158851e8750c6 [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.time.Duration
import java.util.concurrent.TimeUnit
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
import io.restassured.RestAssured.{`given`, `with`, requestSpecification}
import io.restassured.builder.ResponseSpecBuilder
import io.restassured.http.ContentType.JSON
import jakarta.mail.Flags
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.JmapGuiceProbe
import org.apache.james.jmap.api.change.State
import org.apache.james.jmap.api.model.AccountId
import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, 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, MailboxPath, MessageId}
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.awaitility.Awaitility
import org.junit.jupiter.api.{BeforeEach, Nested, Test}
import play.api.libs.json.{JsString, Json}
trait EmailChangesMethodContract {
private lazy val slowPacedPollInterval = Duration.ofMillis(100)
private lazy val calmlyAwait = Awaitility.`with`
.pollInterval(slowPacedPollInterval)
.and.`with`.pollDelay(slowPacedPollInterval)
.await
private lazy val awaitAtMostTenSeconds = calmlyAwait.atMost(10, TimeUnit.SECONDS)
def stateFactory: State.Factory
@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 emailChangesShouldReturnCreatedChanges(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${State.INITIAL.getValue}",
| "hasMoreChanges": false,
| "created": ["${messageId.serialize}"],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnUpdatedChangesWhenAddFlags(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val oldState: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
JmapRequests.markEmailAsSeen(messageId)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": ["${messageId.serialize()}"],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def shouldFailWithCannotCalculateChangesWhenSingleChangeIsTooLarge(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId4: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId5: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId6: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val state6: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val updateEmail =
s"""{
| "using": [
| "urn:ietf:params:jmap:core",
| "urn:ietf:params:jmap:mail"],
| "methodCalls": [
| ["Email/set",
| {
| "accountId": "$ACCOUNT_ID",
| "update": {
| "${messageId1.serialize}":{
| "keywords/$$flagged": true
| },
| "${messageId2.serialize}":{
| "keywords/$$flagged": true
| },
| "${messageId3.serialize}":{
| "keywords/$$flagged": true
| },
| "${messageId4.serialize}":{
| "keywords/$$flagged": true
| },
| "${messageId5.serialize}":{
| "keywords/$$flagged": true
| },
| "${messageId6.serialize}":{
| "keywords/$$flagged": true
| }
| }
| },
| "c1"]]
|}""".stripMargin
`with`()
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(updateEmail)
.post
.`then`
.statusCode(SC_OK)
.contentType(JSON)
.extract
.body
.asString
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "$ACCOUNT_ID",
| "sinceState": "${state6.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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]")
.isEqualTo(
s"""[
| "error",
| {
| "type": "cannotCalculateChanges",
| "description": "Current change collector limit 5 is exceeded by a single change, hence we cannot calculate changes."
| },
| "c1"
|]""".stripMargin)
}
}
@Test
def shouldReturnUpdatedWhenMessageMove(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val mailboxId2 = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val oldState: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val updateEmail =
s"""{
| "using": [
| "urn:ietf:params:jmap:core",
| "urn:ietf:params:jmap:mail"],
| "methodCalls": [
| ["Email/set",
| {
| "accountId": "$ACCOUNT_ID",
| "update": {
| "${messageId.serialize}":{
| "mailboxIds": {
| "${mailboxId2.serialize()}": true
| }
| }
| }
| },
| "c1"]]
|}""".stripMargin
`with`()
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(updateEmail)
.post
.`then`
.statusCode(SC_OK)
.contentType(JSON)
.extract
.body
.asString
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "$ACCOUNT_ID",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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")
.inPath("methodResponses[0][1]")
.isEqualTo(
s"""{
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": ["${messageId.serialize()}"],
| "destroyed": []
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnUpdatedChangesWhenRemoveFlags(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path,
AppendCommand.builder()
.withFlags(new Flags(Flags.Flag.SEEN))
.build("header: value\r\n\r\nbody"))
.getMessageId
val oldState: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
JmapRequests.markEmailAsNotSeen(messageId)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": ["${messageId.serialize()}"],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnDestroyedChanges(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val oldState: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
JmapRequests.destroyEmail(messageId)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": [],
| "destroyed": ["${messageId.serialize()}"]
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnAllTypeOfChanges(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val accountId: AccountId = AccountId.fromUsername(BOB)
val path1: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path1)
val path2: MailboxPath = MailboxPath.forUser(BOB, "mailbox2")
mailboxProbe.createMailbox(path2)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path2, AppendCommand.from(message)).getMessageId
val state1: State = waitForNextState(server, accountId, State.INITIAL)
val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path2, AppendCommand.from(message)).getMessageId
val oldState: State = waitForNextState(server, accountId, state1)
val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path1, AppendCommand.from(message)).getMessageId
JmapRequests.markEmailAsSeen(messageId1)
JmapRequests.destroyEmail(messageId2)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": ["${messageId3.serialize}"],
| "updated": ["${messageId1.serialize}"],
| "destroyed": ["${messageId2.serialize}"]
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldNotReturnDuplicatedIdsAccrossCreatedUpdatedOrDestroyed(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
JmapRequests.markEmailAsSeen(messageId2)
JmapRequests.destroyEmail(messageId3)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${State.INITIAL.getValue}",
| "hasMoreChanges": false,
| "created": ["${messageId1.serialize}", "${messageId2.serialize}"],
| "updated": ["${messageId2.serialize}"],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Nested
class DelegationTest {
@Test
def emailChangesShouldReturnCreatedChanges(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
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
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "oldState": "${State.INITIAL.getValue}",
| "hasMoreChanges": false,
| "created": ["${messageId.serialize}"],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnUpdatedChangesWhenAddFlags(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
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
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val oldState: State = waitForNextStateWithDelegation(server, AccountId.fromUsername(ANDRE), State.INITIAL)
JmapRequests.markEmailAsSeen(messageId)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": ["${messageId.serialize}"],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnUpdatedChangesWhenRemoveFlags(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
server.getProbe(classOf[ACLProbeImpl])
.replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path,
AppendCommand.builder()
.withFlags(new Flags(Flags.Flag.SEEN))
.build("header: value\r\n\r\nbody"))
.getMessageId
val oldState: State = waitForNextStateWithDelegation(server, AccountId.fromUsername(ANDRE), State.INITIAL)
JmapRequests.markEmailAsNotSeen(messageId)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": ["${messageId.serialize}"],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnDestroyedChanges(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
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
val messageId: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val oldState: State = waitForNextStateWithDelegation(server, AccountId.fromUsername(ANDRE), State.INITIAL)
JmapRequests.destroyEmail(messageId)
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "sinceState": "${oldState.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "oldState": "${oldState.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": [],
| "destroyed": ["${messageId.serialize}"]
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldNotReturnUpdatedChangesWhenMissingSharesCapability(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
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": [[
| "Email/changes",
| {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "$ANDRE_ACCOUNT_ID",
| "oldState": "${State.INITIAL.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
}
@Test
def emailChangesShouldReturnHasMoreChangesWhenTrue(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId1: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId2: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId3: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId4: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId5: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${State.INITIAL.getValue}",
| "hasMoreChanges": true,
| "created": ["$messageId1", "$messageId2", "$messageId3", "$messageId4", "$messageId5"],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def maxChangesShouldBeTakenIntoAccount(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId1: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId2: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId3: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId4: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId5: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val messageId6: String = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId.serialize
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.getValue}",
| "maxChanges": 38
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${State.INITIAL.getValue}",
| "hasMoreChanges": false,
| "created": ["$messageId1", "$messageId2", "$messageId3", "$messageId4", "$messageId5", "$messageId6"],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnNoChangesWhenNoNewerState(server: GuiceJamesServer): Unit = {
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "${State.INITIAL.getValue}",
| "newState": "${State.INITIAL.getValue}",
| "hasMoreChanges": false,
| "created": [],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
}
@Test
def emailChangesShouldReturnDifferentStateThanOldState(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
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": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.getValue}"
| },
| "c1"]]
|}""".stripMargin
awaitAtMostTenSeconds.untilAsserted { () =>
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(State.INITIAL.getValue.toString).isNotEqualTo(newState)
}
}
@Test
def emailChangesShouldEventuallyReturnNoChanges(server: GuiceJamesServer): Unit = {
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path = MailboxPath.forUser(BOB, "mailbox1")
mailboxProbe.createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message))
waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val request1 =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "sinceState": "${State.INITIAL.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": [[
| "Email/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": [
| [ "Email/changes", {
| "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
| "oldState": "$newState",
| "newState": "$newState",
| "hasMoreChanges": false,
| "created": [],
| "updated": [],
| "destroyed": []
| }, "c1"]
| ]
|}""".stripMargin)
}
@Test
def emailChangesShouldFailWhenAccountIdNotFound(): Unit = {
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/changes",
| {
| "accountId": "bad",
| "sinceState": "${State.INITIAL.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 emailChangesShouldFailWhenStateNotFound(server: GuiceJamesServer): Unit = {
val state: String = stateFactory.generate().getValue.toString
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
| "methodCalls": [[
| "Email/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)
}
private def waitForNextState(server: GuiceJamesServer, accountId: AccountId, initialState: State): State = {
val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
awaitAtMostTenSeconds.untilAsserted {
() => assertThat(jmapGuiceProbe.getLatestEmailState(accountId)).isNotEqualTo(initialState)
}
jmapGuiceProbe.getLatestEmailState(accountId)
}
private def waitForNextStateWithDelegation(server: GuiceJamesServer, accountId: AccountId, initialState: State): State = {
val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
awaitAtMostTenSeconds.untilAsserted {
() => assertThat(jmapGuiceProbe.getLatestEmailStateWithDelegation(accountId)).isNotEqualTo(initialState)
}
jmapGuiceProbe.getLatestEmailStateWithDelegation(accountId)
}
}