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

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

commit d349bfcf9b0574c9e0219fd53e97fb3b84d26864
Author: TungTV <vtt...@linagora.com>
AuthorDate: Thu Oct 31 09:28:54 2024 +0700

    JAMES-3754 Update IMAP ID Extension - support return id fields response 
when configured
---
 docs/modules/servers/partials/configure/imap.adoc  |  3 ++
 .../apache/james/imap/api/ImapConfiguration.java   | 48 ++++++++++++++++++----
 .../james/imap/encode/IdResponseEncoder.java       | 16 ++++++--
 .../james/imap/message/response/IdResponse.java    |  6 +--
 .../apache/james/imap/processor/IdProcessor.java   | 13 +++++-
 .../apache/james/imapserver/netty/IMAPServer.java  | 14 +++++++
 .../james/imapserver/netty/IMAPServerTest.java     | 34 +++++++++++++++
 .../imapServerIdCommandResponseFields.xml          | 19 +++++++++
 src/site/xdoc/server/config-imap4.xml              |  3 ++
 9 files changed, 142 insertions(+), 14 deletions(-)

diff --git a/docs/modules/servers/partials/configure/imap.adoc 
b/docs/modules/servers/partials/configure/imap.adoc
index ad91001912..ded1332c5d 100644
--- a/docs/modules/servers/partials/configure/imap.adoc
+++ b/docs/modules/servers/partials/configure/imap.adoc
@@ -142,6 +142,9 @@ Optional integer, defaults to 2 times the count of CPUs.
 
 | lowWriteBufferWaterMark
 | Netty's write buffer low watermark configuration. Unit supported: none, K, 
M. Netty defaults applied.
+
+| idCommandResponse.field
+| Store the fields response for ID Command, with each tag containing a 
name-value pair corresponding to the attribute name. Ref: rfc2971
 |===
 
 == OIDC setup
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java 
b/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java
index 7522eaa6e2..7aa919c10d 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java
@@ -20,6 +20,7 @@
 package org.apache.james.imap.api;
 
 import java.time.Duration;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
