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/12/07 10:40:01 UTC

[james-project] 04/08: JAMES-3754 Implement RFC-6154 IMAP LIST Extension for Special-Use Mailboxes

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 4fc47a15ad9a05c2a7f0e0977f0f621983b660b4
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 24 23:36:44 2022 +0700

    JAMES-3754 Implement RFC-6154 IMAP LIST Extension for Special-Use Mailboxes
---
 .../org/apache/james/imap/scripts/XList.test       | 17 +++++++++
 .../imap/api/process/DefaultMailboxTyper.java      |  1 +
 .../apache/james/imap/api/process/MailboxType.java | 25 +++++++++-----
 .../imap/decode/parser/ListCommandParser.java      | 17 +++++++++
 .../james/imap/encode/ListingEncodingUtils.java    |  6 ++--
 .../james/imap/message/request/ListRequest.java    |  4 ++-
 .../message/response/AbstractListingResponse.java  |  2 ++
 .../james/imap/message/response/LSubResponse.java  |  5 +++
 .../james/imap/message/response/ListResponse.java  | 19 +++++++---
 .../james/imap/message/response/XListResponse.java |  4 +++
 .../james/imap/processor/DefaultProcessor.java     |  2 +-
 .../apache/james/imap/processor/ListProcessor.java | 40 ++++++++++++++--------
 .../james/imap/processor/XListProcessor.java       |  7 ++--
 .../james/imap/encode/ListResponseEncoderTest.java |  5 ++-
 .../imap/encode/ListingEncodingUtilsTest.java      | 16 ++++-----
 .../pages/architecture/implemented-standards.adoc  |  1 +
 src/site/xdoc/protocols/imap4.xml                  |  1 +
 17 files changed, 126 insertions(+), 46 deletions(-)

diff --git a/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/XList.test b/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/XList.test
index 57ed556f05..ba13b0da8a 100644
--- a/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/XList.test
+++ b/mpt/impl/imap-mailbox/core/src/main/resources/org/apache/james/imap/scripts/XList.test
@@ -35,3 +35,20 @@ S: \* XLIST \(\\HasNoChildren \\Spam\) \"\.\" "Spam"
 S: \* XLIST \(\\HasNoChildren \\Trash\) \"\.\" "Trash"
 }
 S: 10 OK XLIST completed.
