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 2022/11/16 01:28:40 UTC

[james-project] branch master updated (507e48dc29 -> e329ddd4ba)

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 507e48dc29 JAMES-3854 RFC-8438 IMAP STATUS=SIZE (#1308)
     new 11cd2ddd73 JAMES-3852 Allow parsing escaped MailboxPath
     new e2ecd6b802 JAMES-3852 SubscriptionManager should be backed by MailboxPath
     new e329ddd4ba JAMES-3852 Support subscriptions management on delegated mailboxes

The 3 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:
 .../org/apache/james/mailbox/DefaultMailboxes.java |  8 +++
 .../apache/james/mailbox/SubscriptionManager.java  | 30 ++++----
 .../apache/james/mailbox/model/MailboxPath.java    | 36 ++++++++++
 .../apache/james/mailbox/MailboxManagerTest.java   | 46 ++++++------
 .../james/mailbox/SubscriptionManagerContract.java |  5 +-
 .../james/mailbox/model/MailboxPathTest.java       | 41 +++++++++++
 .../james/mailbox/store/StoreMailboxManager.java   |  9 ++-
 .../mailbox/store/StoreSubscriptionManager.java    | 27 ++++---
 .../mailbox/store/user/model/Subscription.java     |  4 ++
 .../apache/james/imap/processor/LSubProcessor.java |  2 +
 .../james/imap/processor/SubscribeProcessor.java   |  8 ++-
 .../james/imap/processor/UnsubscribeProcessor.java |  8 ++-
 .../james/imap/processor/LSubProcessorTest.java    | 42 ++++++-----
 .../org/apache/james/modules/MailboxProbeImpl.java |  7 +-
 .../methods/SetMailboxesCreationProcessor.java     |  2 +-
 .../methods/SetMailboxesDestructionProcessor.java  |  2 +-
 .../jmap/http/DefaultMailboxesProvisioner.java     |  2 +-
 .../jmap/http/DefaultMailboxesProvisionerTest.java |  7 +-
 .../DefaultMailboxesProvisionerThreadTest.java     |  2 +-
 .../contract/MailboxSetMethodContract.scala        | 84 ++++++++++++++++++++--
 .../james/jmap/http/MailboxesProvisioner.scala     |  2 +-
 .../apache/james/jmap/mail/MailboxFactory.scala    |  8 +--
 .../jmap/method/MailboxSetCreatePerformer.scala    |  2 +-
 .../jmap/method/MailboxSetDeletePerformer.scala    |  4 +-
 .../jmap/method/MailboxSetUpdatePerformer.scala    |  4 +-
 .../james/jmap/http/MailboxesProvisionerTest.scala |  7 +-
 .../james/webadmin/service/SubscribeAllTask.java   |  8 ++-
 .../service/SubscribeAllRequestToTaskTest.java     |  4 +-
 28 files changed, 289 insertions(+), 122 deletions(-)


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


[james-project] 02/03: JAMES-3852 SubscriptionManager should be backed by MailboxPath

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 e2ecd6b80261c20891ee143c3074105c8fd22e91
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 11 16:11:39 2022 +0700

    JAMES-3852 SubscriptionManager should be backed by MailboxPath
---
 .../org/apache/james/mailbox/DefaultMailboxes.java |  8 ++++
 .../apache/james/mailbox/SubscriptionManager.java  | 30 ++++++--------
 .../apache/james/mailbox/MailboxManagerTest.java   | 46 +++++++++++-----------
 .../james/mailbox/SubscriptionManagerContract.java |  5 ++-
 .../james/mailbox/store/StoreMailboxManager.java   |  9 ++++-
 .../mailbox/store/StoreSubscriptionManager.java    | 27 +++++++------
 .../mailbox/store/user/model/Subscription.java     |  4 ++
 .../apache/james/imap/processor/LSubProcessor.java |  2 +
 .../james/imap/processor/SubscribeProcessor.java   |  8 ++--
 .../james/imap/processor/UnsubscribeProcessor.java |  8 ++--
 .../james/imap/processor/LSubProcessorTest.java    | 42 ++++++++++----------
 .../org/apache/james/modules/MailboxProbeImpl.java |  7 +++-
 .../methods/SetMailboxesCreationProcessor.java     |  2 +-
 .../methods/SetMailboxesDestructionProcessor.java  |  2 +-
 .../jmap/http/DefaultMailboxesProvisioner.java     |  2 +-
 .../jmap/http/DefaultMailboxesProvisionerTest.java |  7 +---
 .../DefaultMailboxesProvisionerThreadTest.java     |  2 +-
 .../james/jmap/http/MailboxesProvisioner.scala     |  2 +-
 .../apache/james/jmap/mail/MailboxFactory.scala    |  8 ++--
 .../jmap/method/MailboxSetCreatePerformer.scala    |  2 +-
 .../jmap/method/MailboxSetDeletePerformer.scala    |  4 +-
 .../jmap/method/MailboxSetUpdatePerformer.scala    |  4 +-
 .../james/jmap/http/MailboxesProvisionerTest.scala |  7 +---
 .../james/webadmin/service/SubscribeAllTask.java   |  8 ++--
 .../service/SubscribeAllRequestToTaskTest.java     |  4 +-
 25 files changed, 135 insertions(+), 115 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/DefaultMailboxes.java b/mailbox/api/src/main/java/org/apache/james/mailbox/DefaultMailboxes.java
index fabc8ffb35..7b4ddbbd45 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/DefaultMailboxes.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/DefaultMailboxes.java
@@ -20,7 +20,9 @@ package org.apache.james.mailbox;
 
 import java.util.List;
 
+import org.apache.james.core.Username;
 import org.apache.james.mailbox.model.MailboxConstants;
+import org.apache.james.mailbox.model.MailboxPath;
 
 import com.google.common.collect.ImmutableList;
 
@@ -37,4 +39,10 @@ public interface DefaultMailboxes {
     String RESTORED_MESSAGES = "Restored-Messages";
 
     List<String> DEFAULT_MAILBOXES = ImmutableList.of(INBOX, OUTBOX, SENT, TRASH, DRAFTS, SPAM);
+
+    static List<MailboxPath> defaultMailboxesAsPath(Username username) {
+        return DEFAULT_MAILBOXES.stream()
+            .map(s -> MailboxPath.forUser(username, s))
+            .collect(ImmutableList.toImmutableList());
+    }
 }
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/SubscriptionManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/SubscriptionManager.java
index 1730c3c94b..4f115edb45 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/SubscriptionManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/SubscriptionManager.java
@@ -22,6 +22,7 @@ package org.apache.james.mailbox;
 import java.util.Collection;
 
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.reactivestreams.Publisher;
 
 /**
@@ -30,10 +31,13 @@ import org.reactivestreams.Publisher;
  * 
  */
 public interface SubscriptionManager extends RequestAware {
-
     /**
      * Subscribes the user in the session to the given mailbox.
-     * 
+     *
+     * This mailbox may be shared or belong to another user.
+     *
+     * The corresponding path do NOT need to exist.
+     *
      * @param session
      *            not null
      * @param mailbox
@@ -41,9 +45,9 @@ public interface SubscriptionManager extends RequestAware {
      * @throws SubscriptionException
      *             when subscription fails
      */
-    void subscribe(MailboxSession session, String mailbox) throws SubscriptionException;
+    void subscribe(MailboxSession session, MailboxPath mailbox) throws SubscriptionException;
 
-    Publisher<Void> subscribeReactive(String mailbox, MailboxSession session);
+    Publisher<Void> subscribeReactive(MailboxPath mailbox, MailboxSession session);
 
     /**
      * Finds all subscriptions for the user in the session.
@@ -54,21 +58,11 @@ public interface SubscriptionManager extends RequestAware {
      * @throws SubscriptionException
      *             when subscriptions cannot be read
      */
-    Collection<String> subscriptions(MailboxSession session) throws SubscriptionException;
+    Collection<MailboxPath> subscriptions(MailboxSession session) throws SubscriptionException;
 
-    Publisher<String> subscriptionsReactive(MailboxSession session) throws SubscriptionException;
+    Publisher<MailboxPath> subscriptionsReactive(MailboxSession session) throws SubscriptionException;
 
-    /**
-     * Unsubscribes the user in the session from the given mailbox.
-     * 
-     * @param session
-     *            not null
-     * @param mailbox
-     *            not null
-     * @throws SubscriptionException
-     *             when subscriptions cannot be read
-     */
-    void unsubscribe(MailboxSession session, String mailbox) throws SubscriptionException;
+    void unsubscribe(MailboxSession session, MailboxPath mailbox) throws SubscriptionException;
 
-    Publisher<Void> unsubscribeReactive(String mailbox, MailboxSession session);
+    Publisher<Void> unsubscribeReactive(MailboxPath mailbox, MailboxSession session);
 }
diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
index ad82875e63..ed642c8c1f 100644
--- a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
+++ b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
@@ -1684,19 +1684,18 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
 
             mailboxManager.createMailbox(originalPath, session);
             mailboxManager.createMailbox(mailboxPath2, session);
-            subscriptionManager.subscribe(session, originalPath.getName());
-            subscriptionManager.subscribe(session, mailboxPath2.getName());
+            subscriptionManager.subscribe(session, originalPath);
+            subscriptionManager.subscribe(session, mailboxPath2);
 
             mailboxManager.createMailbox(mailboxPath3, session);
-            subscriptionManager.subscribe(session, mailboxPath3.getName());
+            subscriptionManager.subscribe(session, mailboxPath3);
 
             mailboxManager.renameMailbox(originalPath, newMailboxPath, RENAME_SUBSCRIPTIONS, session);
 
-            assertThat(subscriptionManager.subscriptions(session)).containsExactly(
-                newMailboxPath.getName(),
-                "mbx9.mbx2",
-                "mbx9.mbx2.mbx3"
-            );
+            assertThat(subscriptionManager.subscriptions(session)).containsOnly(
+                newMailboxPath,
+                MailboxPath.forUser(USER_1, "mbx9.mbx2"),
+                MailboxPath.forUser(USER_1, "mbx9.mbx2.mbx3"));
         }
 
         @Test