@@ -30,6 +31,7 @@ import org.apache.james.imap.api.message.Capability;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 public class ImapConfiguration {
@@ -63,6 +65,7 @@ public class ImapConfiguration {
         private Optional<Boolean> provisionDefaultMailboxes;
         private Optional<Properties> customProperties;
         private ImmutableSet<String> additionalConnectionChecks;
+        private ImmutableMap<String, String> idFieldsResponse;
 
         private Builder() {
             this.appendLimit = Optional.empty();
@@ -76,6 +79,7 @@ public class ImapConfiguration {
             this.provisionDefaultMailboxes = Optional.empty();
             this.customProperties = Optional.empty();
             this.additionalConnectionChecks = ImmutableSet.of();
+            this.idFieldsResponse = ImmutableMap.of();
         }
 
         public Builder idleTimeInterval(long idleTimeInterval) {
@@ -150,6 +154,16 @@ public class ImapConfiguration {
             return this;
         }
 
+        public Builder idFieldsResponse(ImmutableMap<String, String> 
idFieldsResponse) {
+            Preconditions.checkArgument(idFieldsResponse.size() <= 30, "The 
number of fields should not exceed 30. (rfc2971)");
+            for (Map.Entry<String, String> keyValue : 
idFieldsResponse.entrySet()) {
+                Preconditions.checkArgument(keyValue.getKey().length() <= 30, 
"The length of field name should not exceed 30. (rfc2971)");
+                Preconditions.checkArgument(keyValue.getValue().length() <= 
1024, "The length of field value should not exceed 1024. (rfc2971)");
+            }
+            this.idFieldsResponse = idFieldsResponse;
+            return this;
+        }
+
         public ImapConfiguration build() {
             ImmutableSet<Capability> normalizeDisableCaps = 
disabledCaps.stream()
                     .filter(Builder::noBlankString)
@@ -167,7 +181,8 @@ public class ImapConfiguration {
                     isCondstoreEnable.orElse(DEFAULT_CONDSTORE_DISABLE),
                     
provisionDefaultMailboxes.orElse(DEFAULT_PROVISION_DEFAULT_MAILBOXES),
                     customProperties.orElseGet(Properties::new),
-                    additionalConnectionChecks);
+                    additionalConnectionChecks,
+                    idFieldsResponse);
         }
     }
 
@@ -182,8 +197,20 @@ public class ImapConfiguration {
     private final boolean provisionDefaultMailboxes;
     private final Properties customProperties;
     private final ImmutableSet<String> additionalConnectionChecks;
-
-    private ImapConfiguration(Optional<Long> appendLimit, boolean enableIdle, 
long idleTimeInterval, int concurrentRequests, int maxQueueSize, TimeUnit 
idleTimeIntervalUnit, ImmutableSet<Capability> disabledCaps, boolean 
isCondstoreEnable, boolean provisionDefaultMailboxes, Properties 
customProperties, ImmutableSet<String> additionalConnectionChecks) {
+    private final ImmutableMap<String, String> idFieldsResponse;
+
+    private ImapConfiguration(Optional<Long> appendLimit,
+                              boolean enableIdle,
+                              long idleTimeInterval,
+                              int concurrentRequests,
+                              int maxQueueSize,
+                              TimeUnit idleTimeIntervalUnit,
+                              ImmutableSet<Capability> disabledCaps,
+                              boolean isCondstoreEnable,
+                              boolean provisionDefaultMailboxes,
+                              Properties customProperties,
+                              ImmutableSet<String> additionalConnectionChecks,
+                              ImmutableMap<String, String> idFieldsResponse) {
         this.appendLimit = appendLimit;
         this.enableIdle = enableIdle;
         this.idleTimeInterval = idleTimeInterval;
@@ -195,6 +222,7 @@ public class ImapConfiguration {
         this.provisionDefaultMailboxes = provisionDefaultMailboxes;
         this.customProperties = customProperties;
         this.additionalConnectionChecks = additionalConnectionChecks;
+        this.idFieldsResponse = idFieldsResponse;
     }
 
     public Optional<Long> getAppendLimit() {
@@ -245,10 +273,13 @@ public class ImapConfiguration {
         return additionalConnectionChecks;
     }
 
+    public ImmutableMap<String, String> getIdFieldsResponse() {
+        return idFieldsResponse;
+    }
+
     @Override
     public final boolean equals(Object obj) {
-        if (obj instanceof ImapConfiguration) {
-            ImapConfiguration that = (ImapConfiguration)obj;
+        if (obj instanceof ImapConfiguration that) {
             return Objects.equal(that.isEnableIdle(), enableIdle)
                 && Objects.equal(that.getIdleTimeInterval(), idleTimeInterval)
                 && Objects.equal(that.getAppendLimit(), appendLimit)
@@ -259,7 +290,8 @@ public class ImapConfiguration {
                 && Objects.equal(that.isProvisionDefaultMailboxes(), 
provisionDefaultMailboxes)
                 && Objects.equal(that.getCustomProperties(), customProperties)
                 && Objects.equal(that.isCondstoreEnable(), isCondstoreEnable)
-                && Objects.equal(that.getAdditionalConnectionChecks(), 
additionalConnectionChecks);
+                && Objects.equal(that.getAdditionalConnectionChecks(), 
additionalConnectionChecks)
+                && Objects.equal(that.getIdFieldsResponse(), idFieldsResponse);
         }
         return false;
     }
@@ -267,7 +299,8 @@ public class ImapConfiguration {
     @Override
     public final int hashCode() {
         return Objects.hashCode(enableIdle, idleTimeInterval, 
idleTimeIntervalUnit, disabledCaps, isCondstoreEnable,
-            concurrentRequests, maxQueueSize, appendLimit, 
provisionDefaultMailboxes, customProperties, additionalConnectionChecks);
+            concurrentRequests, maxQueueSize, appendLimit, 
provisionDefaultMailboxes, customProperties, additionalConnectionChecks,
+            idFieldsResponse);
     }
 
     @Override
@@ -284,6 +317,7 @@ public class ImapConfiguration {
                 .add("provisionDefaultMailboxes", provisionDefaultMailboxes)
                 .add("customProperties", customProperties)
                 .add("additionalConnectionChecks", additionalConnectionChecks)
+                .add("idFieldsResponse", idFieldsResponse)
                 .toString();
     }
 }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/encode/IdResponseEncoder.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/encode/IdResponseEncoder.java
index fa7e7bfcef..6ed6192d73 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/encode/IdResponseEncoder.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/encode/IdResponseEncoder.java
@@ -33,8 +33,18 @@ public class IdResponseEncoder implements 
ImapResponseEncoder<IdResponse> {
     @Override
     public void encode(IdResponse existsResponse, ImapResponseComposer 
composer) throws IOException {
         composer.untagged()
-            .message("ID")
-            .nil()
-            .end();
+            .message("ID");
+
+        if (existsResponse.fields().isEmpty()) {
+            composer.nil();
+        } else {
+            composer.openParen();
+            for (Map.Entry<String, String> keyValue : 
existsResponse.fields().entrySet()) {
+                composer.quote(keyValue.getKey());
+                composer.quote(keyValue.getValue());
+            }
+            composer.closeParen();
+        }
+        composer.end();
     }
 }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/message/response/IdResponse.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/message/response/IdResponse.java
index 463d18bf83..7accaf655f 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/message/response/IdResponse.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/message/response/IdResponse.java
@@ -19,9 +19,9 @@
 
 package org.apache.james.imap.message.response;
 
+import java.util.Map;
+
 import org.apache.james.imap.api.message.response.ImapResponseMessage;
 
-public class IdResponse implements ImapResponseMessage {
-    public IdResponse() {
-    }
+public record IdResponse(Map<String, String> fields) implements 
ImapResponseMessage {
 }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/IdProcessor.java 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/IdProcessor.java
index c1011a2dea..07a38e02ca 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/IdProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/IdProcessor.java
@@ -19,10 +19,13 @@
 
 package org.apache.james.imap.processor;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import jakarta.inject.Inject;
 
+import org.apache.james.imap.api.ImapConfiguration;
 import org.apache.james.imap.api.message.Capability;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
@@ -45,14 +48,22 @@ public class IdProcessor extends 
AbstractMailboxProcessor<IDRequest> implements
     private static final Logger LOGGER = 
LoggerFactory.getLogger(IdProcessor.class);
     private static final ImmutableList<Capability> CAPABILITIES = 
ImmutableList.of(Capability.of("ID"));
 
+    private final Map<String, String> fields = new HashMap<>();
+
     @Inject
     public IdProcessor(MailboxManager mailboxManager, StatusResponseFactory 
factory, MetricFactory metricFactory) {
         super(IDRequest.class, mailboxManager, factory, metricFactory);
     }
 
+    @Override
+    public void configure(ImapConfiguration imapConfiguration) {
+        super.configure(imapConfiguration);
+        fields.putAll(imapConfiguration.getIdFieldsResponse());
+    }
+
     @Override
     protected Mono<Void> processRequestReactive(IDRequest request, ImapSession 
session, Responder responder) {
-        responder.respond(new IdResponse());
+        responder.respond(new IdResponse(fields));
 
         String mailUserAgent = 
request.getParameters().map(Object::toString).orElse("NIL");
         addMailUserAgentToMDC(session, mailUserAgent);
diff --git 
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
 
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
index cc8008fb59..f4b71e8679 100644
--- 
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
+++ 
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
@@ -21,6 +21,7 @@ package org.apache.james.imapserver.netty;
 import java.net.MalformedURLException;
 import java.net.URISyntaxException;
 import java.time.Duration;
+import java.util.LinkedHashMap;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -49,6 +50,7 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 import io.netty.channel.Channel;
@@ -208,9 +210,21 @@ public class IMAPServer extends 
AbstractConfigurableAsyncServer implements ImapC
                 
.concurrentRequests(configuration.getInteger("concurrentRequests", 
ImapConfiguration.DEFAULT_CONCURRENT_REQUESTS))
                 
.isProvisionDefaultMailboxes(configuration.getBoolean("provisionDefaultMailboxes",
 ImapConfiguration.DEFAULT_PROVISION_DEFAULT_MAILBOXES))
                 
.withCustomProperties(configuration.getProperties("customProperties"))
+                .idFieldsResponse(getIdCommandResponseFields(configuration))
                 .build();
     }
 
+    private static ImmutableMap<String, String> 
getIdCommandResponseFields(HierarchicalConfiguration<ImmutableNode> 
configuration) {
+        LinkedHashMap<String, String> fieldsMap = new LinkedHashMap<>();
+        configuration.configurationsAt("idCommandResponse.field")
+            .forEach(field -> {
+                String name = field.getString("[@name]");
+                String value = field.getString("[@value]");
+                fieldsMap.put(name, value);
+            });
+        return ImmutableMap.copyOf(fieldsMap);
+    }
+
     private static TimeUnit getTimeIntervalUnit(String timeIntervalUnit) {
         try {
             return TimeUnit.valueOf(timeIntervalUnit);
diff --git 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
index c39d54c032..10b5d24ebb 100644
--- 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
+++ 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
@@ -3358,4 +3358,38 @@ class IMAPServerTest {
                     .runSuccessfullyWithin(Duration.ofMinutes(10));
         }
     }
+    
+    @Nested
+    class IDCommandTest {
+        IMAPServer imapServer;
+
+        @AfterEach
+        void tearDown() {
+            if (imapServer != null) {   
+                imapServer.destroy();
+            }
+        }
+
+        @Test
+        void idCommandShouldReturnNILWhenNoConfigured() throws Exception {
+            imapServer = createImapServer("imapServer.xml");
+
+            assertThat(
+                testIMAPClient.connect("127.0.0.1", 
imapServer.getListenAddresses().getFirst().getPort())
+                    .sendCommand("ID (\"name\" \"Apache James\")"))
+                .contains("* ID NIL")
+                .contains("OK ID completed.");
+        }
+
+        @Test
+        void idCommandShouldReturnConfiguredResponse() throws Exception {
+            imapServer = 
createImapServer("imapServerIdCommandResponseFields.xml");
+            assertThat(
+                testIMAPClient.connect("127.0.0.1", 
imapServer.getListenAddresses().getFirst().getPort())
+                    .sendCommand("ID (\"name\" \"Apache James\")"))
+                .contains("* ID (\"name\" \"Apache James\" \"version\" 
\"3.9.0\")")
+                .contains("OK ID completed.");
+        }
+    }
+
 }
diff --git 
a/server/protocols/protocols-imap4/src/test/resources/imapServerIdCommandResponseFields.xml
 
b/server/protocols/protocols-imap4/src/test/resources/imapServerIdCommandResponseFields.xml
new file mode 100644
index 0000000000..e480dd450b
--- /dev/null
+++ 
b/server/protocols/protocols-imap4/src/test/resources/imapServerIdCommandResponseFields.xml
@@ -0,0 +1,19 @@
+
+<imapserver enabled="true">
+    <jmxName>imapserver</jmxName>
+    <bind>0.0.0.0:0</bind>
+    <connectionBacklog>200</connectionBacklog>
+    <connectionLimit>0</connectionLimit>
+    <connectionLimitPerIP>0</connectionLimitPerIP>
+    <idleTimeInterval>120</idleTimeInterval>
+    <idleTimeIntervalUnit>SECONDS</idleTimeIntervalUnit>
+    <enableIdle>true</enableIdle>
+    <inMemorySizeLimit>64K</inMemorySizeLimit>
+    <literalSizeLimit>128K</literalSizeLimit>
+    <plainAuthDisallowed>false</plainAuthDisallowed>
+    <gracefulShutdown>false</gracefulShutdown>
+    <idCommandResponse>
+        <field name="name" value="Apache James"/>
+        <field name="version" value="3.9.0"/>
+    </idCommandResponse>
+</imapserver>
\ No newline at end of file
diff --git a/src/site/xdoc/server/config-imap4.xml 
b/src/site/xdoc/server/config-imap4.xml
index 1b10bc1255..c15f709d8e 100644
--- a/src/site/xdoc/server/config-imap4.xml
+++ b/src/site/xdoc/server/config-imap4.xml
@@ -134,6 +134,9 @@
 
         <dt><strong>ignoreIDLEUponProcessing</strong></dt>
         <dd>true or false - Allow disabling the heartbeat handler. Defaults to 
true.</dd>
+
+      <dt><strong>idCommandResponse.field</strong></dt>
+      <dd>Store the fields response for ID Command, with each tag containing a 
name-value pair corresponding to the attribute name. Ref: rfc2971</dd>
       </dl>
 
     <subsection name="OIDC set up">


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

Reply via email to