+
+C: 13 CREATE Archive
+S: 13 OK \[MAILBOXID \(.+\)\] CREATE completed.
+C: 13 CREATE Other
+S: 13 OK \[MAILBOXID \(.+\)\] CREATE completed.
+
+C: 10 LIST "" * RETURN (SPECIAL-USE)
+SUB {
+S: \* LIST \(\\HasNoChildren\) \"\.\" "INBOX"
+S: \* LIST \(\\HasNoChildren\) \"\.\" "Other"
+S: \* LIST \(\\HasNoChildren \\Drafts\) \"\.\" "Drafts"
+S: \* LIST \(\\HasNoChildren \\Sent\) \"\.\" "Sent"
+S: \* LIST \(\\HasNoChildren \\Junk\) \"\.\" "Spam"
+S: \* LIST \(\\HasNoChildren \\Trash\) \"\.\" "Trash"
+S: \* LIST \(\\HasNoChildren \\Archive\) \"\.\" "Archive"
+}
+S: 10 OK LIST completed.
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/process/DefaultMailboxTyper.java b/protocols/imap/src/main/java/org/apache/james/imap/api/process/DefaultMailboxTyper.java
index fd7d5ba3f4..8611ff5b52 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/api/process/DefaultMailboxTyper.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/process/DefaultMailboxTyper.java
@@ -30,6 +30,7 @@ public class DefaultMailboxTyper implements MailboxTyper {
     private static final ImmutableMap<Role, MailboxType> ROLES_TO_MAILBOX_TYPE = ImmutableMap.of(
         Role.INBOX, MailboxType.INBOX,
         Role.SENT, MailboxType.SENT,
+        Role.ARCHIVE, MailboxType.ARCHIVE,
         Role.SPAM, MailboxType.SPAM,
         Role.DRAFTS, MailboxType.DRAFTS,
         Role.TRASH, MailboxType.TRASH);
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/process/MailboxType.java b/protocols/imap/src/main/java/org/apache/james/imap/api/process/MailboxType.java
index 15253042e2..d9db674ab0 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/api/process/MailboxType.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/process/MailboxType.java
@@ -24,22 +24,29 @@ package org.apache.james.imap.api.process;
  */
 public enum MailboxType {
 
-    INBOX("\\Inbox"),
-    DRAFTS("\\Drafts"),
-    TRASH("\\Trash"),
-    SPAM("\\Spam"),
-    SENT("\\Sent"),
-    STARRED("\\Starred"),
-    ALLMAIL("\\AllMail"),
-    OTHER(null);
+    INBOX("\\Inbox", null),
+    DRAFTS("\\Drafts", "\\Drafts"),
+    TRASH("\\Trash", "\\Trash"),
+    SPAM("\\Spam", "\\Junk"),
+    SENT("\\Sent", "\\Sent"),
+    STARRED("\\Starred", "\\Flagged"),
+    ALLMAIL("\\AllMail", "\\All"),
+    ARCHIVE(null, "\\Archive"),
+    OTHER(null, null);
 
     private final String attributeName;
+    private final String rfc6154attributeName;
 
-    MailboxType(String attributeName) {
+    MailboxType(String attributeName, String rfc6154attributeName) {
         this.attributeName = attributeName;
+        this.rfc6154attributeName = rfc6154attributeName;
     }
 
     public String getAttributeName() {
         return attributeName;
     }
+
+    public String getRfc6154attributeName() {
+        return rfc6154attributeName;
+    }
 }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/ListCommandParser.java b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/ListCommandParser.java
index c913e12b4c..288f2ed285 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/ListCommandParser.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/decode/parser/ListCommandParser.java
@@ -134,6 +134,8 @@ public class ListCommandParser extends AbstractUidCommandParser {
         char c = request.nextWordChar();
         if (c == 'T' || c == 't') {
             return readStatus(request);
+        } else if (c == 'P' || c == 'p') {
+            return Pair.of(readSpecialUse(request), Optional.empty());
         } else {
             return Pair.of(readReturnSubscribed(request), Optional.empty());
         }
@@ -149,6 +151,21 @@ public class ListCommandParser extends AbstractUidCommandParser {
         return Pair.of(ListReturnOption.STATUS, Optional.of(StatusCommandParser.statusDataItems(request)));
     }
 
+    private ListReturnOption readSpecialUse(ImapRequestLineReader request) throws DecodingException {
+        // 'S' is already consummed
+        assertChar(request, 'P', 'p');
+        assertChar(request, 'E', 'e');
+        assertChar(request, 'C', 'c');
+        assertChar(request, 'I', 'i');
+        assertChar(request, 'A', 'a');
+        assertChar(request, 'L', 'l');
+        assertChar(request, '-', '-');
+        assertChar(request, 'U', 'u');
+        assertChar(request, 'S', 's');
+        assertChar(request, 'E', 'e');
+        return ListReturnOption.SPECIAL_USE;
+    }
+
     private ListSelectOption readSubscribed(ImapRequestLineReader request) throws DecodingException {
         consumeSubscribed(request);
         return ListSelectOption.SUBSCRIBED;
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/encode/ListingEncodingUtils.java b/protocols/imap/src/main/java/org/apache/james/imap/encode/ListingEncodingUtils.java
index f0b22e2f24..122780802e 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/encode/ListingEncodingUtils.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/encode/ListingEncodingUtils.java
@@ -29,7 +29,6 @@ import java.util.EnumSet;
 
 import org.apache.james.imap.api.ImapCommand;
 import org.apache.james.imap.api.ImapConstants;
-import org.apache.james.imap.api.process.MailboxType;
 import org.apache.james.imap.message.response.AbstractListingResponse;
 import org.apache.james.imap.message.response.ListResponse;
 import org.apache.james.mailbox.model.MailboxMetaData;
@@ -83,7 +82,7 @@ public class ListingEncodingUtils {
 
         selectabilityAsString(response.getSelectability(), builder);
         childrenAsString(response.getChildren(), builder);
-        mailboxAttributeAsString(response.getType(), builder);
+        mailboxAttributeAsString(response.getTypeAsString(), builder);
 
         if (response instanceof ListResponse) {
             ListResponse listResponse = (ListResponse) response;
@@ -121,8 +120,7 @@ public class ListingEncodingUtils {
         }
     }
 
-    private static ImmutableList.Builder<byte[]> mailboxAttributeAsString(MailboxType type, ImmutableList.Builder<byte[]> builder) {
-        String attributeName = type.getAttributeName();
+    private static ImmutableList.Builder<byte[]> mailboxAttributeAsString(String attributeName, ImmutableList.Builder<byte[]> builder) {
         if (attributeName != null) {
             return builder.add(attributeName.getBytes(StandardCharsets.US_ASCII));
         }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/message/request/ListRequest.java b/protocols/imap/src/main/java/org/apache/james/imap/message/request/ListRequest.java
index 37045a001c..bf3e601ae1 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/message/request/ListRequest.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/message/request/ListRequest.java
@@ -44,7 +44,9 @@ public class ListRequest extends AbstractImapRequest {
         // https://www.rfc-editor.org/rfc/rfc5819.html LIST STATUS
         STATUS,
         // https://www.rfc-editor.org/rfc/rfc8440.html
-        MYRIGHTS
+        MYRIGHTS,
+        // https://www.rfc-editor.org/rfc/rfc6154.html
+        SPECIAL_USE
     }
 
     private final String baseReferenceName;
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/message/response/AbstractListingResponse.java b/protocols/imap/src/main/java/org/apache/james/imap/message/response/AbstractListingResponse.java
index c50a882d32..bcf899b8d5 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/message/response/AbstractListingResponse.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/message/response/AbstractListingResponse.java
@@ -65,6 +65,8 @@ public abstract class AbstractListingResponse {
         return type;
     }
 
+    public abstract String getTypeAsString();
+
     public MailboxMetaData.Children getChildren() {
         return children;
     }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/message/response/LSubResponse.java b/protocols/imap/src/main/java/org/apache/james/imap/message/response/LSubResponse.java
index a4b1aa4a7f..9622a2dde4 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/message/response/LSubResponse.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/message/response/LSubResponse.java
@@ -36,4 +36,9 @@ public final class LSubResponse extends AbstractListingResponse implements ImapR
         }
         return MailboxMetaData.Selectability.NONE;
     }
+
+    @Override
+    public String getTypeAsString() {
+        return getType().getRfc6154attributeName();
+    }
 }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/message/response/ListResponse.java b/protocols/imap/src/main/java/org/apache/james/imap/message/response/ListResponse.java
index cc0513b76e..ba8c6f785f 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/message/response/ListResponse.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/message/response/ListResponse.java
@@ -48,6 +48,7 @@ public final class ListResponse extends AbstractListingResponse implements ImapR
         private char hierarchyDelimiter;
         private boolean returnSubscribed;
         private boolean returnNonExistent;
+        private MailboxType mailboxType;
         private ImmutableSet.Builder<ChildInfo> childInfos;
 
         public Builder() {
@@ -89,6 +90,11 @@ public final class ListResponse extends AbstractListingResponse implements ImapR
             return this;
         }
 
+        public Builder mailboxType(MailboxType mailboxType) {
+            this.mailboxType = mailboxType;
+            return this;
+        }
+
         public Builder forMetaData(MailboxMetaData mailboxMetaData) {
             return children(mailboxMetaData.inferiors())
                 .selectability(mailboxMetaData.getSelectability())
@@ -102,7 +108,8 @@ public final class ListResponse extends AbstractListingResponse implements ImapR
                 .selectability(MailboxMetaData.Selectability.NONE)
                 .hierarchyDelimiter(MailboxConstants.DEFAULT_DELIMITER)
                 .returnSubscribed(RETURN_SUBSCRIBED)
-                .returnNonExistent(RETURN_NON_EXISTENT);
+                .returnNonExistent(RETURN_NON_EXISTENT)
+                .mailboxType(MailboxType.OTHER);
         }
 
         private EnumSet<ChildInfo> buildChildInfos() {
@@ -117,7 +124,7 @@ public final class ListResponse extends AbstractListingResponse implements ImapR
         public ListResponse build() {
 
             return new ListResponse(children, selectability, name, hierarchyDelimiter, returnSubscribed,
-                returnNonExistent, buildChildInfos());
+                returnNonExistent, buildChildInfos(), mailboxType);
         }
     }
 
@@ -134,8 +141,8 @@ public final class ListResponse extends AbstractListingResponse implements ImapR
 
     public ListResponse(MailboxMetaData.Children children, MailboxMetaData.Selectability selectability,
                         String name, char hierarchyDelimiter, boolean returnSubscribed, boolean returnNonExistent,
-                        EnumSet<ChildInfo> childInfos) {
-        super(children, selectability, name, hierarchyDelimiter, MailboxType.OTHER);
+                        EnumSet<ChildInfo> childInfos, MailboxType mailboxType) {
+        super(children, selectability, name, hierarchyDelimiter, mailboxType);
         this.returnSubscribed = returnSubscribed;
         this.returnNonExistent = returnNonExistent;
         this.childInfos = childInfos;
@@ -153,4 +160,8 @@ public final class ListResponse extends AbstractListingResponse implements ImapR
         return childInfos;
     }
 
+    @Override
+    public String getTypeAsString() {
+        return getType().getRfc6154attributeName();
+    }
 }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/message/response/XListResponse.java b/protocols/imap/src/main/java/org/apache/james/imap/message/response/XListResponse.java
index df4b392aae..268ad0e9a3 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/message/response/XListResponse.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/message/response/XListResponse.java
@@ -32,4 +32,8 @@ public class XListResponse extends AbstractListingResponse implements ImapRespon
         super(children, selectability, name, hierarchyDelimiter, type);
     }
 
+    @Override
+    public String getTypeAsString() {
+        return getType().getAttributeName();
+    }
 }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
