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 0bd83e9f0e41342c9f2590b24163b67da44d1592 Author: Benoit TELLIER <btell...@linagora.com> AuthorDate: Thu Nov 21 10:30:44 2024 +0100 JAMES-4091 Allow describing opened IMAP channels --- .../apache/james/core/ConnectionDescription.java | 35 +++++++--------- .../james/core/ConnectionDescriptionSupplier.java | 40 +++++++++--------- .../james/imap/api/process/SelectedMailbox.java | 2 + .../imap/processor/base/SelectedMailboxImpl.java | 5 +++ .../james/modules/protocols/IMAPServerModule.java | 2 + .../james/modules/server/ServerRouteModule.java | 5 +++ .../apache/james/imapserver/netty/IMAPServer.java | 45 +++++++++++++++++++- .../james/imapserver/netty/IMAPServerFactory.java | 13 +++++- .../MemoryWebAdminServerIntegrationTest.java | 37 +++++++++++++++++ .../protocols/webadmin/webadmin-protocols/pom.xml | 4 ++ .../protocols/webadmin/ProtocolServerRoutes.java | 48 +++++++++++++++++++++- 11 files changed, 193 insertions(+), 43 deletions(-) diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java b/core/src/main/java/org/apache/james/core/ConnectionDescription.java similarity index 55% copy from server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java copy to core/src/main/java/org/apache/james/core/ConnectionDescription.java index 5dbaa855c9..fc30273d82 100644 --- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java +++ b/core/src/main/java/org/apache/james/core/ConnectionDescription.java @@ -17,26 +17,19 @@ * under the License. * ****************************************************************/ -package org.apache.james.webadmin.integration.memory; +package org.apache.james.core; -import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT; +import java.util.Map; +import java.util.Optional; -import org.apache.james.JamesServerBuilder; -import org.apache.james.JamesServerExtension; -import org.apache.james.MemoryJamesConfiguration; -import org.apache.james.MemoryJamesServerMain; -import org.apache.james.webadmin.integration.WebAdminServerIntegrationTest; -import org.junit.jupiter.api.extension.RegisterExtension; - -class MemoryWebAdminServerIntegrationTest extends WebAdminServerIntegrationTest { - - @RegisterExtension - static JamesServerExtension jamesServerExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir -> - MemoryJamesConfiguration.builder() - .workingDirectory(tmpDir) - .configurationFromClasspath() - .usersRepository(DEFAULT) - .build()) - .server(MemoryJamesServerMain::createServer) - .build(); -} \ No newline at end of file +public record ConnectionDescription( + String protocol, + String endpoint, + Optional<String> remoteAddress, + boolean isActive, + boolean isOpen, + boolean isWritable, + boolean isEncrypted, + Optional<Username> username, + Map<String, String> protocolSpecificInformation) { +} diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java b/core/src/main/java/org/apache/james/core/ConnectionDescriptionSupplier.java similarity index 56% copy from server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java copy to core/src/main/java/org/apache/james/core/ConnectionDescriptionSupplier.java index 5dbaa855c9..16fa7ba6ae 100644 --- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java +++ b/core/src/main/java/org/apache/james/core/ConnectionDescriptionSupplier.java @@ -17,26 +17,28 @@ * under the License. * ****************************************************************/ -package org.apache.james.webadmin.integration.memory; +package org.apache.james.core; -import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT; +import java.util.Set; +import java.util.stream.Stream; -import org.apache.james.JamesServerBuilder; -import org.apache.james.JamesServerExtension; -import org.apache.james.MemoryJamesConfiguration; -import org.apache.james.MemoryJamesServerMain; -import org.apache.james.webadmin.integration.WebAdminServerIntegrationTest; -import org.junit.jupiter.api.extension.RegisterExtension; +import jakarta.inject.Inject; -class MemoryWebAdminServerIntegrationTest extends WebAdminServerIntegrationTest { +public interface ConnectionDescriptionSupplier { + class CompositeConnectionDescriptionSupplier implements ConnectionDescriptionSupplier { + private final Set<ConnectionDescriptionSupplier> connectionDescriptionSupplierSet; - @RegisterExtension - static JamesServerExtension jamesServerExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir -> - MemoryJamesConfiguration.builder() - .workingDirectory(tmpDir) - .configurationFromClasspath() - .usersRepository(DEFAULT) - .build()) - .server(MemoryJamesServerMain::createServer) - .build(); -} \ No newline at end of file + @Inject + public CompositeConnectionDescriptionSupplier(Set<ConnectionDescriptionSupplier> connectionDescriptionSupplierSet) { + this.connectionDescriptionSupplierSet = connectionDescriptionSupplierSet; + } + + @Override + public Stream<ConnectionDescription> describeConnections() { + return connectionDescriptionSupplierSet.stream() + .flatMap(ConnectionDescriptionSupplier::describeConnections); + } + } + + Stream<ConnectionDescription> describeConnections(); +} diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java b/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java index 5aed0be304..241c27c9c5 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/api/process/SelectedMailbox.java @@ -50,6 +50,8 @@ public interface SelectedMailbox { void unregisterIdle(); + boolean isIdling(); + /** * Return the msg index of the given uid or {@link NullableMessageSequenceNumber#noMessage()} instance if no * message with the given uid was found diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java index 95af2d2ac2..6a94411c74 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/base/SelectedMailboxImpl.java @@ -175,6 +175,11 @@ public class SelectedMailboxImpl implements SelectedMailbox, EventListener.React idleEventListener.set(null); } + @Override + public boolean isIdling() { + return idleEventListener.get() != null; + } + @Override public Optional<MessageUid> getFirstUid() { return uidMsnConverter.getFirstUid(); diff --git a/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java b/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java index b79d43afa8..fcea6b9f6c 100644 --- a/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java +++ b/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java @@ -28,6 +28,7 @@ import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.commons.lang3.tuple.Pair; import org.apache.james.ProtocolConfigurationSanitizer; import org.apache.james.RunArguments; +import org.apache.james.core.ConnectionDescriptionSupplier; import org.apache.james.core.Disconnector; import org.apache.james.core.healthcheck.HealthCheck; import org.apache.james.filesystem.api.FileSystem; @@ -119,6 +120,7 @@ public class IMAPServerModule extends AbstractModule { Multibinder.newSetBinder(binder(), HealthCheck.class).addBinding().to(IMAPHealthCheck.class); Multibinder.newSetBinder(binder(), Disconnector.class).addBinding().to(IMAPServerFactory.class); + Multibinder.newSetBinder(binder(), ConnectionDescriptionSupplier.class).addBinding().to(IMAPServerFactory.class); } @Provides diff --git a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/ServerRouteModule.java b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/ServerRouteModule.java index f3e3af0556..8ebf41e2e0 100644 --- a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/ServerRouteModule.java +++ b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/ServerRouteModule.java @@ -20,6 +20,7 @@ package org.apache.james.modules.server; import org.apache.james.DisconnectorNotifier; +import org.apache.james.core.ConnectionDescriptionSupplier; import org.apache.james.core.Disconnector; import org.apache.james.protocols.lib.netty.AbstractServerFactory; import org.apache.james.protocols.webadmin.ProtocolServerRoutes; @@ -39,10 +40,14 @@ public class ServerRouteModule extends AbstractModule { .to(ProtocolServerRoutes.class); Multibinder.newSetBinder(binder(), Disconnector.class); + Multibinder.newSetBinder(binder(), ConnectionDescriptionSupplier.class); bind(Disconnector.class).to(Disconnector.CompositeDisconnector.class); bind(Disconnector.CompositeDisconnector.class).in(Scopes.SINGLETON); + bind(ConnectionDescriptionSupplier.class).to(ConnectionDescriptionSupplier.CompositeConnectionDescriptionSupplier.class); + bind(ConnectionDescriptionSupplier.CompositeConnectionDescriptionSupplier.class).in(Scopes.SINGLETON); + bind(DisconnectorNotifier.class).to(DisconnectorNotifier.InVMDisconnectorNotifier.class); bind(DisconnectorNotifier.InVMDisconnectorNotifier.class).in(Scopes.SINGLETON); } 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 f2e08114a6..a9bac1194a 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 @@ -18,7 +18,9 @@ ****************************************************************/ package org.apache.james.imapserver.netty; +import java.net.InetSocketAddress; import java.net.MalformedURLException; +import java.net.SocketAddress; import java.net.URISyntaxException; import java.time.Duration; import java.util.LinkedHashMap; @@ -26,18 +28,24 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; +import java.util.stream.Stream; import org.apache.commons.configuration2.HierarchicalConfiguration; import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.commons.configuration2.tree.ImmutableNode; +import org.apache.james.core.ConnectionDescription; +import org.apache.james.core.ConnectionDescriptionSupplier; import org.apache.james.core.Disconnector; import org.apache.james.core.Username; import org.apache.james.imap.api.ConnectionCheck; import org.apache.james.imap.api.ImapConfiguration; import org.apache.james.imap.api.ImapConstants; import org.apache.james.imap.api.process.ImapProcessor; +import org.apache.james.imap.api.process.ImapSession; +import org.apache.james.imap.api.process.SelectedMailbox; import org.apache.james.imap.decode.ImapDecoder; import org.apache.james.imap.encode.ImapEncoder; +import org.apache.james.mailbox.model.MailboxId; import org.apache.james.metrics.api.GaugeRegistry; import org.apache.james.protocols.api.OidcSASLConfiguration; import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer; @@ -59,6 +67,7 @@ import com.google.common.collect.ImmutableSet; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.channel.group.DefaultChannelGroup; @@ -70,7 +79,8 @@ import io.netty.util.concurrent.GlobalEventExecutor; /** * NIO IMAP Server which use Netty. */ -public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapConstants, IMAPServerMBean, NettyConstants, Disconnector { +public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapConstants, IMAPServerMBean, NettyConstants, + Disconnector, ConnectionDescriptionSupplier { private static final Logger LOG = LoggerFactory.getLogger(IMAPServer.class); public static class AuthenticationConfiguration { @@ -356,4 +366,37 @@ public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapC ReactiveThrottler getReactiveThrottler() { return reactiveThrottler; } + + @Override + public Stream<ConnectionDescription> describeConnections() { + return imapChannelGroup.stream() + .map(channel -> { + Optional<ImapSession> imapSession = Optional.ofNullable(channel.attr(IMAP_SESSION_ATTRIBUTE_KEY).get()); + return new ConnectionDescription( + "IMAP", + jmxName, + Optional.ofNullable(channel.remoteAddress()).map(this::addressAsString), + channel.isActive(), + channel.isOpen(), + channel.isWritable(), + imapSession.map(ImapSession::isTLSActive).orElse(false), + imapSession.flatMap(session -> Optional.ofNullable(session.getUserName())), + ImmutableMap.of( + "isCompressed", Boolean.toString(imapSession.map(ImapSession::isCompressionActive).orElse(false)), + "selectedMailbox", imapSession.flatMap(session -> Optional.ofNullable(session.getSelected())) + .map(SelectedMailbox::getMailboxId) + .map(MailboxId::serialize) + .orElse(""), + "isIdling", Boolean.toString(imapSession.flatMap(session -> Optional.ofNullable(session.getSelected())) + .map(SelectedMailbox::isIdling) + .orElse(false)))); + }); + } + + private String addressAsString(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress address) { + return address.getAddress().getHostAddress(); + } + return socketAddress.toString(); + } } diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServerFactory.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServerFactory.java index 9ddcdb30b0..ab89490095 100644 --- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServerFactory.java +++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServerFactory.java @@ -21,11 +21,14 @@ package org.apache.james.imapserver.netty; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; +import java.util.stream.Stream; import jakarta.inject.Inject; import org.apache.commons.configuration2.HierarchicalConfiguration; import org.apache.commons.configuration2.tree.ImmutableNode; +import org.apache.james.core.ConnectionDescription; +import org.apache.james.core.ConnectionDescriptionSupplier; import org.apache.james.core.Disconnector; import org.apache.james.core.Username; import org.apache.james.filesystem.api.FileSystem; @@ -41,7 +44,7 @@ import org.apache.james.protocols.lib.netty.AbstractServerFactory; import com.github.fge.lambdas.functions.ThrowingFunction; -public class IMAPServerFactory extends AbstractServerFactory implements Disconnector { +public class IMAPServerFactory extends AbstractServerFactory implements Disconnector, ConnectionDescriptionSupplier { protected final FileSystem fileSystem; protected final ThrowingFunction<HierarchicalConfiguration<ImmutableNode>, ImapSuite> imapSuiteProvider; @@ -105,4 +108,12 @@ public class IMAPServerFactory extends AbstractServerFactory implements Disconne .map(server -> (IMAPServer) server) .forEach(imapServer -> imapServer.disconnect(username)); } + + @Override + public Stream<ConnectionDescription> describeConnections() { + return getServers() + .stream() + .map(server -> (IMAPServer) server) + .flatMap(IMAPServer::describeConnections); + } } diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java index 5dbaa855c9..3c7821de06 100644 --- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java +++ b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryWebAdminServerIntegrationTest.java @@ -19,16 +19,28 @@ package org.apache.james.webadmin.integration.memory; +import static io.restassured.RestAssured.when; import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT; +import static org.apache.james.jmap.JMAPTestingConstants.LOCALHOST_IP; +import static org.hamcrest.Matchers.is; +import org.apache.james.GuiceJamesServer; import org.apache.james.JamesServerBuilder; import org.apache.james.JamesServerExtension; import org.apache.james.MemoryJamesConfiguration; import org.apache.james.MemoryJamesServerMain; +import org.apache.james.modules.protocols.ImapGuiceProbe; +import org.apache.james.utils.DataProbeImpl; +import org.apache.james.utils.TestIMAPClient; import org.apache.james.webadmin.integration.WebAdminServerIntegrationTest; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; class MemoryWebAdminServerIntegrationTest extends WebAdminServerIntegrationTest { + private static final String DOMAIN = "domain"; + private static final String USERNAME = "bob@" + DOMAIN; + private static final String PASSWORD = "password"; @RegisterExtension static JamesServerExtension jamesServerExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir -> @@ -39,4 +51,29 @@ class MemoryWebAdminServerIntegrationTest extends WebAdminServerIntegrationTest .build()) .server(MemoryJamesServerMain::createServer) .build(); + + @RegisterExtension + TestIMAPClient testIMAPClient = new TestIMAPClient(); + + @Test + void shouldDescribeConnectedImapChannels(GuiceJamesServer server) throws Exception { + int imapPort = server.getProbe(ImapGuiceProbe.class).getImapPort(); + + server.getProbe(DataProbeImpl.class).addUser(USERNAME, PASSWORD); + + testIMAPClient.connect(LOCALHOST_IP, imapPort) + .login(USERNAME, PASSWORD) + .select("INBOX"); + + when() + .get("/servers/channels/" + USERNAME) + .prettyPeek() + .then() + .statusCode(HttpStatus.OK_200) + .body("[0].protocol", is("IMAP")) + .body("[0].endpoint", is("imapserver")) + .body("[0].username", is("bob@domain")) + .body("[0].isEncrypted", is(false)) + .body("[0].isEncrypted", is(false)); + } } \ No newline at end of file diff --git a/server/protocols/webadmin/webadmin-protocols/pom.xml b/server/protocols/webadmin/webadmin-protocols/pom.xml index 6c29a73f63..c166a8c465 100644 --- a/server/protocols/webadmin/webadmin-protocols/pom.xml +++ b/server/protocols/webadmin/webadmin-protocols/pom.xml @@ -58,6 +58,10 @@ <artifactId>testing-base</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-jdk8</artifactId> + </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> diff --git a/server/protocols/webadmin/webadmin-protocols/src/main/java/org/apache/james/protocols/webadmin/ProtocolServerRoutes.java b/server/protocols/webadmin/webadmin-protocols/src/main/java/org/apache/james/protocols/webadmin/ProtocolServerRoutes.java index c1901f7f15..d1ba81ee72 100644 --- a/server/protocols/webadmin/webadmin-protocols/src/main/java/org/apache/james/protocols/webadmin/ProtocolServerRoutes.java +++ b/server/protocols/webadmin/webadmin-protocols/src/main/java/org/apache/james/protocols/webadmin/ProtocolServerRoutes.java @@ -20,12 +20,16 @@ package org.apache.james.protocols.webadmin; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import jakarta.inject.Inject; import org.apache.james.DisconnectorNotifier; +import org.apache.james.core.ConnectionDescription; +import org.apache.james.core.ConnectionDescriptionSupplier; import org.apache.james.core.Username; import org.apache.james.protocols.lib.netty.CertificateReloadable; import org.apache.james.util.Port; @@ -46,19 +50,49 @@ import spark.Request; import spark.Service; public class ProtocolServerRoutes implements Routes { + record ConnectionDescriptionDTO( + String protocol, + String endpoint, + Optional<String> remoteAddress, + boolean isActive, + boolean isOpen, + boolean isWritable, + boolean isEncrypted, + Optional<String> username, + Map<String, String> protocolSpecificInformation) { + + static ConnectionDescriptionDTO from(ConnectionDescription domainObject) { + return new ConnectionDescriptionDTO(domainObject.protocol(), + domainObject.endpoint(), + domainObject.remoteAddress(), + domainObject.isActive(), + domainObject.isOpen(), + domainObject.isWritable(), + domainObject.isEncrypted(), + domainObject.username().map(Username::asString), + domainObject.protocolSpecificInformation()); + } + } + public static final String SERVERS = "servers"; public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); public static final TypeReference<List<String>> LIST_OF_STRING = new TypeReference<>() { }; + static { + OBJECT_MAPPER.registerModule(new Jdk8Module()); + } + private final Set<CertificateReloadable.Factory> servers; private final DisconnectorNotifier disconnector; + private final ConnectionDescriptionSupplier connectionDescriptionSupplier; @Inject - public ProtocolServerRoutes(Set<CertificateReloadable.Factory> servers, DisconnectorNotifier disconnector) { + public ProtocolServerRoutes(Set<CertificateReloadable.Factory> servers, DisconnectorNotifier disconnector, ConnectionDescriptionSupplier connectionDescriptionSupplier) { this.servers = servers; this.disconnector = disconnector; + this.connectionDescriptionSupplier = connectionDescriptionSupplier; } @Override @@ -110,6 +144,18 @@ public class ProtocolServerRoutes implements Routes { return Responses.returnNoContent(response); }); + + service.get(SERVERS + "/channels", (request, response) -> OBJECT_MAPPER.writeValueAsString(connectionDescriptionSupplier.describeConnections() + .map(ConnectionDescriptionDTO::from) + .toList())); + + service.get(SERVERS + "/channels/:user", (request, response) -> { + Username username = Username.of(request.params("user")); + return OBJECT_MAPPER.writeValueAsString(connectionDescriptionSupplier.describeConnections() + .filter(connectionDescription -> connectionDescription.username().map(username::equals).orElse(false)) + .map(ConnectionDescriptionDTO::from) + .toList()); + }); } private Predicate<CertificateReloadable> filters(Request request) { --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org