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