You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/08/27 02:24:32 UTC

[james-project] branch master updated (7098d70 -> 5a899ed)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 7098d70  JAMES-3359 Reject Mailbox/set updates when capability is omitted
     new ee6c8bb  JAMES-3359 Mailbox/set parentId updates
     new e099ddb  JAMES-3359 Mailbox/set update should support name/parentId NOOP
     new 1cdd21d  JAMES-3359 Mailbox/set update should handle parentId formatting errors
     new 76068ab  JAMES-3359 Mailbox/set update should handle existing/not found mailboxes
     new 6c75d52  JAMES-3359 Mailbox/set updates parentId: test delegation behaviour
     new 54aa761  JAMES-3359 Lookup right should be propagated to new parent if needed
     new ff10686  JAMES-3359 Mailbox/set name updates should not contain delimiter
     new 746efff  JAMES-3359 Mailbox/set parentId updates: prevent loops
     new 9728174  JAMES-3359 Fix typos in MailboxSetMethodContract
     new c1d704b  JAMES-3359 Review MailboxSetMethodContract indent
     new 54d4e4a  JAMES-3359 Simplify tests by applying Mailbox/get property filtering
     new b3e78a2  JAMES-3359 Fix some issues in MailboxSetMethodContract for the Distributed version
     new 6e58b87  JAMES-3357 Mailbox/set create should reject unknown or server-set properties
     new 5a899ed  JAMES-3095 Avoid listing all subscriptions for each mailbox

The 14 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:
 .../apache/james/mailbox/model/MailboxPath.java    |   10 +
 .../james/mailbox/model/MailboxPathTest.java       |   18 +
 .../contract/MailboxSetMethodContract.scala        | 1812 ++++++++++++++++++--
 .../org/apache/james/jmap/mail/MailboxGet.scala    |    2 +
 .../org/apache/james/jmap/mail/MailboxSet.scala    |  101 +-
 .../james/jmap/method/MailboxSetMethod.scala       |  128 +-
 .../apache/james/jmap/model/MailboxFactory.scala   |    9 +-
 7 files changed, 1848 insertions(+), 232 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 11/14: JAMES-3359 Simplify tests by applying Mailbox/get property filtering

Posted by bt...@apache.org.
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 54d4e4a7c67c32242713ce20ded32f9429f164e7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Aug 25 10:12:47 2020 +0700

    JAMES-3359 Simplify tests by applying Mailbox/get property filtering
---
 .../contract/MailboxSetMethodContract.scala        | 262 +++------------------
 1 file changed, 33 insertions(+), 229 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/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 28f447e..79f9d7f 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
@@ -5098,6 +5098,7 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |           "properties": ["id", "rights"],
          |           "ids": ["${mailboxId.serialize()}"]
          |          },
          |       "c2"]