index 40d153c8c0..6913be379a 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
@@ -83,7 +83,7 @@ public class DefaultProcessor implements ImapProcessor {
         builder.add(statusProcessor);
         builder.add(new LSubProcessor(mailboxManager, subscriptionManager, statusResponseFactory, metricFactory));
         builder.add(new XListProcessor(mailboxManager, statusResponseFactory, mailboxTyper, metricFactory, subscriptionManager));
-        builder.add(new ListProcessor<>(mailboxManager, statusResponseFactory, metricFactory, subscriptionManager, statusProcessor));
+        builder.add(new ListProcessor<>(mailboxManager, statusResponseFactory, metricFactory, subscriptionManager, statusProcessor, mailboxTyper));
         builder.add(new SearchProcessor(mailboxManager, statusResponseFactory, metricFactory));
         SelectProcessor selectProcessor = new SelectProcessor(mailboxManager, eventBus, statusResponseFactory, metricFactory);
         builder.add(selectProcessor);
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/ListProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/ListProcessor.java
index 2e69aa0d44..fd44a047ed 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/ListProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/ListProcessor.java
@@ -39,6 +39,7 @@ import org.apache.james.imap.api.message.response.ImapResponseMessage;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.api.process.MailboxType;
+import org.apache.james.imap.api.process.MailboxTyper;
 import org.apache.james.imap.main.PathConverter;
 import org.apache.james.imap.message.request.ListRequest;
 import org.apache.james.imap.message.response.ListResponse;
@@ -70,23 +71,29 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
     public static final boolean RETURN_SUBSCRIBED = true;
     public static final boolean RETURN_NON_EXISTENT = true;
     private static final Logger LOGGER = LoggerFactory.getLogger(ListProcessor.class);
-    private static final List<Capability> CAPA = ImmutableList.of(Capability.of("LIST-EXTENDED"), Capability.of("LIST-STATUS"), Capability.of("LIST-MYRIGHTS"));
+    private static final List<Capability> CAPA = ImmutableList.of(
+        Capability.of("LIST-EXTENDED"),
+        Capability.of("LIST-STATUS"),
+        Capability.of("LIST-MYRIGHTS"),
+        Capability.of("SPECIAL-USE"));
 
     private final SubscriptionManager subscriptionManager;
     private final StatusProcessor statusProcessor;
+    protected final MailboxTyper mailboxTyper;
 
     public ListProcessor(MailboxManager mailboxManager, StatusResponseFactory factory,
                          MetricFactory metricFactory, SubscriptionManager subscriptionManager,
-                         StatusProcessor statusProcessor) {
-        this((Class<T>) ListRequest.class, mailboxManager, factory, metricFactory, subscriptionManager, statusProcessor);
+                         StatusProcessor statusProcessor, MailboxTyper mailboxTyper) {
+        this((Class<T>) ListRequest.class, mailboxManager, factory, metricFactory, subscriptionManager, statusProcessor, mailboxTyper);
     }
 
     public ListProcessor(Class<T> clazz, MailboxManager mailboxManager, StatusResponseFactory factory,
                          MetricFactory metricFactory, SubscriptionManager subscriptionManager,
-                         StatusProcessor statusProcessor) {
+                         StatusProcessor statusProcessor, MailboxTyper mailboxTyper) {
         super(clazz, mailboxManager, factory, metricFactory);
         this.subscriptionManager = subscriptionManager;
         this.statusProcessor = statusProcessor;
+        this.mailboxTyper = mailboxTyper;
     }
 
     @Override
@@ -131,7 +138,7 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
     protected ImapResponseMessage createResponse(MailboxMetaData.Children children, MailboxMetaData.Selectability selectability, String name,
                                                  char hierarchyDelimiter, MailboxType type) {
         return new ListResponse(children, selectability, name, hierarchyDelimiter, !RETURN_SUBSCRIBED,
-            !RETURN_NON_EXISTENT, EnumSet.noneOf(ListResponse.ChildInfo.class));
+            !RETURN_NON_EXISTENT, EnumSet.noneOf(ListResponse.ChildInfo.class), type);
     }
 
     private void respondNamespace(String referenceName, Responder responder, MailboxSession mailboxSession) {
@@ -197,7 +204,7 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
                     metaData.getSelectability(),
                     mailboxName(isRelative, metaData.getPath(), metaData.getHierarchyDelimiter()),
                     metaData.getHierarchyDelimiter(),
-                    getMailboxType(session, metaData.getPath()))))
+                    getMailboxType(request, session, metaData.getPath()))))
             .doOnNext(metaData -> respondMyRights(request, responder, mailboxSession, metaData))
             .flatMap(metaData -> request.getStatusDataItems().map(statusDataItems -> statusProcessor.sendStatus(metaData.getPath(), statusDataItems, responder, session, mailboxSession)).orElse(Mono.empty()))
             .then();