@@ -1707,11 +1706,11 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
             MailboxPath newMailboxPath = MailboxPath.forUser(USER_1, "mbx2");
 
             mailboxManager.createMailbox(originalPath, session);
-            subscriptionManager.subscribe(session, originalPath.getName());
+            subscriptionManager.subscribe(session, originalPath);
 
             mailboxManager.renameMailbox(originalPath, newMailboxPath, RENAME_SUBSCRIPTIONS, session);
 
-            assertThat(subscriptionManager.subscriptions(session)).containsExactly(newMailboxPath.getName());
+            assertThat(subscriptionManager.subscriptions(session)).containsExactly(newMailboxPath);
         }
 
         @Test
@@ -1736,11 +1735,11 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
             MailboxPath newMailboxPath = MailboxPath.forUser(USER_1, "mbx2");
 
             mailboxManager.createMailbox(originalPath, session);
-            subscriptionManager.subscribe(session, originalPath.getName());
+            subscriptionManager.subscribe(session, originalPath);
 
             mailboxManager.renameMailbox(originalPath, newMailboxPath, MailboxManager.RenameOption.NONE, session);
 
-            assertThat(subscriptionManager.subscriptions(session)).containsExactly(originalPath.getName());
+            assertThat(subscriptionManager.subscriptions(session)).containsExactly(originalPath);
         }
 
         @Test
@@ -1754,19 +1753,18 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
 
             Optional<MailboxId> id = mailboxManager.createMailbox(originalPath, session);
             mailboxManager.createMailbox(mailboxPath2, session);
-            subscriptionManager.subscribe(session, originalPath.getName());
-            subscriptionManager.subscribe(session, mailboxPath2.getName());
+            subscriptionManager.subscribe(session, originalPath);
+            subscriptionManager.subscribe(session, mailboxPath2);
 
             mailboxManager.createMailbox(mailboxPath3, session);
-            subscriptionManager.subscribe(session, mailboxPath3.getName());
+            subscriptionManager.subscribe(session, mailboxPath3);
 
             mailboxManager.renameMailbox(id.get(), newMailboxPath, RENAME_SUBSCRIPTIONS, session);
 
-            assertThat(subscriptionManager.subscriptions(session)).containsExactly(
-                newMailboxPath.getName(),
-                "mbx9.mbx2",
-                "mbx9.mbx2.mbx3"
-            );
+            assertThat(subscriptionManager.subscriptions(session)).containsOnly(
+                newMailboxPath,
+                MailboxPath.forUser(USER_1, "mbx9.mbx2"),
+                MailboxPath.forUser(USER_1, "mbx9.mbx2.mbx3"));
         }
 
         @Test
@@ -1777,11 +1775,11 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
             MailboxPath newMailboxPath = MailboxPath.forUser(USER_1, "mbx2");
 
             Optional<MailboxId> id = mailboxManager.createMailbox(originalPath, session);
-            subscriptionManager.subscribe(session, originalPath.getName());
+            subscriptionManager.subscribe(session, originalPath);
 
             mailboxManager.renameMailbox(id.get(), newMailboxPath, RENAME_SUBSCRIPTIONS, session);
 
-            assertThat(subscriptionManager.subscriptions(session)).containsExactly(newMailboxPath.getName());
+            assertThat(subscriptionManager.subscriptions(session)).containsExactly(newMailboxPath);
         }
 
         @Test
@@ -1806,11 +1804,11 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
             MailboxPath newMailboxPath = MailboxPath.forUser(USER_1, "mbx2");
 
             Optional<MailboxId> id = mailboxManager.createMailbox(originalPath, session);
-            subscriptionManager.subscribe(session, originalPath.getName());
+            subscriptionManager.subscribe(session, originalPath);
 
             mailboxManager.renameMailbox(id.get(), newMailboxPath, MailboxManager.RenameOption.NONE, session);
 
