You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/10/20 02:54:12 UTC
[james-project] branch master updated (5725467 -> cfc6b0d)
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.
from 5725467 JAMES-3414 MailboxIds partial updates patch validation
new f7aa673 JAMES-3411 Email/set update keywords
new b887525 JAMES-3148 Fix instability in CassandraMailboxManagerTest
new 866cf73 JAMES-3407 Read repair: improve build stability
new f70662c JAMES-3407 Disable read repairs upon Ghost mailbox fixing
new 3f9397b JAMES-2891 CORS Options should be supported for JMAP session endpoint
new cfc6b0d JAMES-2891 redirect to JMAP session resource from /.well-known/jmap
The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../mailbox/cassandra/DeleteMessageListener.java | 29 +-
.../cassandra/mail/CassandraMailboxMapper.java | 2 +-
.../rfc8621/contract/EmailSetMethodContract.scala | 550 ++++++++++++++++++++-
.../org/apache/james/jmap/http/SessionRoutes.scala | 15 +-
.../james/jmap/json/EmailSetSerializer.scala | 34 +-
.../org/apache/james/jmap/mail/EmailSet.scala | 27 +-
.../apache/james/jmap/method/EmailSetMethod.scala | 30 +-
.../apache/james/jmap/http/SessionRoutesTest.scala | 16 +
.../java/org/apache/james/jmap/JMAPRoutes.java | 5 +
.../rabbitmq/FixingGhostMailboxTest.java | 9 +-
10 files changed, 685 insertions(+), 32 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 02/06: JAMES-3148 Fix instability in
CassandraMailboxManagerTest
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit b887525f6b24815abaa4db850963d5becd0d3e98
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 11:40:35 2020 +0700
JAMES-3148 Fix instability in CassandraMailboxManagerTest
---
.../mailbox/cassandra/DeleteMessageListener.java | 29 ++++++++++++----------
1 file changed, 16 insertions(+), 13 deletions(-)
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
index d4a3ea7..fa70727 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
@@ -142,17 +142,20 @@ public class DeleteMessageListener implements MailboxListener.GroupMailboxListen
}
private Mono<Void> handleMailboxDeletion(CassandraId mailboxId) {
- return messageIdDAO.retrieveMessages(mailboxId, MessageRange.all(), Limit.unlimited())
- .map(ComposedMessageIdWithMetaData::getComposedMessageId)
- .concatMap(metadata -> handleMessageDeletionAsPartOfMailboxDeletion((CassandraMessageId) metadata.getMessageId(), mailboxId)
- .then(imapUidDAO.delete((CassandraMessageId) metadata.getMessageId(), mailboxId))
- .then(messageIdDAO.delete(mailboxId, metadata.getUid())))
- .then(deleteAcl(mailboxId))
- .then(applicableFlagDAO.delete(mailboxId))
- .then(firstUnseenDAO.removeAll(mailboxId))
- .then(deletedMessageDAO.removeAll(mailboxId))
- .then(counterDAO.delete(mailboxId))
- .then(recentsDAO.delete(mailboxId));
+ int prefetch = 1;
+ return Flux.mergeDelayError(prefetch,
+ messageIdDAO.retrieveMessages(mailboxId, MessageRange.all(), Limit.unlimited())
+ .map(ComposedMessageIdWithMetaData::getComposedMessageId)
+ .concatMap(metadata -> handleMessageDeletionAsPartOfMailboxDeletion((CassandraMessageId) metadata.getMessageId(), mailboxId)
+ .then(imapUidDAO.delete((CassandraMessageId) metadata.getMessageId(), mailboxId))
+ .then(messageIdDAO.delete(mailboxId, metadata.getUid()))),
+ deleteAcl(mailboxId),
+ applicableFlagDAO.delete(mailboxId),
+ firstUnseenDAO.removeAll(mailboxId),
+ deletedMessageDAO.removeAll(mailboxId),
+ counterDAO.delete(mailboxId),
+ recentsDAO.delete(mailboxId))
+ .then();
}
private Mono<Void> handleMessageDeletion(Expunged expunged) {
@@ -166,8 +169,8 @@ public class DeleteMessageListener implements MailboxListener.GroupMailboxListen
private Mono<Void> deleteAcl(CassandraId mailboxId) {
return aclMapper.getACL(mailboxId)
- .flatMap(acl -> rightsDAO.update(mailboxId, ACLDiff.computeDiff(acl, MailboxACL.EMPTY)))
- .then(aclMapper.delete(mailboxId));
+ .flatMap(acl -> rightsDAO.update(mailboxId, ACLDiff.computeDiff(acl, MailboxACL.EMPTY))
+ .then(aclMapper.delete(mailboxId)));
}
private Mono<Void> handleMessageDeletion(CassandraMessageId messageId) {
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 05/06: JAMES-2891 CORS Options should be supported
for JMAP session endpoint
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 3f9397b21844276eb19adeb16752841a4a2a23fb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 08:49:48 2020 +0700
JAMES-2891 CORS Options should be supported for JMAP session endpoint
---
.../src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala | 2 +-
.../test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
index a15726d..c087ed9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
@@ -65,7 +65,7 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
.action(generateSession)
.corsHeaders,
JMAPRoute.builder()
- .endpoint(new Endpoint(HttpMethod.OPTIONS, AUTHENTICATION))
+ .endpoint(new Endpoint(HttpMethod.OPTIONS, JMAP_SESSION))
.action(CORS_CONTROL)
.noCorsHeaders)
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
index 2db8d50..dc7895a 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
@@ -92,6 +92,13 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
.contentType(ContentType.JSON)
}
+ "options" should "return OK status" in {
+ RestAssured.when()
+ .options
+ .`then`
+ .statusCode(HttpStatus.SC_OK)
+ }
+
"get" should "return correct session" in {
val sessionJson = RestAssured.`with`()
.get
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 04/06: JAMES-3407 Disable read repairs upon Ghost
mailbox fixing
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit f70662c3fdc7a615f9358e93ee533c4f8731b4b0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 15:31:42 2020 +0700
JAMES-3407 Disable read repairs upon Ghost mailbox fixing
Read repairs caused already fixed inconsistencies to be fixed again, leading to unexpected results.
---
.../webadmin/integration/rabbitmq/FixingGhostMailboxTest.java | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java
index be3e26b..0581f9b 100644
--- a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java
+++ b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java
@@ -51,6 +51,7 @@ import org.apache.james.JamesServerBuilder;
import org.apache.james.JamesServerExtension;
import org.apache.james.SearchConfiguration;
import org.apache.james.backends.cassandra.init.ClusterFactory;
+import org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
import org.apache.james.backends.cassandra.init.configuration.CassandraConsistenciesConfiguration;
import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration;
import org.apache.james.core.Username;
@@ -128,7 +129,13 @@ class FixingGhostMailboxTest {
.extension(new RabbitMQExtension())
.server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
.overrideWith(new TestJMAPServerModule())
- .overrideWith(new WebadminIntegrationTestModule()))
+ .overrideWith(new WebadminIntegrationTestModule())
+ .overrideWith(binder -> binder.bind(CassandraConfiguration.class)
+ .toInstance(CassandraConfiguration.builder()
+ .mailboxReadRepair(0)
+ .mailboxCountersReadRepairMax(0)
+ .mailboxCountersReadRepairChanceOneHundred(0)
+ .build())))
.build();
private AccessToken accessToken;
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 03/06: JAMES-3407 Read repair: improve build
stability
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 866cf73c139e917d75de136ca2fff6dd2510c1c7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 11:59:00 2020 +0700
JAMES-3407 Read repair: improve build stability
Do not perform read repair upon read fallback to previous tables as it fails
---
.../org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java
index e918b8e..953f1c1 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java
@@ -190,7 +190,7 @@ public class CassandraMailboxMapper implements MailboxMapper {
return mailboxPathV2DAO.retrieveId(path)
.switchIfEmpty(mailboxPathDAO.retrieveId(path))
.map(CassandraIdAndPath::getCassandraId)
- .flatMap(this::retrieveMailbox)
+ .flatMap(mailboxDAO::retrieveMailbox)
.flatMap(this::migrate);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 01/06: JAMES-3411 Email/set update keywords
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit f7aa673640acdee20d3f538fc7a379735cd1b241
Author: duc91 <vd...@linagora.com>
AuthorDate: Tue Oct 13 17:26:03 2020 +0700
JAMES-3411 Email/set update keywords
---
.../rfc8621/contract/EmailSetMethodContract.scala | 550 ++++++++++++++++++++-
.../james/jmap/json/EmailSetSerializer.scala | 34 +-
.../org/apache/james/jmap/mail/EmailSet.scala | 27 +-
.../apache/james/jmap/method/EmailSetMethod.scala | 30 +-
4 files changed, 626 insertions(+), 15 deletions(-)
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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 758d6ac..3267358 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -18,25 +18,34 @@
****************************************************************/
package org.apache.james.jmap.rfc8621.contract
+import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets
+import java.util.Date
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
import io.restassured.RestAssured.{`given`, requestSpecification}
import io.restassured.http.ContentType.JSON
+import javax.mail.Flags
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
import org.apache.james.jmap.draft.{JmapGuiceProbe, MessageIdProbe}
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.FlagsBuilder
import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.MailboxACL.Right
-import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageId}
+import org.apache.james.mailbox.model.{ComposedMessageId, MailboxACL, MailboxConstants, MailboxId, MailboxPath, MessageId}
+import org.apache.james.mailbox.probe.MailboxProbe
import org.apache.james.mime4j.dom.Message
import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
import org.apache.james.utils.DataProbeImpl
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.{BeforeEach, Test}
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+import scala.jdk.CollectionConverters._
trait EmailSetMethodContract {
@BeforeEach
@@ -55,6 +64,543 @@ trait EmailSetMethodContract {
def randomMessageId: MessageId
@Test
+ def shouldResetKeywords(server: GuiceJamesServer): Unit = {
+ val message: Message = Fixture.createTestMessage
+
+ val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+ .withFlags(flags)
+ .build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["${messageId.serialize}"],
+ | "properties": ["keywords"]
+ | },
+ | "c2"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[1][1].list[0]")
+ .isEqualTo(String.format(
+ """{
+ | "id":"%s",
+ | "keywords": {
+ | "music": true
+ | }
+ |}
+ """.stripMargin, messageId.serialize))
+ }
+
+ @Test
+ def shouldNotResetKeywordWhenFalseValue(server: GuiceJamesServer): Unit = {
+ val message: Message = Fixture.createTestMessage
+
+ val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+ .withFlags(flags)
+ .build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "music": true,
+ | "movie": false
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
+ .isEqualTo(
+ """|{
+ | "type":"invalidPatch",
+ | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
+ |}""".stripMargin)
+ }
+
+ @Test
+ def shouldNotResetKeywordWhenInvalidKeyword(server: GuiceJamesServer): Unit = {
+ val message: Message = Fixture.createTestMessage
+
+ val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+ .withFlags(flags)
+ .build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "mus*c": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
+ .isEqualTo(
+ """|{
+ | "type":"invalidPatch",
+ | "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
+ |}""".stripMargin)
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = Array(
+ "$Recent",
+ "$Deleted"
+ ))
+ def shouldNotResetNonExposedKeyword(unexposedKeyword: String, server: GuiceJamesServer): Unit = {
+ val message: Message = Fixture.createTestMessage
+
+ val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+ .withFlags(flags)
+ .build(message))
+ .getMessageId
+
+ val request = String.format(
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "music": true,
+ | "$unexposedKeyword": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin)
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notUpdated")
+ .isEqualTo(
+ s"""{
+ | "${messageId.serialize}":{
+ | "type":"invalidPatch",
+ | "description":"Message 1 update is invalid: Does not allow to update 'Deleted' or 'Recent' flag"}
+ | }
+ |}"""
+ .stripMargin)
+ }
+
+ @Test
+ def shouldKeepUnexposedKeywordWhenResetKeywords(server: GuiceJamesServer): Unit = {
+ val mailboxProbe: MailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, BOB.asString(), "mailbox");
+
+ val bobPath = MailboxPath.forUser(BOB, "mailbox")
+ val message: ComposedMessageId = mailboxProbe.appendMessage(BOB.asString, bobPath,
+ new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)),
+ new Date, false, new Flags(Flags.Flag.DELETED))
+
+ val messageId: String = message.getMessageId.serialize
+
+ val request = String.format(s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "$messageId":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin)
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+
+ val flags: List[Flags] = server.getProbe(classOf[MessageIdProbe]).getMessages(message.getMessageId, BOB).asScala.map(m => m.getFlags).toList
+ val expectedFlags: Flags = FlagsBuilder.builder.add("music").add(Flags.Flag.DELETED).build
+
+ assertThat(flags.asJava)
+ .containsExactly(expectedFlags)
+ }
+
+ @Test
+ def shouldResetKeywordsWhenNotDefault(server: GuiceJamesServer): Unit = {
+ val message: Message = Fixture.createTestMessage
+
+ val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+ .withFlags(flags)
+ .build(message))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["${messageId.serialize}"],
+ | "properties": ["keywords"]
+ | },
+ | "c2"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[1][1].list[0]")
+ .isEqualTo(String.format(
+ """{
+ | "id":"%s",
+ | "keywords": {
+ | "music": true
+ | }
+ |}
+ """.stripMargin, messageId.serialize))
+ }
+
+ @Test
+ def shouldNotResetKeywordWhenInvalidMessageId(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "invalid":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notUpdated")
+ .isEqualTo("""{
+ | "invalid": {
+ | "type":"invalidPatch",
+ | "description":"Message invalid update is invalid: For input string: \"invalid\""
+ | }
+ |}""".stripMargin)
+ }
+
+ @Test
+ def shouldNotResetKeywordWhenMessageIdNonExisted(server: GuiceJamesServer): Unit = {
+ val invalidMessageId: MessageId = randomMessageId
+
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ val request = s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${invalidMessageId.serialize}":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notUpdated")
+ .isEqualTo(s"""{
+ | "${invalidMessageId.serialize}": {
+ | "type":"notFound",
+ | "description":"Cannot find message with messageId: ${invalidMessageId.serialize}"
+ | }
+ |}""".stripMargin)
+ }
+
+ @Test
+ def shouldNotUpdateInDelegatedMailboxesWhenReadOnly(server: GuiceJamesServer): Unit = {
+ val andreMailbox: String = "andrecustom"
+ val andrePath = MailboxPath.forUser(ANDRE, andreMailbox)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ val message: Message = Message.Builder
+ .of
+ .setSender(BOB.asString())
+ .setFrom(ANDRE.asString())
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(ANDRE.asString, andrePath, AppendCommand.from(message))
+ .getMessageId
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(andrePath, BOB.asString, MailboxACL.Rfc4314Rights.of(Set(Right.Read, Right.Lookup).asJava))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notUpdated")
+ .isEqualTo(
+ s"""{
+ | "${messageId.serialize}":{
+ | "type": "notFound",
+ | "description": "Mailbox not found"
+ | }
+ |}""".stripMargin)
+ }
+
+ @Test
+ def shouldResetFlagsInDelegatedMailboxesWhenHadAtLeastWriteRight(server: GuiceJamesServer): Unit = {
+ val andreMailbox: String = "andrecustom"
+ val andrePath = MailboxPath.forUser(ANDRE, andreMailbox)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ val message: Message = Message.Builder
+ .of
+ .setSender(BOB.asString())
+ .setFrom(ANDRE.asString())
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(ANDRE.asString, andrePath, AppendCommand.from(message))
+ .getMessageId
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(andrePath, BOB.asString, MailboxACL.Rfc4314Rights.of(Set(Right.Write, Right.Read).asJava))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["${messageId.serialize}"],
+ | "update": {
+ | "${messageId.serialize}":{
+ | "keywords": {
+ | "music": true
+ | }
+ | }
+ | }
+ | },
+ | "c1"],
+ | ["Email/get",
+ | {
+ | "accountId": "$ACCOUNT_ID",
+ | "ids": ["${messageId.serialize}"],
+ | "properties": ["keywords"]
+ | },
+ | "c2"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[1][1].list[0]")
+ .isEqualTo(String.format(
+ """{
+ | "id":"%s",
+ | "keywords": {
+ | "music":true
+ | }
+ |}
+ """.stripMargin, messageId.serialize))
+ }
+
+ @Test
def emailSetShouldDestroyEmail(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 556f194..28e79c2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -23,7 +23,7 @@ import eu.timepit.refined.refineV
import javax.inject.Inject
import org.apache.james.jmap.mail.EmailSet.{UnparsedMessageId, UnparsedMessageIdConstraint}
import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds}
-import org.apache.james.jmap.model.SetError
+import org.apache.james.jmap.model.{Keyword, Keywords, SetError}
import org.apache.james.mailbox.model.{MailboxId, MessageId}
import play.api.libs.json.{JsBoolean, JsError, JsNull, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
@@ -46,6 +46,12 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
}.headOption
.map(_.ids)
+ val keywordsReset: Option[Keywords] = entries.flatMap {
+ case update: KeywordsReset => Some(update)
+ case _ => None
+ }.headOption
+ .map(_.keywords)
+
val mailboxesToAdd: Option[MailboxIds] = Some(entries
.flatMap {
case update: MailboxAddition => Some(update)
@@ -62,7 +68,8 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
.filter(_.nonEmpty)
.map(MailboxIds)
- JsSuccess(EmailSetUpdate(mailboxIds = mailboxReset,
+ JsSuccess(EmailSetUpdate(keywords = keywordsReset,
+ mailboxIds = mailboxReset,
mailboxIdsToAdd = mailboxesToAdd,
mailboxIdsToRemove = mailboxesToRemove))
})
@@ -75,6 +82,10 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
.fold(
e => InvalidPatchEntryValue(property, e.toString()),
MailboxReset)
+ case "keywords" => keywordsReads.reads(value)
+ .fold(
+ e => InvalidPatchEntryValue(property, e.toString()),
+ KeywordsReset)
case name if name.startsWith(mailboxIdPrefix) => Try(mailboxIdFactory.fromString(name.substring(mailboxIdPrefix.length)))
.fold(e => InvalidPatchEntryNameWithDetails(property, e.getMessage),
id => value match {
@@ -108,6 +119,8 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
private case class MailboxReset(ids: MailboxIds) extends EntryValidation
+ private case class KeywordsReset(keywords: Keywords) extends EntryValidation
+
}
private implicit val messageIdWrites: Writes[MessageId] = messageId => JsString(messageId.serialize)
@@ -140,6 +153,23 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
case _ => JsError("Expecting a JsObject as an update entry")
})
+ private implicit val keywordReads: Reads[Keyword] = {
+ case jsString: JsString => Keyword.parse(jsString.value)
+ .fold(JsError(_),
+ JsSuccess(_))
+ case _ => JsError("Expecting a string as a keyword")
+ }
+
+ private implicit val keywordsMapReads: Reads[Map[Keyword, Boolean]] =
+ readMapEntry[Keyword, Boolean](s => Keyword.parse(s),
+ {
+ case JsBoolean(true) => JsSuccess(true)
+ case JsBoolean(false) => JsError("keyword value can only be true")
+ case _ => JsError("Expecting keyword value to be a boolean")
+ })
+ private implicit val keywordsReads: Reads[Keywords] = jsValue => keywordsMapReads.reads(jsValue).map(
+ keywordsMap => Keywords(keywordsMap.keys.toSet))
+
private implicit val unitWrites: Writes[Unit] = _ => JsNull
private implicit val updatedWrites: Writes[Map[MessageId, Unit]] = mapWrites[MessageId, Unit](_.serialize, unitWrites)
private implicit val notDestroyedWrites: Writes[Map[UnparsedMessageId, SetError]] = mapWrites[UnparsedMessageId, SetError](_.value, setErrorWrites)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index ab692e5..c2ea230 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -23,12 +23,13 @@ import eu.timepit.refined.api.Refined
import eu.timepit.refined.collection.NonEmpty
import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.jmap.model.KeywordsFactory.STRICT_KEYWORDS_FACTORY
import org.apache.james.jmap.model.State.State
-import org.apache.james.jmap.model.{AccountId, SetError}
+import org.apache.james.jmap.model.{AccountId, Keywords, SetError}
import org.apache.james.mailbox.model.MessageId
import play.api.libs.json.JsObject
-import scala.util.Try
+import scala.util.{Failure, Right, Success, Try}
object EmailSet {
type UnparsedMessageIdConstraint = NonEmpty
@@ -56,7 +57,8 @@ case class EmailSetResponse(accountId: AccountId,
destroyed: Option[DestroyIds],
notDestroyed: Option[Map[UnparsedMessageId, SetError]])
-case class EmailSetUpdate(mailboxIds: Option[MailboxIds],
+case class EmailSetUpdate(keywords: Option[Keywords],
+ mailboxIds: Option[MailboxIds],
mailboxIdsToAdd: Option[MailboxIds],
mailboxIdsToRemove: Option[MailboxIds]) {
def validate: Either[IllegalArgumentException, ValidatedEmailSetUpdate] = if (mailboxIds.isDefined && (mailboxIdsToAdd.isDefined || mailboxIdsToRemove.isDefined)) {
@@ -75,15 +77,26 @@ case class EmailSetUpdate(mailboxIds: Option[MailboxIds],
val mailboxIdsTransformation: Function[MailboxIds, MailboxIds] = mailboxIdsAddition
.compose(mailboxIdsRemoval)
.compose(mailboxIdsReset)
- scala.Right(ValidatedEmailSetUpdate(mailboxIdsTransformation))
+ Right(mailboxIdsTransformation)
+ .flatMap(mailboxIdsTransformation => validateKeywords
+ .map(validatedKeywords => ValidatedEmailSetUpdate(validatedKeywords, mailboxIdsTransformation)))
+ }
+
+ private def validateKeywords: Either[IllegalArgumentException, Option[Keywords]] = {
+ keywords.map(_.getKeywords)
+ .map(STRICT_KEYWORDS_FACTORY.fromSet)
+ .map {
+ case Success(validatedKeywords: Keywords) => Right(Some(validatedKeywords))
+ case Failure(throwable: IllegalArgumentException) => Left(throwable)
+ }
+ .getOrElse(Right(None))
}
}
-case class ValidatedEmailSetUpdate private (mailboxIdsTransformation: Function[MailboxIds, MailboxIds])
+case class ValidatedEmailSetUpdate private (keywords: Option[Keywords],
+ mailboxIdsTransformation: Function[MailboxIds, MailboxIds])
class EmailUpdateValidationException() extends IllegalArgumentException
case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
case class InvalidEmailUpdateException(property: String, cause: String) extends EmailUpdateValidationException
-
-
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index c7022f8..74d72b3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -18,8 +18,10 @@
****************************************************************/
package org.apache.james.jmap.method
+import com.google.common.collect.ImmutableList
import eu.timepit.refined.auto._
import javax.inject.Inject
+import javax.mail.Flags
import org.apache.james.jmap.http.SessionSupplier
import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
@@ -29,6 +31,7 @@ import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CA
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model.SetError.SetErrorDescription
import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
+import org.apache.james.mailbox.MessageManager.FlagsUpdateMode
import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MessageId}
import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
@@ -195,7 +198,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
.collectMultimap(metaData => metaData.getComposedMessageId.getMessageId)
.flatMap(metaData => {
SFlux.fromIterable(validUpdates)
- .flatMap[UpdateResult]({
+ .concatMap[UpdateResult]({
case (messageId, updatePatch) =>
doUpdate(messageId, updatePatch, metaData.get(messageId).toList.flatten, session)
})
@@ -208,6 +211,11 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
private def doUpdate(messageId: MessageId, update: EmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
val mailboxIds: MailboxIds = MailboxIds(storedMetaData.map(metaData => metaData.getComposedMessageId.getMailboxId))
+ val originFlags: Flags = storedMetaData
+ .foldLeft[Flags](new Flags())((flags: Flags, m: ComposedMessageIdWithMetaData) => {
+ flags.add(m.getFlags)
+ flags
+ })
if (mailboxIds.value.isEmpty) {
SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), MessageNotFoundExeception(messageId)))
@@ -215,9 +223,14 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
update.validate
.fold(
e => SMono.just(UpdateFailure(EmailSet.asUnparsed(messageId), e)),
- validatedUpdate => updateMailboxIds(messageId, validatedUpdate, mailboxIds, session)
- .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
- .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+ validatedUpdate =>
+ resetFlags(messageId, validatedUpdate, mailboxIds, originFlags, session)
+ .flatMap {
+ case failure: UpdateFailure => SMono.just[UpdateResult](failure)
+ case _: UpdateSuccess => updateMailboxIds(messageId, validatedUpdate, mailboxIds, session)
+ }
+ .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
+ .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId))))
}
}
@@ -234,6 +247,15 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
}
}
+ private def resetFlags(messageId: MessageId, update: ValidatedEmailSetUpdate, mailboxIds: MailboxIds, originalFlags: Flags, session: MailboxSession): SMono[UpdateResult] =
+ update.keywords
+ .map(keywords => keywords.asFlagsWithRecentAndDeletedFrom(originalFlags))
+ .map(flags => SMono.fromCallable(() =>
+ messageIdManager.setFlags(flags, FlagsUpdateMode.REPLACE, messageId, ImmutableList.copyOf(mailboxIds.value.asJavaCollection), session))
+ .subscribeOn(Schedulers.elastic())
+ .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+ .getOrElse(SMono.just[UpdateResult](UpdateSuccess(messageId)))
+
private def deleteMessage(destroyId: UnparsedMessageId, mailboxSession: MailboxSession): SMono[DestroyResult] =
EmailSet.parse(messageIdFactory)(destroyId)
.fold(e => SMono.just(DestroyFailure(destroyId, e)),
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
[james-project] 06/06: JAMES-2891 redirect to JMAP session resource
from /.well-known/jmap
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit cfc6b0dbd3da81b06e767c04c498c0d71e0e4176
Author: Anton Lazarev <an...@gmail.com>
AuthorDate: Fri Oct 16 18:42:20 2020 -0400
JAMES-2891 redirect to JMAP session resource from /.well-known/jmap
---
.../scala/org/apache/james/jmap/http/SessionRoutes.scala | 13 ++++++++++++-
.../org/apache/james/jmap/http/SessionRoutesTest.scala | 9 +++++++++
.../src/main/java/org/apache/james/jmap/JMAPRoutes.java | 5 +++++
3 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
index c087ed9..10f1565 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
@@ -29,7 +29,7 @@ import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8
import org.apache.james.jmap.JMAPRoutes.CORS_CONTROL
import org.apache.james.jmap.JMAPUrls.AUTHENTICATION
import org.apache.james.jmap.exceptions.UnauthorizedException
-import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, LOGGER}
+import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, WELL_KNOWN_JMAP, LOGGER}
import org.apache.james.jmap.http.rfc8621.InjectionKeys
import org.apache.james.jmap.json.ResponseSerializer
import org.apache.james.jmap.model.Session
@@ -43,6 +43,7 @@ import reactor.netty.http.server.HttpServerResponse
object SessionRoutes {
private val JMAP_SESSION: String = "/jmap/session"
+ private val WELL_KNOWN_JMAP: String = "/.well-known/jmap"
private val LOGGER = LoggerFactory.getLogger(classOf[SessionRoutes])
}
@@ -58,6 +59,8 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
.subscribeOn(Schedulers.elastic())
.asJava()
+ private val redirectToSession: JMAPRoute.Action = JMAPRoutes.redirectTo(JMAP_SESSION)
+
override def routes: Stream[JMAPRoute] =
Stream.of(
JMAPRoute.builder()
@@ -67,6 +70,14 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
JMAPRoute.builder()
.endpoint(new Endpoint(HttpMethod.OPTIONS, JMAP_SESSION))
.action(CORS_CONTROL)
+ .noCorsHeaders,
+ JMAPRoute.builder()
+ .endpoint(new Endpoint(HttpMethod.GET, WELL_KNOWN_JMAP))
+ .action(redirectToSession)
+ .corsHeaders,
+ JMAPRoute.builder()
+ .endpoint(new Endpoint(HttpMethod.OPTIONS, WELL_KNOWN_JMAP))
+ .action(CORS_CONTROL)
.noCorsHeaders)
private def sendRespond(session: Session, resp: HttpServerResponse) =
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
index dc7895a..a1a9e15 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
@@ -99,6 +99,15 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
.statusCode(HttpStatus.SC_OK)
}
+ "get .well-known/jmap" should "redirect" in {
+ RestAssured.`given`()
+ .basePath(".well-known/jmap")
+ .get
+ .`then`
+ .statusCode(308)
+ .header("Location", "/jmap/session")
+ }
+
"get" should "return correct session" in {
val sessionJson = RestAssured.`with`()
.get
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java
index 2908216..8c91027 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
+import static io.netty.handler.codec.http.HttpResponseStatus.PERMANENT_REDIRECT;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import java.util.stream.Stream;
@@ -42,6 +43,10 @@ public interface JMAPRoutes {
.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept"));
}
+ static JMAPRoute.Action redirectTo(String location) {
+ return (req, res) -> res.status(PERMANENT_REDIRECT).header("Location", location).send();
+ }
+
default Mono<Void> handleInternalError(HttpServerResponse response, Logger logger, Throwable e) {
logger.error("Internal server error", e);
return response.status(INTERNAL_SERVER_ERROR).send();
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org