@@ -207,7 +214,7 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
         return Mono.zip(getMailboxManager().search(mailboxQuery, Minimal, mailboxSession).collectList()
                     .map(searchedResultList -> searchedResultList.stream().collect(Collectors.toMap(MailboxMetaData::getPath, Function.identity()))),
                 Flux.from(Throwing.supplier(() -> subscriptionManager.subscriptionsReactive(mailboxSession)).get()).collectList())
-            .map(tuple -> getListResponseForSelectSubscribed(tuple.getT1(), tuple.getT2(), request, mailboxSession, isRelative, mailboxQuery))
+            .map(tuple -> getListResponseForSelectSubscribed(session, tuple.getT1(), tuple.getT2(), request, mailboxSession, isRelative, mailboxQuery))
             .flatMapIterable(list -> list)
             .doOnNext(pathAndResponse -> responder.respond(pathAndResponse.getMiddle()))
             .doOnNext(pathAndResponse -> pathAndResponse.getRight().ifPresent(mailboxMetaData -> respondMyRights(request, responder, mailboxSession, mailboxMetaData)))
@@ -215,10 +222,10 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
             .then();
     }
 
-    private List<Triple<MailboxPath, ListResponse, Optional<MailboxMetaData>>> getListResponseForSelectSubscribed(Map<MailboxPath, MailboxMetaData> searchedResultMap, List<MailboxPath> allSubscribedSearch,
+    private List<Triple<MailboxPath, ListResponse, Optional<MailboxMetaData>>> getListResponseForSelectSubscribed(ImapSession session, Map<MailboxPath, MailboxMetaData> searchedResultMap, List<MailboxPath> allSubscribedSearch,
                                                                                      ListRequest listRequest, MailboxSession mailboxSession, boolean relative, MailboxQuery mailboxQuery) {
         ImmutableList.Builder<Triple<MailboxPath, ListResponse, Optional<MailboxMetaData>>> responseBuilders = ImmutableList.builder();
-        List<Pair<MailboxPath, ListResponse>> listRecursiveMatch = listRecursiveMatch(searchedResultMap, allSubscribedSearch, mailboxSession, relative, listRequest);
+        List<Pair<MailboxPath, ListResponse>> listRecursiveMatch = listRecursiveMatch(session, searchedResultMap, allSubscribedSearch, mailboxSession, relative, listRequest);
 
         listRecursiveMatch.forEach(pair -> responseBuilders.add(Triple.of(pair.getLeft(), pair.getRight(), Optional.ofNullable(searchedResultMap.get(pair.getLeft())))));
         Set<MailboxPath> listRecursiveMatchPath = listRecursiveMatch.stream().map(Pair::getKey).collect(Collectors.toUnmodifiableSet());
@@ -226,24 +233,25 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
         allSubscribedSearch.stream()
             .filter(subscribed -> !listRecursiveMatchPath.contains(subscribed))
             .filter(mailboxQuery::isPathMatch)
-            .map(subscribed -> buildListResponse(searchedResultMap, mailboxSession, relative, subscribed))
+            .map(subscribed -> buildListResponse(listRequest, searchedResultMap, session, relative, subscribed))
             .forEach(pair -> responseBuilders.add(Triple.of(pair.getLeft(), pair.getRight(), Optional.ofNullable(searchedResultMap.get(pair.getLeft())))));
 
         return responseBuilders.build();
     }
 
-    private Pair<MailboxPath, ListResponse> buildListResponse(Map<MailboxPath, MailboxMetaData> searchedResultMap, MailboxSession mailboxSession, boolean relative, MailboxPath subscribed) {
+    private Pair<MailboxPath, ListResponse> buildListResponse(ListRequest listRequest, Map<MailboxPath, MailboxMetaData> searchedResultMap, ImapSession session, boolean relative, MailboxPath subscribed) {
         return Pair.of(subscribed, Optional.ofNullable(searchedResultMap.get(subscribed))
             .map(mailboxMetaData -> ListResponse.builder()
                 .returnSubscribed(RETURN_SUBSCRIBED)
                 .forMetaData(mailboxMetaData)
                 .name(mailboxName(relative, subscribed, mailboxMetaData.getHierarchyDelimiter()))
-                .returnNonExistent(!RETURN_NON_EXISTENT))
+                .returnNonExistent(!RETURN_NON_EXISTENT)
+                .mailboxType(getMailboxType(listRequest, session, mailboxMetaData.getPath())))
             .orElseGet(() -> ListResponse.builder().nonExitingSubscribedMailbox(subscribed))
             .build());
     }
 
-    private List<Pair<MailboxPath, ListResponse>> listRecursiveMatch(Map<MailboxPath, MailboxMetaData> searchedResultMap,
+    private List<Pair<MailboxPath, ListResponse>> listRecursiveMatch(ImapSession session, Map<MailboxPath, MailboxMetaData> searchedResultMap,
                                                                      List<MailboxPath> allSubscribedSearch, MailboxSession mailboxSession,
                                                                      boolean relative, ListRequest listRequest) {
         if (!listRequest.getSelectOptions().contains(RECURSIVEMATCH)) {
@@ -263,6 +271,7 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
                     .name(mailboxName(relative, metaData.getPath(), metaData.getHierarchyDelimiter()))
                     .childInfos(ListResponse.ChildInfo.SUBSCRIBED)
                     .returnSubscribed(allSubscribedSearch.contains(pair.getKey()))
+                    .mailboxType(getMailboxType(listRequest, session, metaData.getPath()))
                     .build();
                 return Pair.of(pair.getKey(), listResponse);
             })
@@ -323,7 +332,10 @@ public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcess
      * @param path    mailbox's path
      * @return MailboxType value
      */
-    protected MailboxType getMailboxType(ImapSession session, MailboxPath path) {
+    protected MailboxType getMailboxType(ListRequest listRequest, ImapSession session, MailboxPath path) {
+        if (listRequest.getReturnOptions().contains(ListRequest.ListReturnOption.SPECIAL_USE)) {
+            return mailboxTyper.getMailboxType(session, path);
+        }
         return MailboxType.OTHER;
     }
 
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/XListProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/XListProcessor.java
index 7bab0ff87e..5ae2066be6 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/XListProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/XListProcessor.java
@@ -29,6 +29,7 @@ import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.api.process.MailboxType;
 import org.apache.james.imap.api.process.MailboxTyper;
+import org.apache.james.imap.message.request.ListRequest;
 import org.apache.james.imap.message.request.XListRequest;
 import org.apache.james.imap.message.response.XListResponse;
 import org.apache.james.mailbox.MailboxManager;
@@ -45,12 +46,10 @@ import com.google.common.collect.ImmutableList;
 public class XListProcessor extends ListProcessor<XListRequest> implements CapabilityImplementingProcessor {
 
     private static final List<Capability> XLIST_CAPS = ImmutableList.of(SUPPORTS_XLIST);
-    private final MailboxTyper mailboxTyper;
 
     public XListProcessor(MailboxManager mailboxManager, StatusResponseFactory factory, MailboxTyper mailboxTyper,
                           MetricFactory metricFactory, SubscriptionManager subscriptionManager) {
-        super(XListRequest.class, mailboxManager, factory, metricFactory, subscriptionManager, null);
-        this.mailboxTyper = mailboxTyper;
+        super(XListRequest.class, mailboxManager, factory, metricFactory, subscriptionManager, null, mailboxTyper);
     }
 
     @Override
@@ -70,7 +69,7 @@ public class XListProcessor extends ListProcessor<XListRequest> implements Capab
     }
 
     @Override
-    protected MailboxType getMailboxType(ImapSession session, MailboxPath path) {
+    protected MailboxType getMailboxType(ListRequest request, ImapSession session, MailboxPath path) {
         return mailboxTyper.getMailboxType(session, path);
     }
 }
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/encode/ListResponseEncoderTest.java b/protocols/imap/src/test/java/org/apache/james/imap/encode/ListResponseEncoderTest.java
index 8cbb86f9d2..d6559d7dda 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/encode/ListResponseEncoderTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/encode/ListResponseEncoderTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.EnumSet;
 
+import org.apache.james.imap.api.process.MailboxType;
 import org.apache.james.imap.encode.base.ByteImapResponseWriter;
 import org.apache.james.imap.encode.base.ImapResponseComposerImpl;
 import org.apache.james.imap.message.response.ListResponse;
@@ -47,7 +48,9 @@ class ListResponseEncoderTest {
 
     @Test
     void encoderShouldIncludeListCommand() throws Exception {
-        encoder.encode(new ListResponse(MailboxMetaData.Children.HAS_CHILDREN, MailboxMetaData.Selectability.NONE, "name", '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class)), composer);
+        encoder.encode(new ListResponse(MailboxMetaData.Children.HAS_CHILDREN, MailboxMetaData.Selectability.NONE,
+            "name", '.', false,
+            false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER), composer);
         assertThat(writer.getString()).startsWith("* LIST");
     }
 }
diff --git a/protocols/imap/src/test/java/org/apache/james/imap/encode/ListingEncodingUtilsTest.java b/protocols/imap/src/test/java/org/apache/james/imap/encode/ListingEncodingUtilsTest.java
index 1d3728f430..ab748e79a8 100644
--- a/protocols/imap/src/test/java/org/apache/james/imap/encode/ListingEncodingUtilsTest.java
+++ b/protocols/imap/src/test/java/org/apache/james/imap/encode/ListingEncodingUtilsTest.java
@@ -43,7 +43,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldWriteNilDelimiterWhenUnassigned() throws Exception {
-        ListResponse input = new ListResponse(Children.HAS_CHILDREN, Selectability.NONE, nameParameter, ((char) Character.UNASSIGNED), false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.HAS_CHILDREN, Selectability.NONE, nameParameter, ((char) Character.UNASSIGNED), false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\HasChildren) NIL \"mailbox\"\r\n");
@@ -51,7 +51,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldWriteAnyDelimiter() throws Exception {
-        ListResponse input = new ListResponse(Children.HAS_CHILDREN, Selectability.NONE, nameParameter, '#', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.HAS_CHILDREN, Selectability.NONE, nameParameter, '#', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\HasChildren) \"#\" \"mailbox\"\r\n");
@@ -59,7 +59,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldNotIncludeAttributeWhenNone() throws Exception {
-        ListResponse input = new ListResponse(Children.CHILDREN_ALLOWED_BUT_UNKNOWN, MailboxMetaData.Selectability.NONE, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.CHILDREN_ALLOWED_BUT_UNKNOWN, MailboxMetaData.Selectability.NONE, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST () \".\" \"mailbox\"\r\n");
@@ -67,7 +67,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldAddHasChildrenToAttributes() throws Exception {
-        ListResponse input = new ListResponse(Children.HAS_CHILDREN, Selectability.NONE, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.HAS_CHILDREN, Selectability.NONE, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\HasChildren) \".\" \"mailbox\"\r\n");
@@ -75,7 +75,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldAddHasNoChildrenToAttributes() throws Exception {
-        ListResponse input = new ListResponse(Children.HAS_NO_CHILDREN, Selectability.NONE, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.HAS_NO_CHILDREN, Selectability.NONE, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\HasNoChildren) \".\" \"mailbox\"\r\n");
@@ -83,7 +83,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldAddSeveralAttributes() throws Exception {
-        ListResponse input = new ListResponse(Children.NO_INFERIORS, Selectability.NOSELECT, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.NO_INFERIORS, Selectability.NOSELECT, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\Noselect \\Noinferiors) \".\" \"mailbox\"\r\n");
@@ -91,7 +91,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldAddMarkedAttribute() throws Exception {
-        ListResponse input = new ListResponse(Children.CHILDREN_ALLOWED_BUT_UNKNOWN, Selectability.MARKED, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.CHILDREN_ALLOWED_BUT_UNKNOWN, Selectability.MARKED, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\Marked) \".\" \"mailbox\"\r\n");
@@ -99,7 +99,7 @@ public class ListingEncodingUtilsTest  {
 
     @Test
     void encodeShouldAddUnmarkedAttribute() throws Exception {
-        ListResponse input = new ListResponse(Children.CHILDREN_ALLOWED_BUT_UNKNOWN, Selectability.UNMARKED, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class));
+        ListResponse input = new ListResponse(Children.CHILDREN_ALLOWED_BUT_UNKNOWN, Selectability.UNMARKED, nameParameter, '.', false, false, EnumSet.noneOf(ListResponse.ChildInfo.class), MailboxType.OTHER);
 
         ListingEncodingUtils.encodeListingResponse(LIST_COMMAND, composer, input);
         assertThat(writer.getString()).isEqualTo("* LIST (\\Unmarked) \".\" \"mailbox\"\r\n");
diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/architecture/implemented-standards.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/architecture/implemented-standards.adoc
index 398180a13a..7ed8e6fd5a 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/architecture/implemented-standards.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/architecture/implemented-standards.adoc
@@ -71,6 +71,7 @@ The following IMAP specifications are implemented:
  - link:https://www.rfc-editor.org/rfc/rfc5258.html[RFC-5258] IMAP LIST Command Extensions
  - link:https://www.rfc-editor.org/rfc/rfc5819.html[RFC-5819] IMAP4 Extension for Returning STATUS Information in Extended LIST
  - link:https://www.rfc-editor.org/rfc/rfc8440.html[RFC-8440] IMAP4 Extension for Returning MYRIGHTS Information in Extended LIST
+ - link:https://www.rfc-editor.org/rfc/rfc8440.html[RFC-6154] IMAP LIST Extension for Special-Use Mailboxes
 
 Partially implemented specifications:
 
diff --git a/src/site/xdoc/protocols/imap4.xml b/src/site/xdoc/protocols/imap4.xml
index 1af6934fd2..96c1be2cab 100644
--- a/src/site/xdoc/protocols/imap4.xml
+++ b/src/site/xdoc/protocols/imap4.xml
@@ -66,6 +66,7 @@
        <li>IMAP LIST Command Extensions (link:https://www.rfc-editor.org/rfc/rfc5258.html)</li>
        <li>IMAP4 Extension for Returning STATUS Information in Extended LIST (https://www.rfc-editor.org/rfc/rfc5819.html)</li>
        <li>IMAP4 Extension for Returning MYRIGHTS Information in Extended LIST (https://www.rfc-editor.org/rfc/rfc8440.html)</li>
+       <li>IMAP LIST Extension for Special-Use Mailboxes (https://www.rfc-editor.org/rfc/rfc6154.html)</li>
      </ul>
      <p>We follow RFC2683 recommendations for our implementations:</p>
      <ul>


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