This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit ee6c8bb7436ad335f654b723064352296d71fd11 Author: Benoit Tellier <[email protected]> AuthorDate: Thu Aug 20 11:51:11 2020 +0700 JAMES-3359 Mailbox/set parentId updates --- .../apache/james/mailbox/model/MailboxPath.java | 10 + .../james/mailbox/model/MailboxPathTest.java | 18 + .../contract/MailboxSetMethodContract.scala | 625 +++++++++++++++++++++ .../org/apache/james/jmap/mail/MailboxSet.scala | 38 +- .../james/jmap/method/MailboxSetMethod.scala | 59 +- 5 files changed, 728 insertions(+), 22 deletions(-) diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java index 4e6b194..29507a4 100644 --- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java +++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/MailboxPath.java @@ -34,8 +34,10 @@ import org.apache.james.mailbox.exception.TooLongMailboxNameException; import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; /** * The path to a mailbox. @@ -145,6 +147,14 @@ public class MailboxPath { return levels; } + /** + * @return the name of the mailbox, accounting for the delimiter + */ + public String getName(char delimiter) { + return Iterables.getLast(Splitter.on(delimiter) + .splitToList(name)); + } + public MailboxPath sanitize(char delimiter) { if (name == null) { return this; diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java index f421ed7..5e4a31f 100644 --- a/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java +++ b/mailbox/api/src/test/java/org/apache/james/mailbox/model/MailboxPathTest.java @@ -51,6 +51,24 @@ class MailboxPathTest { } @Test + void getNameShouldReturnSubfolder() { + assertThat(MailboxPath.forUser(USER, "inbox.folder.subfolder").getName('.')) + .isEqualTo("subfolder"); + } + + @Test + void getNameShouldNoopWhenNoDelimiter() { + assertThat(MailboxPath.forUser(USER, "name").getName('.')) + .isEqualTo("name"); + } + + @Test + void getNameShouldNoopWhenEmpty() { + assertThat(MailboxPath.forUser(USER, "").getName('.')) + .isEqualTo(""); + } + + @Test void getHierarchyLevelsShouldBeOrdered() { assertThat(MailboxPath.forUser(USER, "inbox.folder.subfolder") .getHierarchyLevels('.')) 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/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala index 2a0fe99..0c67ce4 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala @@ -5422,4 +5422,629 @@ trait MailboxSetMethodContract { .getEntries) .doesNotContainKeys(EntryKey.createUserEntryKey(DAVID)) } + + @Test + def updateShouldAllowParentIdChangeWhenTopLevelMailbox(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": "${parentId.serialize()}" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "mailbox", + | "parentId": "${parentId.serialize()}", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowParentIdChangeWhenChildMailbox(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val newParentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "newParent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent.mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": "${newParentId.serialize()}" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "mailbox", + | "parentId": "${newParentId.serialize()}", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowParentIdChangeWhenTopLevelMailboxAndNewMane(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": "${parentId.serialize()}", + | "/name": "newName" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "newName", + | "parentId": "${parentId.serialize()}", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowParentIdChangeWhenChildMailboxAndNewName(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val newParentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "newParent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent.mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": "${newParentId.serialize()}", + | "/name": "newName" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "newName", + | "parentId": "${newParentId.serialize()}", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowParentIdDropWhenChildMailbox(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val newParentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "newParent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent.mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": null + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "mailbox", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowParentIdDropWhenTopLevelMailboxAndNewName(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": null, + | "/name": "newName" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "newName", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowParentIdDropWhenChildMailboxAndNewName(server: GuiceJamesServer): Unit = { + val parentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent")) + val newParentId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "newParent")) + val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]) + .createMailbox(MailboxPath.forUser(BOB, "parent.mailbox")) + val request = + s""" + |{ + | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + | "methodCalls": [ + | [ + | "Mailbox/set", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "update": { + | "${mailboxId.serialize()}": { + | "/parentId": null, + | "/name": "newName" + | } + | } + | }, + | "c1" + | ], + | ["Mailbox/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "ids": ["${mailboxId.serialize()}"] + | }, + | "c2"] + | ] + |} + |""".stripMargin + + val response = `given` + .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .body(request) + .when + .post + .`then` + .log().ifValidationFails() + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response).isEqualTo( + s"""{ + | "sessionState": "75128aab4b1b", + | "methodResponses": [ + | ["Mailbox/set", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "newState": "000001", + | "updated": { + | "${mailboxId.serialize()}": {} + | } + | }, "c1"], + | ["Mailbox/get", { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "state": "000001", + | "list": [{ + | "id": "${mailboxId.serialize()}", + | "name": "newName", + | "sortOrder": 1000, + | "totalEmails": 0, + | "unreadEmails": 0, + | "totalThreads": 0, + | "unreadThreads": 0, + | "myRights": { + | "mayReadItems": true, + | "mayAddItems": true, + | "mayRemoveItems": true, + | "maySetSeen": true, + | "maySetKeywords": true, + | "mayCreateChild": true, + | "mayRename": true, + | "mayDelete": true, + | "maySubmit": true + | }, + | "isSubscribed": false + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } } diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala index f3f8db5..6c9ff08 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala @@ -34,6 +34,7 @@ import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, Unparsed import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier import org.apache.james.jmap.model.State.State import org.apache.james.jmap.model.{AccountId, CapabilityIdentifier} +import org.apache.james.jmap.routes.ProcessingContext import org.apache.james.mailbox.Role import org.apache.james.mailbox.model.{MailboxId, MailboxACL => JavaMailboxACL} import play.api.libs.json.{JsBoolean, JsError, JsNull, JsObject, JsString, JsSuccess, JsValue} @@ -86,8 +87,11 @@ object MailboxPatchObject { } case class MailboxPatchObject(value: Map[String, JsValue]) { - def validate(serializer: Serializer, capabilities: Set[CapabilityIdentifier]): Either[PatchUpdateValidationException, ValidatedMailboxPathObject] = { - val asUpdatedIterable = updates(serializer, capabilities) + def validate(processingContext: ProcessingContext, + mailboxIdFactory: MailboxId.Factory, + serializer: Serializer, + capabilities: Set[CapabilityIdentifier]): Either[PatchUpdateValidationException, ValidatedMailboxPathObject] = { + val asUpdatedIterable = updates(serializer, capabilities, processingContext, mailboxIdFactory) val maybeParseException: Option[PatchUpdateValidationException] = asUpdatedIterable .flatMap(x => x match { @@ -113,6 +117,12 @@ case class MailboxPatchObject(value: Map[String, JsValue]) { case _ => None }).headOption + val parentIdUpdate: Option[ParentIdUpdate] = asUpdatedIterable + .flatMap(x => x match { + case scala.Right(ParentIdUpdate(newId)) => Some(ParentIdUpdate(newId)) + case _ => None + }).headOption + val partialRightsUpdates: Seq[SharedWithPartialUpdate] = asUpdatedIterable.flatMap(x => x match { case scala.Right(SharedWithPartialUpdate(username, rights)) => Some(SharedWithPartialUpdate(username, rights)) case _ => None @@ -128,14 +138,16 @@ case class MailboxPatchObject(value: Map[String, JsValue]) { .map(e => Left(e)) .getOrElse(scala.Right(ValidatedMailboxPathObject( nameUpdate = nameUpdate, + parentIdUpdate = parentIdUpdate, isSubscribedUpdate = isSubscribedUpdate, rightsReset = rightsReset, rightsPartialUpdates = partialRightsUpdates))) } - private def updates(serializer: Serializer, capabilities: Set[CapabilityIdentifier]): Iterable[Either[PatchUpdateValidationException, Update]] = value.map({ + def updates(serializer: Serializer, capabilities: Set[CapabilityIdentifier], processingContext: ProcessingContext, mailboxIdFactory: MailboxId.Factory): Iterable[Either[PatchUpdateValidationException, Update]] = value.map({ case (property, newValue) => property match { case "/name" => NameUpdate.parse(newValue) + case "/parentId" => ParentIdUpdate.parse(newValue, processingContext, mailboxIdFactory) case "/sharedWith" => SharedWithResetUpdate.parse(serializer, capabilities)(newValue) case "/role" => Left(ServerSetPropertyException(MailboxPatchObject.roleProperty)) case "/sortOrder" => Left(ServerSetPropertyException(MailboxPatchObject.sortOrderProperty)) @@ -161,9 +173,12 @@ case class MailboxPatchObject(value: Map[String, JsValue]) { } case class ValidatedMailboxPathObject(nameUpdate: Option[NameUpdate], + parentIdUpdate: Option[ParentIdUpdate], isSubscribedUpdate: Option[IsSubscribedUpdate], rightsReset: Option[SharedWithResetUpdate], - rightsPartialUpdates: Seq[SharedWithPartialUpdate]) + rightsPartialUpdates: Seq[SharedWithPartialUpdate]) { + val shouldUpdateMailboxPath: Boolean = nameUpdate.isDefined || parentIdUpdate.isDefined +} case class MailboxSetResponse(accountId: AccountId, oldState: Option[State], @@ -284,6 +299,20 @@ object SharedWithPartialUpdate { } } +object ParentIdUpdate { + def parse(newValue: JsValue, processingContext: ProcessingContext, mailboxIdFactory: MailboxId.Factory): Either[PatchUpdateValidationException, Update] = + newValue match { + case JsString(id) => + val value: Either[String, UnparsedMailboxId] = refineV(id) + value.fold(error => Left(InvalidUpdateException("/parentId", error)), + id => processingContext.resolveMailboxId(id, mailboxIdFactory) + .fold(e => Left(InvalidUpdateException("/parentId", e.getMessage)), + mailboxId => scala.Right(ParentIdUpdate(Some(mailboxId))))) + case JsNull => scala.Right(ParentIdUpdate(None)) + case _ => Left(InvalidUpdateException("/parentId", "Expecting a JSON string or null as an argument")) + } +} + sealed trait Update case class NameUpdate(newName: String) extends Update case class SharedWithResetUpdate(rights: Rights) extends Update @@ -291,6 +320,7 @@ case class IsSubscribedUpdate(isSubscribed: Option[IsSubscribed]) extends Update case class SharedWithPartialUpdate(username: Username, rights: Rfc4314Rights) extends Update { def asACLCommand(): JavaMailboxACL.ACLCommand = JavaMailboxACL.command().forUser(username).rights(rights.asJava).asReplacement() } +case class ParentIdUpdate(newId: Option[MailboxId]) extends Update class PatchUpdateValidationException() extends IllegalArgumentException case class UnsupportedPropertyUpdatedException(property: MailboxPatchObjectKey) extends PatchUpdateValidationException diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala index 3613c27..b223b03 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala @@ -23,7 +23,7 @@ import eu.timepit.refined.auto._ import javax.inject.Inject import org.apache.james.jmap.json.Serializer import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId} -import org.apache.james.jmap.mail.{InvalidPatchException, InvalidPropertyException, InvalidUpdateException, IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, Properties, RemoveEmailsOnDestroy, ServerSetPropertyException, SetErrorDescription, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedException, Validat [...] +import org.apache.james.jmap.mail.{InvalidPatchException, InvalidPropertyException, InvalidUpdateException, IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, ParentIdUpdate, Properties, RemoveEmailsOnDestroy, ServerSetPropertyException, SetErrorDescription, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedEx [...] import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier import org.apache.james.jmap.model.Invocation.{Arguments, MethodName} import org.apache.james.jmap.model.{ClientId, Id, Invocation, ServerId, State} @@ -162,7 +162,7 @@ class MailboxSetMethod @Inject()(serializer: Serializer, case (unparsedMailboxId: UnparsedMailboxId, patch: MailboxPatchObject) => processingContext.resolveMailboxId(unparsedMailboxId, mailboxIdFactory).fold( e => SMono.just(UpdateFailure(unparsedMailboxId, e)), - mailboxId => updateMailbox(mailboxSession, mailboxId, patch, capabilities)) + mailboxId => updateMailbox(mailboxSession, processingContext, mailboxId, patch, capabilities)) .onErrorResume(e => SMono.just(UpdateFailure(unparsedMailboxId, e))) }) .collectSeq() @@ -170,10 +170,11 @@ class MailboxSetMethod @Inject()(serializer: Serializer, } private def updateMailbox(mailboxSession: MailboxSession, + processingContext: ProcessingContext, mailboxId: MailboxId, patch: MailboxPatchObject, capabilities: Set[CapabilityIdentifier]): SMono[UpdateResult] = { - patch.validate(serializer, capabilities) + patch.validate(processingContext, mailboxIdFactory, serializer, capabilities) .fold(e => SMono.raiseError(e), validatedPatch => updateMailboxPath(mailboxId, validatedPatch, mailboxSession) .`then`(updateMailboxRights(mailboxId, validatedPatch, mailboxSession)) @@ -199,21 +200,53 @@ class MailboxSetMethod @Inject()(serializer: Serializer, } private def updateMailboxPath(mailboxId: MailboxId, validatedPatch: ValidatedMailboxPathObject, mailboxSession: MailboxSession): SMono[UpdateResult] = { - validatedPatch.nameUpdate.map(nameUpdate => { + if (validatedPatch.shouldUpdateMailboxPath) { SMono.fromCallable(() => { val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession) if (isASystemMailbox(mailbox)) { throw SystemMailboxChangeException(mailboxId) } + val newPath = applyNameUpdate(validatedPatch.nameUpdate, mailboxSession) + .andThen(applyParentIdUpdate(validatedPatch.parentIdUpdate, mailboxSession)) + .apply(mailbox.getMailboxPath) mailboxManager.renameMailbox(mailboxId, - computeMailboxPath(mailbox, nameUpdate, mailboxSession), + newPath, RenameOption.RENAME_SUBSCRIPTIONS, mailboxSession) }).`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId))) .subscribeOn(Schedulers.elastic()) - }) - // No updated properties passed. Noop. - .getOrElse(SMono.just[UpdateResult](UpdateSuccess(mailboxId))) + } else { + SMono.just[UpdateResult](UpdateSuccess(mailboxId)) + } + } + + private def applyParentIdUpdate(maybeParentIdUpdate: Option[ParentIdUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = { + maybeParentIdUpdate.map(parentIdUpdate => applyParentIdUpdate(parentIdUpdate, mailboxSession)) + .getOrElse(x => x) + } + + private def applyNameUpdate(maybeNameUpdate: Option[NameUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = { + originalPath => maybeNameUpdate.map(nameUpdate => { + val originalParentPath: Option[MailboxPath] = originalPath.getHierarchyLevels(mailboxSession.getPathDelimiter) + .asScala + .reverse + .drop(1) + .headOption + originalParentPath.map(_.child(nameUpdate.newName, mailboxSession.getPathDelimiter)) + .getOrElse(MailboxPath.forUser(mailboxSession.getUser, nameUpdate.newName)) + }).getOrElse(originalPath) + } + + private def applyParentIdUpdate(parentIdUpdate: ParentIdUpdate, mailboxSession: MailboxSession): MailboxPath => MailboxPath = { + originalPath => { + val currentName = originalPath.getName(mailboxSession.getPathDelimiter) + parentIdUpdate.newId + .map(id => { + val parentPath = mailboxManager.getMailbox(id, mailboxSession).getMailboxPath + parentPath.child(currentName, mailboxSession.getPathDelimiter) + }) + .getOrElse(MailboxPath.forUser(originalPath.getUser, currentName)) + } } private def updateMailboxRights(mailboxId: MailboxId, @@ -239,16 +272,6 @@ class MailboxSetMethod @Inject()(serializer: Serializer, } - private def computeMailboxPath(mailbox: MessageManager, nameUpdate: NameUpdate, mailboxSession: MailboxSession): MailboxPath = { - val originalPath: MailboxPath = mailbox.getMailboxPath - val maybeParentPath: Option[MailboxPath] = originalPath.getHierarchyLevels(mailboxSession.getPathDelimiter) - .asScala - .reverse - .drop(1) - .headOption - maybeParentPath.map(_.child(nameUpdate.newName, mailboxSession.getPathDelimiter)) - .getOrElse(MailboxPath.forUser(mailboxSession.getUser, nameUpdate.newName)) - } private def deleteMailboxes(mailboxSession: MailboxSession, mailboxSetRequest: MailboxSetRequest, processingContext: ProcessingContext): SMono[DeletionResults] = { SFlux.fromIterable(mailboxSetRequest.destroy.getOrElse(Seq())) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