-            assertThat(subscriptionManager.subscriptions(session)).containsExactly(originalPath.getName());
+            assertThat(subscriptionManager.subscriptions(session)).containsExactly(originalPath);
         }
 
         @Test
diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/SubscriptionManagerContract.java b/mailbox/api/src/test/java/org/apache/james/mailbox/SubscriptionManagerContract.java
index ef5a19afde..33900e1988 100644
--- a/mailbox/api/src/test/java/org/apache/james/mailbox/SubscriptionManagerContract.java
+++ b/mailbox/api/src/test/java/org/apache/james/mailbox/SubscriptionManagerContract.java
@@ -22,13 +22,14 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import org.apache.james.core.Username;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.junit.jupiter.api.Test;
 
 public interface SubscriptionManagerContract {
 
     Username USER1 = Username.of("test");
-    String MAILBOX1 = "test1";
-    String MAILBOX2 = "test2";
+    MailboxPath MAILBOX1 = MailboxPath.forUser(USER1, "test1");
+    MailboxPath MAILBOX2 = MailboxPath.forUser(USER1, "test2");
     MailboxSession SESSION = MailboxSessionUtil.create(USER1);
 
     SubscriptionManager getSubscriptionManager();
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index 031bb88df4..e506b44671 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -565,10 +565,15 @@ public class StoreMailboxManager implements MailboxManager {
                 .collectList()
                 .flatMap(subscriptions -> Flux.fromIterable(renamedResults)
                     .flatMap(renamedResult -> {
-                        Subscription subscription = new Subscription(session.getUser(), renamedResult.getOriginPath().getName());
+                        Subscription legacySubscription = new Subscription(session.getUser(), renamedResult.getOriginPath().getName());
+                        if (subscriptions.contains(legacySubscription)) {
+                            return subscriptionMapper.deleteReactive(legacySubscription)
+                                .then(subscriptionMapper.saveReactive(new Subscription(session.getUser(), renamedResult.getDestinationPath().asEscapedString())));
+                        }
+                        Subscription subscription = new Subscription(session.getUser(), renamedResult.getOriginPath().asEscapedString());
                         if (subscriptions.contains(subscription)) {
                             return subscriptionMapper.deleteReactive(subscription)
-                                .then(subscriptionMapper.saveReactive(new Subscription(session.getUser(), renamedResult.getDestinationPath().getName())));
+                                .then(subscriptionMapper.saveReactive(new Subscription(session.getUser(), renamedResult.getDestinationPath().asEscapedString())));
                         }
                         return Mono.empty();
                     })
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreSubscriptionManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreSubscriptionManager.java
index d3e16e1212..3a92db7f4d 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreSubscriptionManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreSubscriptionManager.java
@@ -29,6 +29,7 @@ import org.apache.james.mailbox.RequestAware;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.store.transaction.Mapper;
 import org.apache.james.mailbox.store.user.SubscriptionMapper;
 import org.apache.james.mailbox.store.user.SubscriptionMapperFactory;
@@ -51,11 +52,11 @@ public class StoreSubscriptionManager implements SubscriptionManager {
     }
 
     @Override
-    public void subscribe(MailboxSession session, String mailbox) throws SubscriptionException {
+    public void subscribe(MailboxSession session, MailboxPath mailbox) throws SubscriptionException {
         SubscriptionMapper mapper = mapperFactory.getSubscriptionMapper(session);
         try {
             mapper.execute(Mapper.toTransaction(() -> {
-                Subscription newSubscription = new Subscription(session.getUser(), mailbox);
+                Subscription newSubscription = new Subscription(session.getUser(), mailbox.asEscapedString());
                 mapper.save(newSubscription);
             }));
         } catch (MailboxException e) {
@@ -64,10 +65,10 @@ public class StoreSubscriptionManager implements SubscriptionManager {
     }
 
     @Override
-    public Publisher<Void> subscribeReactive(String mailbox, MailboxSession session) {
+    public Publisher<Void> subscribeReactive(MailboxPath mailbox, MailboxSession session) {
         try {
             SubscriptionMapper mapper = mapperFactory.getSubscriptionMapper(session);
-            Subscription newSubscription = new Subscription(session.getUser(), mailbox);
+            Subscription newSubscription = new Subscription(session.getUser(), mailbox.asEscapedString());
             return mapper.executeReactive(mapper.saveReactive(newSubscription));
         } catch (SubscriptionException e) {
             return Mono.error(e);
@@ -75,10 +76,10 @@ public class StoreSubscriptionManager implements SubscriptionManager {
     }
 
     @Override
-    public Publisher<Void> unsubscribeReactive(String mailbox, MailboxSession session) {
+    public Publisher<Void> unsubscribeReactive(MailboxPath mailbox, MailboxSession session) {
         try {
             SubscriptionMapper mapper = mapperFactory.getSubscriptionMapper(session);
-            Subscription oldSubscription = new Subscription(session.getUser(), mailbox);
+            Subscription oldSubscription = new Subscription(session.getUser(), mailbox.asEscapedString());
             return mapper.executeReactive(mapper.deleteReactive(oldSubscription));
         } catch (SubscriptionException e) {
             return Mono.error(e);
@@ -86,26 +87,30 @@ public class StoreSubscriptionManager implements SubscriptionManager {
     }
 
     @Override
-    public Collection<String> subscriptions(MailboxSession session) throws SubscriptionException {
+    public Collection<MailboxPath> subscriptions(MailboxSession session) throws SubscriptionException {
         return mapperFactory.getSubscriptionMapper(session)
             .findSubscriptionsForUser(session.getUser())
             .stream()
             .map(Subscription::getMailbox)
+            .map(s -> MailboxPath.parseEscaped(s).orElse(MailboxPath.forUser(session.getUser(), s)))
             .collect(Collectors.toCollection(() -> new HashSet<>(INITIAL_SIZE)));
     }
 
     @Override
-    public Publisher<String> subscriptionsReactive(MailboxSession session) throws SubscriptionException {
+    public Publisher<MailboxPath> subscriptionsReactive(MailboxSession session) throws SubscriptionException {
         return mapperFactory.getSubscriptionMapper(session)
             .findSubscriptionsForUserReactive(session.getUser())
-            .map(Subscription::getMailbox);
+            .map(Subscription::getMailbox)
+            .map(s -> MailboxPath.parseEscaped(s).orElse(MailboxPath.forUser(session.getUser(), s)));
     }
 
     @Override
-    public void unsubscribe(MailboxSession session, String mailbox) throws SubscriptionException {
+    public void unsubscribe(MailboxSession session, MailboxPath mailbox) throws SubscriptionException {
         SubscriptionMapper mapper = mapperFactory.getSubscriptionMapper(session);
         try {
-            mapper.execute(Mapper.toTransaction(() -> mapper.delete(new Subscription(session.getUser(), mailbox))));
+            mapper.execute(Mapper.toTransaction(() -> mapper.delete(new Subscription(session.getUser(), mailbox.asEscapedString()))));
+            // Legacy purposes, remove subscriptions created prior to the MailboxPath migration. Noops for those created after.
+            mapper.execute(Mapper.toTransaction(() -> mapper.delete(new Subscription(session.getUser(), mailbox.getName()))));
         } catch (MailboxException e) {
             throw new SubscriptionException(e);
         }
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/user/model/Subscription.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/user/model/Subscription.java
index 284b4bd965..b4b965f31a 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/user/model/Subscription.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/user/model/Subscription.java
@@ -29,6 +29,10 @@ import com.google.common.base.Objects;
  */
 public class Subscription {
     private final Username user;
+    /**
+     * Mailbox needs to be a mailbox path in its escaped form.
+     * Legacy versions might only include mailbox name.
+     */
     private final String mailbox;
 
     public Subscription(Username user, String mailbox) {
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/LSubProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/LSubProcessor.java
index 2b4498e124..ad87a9bb09 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/LSubProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/LSubProcessor.java
@@ -34,6 +34,7 @@ import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.model.search.MailboxNameExpression;
 import org.apache.james.mailbox.model.search.PrefixedRegex;
 import org.apache.james.metrics.api.MetricFactory;
@@ -73,6 +74,7 @@ public class LSubProcessor extends AbstractMailboxProcessor<LsubRequest> {
         MailboxSession mailboxSession = session.getMailboxSession();
         try {
             Mono<List<String>> mailboxesMono = Flux.from(subscriptionManager.subscriptionsReactive(mailboxSession))
+                .map(MailboxPath::getName)
                 .collectList();
 
             String decodedMailName = ModifiedUtf7.decodeModifiedUTF7(referenceName);
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/SubscribeProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/SubscribeProcessor.java
index a664f12a49..1b36a638b7 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/SubscribeProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/SubscribeProcessor.java
@@ -22,11 +22,13 @@ package org.apache.james.imap.processor;
 import org.apache.james.imap.api.display.HumanReadableText;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
+import org.apache.james.imap.main.PathConverter;
 import org.apache.james.imap.message.request.SubscribeRequest;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.util.MDCBuilder;
 import org.apache.james.util.ReactorUtils;
@@ -45,15 +47,15 @@ public class SubscribeProcessor extends AbstractSubscriptionProcessor<SubscribeR
 
     @Override
     protected Mono<Void> doProcessRequest(SubscribeRequest request, ImapSession session, Responder responder) {
-        String mailboxName = request.getMailboxName();
+        MailboxPath mailboxPath = PathConverter.forSession(session).buildFullPath(request.getMailboxName());
         MailboxSession mailboxSession = session.getMailboxSession();
 
-        return Mono.from(getSubscriptionManager().subscribeReactive(mailboxName, mailboxSession))
+        return Mono.from(getSubscriptionManager().subscribeReactive(mailboxPath, mailboxSession))
             .then(unsolicitedResponses(session, responder, false))
             .then(Mono.fromRunnable(() -> okComplete(request, responder)))
             .onErrorResume(SubscriptionException.class, e -> {
                 no(request, responder, HumanReadableText.GENERIC_SUBSCRIPTION_FAILURE);
-                return ReactorUtils.logAsMono(() -> LOGGER.info("Subscribe failed for mailbox {}", mailboxName, e));
+                return ReactorUtils.logAsMono(() -> LOGGER.info("Subscribe failed for mailbox {}", request.getMailboxName(), e));
             })
             .then();
     }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/UnsubscribeProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/UnsubscribeProcessor.java
index 806d7f9c51..ef7c8457cf 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/UnsubscribeProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/UnsubscribeProcessor.java
@@ -24,11 +24,13 @@ import static org.apache.james.util.ReactorUtils.logOnError;
 import org.apache.james.imap.api.display.HumanReadableText;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
+import org.apache.james.imap.main.PathConverter;
 import org.apache.james.imap.message.request.UnsubscribeRequest;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.util.MDCBuilder;
 import org.slf4j.Logger;
@@ -46,13 +48,13 @@ public class UnsubscribeProcessor extends AbstractSubscriptionProcessor<Unsubscr
 
     @Override
     protected Mono<Void> doProcessRequest(UnsubscribeRequest request, ImapSession session, Responder responder) {
-        String mailboxName = request.getMailboxName();
+        MailboxPath mailboxPath = PathConverter.forSession(session).buildFullPath(request.getMailboxName());
         MailboxSession mailboxSession = session.getMailboxSession();
 
-        return Mono.from(getSubscriptionManager().unsubscribeReactive(mailboxName, mailboxSession))
+        return Mono.from(getSubscriptionManager().unsubscribeReactive(mailboxPath, mailboxSession))
             .then(unsolicitedResponses(session, responder, false))
             .then(Mono.fromRunnable(() -> okComplete(request, responder)))
-            .doOnEach(logOnError(SubscriptionException.class, e -> LOGGER.info("Unsubscribe failed for mailbox {}", mailboxName, e)))
+            .doOnEach(logOnError(SubscriptionException.class, e -> LOGGER.info("Unsubscribe failed for mailbox {}", request.getMailboxName(), e)))
             .onErrorResume(SubscriptionException.class, e ->
                 unsolicitedResponses(session, responder, false)
                 .then(Mono.fromRunnable(() -> no(request, responder, HumanReadableText.GENERIC_SUBSCRIPTION_FAILURE)))).then();
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/processor/LSubProcessorTest.java b/protocols/imap/src/test/java/org/apache/james/imap/processor/LSubProcessorTest.java
index d9fd0fa05f..d24da477ec 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/processor/LSubProcessorTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/processor/LSubProcessorTest.java
@@ -45,6 +45,7 @@ import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MailboxSessionUtil;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.model.MailboxMetaData;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -54,27 +55,24 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 class LSubProcessorTest {
+    public static final Username USER = Username.of("test");
 
-    private static final String ROOT = "ROOT";
+    private static final MailboxPath ROOT = MailboxPath.forUser(USER, "ROOT");
     
     private static final char HIERARCHY_DELIMITER = '.';
 
-    private static final String PARENT = ROOT
-            + HIERARCHY_DELIMITER + "PARENT";
+    private static final MailboxPath PARENT = ROOT.child("PARENT", HIERARCHY_DELIMITER);
 
-    private static final String CHILD_ONE = PARENT
-            + HIERARCHY_DELIMITER + "CHILD_ONE";
+    private static final MailboxPath CHILD_ONE = PARENT.child("CHILD_ONE", HIERARCHY_DELIMITER);
 
-    private static final String CHILD_TWO = PARENT
-            + HIERARCHY_DELIMITER + "CHILD_TWO";
+    private static final MailboxPath CHILD_TWO = PARENT.child("CHILD_TWO", HIERARCHY_DELIMITER);
 
-    private static final String MAILBOX_C = "C.MAILBOX";
+    private static final MailboxPath MAILBOX_C = MailboxPath.forUser(USER, "C.MAILBOX");
 
-    private static final String MAILBOX_B = "B.MAILBOX";
+    private static final MailboxPath MAILBOX_B = MailboxPath.forUser(USER, "B.MAILBOX");
 
-    private static final String MAILBOX_A = "A.MAILBOX";
+    private static final MailboxPath MAILBOX_A = MailboxPath.forUser(USER, "A.MAILBOX");
 
-    public static final Username USER = Username.of("test");
 
     LSubProcessor processor;
     SubscriptionManager manager;
@@ -84,7 +82,7 @@ class LSubProcessorTest {
     MailboxSession mailboxSession;
     StatusResponseFactory serverResponseFactory;
     StatusResponse statusResponse;
-    Collection<String> subscriptions;
+    Collection<MailboxPath> subscriptions;
     private ImapProcessor.Responder responderImpl;
 
     @BeforeEach
@@ -120,12 +118,12 @@ class LSubProcessorTest {
         when(serverResponseFactory.taggedOk(eq(TAG), same(ImapConstants.LSUB_COMMAND), eq(HumanReadableText.COMPLETED)))
             .thenReturn(statusResponse);
 
-        LsubRequest request = new LsubRequest("", PARENT
+        LsubRequest request = new LsubRequest("", PARENT.getName()
                 + HIERARCHY_DELIMITER + "%", TAG);
         processor.processRequest(request, session, responderImpl);
 
-        verify(responder).respond(new LSubResponse(CHILD_ONE, false, HIERARCHY_DELIMITER));
-        verify(responder).respond(new LSubResponse(CHILD_TWO, false, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(CHILD_ONE.getName(), false, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(CHILD_TWO.getName(), false, HIERARCHY_DELIMITER));
         verify(responder).respond(statusResponse);
     }
 
@@ -142,11 +140,11 @@ class LSubProcessorTest {
         when(serverResponseFactory.taggedOk(eq(TAG), same(ImapConstants.LSUB_COMMAND), eq(HumanReadableText.COMPLETED)))
             .thenReturn(statusResponse);
 
-        LsubRequest request = new LsubRequest("", ROOT
+        LsubRequest request = new LsubRequest("", ROOT.getName()
                 + HIERARCHY_DELIMITER + "%", TAG);
         processor.processRequest(request, session, responderImpl);
 
-        verify(responder).respond(new LSubResponse(PARENT, true, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(PARENT.getName(), true, HIERARCHY_DELIMITER));
         verify(responder).respond(statusResponse);
     }
 
@@ -164,11 +162,11 @@ class LSubProcessorTest {
         when(serverResponseFactory.taggedOk(eq(TAG), same(ImapConstants.LSUB_COMMAND), eq(HumanReadableText.COMPLETED)))
             .thenReturn(statusResponse);
 
-        LsubRequest request = new LsubRequest("", ROOT
+        LsubRequest request = new LsubRequest("", ROOT.getName()
                 + HIERARCHY_DELIMITER + "%", TAG);
         processor.processRequest(request, session, responderImpl);
 
-        verify(responder).respond(new LSubResponse(PARENT, false, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(PARENT.getName(), false, HIERARCHY_DELIMITER));
         verify(responder).respond(statusResponse);
     }
 
@@ -184,9 +182,9 @@ class LSubProcessorTest {
         LsubRequest request = new LsubRequest("", "*", TAG);
         processor.processRequest(request, session, responderImpl);
 
-        verify(responder).respond(new LSubResponse(MAILBOX_A, false, HIERARCHY_DELIMITER));
-        verify(responder).respond(new LSubResponse(MAILBOX_B, false, HIERARCHY_DELIMITER));
-        verify(responder).respond(new LSubResponse(MAILBOX_C, false, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(MAILBOX_A.getName(), false, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(MAILBOX_B.getName(), false, HIERARCHY_DELIMITER));
+        verify(responder).respond(new LSubResponse(MAILBOX_C.getName(), false, HIERARCHY_DELIMITER));
         verify(responder).respond(statusResponse);
     }
 
diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
index 56e31fd59e..54235188c4 100644
--- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
+++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
@@ -49,6 +49,8 @@ import org.apache.james.mailbox.model.search.Wildcard;
 import org.apache.james.mailbox.probe.MailboxProbe;
 import org.apache.james.utils.GuiceProbe;
 
+import com.google.common.collect.ImmutableList;
+
 import reactor.core.publisher.Flux;
 
 public class MailboxProbeImpl implements GuiceProbe, MailboxProbe {
@@ -183,7 +185,10 @@ public class MailboxProbeImpl implements GuiceProbe, MailboxProbe {
     @Override
     public Collection<String> listSubscriptions(String user) throws Exception {
         MailboxSession mailboxSession = mailboxManager.createSystemSession(Username.of(user));
-        return subscriptionManager.subscriptions(mailboxSession);
+        return subscriptionManager.subscriptions(mailboxSession)
+            .stream()
+            .map(MailboxPath::getName)
+            .collect(ImmutableList.toImmutableList());
     }
 
     @Override
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesCreationProcessor.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesCreationProcessor.java
index 25a1a69a00..7c4aa1d86b 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesCreationProcessor.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesCreationProcessor.java
@@ -124,7 +124,7 @@ public class SetMailboxesCreationProcessor implements SetMailboxesProcessor {
                     .build()
                     .blockOptional());
             if (mailbox.isPresent()) {
-                subscriptionManager.subscribe(mailboxSession, mailboxPath.getName());
+                subscriptionManager.subscribe(mailboxSession, mailboxPath);
                 builder.created(mailboxCreationId, mailbox.get());
                 creationIdsToCreatedMailboxId.put(mailboxCreationId, mailbox.get().getId());
             } else {
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesDestructionProcessor.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesDestructionProcessor.java
index dbd2e1e4b8..a0243c2334 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesDestructionProcessor.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMailboxesDestructionProcessor.java
@@ -117,7 +117,7 @@ public class SetMailboxesDestructionProcessor implements SetMailboxesProcessor {
             preconditions(mailbox, mailboxSession);
 
             MailboxPath deletedMailbox = mailboxManager.deleteMailbox(mailbox.getId(), mailboxSession).generateAssociatedPath();
-            subscriptionManager.unsubscribe(mailboxSession, deletedMailbox.getName());
+            subscriptionManager.unsubscribe(mailboxSession, deletedMailbox);
             builder.destroyed(entry.getKey());
         } catch (MailboxHasChildException e) {
             builder.notDestroyed(entry.getKey(), SetError.builder()
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DefaultMailboxesProvisioner.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DefaultMailboxesProvisioner.java
index b42091b7d5..214a6525f8 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DefaultMailboxesProvisioner.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DefaultMailboxesProvisioner.java
@@ -80,7 +80,7 @@ public class DefaultMailboxesProvisioner {
     
     private Mono<Void> createMailbox(MailboxPath mailboxPath, MailboxSession session) {
         return Mono.from(mailboxManager.createMailboxReactive(mailboxPath, session))
-            .flatMap(id -> Mono.from(subscriptionManager.subscribeReactive(mailboxPath.getName(), session)))
+            .flatMap(id -> Mono.from(subscriptionManager.subscribeReactive(mailboxPath, session)))
             .onErrorResume(MailboxExistsException.class, e -> {
                 LOGGER.info("Mailbox {} have been created concurrently", mailboxPath);
                 return Mono.empty();
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerTest.java
index 6ea328e49a..03810bdd09 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerTest.java
@@ -84,7 +84,7 @@ public class DefaultMailboxesProvisionerTest {
         testee.createMailboxesIfNeeded(session).block();
 
         assertThat(subscriptionManager.subscriptions(session))
-            .containsOnlyElementsOf(DefaultMailboxes.DEFAULT_MAILBOXES);
+            .containsOnlyElementsOf(DefaultMailboxes.defaultMailboxesAsPath(USERNAME));
     }
 
     @Test
@@ -95,10 +95,7 @@ public class DefaultMailboxesProvisionerTest {
             .runSuccessfullyWithin(Duration.ofSeconds(10));
 
         assertThat(mailboxManager.list(session))
-            .containsOnlyElementsOf(DefaultMailboxes.DEFAULT_MAILBOXES
-                .stream()
-                .map(mailboxName -> MailboxPath.forUser(USERNAME, mailboxName))
-                .collect(ImmutableList.toImmutableList()));
+            .containsOnlyElementsOf(DefaultMailboxes.defaultMailboxesAsPath(USERNAME));
     }
 
 }
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerThreadTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerThreadTest.java
index 85f93449fe..cd4fb237a8 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerThreadTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DefaultMailboxesProvisionerThreadTest.java
@@ -62,7 +62,7 @@ public class DefaultMailboxesProvisionerThreadTest {
 
     @Test
     public void testConcurrentAccessToFilterShouldNotThrow() throws Exception {
-        doNothing().when(subscriptionManager).subscribe(eq(session), anyString());
+        doNothing().when(subscriptionManager).subscribe(eq(session), any(MailboxPath.class));
 
         when(mailboxManager.createMailbox(any(MailboxPath.class), eq(session))).thenReturn(Optional.of(TestId.of(18L)));
         when(mailboxManager.mailboxExists(any(MailboxPath.class), eq(session))).thenReturn(Mono.just(false));
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/MailboxesProvisioner.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/MailboxesProvisioner.scala
index 3fddd83217..901f6431fe 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/MailboxesProvisioner.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/MailboxesProvisioner.scala
@@ -64,7 +64,7 @@ class MailboxesProvisioner @Inject() (mailboxManager: MailboxManager,
 
   private def createMailbox(mailboxPath: MailboxPath, session: MailboxSession): SMono[Unit] = {
     SMono(mailboxManager.createMailboxReactive(mailboxPath, session))
-      .flatMap(id => SMono(subscriptionManager.subscribeReactive(mailboxPath.getName, session)))
+      .flatMap(id => SMono(subscriptionManager.subscribeReactive(mailboxPath, session)))
       .onErrorResume {
         case _: MailboxExistsException => LOGGER.info("Mailbox {} have been created concurrently", mailboxPath)
           SMono.empty
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxFactory.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxFactory.scala
index c28dd79b78..fa6aed840d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxFactory.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxFactory.scala
@@ -68,10 +68,10 @@ case class MailboxValidation(mailboxName: MailboxName,
                              totalEmails: TotalEmails,
                              totalThreads: TotalThreads)
 
-case class Subscriptions(subscribedNames: Set[String]) {
-  def isSubscribed(name: String): IsSubscribed = IsSubscribed(subscribedNames.contains(name))
+case class Subscriptions(subscribedNames: Set[MailboxPath]) {
+  def isSubscribed(name: MailboxPath): IsSubscribed = IsSubscribed(subscribedNames.contains(name))
 
-  def isSubscribed(metaData: MailboxMetaData): IsSubscribed = isSubscribed(metaData.getPath.getName)
+  def isSubscribed(metaData: MailboxMetaData): IsSubscribed = isSubscribed(metaData.getPath)
 }
 
 class MailboxFactory @Inject() (mailboxManager: MailboxManager,
@@ -187,7 +187,7 @@ class MailboxFactory @Inject() (mailboxManager: MailboxManager,
               val rights: Rights = getRights(resolvedACL)
               val namespace: MailboxNamespace = getNamespace(messageManager.getMailboxPath, mailboxSession)
               val myRights: MailboxRights = getMyRights(messageManager.getMailboxPath, resolvedACL, mailboxSession)
-              val isSubscribed: IsSubscribed = subscriptions.isSubscribed(messageManager.getMailboxPath.getName)
+              val isSubscribed: IsSubscribed = subscriptions.isSubscribed(messageManager.getMailboxPath)
 
               Mailbox(
                 id = id,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
index 8455dc2d18..4d8b192005 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
@@ -171,7 +171,7 @@ class MailboxSetCreatePerformer @Inject()(serializer: MailboxSerializer,
 
       val defaultSubscribed = IsSubscribed(true)
       if (mailboxCreationRequest.isSubscribed.getOrElse(defaultSubscribed).value) {
-        subscriptionManager.subscribe(mailboxSession, path.getName)
+        subscriptionManager.subscribe(mailboxSession, path)
       }
 
       mailboxCreationRequest.rights
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
index d04b17a74e..af997b1926 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
@@ -92,7 +92,7 @@ class MailboxSetDeletePerformer @Inject()(mailboxManager: MailboxManager,
 
             if (onDestroy.value) {
               SMono(mailboxManager.deleteMailboxReactive(id, mailboxSession))
-                .flatMap(deletedMailbox => SMono(subscriptionManager.unsubscribeReactive(deletedMailbox.getName, mailboxSession)))
+                .flatMap(deletedMailbox => SMono(subscriptionManager.unsubscribeReactive(deletedMailbox.generateAssociatedPath(), mailboxSession)))
                 .`then`()
             } else {
               SMono(mailbox.getMessagesReactive(MessageRange.all(), FetchGroup.MINIMAL, mailboxSession)).hasElement
@@ -102,7 +102,7 @@ class MailboxSetDeletePerformer @Inject()(mailboxManager: MailboxManager,
                   }
                 })
                 .`then`(SMono(mailboxManager.deleteMailboxReactive(id, mailboxSession))
-                  .flatMap(deletedMailbox => SMono(subscriptionManager.unsubscribeReactive(deletedMailbox.getName, mailboxSession)))
+                  .flatMap(deletedMailbox => SMono(subscriptionManager.unsubscribeReactive(deletedMailbox.generateAssociatedPath(), mailboxSession)))
                   .`then`())
             }
           }))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
index 568d86ac5b..dc938f5b40 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
@@ -116,9 +116,9 @@ class MailboxSetUpdatePerformer @Inject()(serializer: MailboxSerializer,
         val shouldSubscribe = isSubscribedUpdate.isSubscribed.map(_.value).getOrElse(isOwner)
 
         if (shouldSubscribe) {
-          subscriptionManager.subscribe(mailboxSession, mailbox.getMailboxPath.getName)
+          subscriptionManager.subscribe(mailboxSession, mailbox.getMailboxPath)
         } else {
-          subscriptionManager.unsubscribe(mailboxSession, mailbox.getMailboxPath.getName)
+          subscriptionManager.unsubscribe(mailboxSession, mailbox.getMailboxPath)
         }
       }).`then`(SMono.just[MailboxUpdateResult](MailboxUpdateSuccess(mailboxId)))
         .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER)
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/MailboxesProvisionerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/MailboxesProvisionerTest.scala
index 37089d87e4..c9d8400043 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/MailboxesProvisionerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/MailboxesProvisionerTest.scala
@@ -84,7 +84,7 @@ class MailboxesProvisionerTest {
     testee.createMailboxesIfNeeded(session).block()
 
     assertThat(subscriptionManager.subscriptions(session))
-      .containsOnlyElementsOf(DefaultMailboxes.DEFAULT_MAILBOXES)
+      .containsOnlyElementsOf(DefaultMailboxes.defaultMailboxesAsPath(USERNAME))
   }
 
   @Test
@@ -95,9 +95,6 @@ class MailboxesProvisionerTest {
       .runSuccessfullyWithin(Duration.ofSeconds(10))
 
     assertThat(mailboxManager.list(session))
-      .containsOnlyElementsOf(DefaultMailboxes.DEFAULT_MAILBOXES
-        .stream
-        .map((mailboxName: String) => MailboxPath.forUser(USERNAME, mailboxName))
-        .collect(ImmutableList.toImmutableList()))
+      .containsOnlyElementsOf(DefaultMailboxes.defaultMailboxesAsPath(USERNAME))
   }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/SubscribeAllTask.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/SubscribeAllTask.java
index 0a4a972999..05d3c3dadd 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/SubscribeAllTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/SubscribeAllTask.java
@@ -31,6 +31,8 @@ import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.exception.SubscriptionException;
+import org.apache.james.mailbox.model.MailboxMetaData;
+import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.model.search.MailboxQuery;
 import org.apache.james.task.Task;
 import org.apache.james.task.TaskExecutionDetails;
@@ -93,9 +95,9 @@ public class SubscribeAllTask implements Task {
     public Result run() {
         final MailboxSession session = mailboxManager.createSystemSession(username);
         try {
-            Collection<String> subscriptions = subscriptionManager.subscriptions(session);
-            List<String> names = mailboxManager.search(MailboxQuery.privateMailboxesBuilder(session).build(), session)
-                .map(mailbox -> mailbox.getPath().getName())
+            Collection<MailboxPath> subscriptions = subscriptionManager.subscriptions(session);
+            List<MailboxPath> names = mailboxManager.search(MailboxQuery.privateMailboxesBuilder(session).build(), session)
+                .map(MailboxMetaData::getPath)
                 .collectList()
                 .block();
 
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/SubscribeAllRequestToTaskTest.java b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/SubscribeAllRequestToTaskTest.java
index 96d52cdbf0..09bee339bb 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/SubscribeAllRequestToTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/SubscribeAllRequestToTaskTest.java
@@ -249,12 +249,12 @@ class SubscribeAllRequestToTaskTest {
             .body("submitDate", is(notNullValue()))
             .body("completedDate", is(notNullValue()));
 
-        assertThat(subscriptionManager.subscriptions(session)).containsOnly("INBOX");
+        assertThat(subscriptionManager.subscriptions(session)).containsOnly(MailboxPath.inbox(BOB));
     }
 
     @Test
     void subscribeAllMailboxesShouldUnregisterAdditionalMailbox() throws Exception {
-        subscriptionManager.subscribe(session, "any");
+        subscriptionManager.subscribe(session, MailboxPath.forUser(BOB, "any"));
 
         String taskId = with()
             .queryParam("action", "subscribeAll")


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


[james-project] 01/03: JAMES-3852 Allow parsing escaped MailboxPath

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 11cd2ddd73add2589092ed2527fd3e7d0d371fe3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 11 16:10:18 2022 +0700

    JAMES-3852 Allow parsing escaped MailboxPath
    
    Can be used for storage under string form while still preserving arbitrary
    char in the username.
---
 .../apache/james/mailbox/model/MailboxPath.java    | 36 +++++++++++++++++++
 .../james/mailbox/model/MailboxPathTest.java       | 41 ++++++++++++++++++++++
 2 files changed, 77 insertions(+)

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 dd7e0fe72a..b78d477128 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
@@ -21,11 +21,13 @@ package org.apache.james.mailbox.model;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Predicate;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.translate.LookupTranslator;
 import org.apache.james.core.Username;
 import org.apache.james.mailbox.DefaultMailboxes;
 import org.apache.james.mailbox.MailboxSession;
@@ -34,6 +36,7 @@ import org.apache.james.mailbox.exception.MailboxNameException;
 import org.apache.james.mailbox.exception.TooLongMailboxNameException;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -43,6 +46,12 @@ import com.google.common.collect.Iterables;
  * The path to a mailbox.
  */
 public class MailboxPath {
+
+    private static final Splitter PART_SPLITTER = Splitter.on(':');
+    private static final Joiner PARTS_JOINER = Joiner.on(':');
+    private static final LookupTranslator USERNAME_ESCAPER = new LookupTranslator(Map.of(":", "/;", "/", "//"));
+    private static final LookupTranslator USERNAME_UNESCAPER = new LookupTranslator(Map.of("/;", ":", "//", "/"));
+
     /**
      * Return a {@link MailboxPath} which represent the INBOX of the given
      * session
@@ -62,6 +71,29 @@ public class MailboxPath {
         return new MailboxPath(MailboxConstants.USER_NAMESPACE, username, mailboxName);
     }
 
+    /**
+     * Parses a MailboxPath from the result of MailboxPath::asEscapedString thus supporting the use of ':' character
+     * in the serialized form, in the user part.
+     */
+    public static Optional<MailboxPath> parseEscaped(String asEscapedString) {
+        List<String> parts = PART_SPLITTER.splitToList(asEscapedString);
+        if (parts.size() == 2) {
+            return Optional.of(new MailboxPath(parts.get(0), getUsername(parts), ""));
+        }
+        if (parts.size() == 3) {
+            return Optional.of(new MailboxPath(parts.get(0), getUsername(parts), parts.get(2)));
+        }
+        if (parts.size() > 3) {
+            return Optional.of(new MailboxPath(parts.get(0), getUsername(parts),
+                PARTS_JOINER.join(Iterables.skip(parts, 2))));
+        }
+        return Optional.empty();
+    }
+
+    private static Username getUsername(List<String> parts) {
+        return Username.of(USERNAME_UNESCAPER.translate(parts.get(1)));
+    }
+
     private static final String INVALID_CHARS = "%*";
     private static final CharMatcher INVALID_CHARS_MATCHER = CharMatcher.anyOf(INVALID_CHARS);
     // This is the size that all mailbox backend should support
@@ -214,6 +246,10 @@ public class MailboxPath {
         return namespace + ":" + user.asString() + ":" + name;
     }
 
+    public String asEscapedString() {
+        return namespace + ":" + USERNAME_ESCAPER.translate(user.asString()) + ":" + name;
+    }
+
     public boolean isInbox() {
         return DefaultMailboxes.INBOX.equalsIgnoreCase(name);
     }
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 66fe741230..1a09420637 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
@@ -24,12 +24,17 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.util.stream.Stream;
+
 import org.apache.james.core.Username;
 import org.apache.james.mailbox.DefaultMailboxes;
 import org.apache.james.mailbox.exception.HasEmptyMailboxNameInHierarchyException;
 import org.apache.james.mailbox.exception.MailboxNameException;
 import org.apache.james.mailbox.exception.TooLongMailboxNameException;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 import com.google.common.base.Strings;
 
@@ -37,6 +42,7 @@ import nl.jqno.equalsverifier.EqualsVerifier;
 
 class MailboxPathTest {
     private static final Username USER = Username.of("user");
+    private static final Username BUGGY_USER = Username.of("buggy:bob");
 
     @Test
     void shouldMatchBeanContract() {
@@ -45,6 +51,41 @@ class MailboxPathTest {
             .verify();
     }
 
+    static Stream<Arguments> parseShouldYieldCorrectResults() {
+        return Stream.of(
+            Arguments.of(MailboxPath.forUser(USER, "test")),
+            Arguments.of(MailboxPath.forUser(USER, "a:b")),
+            Arguments.of(MailboxPath.forUser(USER, "a;b")),
+            Arguments.of(MailboxPath.forUser(USER, "a;;b")),
+            Arguments.of(MailboxPath.forUser(USER, "a:b:c:")),
+            Arguments.of(MailboxPath.forUser(USER, "a:b:c:")),
+            Arguments.of(MailboxPath.forUser(USER, ":")),
+            Arguments.of(MailboxPath.forUser(USER, ";")),
+            Arguments.of(MailboxPath.forUser(USER, "")),
+            Arguments.of(MailboxPath.inbox(USER)),
+            Arguments.of(MailboxPath.inbox(Username.of("a;b"))),
+            Arguments.of(MailboxPath.inbox(Username.of(";"))),
+            Arguments.of(MailboxPath.inbox(Username.of(":"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a;;a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a:::;:::;:;;;a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a::a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a/a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a//a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a/:a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a/;a"))),
+            Arguments.of(MailboxPath.inbox(Username.of("a/:::;;/:://:;//:/a"))),
+            Arguments.of(MailboxPath.inbox(BUGGY_USER)),
+            Arguments.of(new MailboxPath("#whatever", USER, "whatever")),
+            Arguments.of(new MailboxPath(null, USER, "whatever")));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void parseShouldYieldCorrectResults(MailboxPath mailboxPath) {
+        assertThat(MailboxPath.parseEscaped(mailboxPath.asEscapedString()))
+            .contains(mailboxPath);
+    }
+
     @Test
     void asStringShouldFormatUser() {
         assertThat(MailboxPath.forUser(USER, "inbox.folder.subfolder").asString())


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


[james-project] 03/03: JAMES-3852 Support subscriptions management on delegated 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 e329ddd4bab93f54a7ae28934dcd51855ec232a9
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 11 16:12:31 2022 +0700

    JAMES-3852 Support subscriptions management on delegated mailboxes
    
    Subscriptions are distinct to the one of mailboxes with similar names
    in one's account.
---
 .../contract/MailboxSetMethodContract.scala        | 84 ++++++++++++++++++++--
 1 file changed, 77 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 5ea41e8d7b..bed70285a0 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
@@ -4638,7 +4638,6 @@ trait MailboxSetMethodContract {
          |}""".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 = {
     val andrePath = MailboxPath.forUser(ANDRE, "mailbox")
@@ -4669,9 +4668,43 @@ trait MailboxSetMethodContract {
          |         {
          |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
          |           "properties": ["id", "isSubscribed"],
-         |           "ids": ["${andreId.serialize}", "${bobId.serialize}"]
+         |           "ids": ["${andreId.serialize}"]
          |          },
-         |       "c4"]
+         |       "c3"],
+         |      ["Mailbox/get",
+         |         {
+         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |           "properties": ["id", "isSubscribed"],
+         |           "ids": ["${bobId.serialize}"]
+         |          },
+         |       "c4"],
+         |      ["Mailbox/set",
+         |          {
+         |               "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |               "update": {
+         |                 "${andreId.serialize}" : {
+         |                   "isSubscribed": false
+         |                 },
+         |                 "${bobId.serialize}" : {
+         |                   "isSubscribed": true
+         |                 }
+         |               }
+         |          },
+         |   "c5"],
+         |      ["Mailbox/get",
+         |         {
+         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |           "properties": ["id", "isSubscribed"],
+         |           "ids": ["${andreId.serialize}"]
+         |          },
+         |       "c6"],
+         |      ["Mailbox/get",
+         |         {
+         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |           "properties": ["id", "isSubscribed"],
+         |           "ids": ["${bobId.serialize}"]
+         |          },
+         |       "c7"]
          |   ]
          |}
          |""".stripMargin
@@ -4689,7 +4722,17 @@ trait MailboxSetMethodContract {
       .body
       .asString
 
-    assertThatJson(response).isEqualTo(
+    assertThatJson(response)
+      .whenIgnoringPaths(
+        "methodResponses[0][1].newState",
+        "methodResponses[0][1].oldState",
+        "methodResponses[2][1].state",
+        "methodResponses[1][1].state",
+        "methodResponses[3][1].newState",
+        "methodResponses[3][1].oldState",
+        "methodResponses[4][1].state",
+        "methodResponses[5][1].state")
+      .isEqualTo(
       s"""{
          |  "sessionState": "${SESSION_STATE.value}",
          |  "methodResponses": [
@@ -4702,16 +4745,43 @@ trait MailboxSetMethodContract {
          |    }, "c2"],
          |    ["Mailbox/get", {
          |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "state": "${INSTANCE.value}",
          |      "list": [{
          |        "id": "${andreId.serialize}",
          |        "isSubscribed": true
-         |      }, {
+         |      }],
+         |      "notFound": []
+         |    }, "c3"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "list": [{
          |        "id": "${bobId.serialize}",
          |        "isSubscribed": false
          |      }],
          |      "notFound": []
-         |    }, "c4"]
+         |    }, "c4"],
+         |    ["Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "updated": {
+         |        "${andreId.serialize}": {},
+         |        "${bobId.serialize}": {}
+         |      }
+         |    }, "c5"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "list": [{
+         |        "id": "${andreId.serialize}",
+         |        "isSubscribed": false
+         |      }],
+         |      "notFound": []
+         |    }, "c6"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "list": [{
+         |        "id": "${bobId.serialize}",
+         |        "isSubscribed": true
+         |      }],
+         |      "notFound": []
+         |    }, "c7"]
          |  ]
          |}""".stripMargin)
   }


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