@@ -5134,25 +5135,6 @@ trait MailboxSetMethodContract {
          |      "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,
-         |        "namespace": "Personal",
          |        "rights": {
          |          "${ANDRE.asString()}": ["l", "r"],
          |          "${DAVID.asString()}": ["l"]
@@ -5502,7 +5484,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -5539,24 +5522,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "parentId": "${parentId.serialize()}"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -5590,7 +5556,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -5627,24 +5594,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "parentId": "${newParentId.serialize()}"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -5678,7 +5628,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -5715,24 +5666,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "parentId": "${parentId.serialize()}"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -5767,7 +5701,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -5804,24 +5739,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "parentId": "${newParentId.serialize()}"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -5855,7 +5773,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -5891,24 +5810,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "name": "mailbox"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -5942,7 +5844,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -5978,24 +5881,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "name": "newName"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -6030,7 +5916,8 @@ trait MailboxSetMethodContract {
          |       ["Mailbox/get",
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |           "ids": ["${mailboxId.serialize()}"]
+         |           "ids": ["${mailboxId.serialize()}"],
+         |           "properties": ["id", "name", "parentId"]
          |          },
          |       "c2"]
          |   ]
@@ -6066,24 +5953,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "name": "newName"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -6115,7 +5985,8 @@ trait MailboxSetMethodContract {
         |       ["Mailbox/get",
         |         {
         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-        |           "ids": ["${mailboxId.serialize()}"]
+        |           "ids": ["${mailboxId.serialize()}"],
+        |           "properties": ["id", "name", "parentId"]
         |          },
         |       "c2"]
         |   ]
@@ -6151,24 +6022,7 @@ trait MailboxSetMethodContract {
          |      "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
+         |        "name": "mailbox"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -6202,7 +6056,8 @@ trait MailboxSetMethodContract {
         |       ["Mailbox/get",
         |         {
         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-        |           "ids": ["${mailboxId.serialize()}"]
+        |           "ids": ["${mailboxId.serialize()}"],
+        |           "properties": ["id", "name", "parentId"]
         |          },
         |       "c2"]
         |   ]
@@ -6239,24 +6094,7 @@ trait MailboxSetMethodContract {
          |      "list": [{
          |        "id": "${mailboxId.serialize()}",
          |        "parentId": "${parentId.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
+         |        "name": "mailbox"
          |      }],
          |      "notFound": []
          |    }, "c2"]
@@ -6299,7 +6137,8 @@ trait MailboxSetMethodContract {
         |       ["Mailbox/get",
         |         {
         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-        |           "ids": ["${mailboxId.serialize()}"]
+        |           "ids": ["${mailboxId.serialize()}"],
+        |           "properties": ["id", "name", "parentId"]
         |          },
         |          "c3"
         |       ]
@@ -6366,26 +6205,8 @@ trait MailboxSetMethodContract {
          |      "state": "000001",
          |      "list": [{
          |        "id": "${mailboxId.serialize()}",
-         |        "sortOrder":1000,
          |        "name": "mailbox",
-         |        "parentId": "$parentId",
-         |        "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
+         |        "parentId": "$parentId"
          |      }],
          |      "notFound": []
          |    }, "c3"]
@@ -6895,7 +6716,8 @@ trait MailboxSetMethodContract {
         |       ["Mailbox/get",
         |         {
         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-        |           "ids": ["${parentId.serialize()}"]
+        |           "ids": ["${parentId.serialize()}"],
+        |           "properties": ["id", "name", "parentId", "rights"]
         |          },
         |          "c2"]
         |   ]
@@ -6932,24 +6754,6 @@ trait MailboxSetMethodContract {
          |      "list": [{
          |        "id": "${parentId.serialize()}",
          |        "name": "parent",
-         |        "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,
-         |        "namespace": "Personal",
          |        "rights": {
          |          "${ANDRE.asString()}": ["l"]
          |        }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 01/14: JAMES-3359 Mailbox/set parentId updates

Posted by bt...@apache.org.
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 <bt...@linagora.com>
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: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 06/14: JAMES-3359 Lookup right should be propagated to new parent if needed

Posted by bt...@apache.org.
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 54aa76130e8d71111382b7bc8ed24cab77ae92f8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 14:32:16 2020 +0700

    JAMES-3359 Lookup right should be propagated to new parent if needed
---
 .../contract/MailboxSetMethodContract.scala        | 95 ++++++++++++++++++++++
 1 file changed, 95 insertions(+)

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 e388765..3d2a337 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
@@ -6821,4 +6821,99 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def lookupRightShouldBePropagated(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val path = MailboxPath.forUser(BOB, "mailbox")
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(path)
+    val parentPath = MailboxPath.forUser(BOB, "parent")
+    val parentId = mailboxProbe.createMailbox(parentPath)
+
+    val aCLProbeImpl = server.getProbe(classOf[ACLProbeImpl])
+    aCLProbeImpl.replaceRights(path, ANDRE.asString, MailboxACL.FULL_RIGHTS)
+
+    val request = s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares" ],
+        |   "methodCalls": [
+        |       [
+        |           "Mailbox/set",
+        |           {
+        |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "update": {
+        |                    "${mailboxId.serialize()}": {
+        |                      "/parentId": "${parentId.serialize()}"
+        |                    }
+        |                }
+        |           },
+        |           "c1"
+        |       ],
+        |       ["Mailbox/get",
+        |         {
+        |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |           "ids": ["${parentId.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": "${parentId.serialize()}",
+         |        "name": "parent",
+         |        "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,
+         |        "namespace": "Personal",
+         |        "rights": {
+         |          "${ANDRE.asString()}": ["l"]
+         |        }
+         |      }],
+         |      "notFound": []
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 02/14: JAMES-3359 Mailbox/set update should support name/parentId NOOP

Posted by bt...@apache.org.
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 e099ddb8e3fa43ed2f01ad5450dcd5043fd7491c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 11:55:34 2020 +0700

    JAMES-3359 Mailbox/set update should support name/parentId NOOP
---
 .../contract/MailboxSetMethodContract.scala        | 173 +++++++++++++++++++++
 .../james/jmap/method/MailboxSetMethod.scala       |  13 +-
 2 files changed, 181 insertions(+), 5 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/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 0c67ce4..871345f 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
@@ -6047,4 +6047,177 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def updateShouldAllowNameNoop(server: GuiceJamesServer): Unit = {
+    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()}": {
+        |                      "/name": "mailbox"
+        |                    }
+        |                }
+        |           },
+        |    "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 updateShouldAllowParentIdNoop(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, "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": "${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()}",
+         |        "parentId": "${parentId.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)
+  }
 }
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 b223b03..3e85d105 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
@@ -206,13 +206,16 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
         if (isASystemMailbox(mailbox)) {
           throw SystemMailboxChangeException(mailboxId)
         }
+        val oldPath = mailbox.getMailboxPath
         val newPath = applyNameUpdate(validatedPatch.nameUpdate, mailboxSession)
           .andThen(applyParentIdUpdate(validatedPatch.parentIdUpdate, mailboxSession))
-          .apply(mailbox.getMailboxPath)
-        mailboxManager.renameMailbox(mailboxId,
-          newPath,
-          RenameOption.RENAME_SUBSCRIPTIONS,
-          mailboxSession)
+          .apply(oldPath)
+        if (!oldPath.equals(newPath)) {
+          mailboxManager.renameMailbox(mailboxId,
+            newPath,
+            RenameOption.RENAME_SUBSCRIPTIONS,
+            mailboxSession)
+        }
       }).`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
         .subscribeOn(Schedulers.elastic())
     } else {


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 04/14: JAMES-3359 Mailbox/set update should handle existing/not found mailboxes

Posted by bt...@apache.org.
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 76068ab22c014b1ca5dd6a8ea6d65da324c4d499
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 13:55:17 2020 +0700

    JAMES-3359 Mailbox/set update should handle existing/not found mailboxes
---
 .../contract/MailboxSetMethodContract.scala        | 175 +++++++++++++++++++++
 .../org/apache/james/jmap/mail/MailboxGet.scala    |   2 +
 .../org/apache/james/jmap/mail/MailboxSet.scala    |  25 ++-
 .../james/jmap/method/MailboxSetMethod.scala       |  71 +++++----
 4 files changed, 237 insertions(+), 36 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/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 07a7ed9..25760d5 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
@@ -6461,4 +6461,179 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def updateShouldFailWhenParentIdNotFound(server: GuiceJamesServer): Unit = {
+    val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+      .createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+    val parentId = randomMailboxId
+    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()}"
+        |                    }
+        |                }
+        |           },
+        |           "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",
+         |      "notUpdated": {
+         |        "${mailboxId.serialize()}": {
+         |          "type": "notFound",
+         |          "description": "${parentId.serialize()} can not be found"
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenTargetMailboxAlreadyExists(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+    val parentId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    mailboxProbe.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": "${parentId.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",
+         |      "notUpdated": {
+         |        "${mailboxId.serialize()}": {
+         |          "type": "invalidArguments",
+         |          "description": "Mailbox with name=#private:bob@domain.tld:parent.mailbox already exists.",
+         |          "properties":["/parentId"]
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenTargetMailboxAlreadyExistsAndBothPropertiesSpecified(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+    val parentId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent.newMailbox"))
+    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": "newMailbox"
+        |                    }
+        |                }
+        |           },
+        |           "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
+
+   println(response)
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |  "sessionState": "75128aab4b1b",
+         |  "methodResponses": [
+         |    ["Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "newState": "000001",
+         |      "notUpdated": {
+         |        "${mailboxId.serialize()}": {
+         |          "type": "invalidArguments",
+         |          "description": "Mailbox with name=#private:bob@domain.tld:parent.newMailbox already exists.",
+         |          "properties":["/name", "/parentId"]
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
index 1169652..1fde4e0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
@@ -28,6 +28,8 @@ case class Ids(value: List[UnparsedMailboxId])
 
 case class Properties(value: List[NonEmptyString]) {
   def asSetOfString: Set[String] = value.map(_.toString()).toSet
+
+  def intersect(properties: Properties): Properties = Properties(value.toSet.intersect(properties.value.toSet).toList)
 }
 
 
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 6c9ff08..3bc5fac 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
@@ -26,6 +26,7 @@ import eu.timepit.refined.boolean.And
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import eu.timepit.refined.string.StartsWith
+import eu.timepit.refined.types.string.NonEmptyString
 import org.apache.james.core.Username
 import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.mail.MailboxName.MailboxName
@@ -90,7 +91,7 @@ case class MailboxPatchObject(value: Map[String, JsValue]) {
   def validate(processingContext: ProcessingContext,
                mailboxIdFactory: MailboxId.Factory,
                serializer: Serializer,
-               capabilities: Set[CapabilityIdentifier]): Either[PatchUpdateValidationException, ValidatedMailboxPathObject] = {
+               capabilities: Set[CapabilityIdentifier]): Either[PatchUpdateValidationException, ValidatedMailboxPatchObject] = {
     val asUpdatedIterable = updates(serializer, capabilities, processingContext, mailboxIdFactory)
 
     val maybeParseException: Option[PatchUpdateValidationException] = asUpdatedIterable
@@ -136,7 +137,7 @@ case class MailboxPatchObject(value: Map[String, JsValue]) {
     maybeParseException
       .orElse(bothPartialAndResetRights)
       .map(e => Left(e))
-      .getOrElse(scala.Right(ValidatedMailboxPathObject(
+      .getOrElse(scala.Right(ValidatedMailboxPatchObject(
         nameUpdate = nameUpdate,
         parentIdUpdate = parentIdUpdate,
         isSubscribedUpdate = isSubscribedUpdate,
@@ -172,12 +173,22 @@ 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]) {
+object ValidatedMailboxPatchObject {
+  val nameProperty: NonEmptyString = "/name"
+  val parentIdProperty: NonEmptyString = "/parentId"
+}
+
+case class ValidatedMailboxPatchObject(nameUpdate: Option[NameUpdate],
+                                       parentIdUpdate: Option[ParentIdUpdate],
+                                       isSubscribedUpdate: Option[IsSubscribedUpdate],
+                                       rightsReset: Option[SharedWithResetUpdate],
+                                       rightsPartialUpdates: Seq[SharedWithPartialUpdate]) {
   val shouldUpdateMailboxPath: Boolean = nameUpdate.isDefined || parentIdUpdate.isDefined
+
+  val updatedProperties: Properties = Properties(List(
+      nameUpdate.map(_ => ValidatedMailboxPatchObject.nameProperty),
+      parentIdUpdate.map(_ => ValidatedMailboxPatchObject.parentIdProperty))
+    .flatMap(_.toList))
 }
 
 case class MailboxSetResponse(accountId: AccountId,
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 3e85d105..2a22946 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, ParentIdUpdate, Properties, RemoveEmailsOnDestroy, ServerSetPropertyException, SetErrorDescription, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedEx [...]
+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}
@@ -106,17 +106,21 @@ case class DeletionResults(results: Seq[DeletionResult]) {
 
 sealed trait UpdateResult
 case class UpdateSuccess(mailboxId: MailboxId) extends UpdateResult
-case class UpdateFailure(mailboxId: UnparsedMailboxId, exception: Throwable) extends UpdateResult {
+case class UpdateFailure(mailboxId: UnparsedMailboxId, exception: Throwable, patch: Option[ValidatedMailboxPatchObject]) extends UpdateResult {
+  def filter(acceptableProperties: Properties): Option[Properties] = Some(patch
+    .map(_.updatedProperties.intersect(acceptableProperties))
+    .getOrElse(acceptableProperties))
+
   def asMailboxSetError: MailboxSetError = exception match {
     case e: MailboxNotFoundException => MailboxSetError.notFound(Some(SetErrorDescription(e.getMessage)))
-    case e: MailboxNameException => MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), Some(Properties(List("/name"))))
-    case e: MailboxExistsException => MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), Some(Properties(List("/name"))))
+    case e: MailboxNameException => MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), filter(Properties(List("/name", "/parentId"))))
+    case e: MailboxExistsException => MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), filter(Properties(List("/name", "/parentId"))))
     case e: UnsupportedPropertyUpdatedException => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${e.property} property do not exist thus cannot be updated")), Some(Properties(List(e.property))))
     case e: InvalidUpdateException => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${e.cause}")), Some(Properties(List(e.property))))
     case e: ServerSetPropertyException => MailboxSetError.invalidArgument(Some(SetErrorDescription("Can not modify server-set properties")), Some(Properties(List(e.property))))
     case e: InvalidPropertyException => MailboxSetError.invalidPatch(Some(SetErrorDescription(s"${e.cause}")))
     case e: InvalidPatchException => MailboxSetError.invalidPatch(Some(SetErrorDescription(s"${e.cause}")))
-    case e: SystemMailboxChangeException => MailboxSetError.invalidArgument(Some(SetErrorDescription("Invalid change to a system mailbox")), Some(Properties(List("/name"))))
+    case e: SystemMailboxChangeException => MailboxSetError.invalidArgument(Some(SetErrorDescription("Invalid change to a system mailbox")), filter(Properties(List("/name", "parentId"))))
     case e: InsufficientRightsException => MailboxSetError.invalidArgument(Some(SetErrorDescription("Invalid change to a delegated mailbox")), None)
     case _ => MailboxSetError.serverFail(Some(SetErrorDescription(exception.getMessage)), None)
   }
@@ -161,9 +165,9 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
       .flatMap({
         case (unparsedMailboxId: UnparsedMailboxId, patch: MailboxPatchObject) =>
           processingContext.resolveMailboxId(unparsedMailboxId, mailboxIdFactory).fold(
-            e => SMono.just(UpdateFailure(unparsedMailboxId, e)),
-            mailboxId => updateMailbox(mailboxSession, processingContext, mailboxId, patch, capabilities))
-            .onErrorResume(e => SMono.just(UpdateFailure(unparsedMailboxId, e)))
+              e => SMono.just(UpdateFailure(unparsedMailboxId, e, None)),
+              mailboxId => updateMailbox(mailboxSession, processingContext, mailboxId, unparsedMailboxId, patch, capabilities))
+            .onErrorResume(e => SMono.just(UpdateFailure(unparsedMailboxId, e, None)))
       })
       .collectSeq()
       .map(UpdateResults)
@@ -172,16 +176,17 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
   private def updateMailbox(mailboxSession: MailboxSession,
                             processingContext: ProcessingContext,
                             mailboxId: MailboxId,
+                            unparsedMailboxId: UnparsedMailboxId,
                             patch: MailboxPatchObject,
                             capabilities: Set[CapabilityIdentifier]): SMono[UpdateResult] = {
     patch.validate(processingContext, mailboxIdFactory, serializer, capabilities)
       .fold(e => SMono.raiseError(e), validatedPatch =>
-        updateMailboxPath(mailboxId, validatedPatch, mailboxSession)
-          .`then`(updateMailboxRights(mailboxId, validatedPatch, mailboxSession))
-          .`then`(updateSubscription(mailboxId, validatedPatch, mailboxSession)))
+        updateMailboxRights(mailboxId, validatedPatch, mailboxSession)
+          .`then`(updateSubscription(mailboxId, validatedPatch, mailboxSession))
+          .`then`(updateMailboxPath(mailboxId, unparsedMailboxId, validatedPatch, mailboxSession)))
   }
 
-  private def updateSubscription(mailboxId: MailboxId, validatedPatch: ValidatedMailboxPathObject, mailboxSession: MailboxSession): SMono[UpdateResult] = {
+  private def updateSubscription(mailboxId: MailboxId, validatedPatch: ValidatedMailboxPatchObject, mailboxSession: MailboxSession): SMono[UpdateResult] = {
     validatedPatch.isSubscribedUpdate.map(isSubscribedUpdate => {
       SMono.fromCallable(() => {
         val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
@@ -199,24 +204,32 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
       .getOrElse(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
   }
 
-  private def updateMailboxPath(mailboxId: MailboxId, validatedPatch: ValidatedMailboxPathObject, mailboxSession: MailboxSession): SMono[UpdateResult] = {
+  private def updateMailboxPath(mailboxId: MailboxId,
+                                unparsedMailboxId: UnparsedMailboxId,
+                                validatedPatch: ValidatedMailboxPatchObject,
+                                mailboxSession: MailboxSession): SMono[UpdateResult] = {
     if (validatedPatch.shouldUpdateMailboxPath) {
-      SMono.fromCallable(() => {
-        val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
-        if (isASystemMailbox(mailbox)) {
-          throw SystemMailboxChangeException(mailboxId)
+      SMono.fromCallable[UpdateResult](() => {
+        try {
+          val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
+          if (isASystemMailbox(mailbox)) {
+            throw SystemMailboxChangeException(mailboxId)
+          }
+          val oldPath = mailbox.getMailboxPath
+          val newPath = applyNameUpdate(validatedPatch.nameUpdate, mailboxSession)
+            .andThen(applyParentIdUpdate(validatedPatch.parentIdUpdate, mailboxSession))
+            .apply(oldPath)
+          if (!oldPath.equals(newPath)) {
+            mailboxManager.renameMailbox(mailboxId,
+              newPath,
+              RenameOption.RENAME_SUBSCRIPTIONS,
+              mailboxSession)
+          }
+          UpdateSuccess(mailboxId)
+        } catch {
+          case e: Exception => UpdateFailure(unparsedMailboxId, e, Some(validatedPatch))
         }
-        val oldPath = mailbox.getMailboxPath
-        val newPath = applyNameUpdate(validatedPatch.nameUpdate, mailboxSession)
-          .andThen(applyParentIdUpdate(validatedPatch.parentIdUpdate, mailboxSession))
-          .apply(oldPath)
-        if (!oldPath.equals(newPath)) {
-          mailboxManager.renameMailbox(mailboxId,
-            newPath,
-            RenameOption.RENAME_SUBSCRIPTIONS,
-            mailboxSession)
-        }
-      }).`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
+      })
         .subscribeOn(Schedulers.elastic())
     } else {
       SMono.just[UpdateResult](UpdateSuccess(mailboxId))
@@ -253,7 +266,7 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
   }
 
   private def updateMailboxRights(mailboxId: MailboxId,
-                                  validatedPatch: ValidatedMailboxPathObject,
+                                  validatedPatch: ValidatedMailboxPatchObject,
                                   mailboxSession: MailboxSession): SMono[UpdateResult] = {
 
     val resetOperation: SMono[Unit] = validatedPatch.rightsReset.map(sharedWithResetUpdate => {


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 12/14: JAMES-3359 Fix some issues in MailboxSetMethodContract for the Distributed version

Posted by bt...@apache.org.
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 b3e78a28fa23a4780f6a9745e046850c1f5d10aa
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed Aug 26 11:25:04 2020 +0700

    JAMES-3359 Fix some issues in MailboxSetMethodContract for the Distributed version
---
 .../jmap/rfc8621/contract/MailboxSetMethodContract.scala   | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 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/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 79f9d7f..50434e1 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
@@ -4577,7 +4577,7 @@ trait MailboxSetMethodContract {
          |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |                "newState": "000001",
          |                "notUpdated": {
-         |                    "1": {
+         |                    "${mailboxId.serialize}": {
          |                        "type": "invalidArguments",
          |                        "description": "/sharedWith property do not exist thus cannot be updated",
          |                        "properties": ["/sharedWith"]
@@ -4637,7 +4637,7 @@ trait MailboxSetMethodContract {
          |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |                "newState": "000001",
          |                "notUpdated": {
-         |                    "1": {
+         |                    "${mailboxId.serialize}": {
          |                        "type": "invalidArguments",
          |                        "description": "/sharedWith/${ANDRE.asString()} property do not exist thus cannot be updated",
          |                        "properties": ["/sharedWith/${ANDRE.asString()}"]
@@ -4697,7 +4697,7 @@ trait MailboxSetMethodContract {
          |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |                "newState": "000001",
          |                "notUpdated": {
-         |                    "1": {
+         |                    "${mailboxId.serialize}": {
          |                        "type": "invalidArguments",
          |                        "description": "/quotas property do not exist thus cannot be updated",
          |                        "properties": ["/quotas"]
@@ -6259,7 +6259,7 @@ trait MailboxSetMethodContract {
          |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |      "newState": "000001",
          |      "notUpdated": {
-         |        "1": {
+         |        "${mailboxId.serialize}": {
          |          "type": "invalidArguments",
          |          "description": "Expecting a JSON string or null as an argument",
          |          "properties": ["/parentId"]
@@ -6315,7 +6315,7 @@ trait MailboxSetMethodContract {
          |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |      "newState": "000001",
          |      "notUpdated": {
-         |        "1": {
+         |        "${mailboxId.serialize}": {
          |          "type": "invalidArguments",
          |          "description": "ClientId(#C42) was not used in previously defined creationIds",
          |          "properties": ["/parentId"]
@@ -6812,9 +6812,9 @@ trait MailboxSetMethodContract {
          |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |      "newState": "000001",
          |      "notUpdated": {
-         |        "1": {
+         |        "${mailboxId.serialize}": {
          |          "type": "invalidArguments",
-         |          "description": "1 parentId property cannot be updated as this mailbox has child mailboxes",
+         |          "description": "${mailboxId.serialize} parentId property cannot be updated as this mailbox has child mailboxes",
          |          "properties": ["parentId"]
          |        }
          |      }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 13/14: JAMES-3357 Mailbox/set create should reject unknown or server-set properties

Posted by bt...@apache.org.
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 6e58b8789ce700e6db70988202e6f851c298160a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Aug 25 14:27:44 2020 +0700

    JAMES-3357 Mailbox/set create should reject unknown or server-set properties
---
 .../contract/MailboxSetMethodContract.scala        | 62 +++++++++++++++++++++-
 .../org/apache/james/jmap/mail/MailboxSet.scala    | 27 ++++++++++
 .../james/jmap/method/MailboxSetMethod.scala       |  9 ++--
 3 files changed, 92 insertions(+), 6 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/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 50434e1..8805072 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
@@ -657,7 +657,6 @@ trait MailboxSetMethodContract {
   }
 
   @Test
-  @Disabled("should we support that? Anyway seems hard with Play-JSON")
   def mailboxSetShouldReturnNotCreatedWhenUnknownParameter(): Unit = {
     val request =
       """
@@ -706,7 +705,66 @@ trait MailboxSetMethodContract {
          |      "notCreated": {
          |        "C42": {
          |          "type": "invalidArguments",
-         |          "description": "Unknown 'unknown' property in mailbox object"
+         |          "description": "Some unknown properties were specified",
+         |          "properties": ["unknown"]
+         |        }
+         |      }
+         |    },
+         |    "c1"]]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def mailboxSetShouldReturnNotCreatedWhenServerSetParameter(): Unit = {
+    val request =
+      """
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |       [
+        |           "Mailbox/set",
+        |           {
+        |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "create": {
+        |                    "C42": {
+        |                      "name": "plop",
+        |                      "id": "what?"
+        |                    }
+        |                }
+        |           },
+        |    "c1"
+        |       ]
+        |   ]
+        |}
+        |""".stripMargin
+
+    val response: String =
+      `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",
+         |      "notCreated": {
+         |        "C42": {
+         |          "type": "invalidArguments",
+         |          "description": "Some server-set properties were specified",
+         |          "properties": ["id"]
          |        }
          |      }
          |    },
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 73fc849..d1620aa 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
@@ -32,6 +32,7 @@ import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.mail.MailboxName.MailboxName
 import org.apache.james.jmap.mail.MailboxPatchObject.MailboxPatchObjectKey
 import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId}
+import org.apache.james.jmap.method.MailboxCreationParseException
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.State.State
 import org.apache.james.jmap.model.{AccountId, CapabilityIdentifier}
@@ -59,6 +60,32 @@ object MailboxSetRequest {
 }
 
 case class RemoveEmailsOnDestroy(value: Boolean) extends AnyVal
+
+object MailboxCreationRequest {
+  private val serverSetProperty = Set("id", "sortOrder", "role", "totalEmails", "totalThreads", "unreadEmails", "unreadThreads", "myRights")
+  private val assignableProperties = Set("name", "parentId", "isSubscribed", "rights")
+  private val knownProperties = assignableProperties ++ serverSetProperty
+
+  def validateProperties(jsObject: JsObject): Either[MailboxCreationParseException, JsObject] =
+    (jsObject.keys.intersect(serverSetProperty), jsObject.keys.diff(knownProperties)) match {
+      case (_, unknownProperties) if unknownProperties.nonEmpty =>
+        Left(MailboxCreationParseException(MailboxSetError.invalidArgument(
+          Some(SetErrorDescription("Some unknown properties were specified")),
+          Some(toProperties(unknownProperties.toList)))))
+      case (specifiedServerSetProperties, _) if specifiedServerSetProperties.nonEmpty =>
+        Left(MailboxCreationParseException(MailboxSetError.invalidArgument(
+          Some(SetErrorDescription("Some server-set properties were specified")),
+          Some(toProperties(specifiedServerSetProperties.toList)))))
+      case _ => scala.Right(jsObject)
+    }
+
+  private def toProperties(strings: List[String]): Properties = Properties(strings
+    .flatMap(string => {
+      val refinedValue: Either[String, NonEmptyString] = refineV[NonEmpty](string)
+      refinedValue.fold(_ => None,  Some(_))
+    }))
+}
+
 case class MailboxCreationRequest(name: MailboxName,
                                   parentId: Option[UnparsedMailboxId],
                                   isSubscribed: Option[IsSubscribed],
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 a412f8a..3f532d9 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
@@ -369,10 +369,11 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
   }
 
   private def parseCreate(jsObject: JsObject): Either[MailboxCreationParseException, MailboxCreationRequest] =
-    Json.fromJson(jsObject)(serializer.mailboxCreationRequest) match {
-      case JsSuccess(creationRequest, _) => Right(creationRequest)
-      case JsError(errors) => Left(MailboxCreationParseException(mailboxSetError(errors)))
-    }
+    MailboxCreationRequest.validateProperties(jsObject)
+      .flatMap(validJsObject => Json.fromJson(validJsObject)(serializer.mailboxCreationRequest) match {
+        case JsSuccess(creationRequest, _) => Right(creationRequest)
+        case JsError(errors) => Left(MailboxCreationParseException(mailboxSetError(errors)))
+      })
 
   private def mailboxSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): MailboxSetError =
     errors.head match {


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 07/14: JAMES-3359 Mailbox/set name updates should not contain delimiter

Posted by bt...@apache.org.
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 ff10686887debdd0936e9001a52215cd8883fced
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 14:45:23 2020 +0700

    JAMES-3359 Mailbox/set name updates should not contain delimiter
---
 .../contract/MailboxSetMethodContract.scala        | 53 ++++++++++++++++++++++
 .../org/apache/james/jmap/mail/MailboxSet.scala    | 23 +++++++---
 .../james/jmap/method/MailboxSetMethod.scala       |  2 +-
 3 files changed, 70 insertions(+), 8 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/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 3d2a337..765675d 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
@@ -4167,6 +4167,59 @@ trait MailboxSetMethodContract {
          |}""".stripMargin)
   }
 
+  @Test
+  def updateShouldFailWhenMailboxNameContainsDelimiter(server: GuiceJamesServer): Unit = {
+    val mailboxId1: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "previousName"))
+    val request =
+      s"""
+         |{
+         |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+         |   "methodCalls": [
+         |       ["Mailbox/set",
+         |           {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "update": {
+         |                    "${mailboxId1.serialize()}": {
+         |                      "/name": "a.b"
+         |                    }
+         |                }
+         |           },
+         |    "c1"]]
+         |}
+         |""".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",
+         |      "notUpdated": {
+         |        "${mailboxId1.serialize()}": {
+         |          "type": "invalidArguments",
+         |          "description": "The mailbox 'a.b' contains an illegal character: '.'",
+         |          "properties": ["/name"]
+         |        }
+         |      }
+         |    }, "c1"]
+         |  ]
+         |}""".stripMargin)
+  }
+
   @Disabled("JAMES-3359 The storage layer should rely on the mailbox path and not the name to allow handling of this case")
   @Test
   def updateShouldAllowDifferentIsSubscribedValuesWhenMailboxHaveTheSameName(server: GuiceJamesServer): Unit = {
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 3bc5fac..73fc849 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
@@ -36,8 +36,8 @@ 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 org.apache.james.mailbox.{MailboxSession, Role}
 import play.api.libs.json.{JsBoolean, JsError, JsNull, JsObject, JsString, JsSuccess, JsValue}
 
 case class MailboxSetRequest(accountId: AccountId,
@@ -91,8 +91,9 @@ case class MailboxPatchObject(value: Map[String, JsValue]) {
   def validate(processingContext: ProcessingContext,
                mailboxIdFactory: MailboxId.Factory,
                serializer: Serializer,
-               capabilities: Set[CapabilityIdentifier]): Either[PatchUpdateValidationException, ValidatedMailboxPatchObject] = {
-    val asUpdatedIterable = updates(serializer, capabilities, processingContext, mailboxIdFactory)
+               capabilities: Set[CapabilityIdentifier],
+               mailboxSession: MailboxSession): Either[PatchUpdateValidationException, ValidatedMailboxPatchObject] = {
+    val asUpdatedIterable = updates(serializer, capabilities, processingContext, mailboxIdFactory, mailboxSession)
 
     val maybeParseException: Option[PatchUpdateValidationException] = asUpdatedIterable
       .flatMap(x => x match {
@@ -145,9 +146,13 @@ case class MailboxPatchObject(value: Map[String, JsValue]) {
         rightsPartialUpdates = partialRightsUpdates)))
   }
 
-  def updates(serializer: Serializer, capabilities: Set[CapabilityIdentifier], processingContext: ProcessingContext, mailboxIdFactory: MailboxId.Factory): Iterable[Either[PatchUpdateValidationException, Update]] = value.map({
+  def updates(serializer: Serializer,
+              capabilities: Set[CapabilityIdentifier],
+              processingContext: ProcessingContext,
+              mailboxIdFactory: MailboxId.Factory,
+              mailboxSession: MailboxSession): Iterable[Either[PatchUpdateValidationException, Update]] = value.map({
     case (property, newValue) => property match {
-      case "/name" => NameUpdate.parse(newValue)
+      case "/name" => NameUpdate.parse(newValue, mailboxSession)
       case "/parentId" => ParentIdUpdate.parse(newValue, processingContext, mailboxIdFactory)
       case "/sharedWith" => SharedWithResetUpdate.parse(serializer, capabilities)(newValue)
       case "/role" => Left(ServerSetPropertyException(MailboxPatchObject.roleProperty))
@@ -256,8 +261,12 @@ object MailboxSetResponse {
 case class MailboxUpdateResponse(value: JsObject)
 
 object NameUpdate {
-  def parse(newValue: JsValue): Either[PatchUpdateValidationException, Update] = newValue match {
-    case JsString(newName) => scala.Right(NameUpdate(newName))
+  def parse(newValue: JsValue, mailboxSession: MailboxSession): Either[PatchUpdateValidationException, Update] = newValue match {
+    case JsString(newName) => if (newName.contains(mailboxSession.getPathDelimiter)) {
+      Left(InvalidUpdateException("/name", s"The mailbox '$newName' contains an illegal character: '${mailboxSession.getPathDelimiter}'"))
+    } else {
+      scala.Right(NameUpdate(newName))
+    }
     case _ => Left(InvalidUpdateException("/name", "Expecting a JSON string as an argument"))
   }
 }
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 2a22946..ba022f4 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
@@ -179,7 +179,7 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
                             unparsedMailboxId: UnparsedMailboxId,
                             patch: MailboxPatchObject,
                             capabilities: Set[CapabilityIdentifier]): SMono[UpdateResult] = {
-    patch.validate(processingContext, mailboxIdFactory, serializer, capabilities)
+    patch.validate(processingContext, mailboxIdFactory, serializer, capabilities, mailboxSession)
       .fold(e => SMono.raiseError(e), validatedPatch =>
         updateMailboxRights(mailboxId, validatedPatch, mailboxSession)
           .`then`(updateSubscription(mailboxId, validatedPatch, mailboxSession))


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 14/14: JAMES-3095 Avoid listing all subscriptions for each mailbox

Posted by bt...@apache.org.
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 5a899ed533056c98c4179acdf6a211ab5f2dc8f9
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Aug 26 14:05:11 2020 +0700

    JAMES-3095 Avoid listing all subscriptions for each mailbox
---
 .../main/scala/org/apache/james/jmap/model/MailboxFactory.scala  | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala
index 91e63b9..7b9f57f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala
@@ -67,6 +67,12 @@ case class MailboxValidation(mailboxName: MailboxName,
                              totalEmails: TotalEmails,
                              totalThreads: TotalThreads)
 
+case class Subscriptions(subscribedNames: Set[String]) {
+  def isSubscribed(name: String): IsSubscribed = IsSubscribed(subscribedNames.contains(name))
+
+  def isSubscribed(metaData: MailboxMetaData): IsSubscribed = isSubscribed(metaData.getPath.getName)
+}
+
 class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager, mailboxManager: MailboxManager) {
 
   private def getRole(mailboxPath: MailboxPath, mailboxSession: MailboxSession): Option[Role] = Role.from(mailboxPath.getName)
@@ -117,6 +123,7 @@ class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager, mailbo
              mailboxSession: MailboxSession,
              allMailboxesMetadata: Seq[MailboxMetaData],
              quotaLoader: QuotaLoader): SMono[Mailbox] = {
+    val subscriptions: Subscriptions = Subscriptions(subscriptionManager.subscriptions(mailboxSession).asScala.toSet)
     val sanitizedCounters: MailboxCounters = mailboxMetaData.getCounters.sanitize()
 
     MailboxValidation.validate(mailboxMetaData.getPath, mailboxSession.getPathDelimiter, sanitizedCounters.getUnseen, sanitizedCounters.getUnseen, sanitizedCounters.getCount, sanitizedCounters.getCount) match {
@@ -134,7 +141,7 @@ class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager, mailbo
               .map(_.getId)
               .headOption
             val myRights: MailboxRights = getMyRights(mailboxMetaData.getPath, mailboxMetaData.getResolvedAcls, mailboxSession)
-            val isSubscribed: IsSubscribed = retrieveIsSubscribed(mailboxMetaData.getPath, mailboxSession)
+            val isSubscribed: IsSubscribed = subscriptions.isSubscribed(mailboxMetaData)
 
             Mailbox(
               id = id,


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 05/14: JAMES-3359 Mailbox/set updates parentId: test delegation behaviour

Posted by bt...@apache.org.
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 6c75d5249e320f56c240b7392853d80ec4d5c16b
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 14:21:55 2020 +0700

    JAMES-3359 Mailbox/set updates parentId: test delegation behaviour
---
 .../contract/MailboxSetMethodContract.scala        | 189 ++++++++++++++++++++-
 1 file changed, 187 insertions(+), 2 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/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 25760d5..e388765 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
@@ -6616,8 +6616,6 @@ trait MailboxSetMethodContract {
       .body
       .asString
 
-   println(response)
-
     assertThatJson(response).isEqualTo(
       s"""{
          |  "sessionState": "75128aab4b1b",
@@ -6636,4 +6634,191 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def updateShouldFailWhenRenamingParentIdWithinADelegatedAccount(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val path = MailboxPath.forUser(ANDRE, "mailbox")
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(path)
+    val parentPath = MailboxPath.forUser(ANDRE, "parent")
+    val parentId = mailboxProbe.createMailbox(parentPath)
+
+    val aCLProbeImpl = server.getProbe(classOf[ACLProbeImpl])
+    aCLProbeImpl.replaceRights(path, BOB.asString, MailboxACL.FULL_RIGHTS)
+    aCLProbeImpl.replaceRights(parentPath, BOB.asString, MailboxACL.FULL_RIGHTS)
+
+    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()}"
+        |                    }
+        |                }
+        |           },
+        |           "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",
+         |      "notUpdated": {
+         |        "${mailboxId.serialize()}": {
+         |          "type": "notFound",
+         |          "description": "#private:andre@domain.tld:parent.mailbox"
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenRenamingParentIdFromADelegatedAccount(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val path = MailboxPath.forUser(ANDRE, "mailbox")
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(path)
+    val parentPath = MailboxPath.forUser(BOB, "parent")
+    val parentId = mailboxProbe.createMailbox(parentPath)
+
+    val aCLProbeImpl = server.getProbe(classOf[ACLProbeImpl])
+    aCLProbeImpl.replaceRights(path, BOB.asString, MailboxACL.FULL_RIGHTS)
+
+    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()}"
+        |                    }
+        |                }
+        |           },
+        |           "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",
+         |      "notUpdated": {
+         |        "${mailboxId.serialize()}": {
+         |          "type": "notFound",
+         |          "description": "#private:andre@domain.tld:mailbox"
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenRenamingParentIdToADelegatedAccount(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val path = MailboxPath.forUser(BOB, "mailbox")
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(path)
+    val parentPath = MailboxPath.forUser(ANDRE, "parent")
+    val parentId = mailboxProbe.createMailbox(parentPath)
+
+    val aCLProbeImpl = server.getProbe(classOf[ACLProbeImpl])
+    aCLProbeImpl.replaceRights(parentPath, BOB.asString, MailboxACL.FULL_RIGHTS)
+
+    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()}"
+        |                    }
+        |                }
+        |           },
+        |           "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",
+         |      "notUpdated": {
+         |        "${mailboxId.serialize()}": {
+         |          "type": "notFound",
+         |          "description": "#private:andre@domain.tld:parent.mailbox"
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 10/14: JAMES-3359 Review MailboxSetMethodContract indent

Posted by bt...@apache.org.
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 c1d704bb5fc4850f85754834d85152b1a49baeeb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Aug 24 10:00:43 2020 +0700

    JAMES-3359 Review MailboxSetMethodContract indent
---
 .../contract/MailboxSetMethodContract.scala        | 306 ++++++++++-----------
 1 file changed, 153 insertions(+), 153 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/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 9a06964..28f447e 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
@@ -1632,18 +1632,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -1689,18 +1689,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -1745,18 +1745,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -1807,18 +1807,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -1861,18 +1861,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -1909,15 +1909,15 @@ trait MailboxSetMethodContract {
         |}
         |""".stripMargin
 
-     `given`
-        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-        .body(request)
-      .when
-        .post
-      .`then`
-        .log().ifValidationFails()
-        .statusCode(SC_OK)
-        .contentType(JSON)
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
 
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
@@ -1985,15 +1985,15 @@ trait MailboxSetMethodContract {
         |}
         |""".stripMargin
 
-     `given`
-        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-        .body(request)
-      .when
-        .post
-      .`then`
-        .log().ifValidationFails()
-        .statusCode(SC_OK)
-        .contentType(JSON)
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+     .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
 
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
@@ -2061,18 +2061,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -2122,18 +2122,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -2176,18 +2176,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -2232,18 +2232,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -2283,18 +2283,18 @@ trait MailboxSetMethodContract {
         |}
         |""".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
+    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"""{
@@ -2343,17 +2343,17 @@ trait MailboxSetMethodContract {
         |""".stripMargin
 
      `given`
-        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
-        .body(request)
-      .when
-        .post
-      .`then`
-        .log().ifValidationFails()
-        .statusCode(SC_OK)
-        .contentType(JSON)
-         // We need to limit ourself to simple body assertions in order not to infer id allocation
-         .body("methodResponses[0][1].created.C42.totalThreads", equalTo(0))
-         .body("methodResponses[1][1].destroyed", hasSize(1))
+       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+       .body(request)
+     .when
+       .post
+     .`then`
+       .log().ifValidationFails()
+       .statusCode(SC_OK)
+       .contentType(JSON)
+        // We need to limit ourself to simple body assertions in order not to infer id allocation
+        .body("methodResponses[0][1].created.C42.totalThreads", equalTo(0))
+        .body("methodResponses[1][1].destroyed", hasSize(1))
   }
 
   @Test
@@ -4746,9 +4746,9 @@ trait MailboxSetMethodContract {
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
-      .when
+    .when
       .post
-      .`then`
+    .`then`
       .log().ifValidationFails()
       .statusCode(SC_OK)
       .contentType(JSON)
@@ -4826,9 +4826,9 @@ trait MailboxSetMethodContract {
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
-      .when
+    .when
       .post
-      .`then`
+    .`then`
       .log().ifValidationFails()
       .statusCode(SC_OK)
       .contentType(JSON)


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 08/14: JAMES-3359 Mailbox/set parentId updates: prevent loops

Posted by bt...@apache.org.
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 746efffb4e8b905b5155ec49e6e2bfe6d17ed288
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 14:59:10 2020 +0700

    JAMES-3359 Mailbox/set parentId updates: prevent loops
    
    We apply the same strategy than in Draft: preventing changing parentId of a mailbox having childs
    respects the mailbox tree structure.
---
 .../contract/MailboxSetMethodContract.scala        | 59 ++++++++++++++++++++++
 .../james/jmap/method/MailboxSetMethod.scala       | 14 +++--
 2 files changed, 68 insertions(+), 5 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/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 765675d..355e3d5 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
@@ -6969,4 +6969,63 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def updatingParentIdShouldFailWhenMailboxHasAChild(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox.child"))
+    val parentId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+
+    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"
+        |       ]
+        |   ]
+        |}
+        |""".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",
+         |      "notUpdated": {
+         |        "1": {
+         |          "type": "invalidArguments",
+         |          "description": "1 parentId property cannot be updated as this mailbox has child mailboxes",
+         |          "properties": ["parentId"]
+         |        }
+         |      }
+         |    }, "c1"]
+         |  ]
+         |}""".stripMargin)
+  }
 }
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 ba022f4..a412f8a 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
@@ -122,6 +122,7 @@ case class UpdateFailure(mailboxId: UnparsedMailboxId, exception: Throwable, pat
     case e: InvalidPatchException => MailboxSetError.invalidPatch(Some(SetErrorDescription(s"${e.cause}")))
     case e: SystemMailboxChangeException => MailboxSetError.invalidArgument(Some(SetErrorDescription("Invalid change to a system mailbox")), filter(Properties(List("/name", "parentId"))))
     case e: InsufficientRightsException => MailboxSetError.invalidArgument(Some(SetErrorDescription("Invalid change to a delegated mailbox")), None)
+    case e: MailboxHasChildException => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${e.mailboxId.serialize()} parentId property cannot be updated as this mailbox has child mailboxes")), Some(Properties(List("parentId"))))
     case _ => MailboxSetError.serverFail(Some(SetErrorDescription(exception.getMessage)), None)
   }
 }
@@ -216,8 +217,8 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
             throw SystemMailboxChangeException(mailboxId)
           }
           val oldPath = mailbox.getMailboxPath
-          val newPath = applyNameUpdate(validatedPatch.nameUpdate, mailboxSession)
-            .andThen(applyParentIdUpdate(validatedPatch.parentIdUpdate, mailboxSession))
+          val newPath = applyParentIdUpdate(mailboxId, validatedPatch.parentIdUpdate, mailboxSession)
+            .andThen(applyNameUpdate(validatedPatch.nameUpdate, mailboxSession))
             .apply(oldPath)
           if (!oldPath.equals(newPath)) {
             mailboxManager.renameMailbox(mailboxId,
@@ -236,8 +237,8 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
     }
   }
 
-  private def applyParentIdUpdate(maybeParentIdUpdate: Option[ParentIdUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
-    maybeParentIdUpdate.map(parentIdUpdate => applyParentIdUpdate(parentIdUpdate, mailboxSession))
+  private def applyParentIdUpdate(mailboxId: MailboxId, maybeParentIdUpdate: Option[ParentIdUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
+    maybeParentIdUpdate.map(parentIdUpdate => applyParentIdUpdate(mailboxId, parentIdUpdate, mailboxSession))
       .getOrElse(x => x)
   }
 
@@ -253,11 +254,14 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
     }).getOrElse(originalPath)
   }
 
-  private def applyParentIdUpdate(parentIdUpdate: ParentIdUpdate, mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
+  private def applyParentIdUpdate(mailboxId: MailboxId, parentIdUpdate: ParentIdUpdate, mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
     originalPath => {
       val currentName = originalPath.getName(mailboxSession.getPathDelimiter)
       parentIdUpdate.newId
         .map(id => {
+          if (mailboxManager.hasChildren(originalPath, mailboxSession)) {
+            throw MailboxHasChildException(mailboxId)
+          }
           val parentPath = mailboxManager.getMailbox(id, mailboxSession).getMailboxPath
           parentPath.child(currentName, mailboxSession.getPathDelimiter)
         })


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 09/14: JAMES-3359 Fix typos in MailboxSetMethodContract

Posted by bt...@apache.org.
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 97281746651fa54a4549c170d72a5f3ae0979e42
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Aug 21 14:27:04 2020 +0700

    JAMES-3359 Fix typos in MailboxSetMethodContract
---
 .../contract/MailboxSetMethodContract.scala        | 56 +++++++++-------------
 1 file changed, 23 insertions(+), 33 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/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 355e3d5..9a06964 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
@@ -5566,12 +5566,10 @@ trait MailboxSetMethodContract {
 
   @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 mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    val newParentId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "newParent"))
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent.mailbox"))
     val request =
       s"""
          |{
@@ -5655,11 +5653,10 @@ trait MailboxSetMethodContract {
   }
 
   @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"))
+  def updateShouldAllowParentIdChangeWhenTopLevelMailboxAndNewName(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val parentId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
     val request =
       s"""
          |{
@@ -5745,12 +5742,10 @@ trait MailboxSetMethodContract {
 
   @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 mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    val newParentId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "newParent"))
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent.mailbox"))
     val request =
       s"""
          |{
@@ -5836,12 +5831,10 @@ trait MailboxSetMethodContract {
 
   @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 mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "newParent"))
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent.mailbox"))
     val request =
       s"""
          |{
@@ -5925,10 +5918,9 @@ trait MailboxSetMethodContract {
 
   @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 mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
     val request =
       s"""
          |{
@@ -6013,12 +6005,10 @@ trait MailboxSetMethodContract {
 
   @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 mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent"))
+    mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "newParent"))
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "parent.mailbox"))
     val request =
       s"""
          |{


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 03/14: JAMES-3359 Mailbox/set update should handle parentId formatting errors

Posted by bt...@apache.org.
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 1cdd21d0b358f7c23b95da99dae026cda33bda88
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 20 12:36:08 2020 +0700

    JAMES-3359 Mailbox/set update should handle parentId formatting errors
---
 .../contract/MailboxSetMethodContract.scala        | 241 +++++++++++++++++++++
 1 file changed, 241 insertions(+)

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 871345f..07a7ed9 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
@@ -6220,4 +6220,245 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def updateShouldAcceptCreationIdsForParentId(server: GuiceJamesServer): Unit = {
+    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",
+        |                "create": {
+        |                    "C42": {
+        |                        "name": "parent"
+        |                    }
+        |                }
+        |           },
+        |           "c1"
+        |       ],
+        |       [
+        |           "Mailbox/set",
+        |           {
+        |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "update": {
+        |                    "${mailboxId.serialize()}": {
+        |                      "/parentId": "#C42"
+        |                    }
+        |                }
+        |           },
+        |           "c2"
+        |       ],
+        |       ["Mailbox/get",
+        |         {
+        |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |           "ids": ["${mailboxId.serialize()}"]
+        |          },
+        |          "c3"
+        |       ]
+        |   ]
+        |}
+        |""".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
+
+    val parentId: String = server.getProbe(classOf[MailboxProbeImpl])
+      .getMailboxId("#private", BOB.asString(), "parent")
+      .serialize()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |  "sessionState": "75128aab4b1b",
+         |  "methodResponses": [
+         |    ["Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "newState": "000001",
+         |      "created": {
+         |        "C42": {
+         |          "id": "$parentId",
+         |          "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": true
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "newState": "000001",
+         |      "updated": {
+         |        "${mailboxId.serialize()}": {}
+         |      }
+         |    }, "c2"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "state": "000001",
+         |      "list": [{
+         |        "id": "${mailboxId.serialize()}",
+         |        "sortOrder":1000,
+         |        "name": "mailbox",
+         |        "parentId": "$parentId",
+         |        "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": []
+         |    }, "c3"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldRejectInvalidParentIdJsonPayload(server: GuiceJamesServer): Unit = {
+    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": ["invalid"]
+        |                    }
+        |                }
+        |           },
+        |           "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",
+         |      "notUpdated": {
+         |        "1": {
+         |          "type": "invalidArguments",
+         |          "description": "Expecting a JSON string or null as an argument",
+         |          "properties": ["/parentId"]
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldRejectUnresolvableCreationIds(server: GuiceJamesServer): Unit = {
+    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": "#C42"
+        |                    }
+        |                }
+        |           },
+        |           "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",
+         |      "notUpdated": {
+         |        "1": {
+         |          "type": "invalidArguments",
+         |          "description": "ClientId(#C42) was not used in previously defined creationIds",
+         |          "properties": ["/parentId"]
+         |        }
+         |      }
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org