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


The following commit(s) were added to refs/heads/master by this push:
     new 799bc6d6ce JAMES-4091 Paginate and sort connected users
799bc6d6ce is described below

commit 799bc6d6cedd366a1a2fdce764f952763b77336d
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri Mar 27 15:08:39 2026 +0100

    JAMES-4091 Paginate and sort connected users
---
 .../modules/servers/partials/operate/webadmin.adoc |  25 +-
 .../protocols/webadmin/webadmin-protocols/pom.xml  |  44 ++++
 .../protocols/webadmin/ProtocolServerRoutes.java   | 110 +++++++-
 .../webadmin/ProtocolServerRoutesChannelsTest.java | 290 +++++++++++++++++++++
 .../src/test/resources/imapServer.xml              |  16 ++
 5 files changed, 479 insertions(+), 6 deletions(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc 
b/docs/modules/servers/partials/operate/webadmin.adoc
index a707c3c20e..7fcbcd3edb 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -5251,6 +5251,7 @@ Will return a description and statistics for channels of 
a user:
 ]
 ....
 
+The same optional query parameters as `GET /servers/channels` are supported 
(see below).
 
 === Listing all channels
 
@@ -5288,4 +5289,26 @@ Will return a description and statistics for channels of 
all users:
 ]
 ....
 
-Be warned that the output can be very large if a significant count of channels 
is opened.
\ No newline at end of file
+Be warned that the output can be very large if a significant count of channels 
is opened.
+
+The following optional query parameters are supported to sort and paginate 
results:
+
+[cols="~,~,~", options="header"]
+|===
+| Parameter | Default | Description
+| `limit` | unlimited | Maximum number of results to return.
+| `offset` | `0` | Number of results to skip before returning.
+| `sortBy` | none (no sort) | JSON path of the field to sort by. Supports 
top-level fields (`protocol`, `endpoint`, `remoteAddress`, `connectionDate`, 
`isActive`, `isOpen`, `isWritable`, `isEncrypted`, `username`) and nested keys 
inside `protocolSpecificInformation` using dot notation (e.g. 
`protocolSpecificInformation.cumulativeWrittenBytes`). Unknown paths are 
treated as an empty value without error.
+| `sortDirection` | `asc` | Sort direction. Accepted values: `asc`, `desc` 
(case-insensitive). Only applied when `sortBy` is set.
+| `sortType` | `alphabetical` | How to compare values. `alphabetical` uses 
natural string order; `numerical` parses values as long integers (non-numeric 
values sort as `0`). Only applied when `sortBy` is set.
+|===
+
+Example — list the 10 most data-hungry IMAP connections:
+
+....
+curl -XGET 
"/servers/channels?sortBy=protocolSpecificInformation.cumulativeWrittenBytes&sortType=numerical&sortDirection=desc&limit=10"
+....
+
+Return codes:
+
+- 200: the list of channels, possibly empty
\ 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 65b20e7d40..e72a8f145c 100644
--- a/server/protocols/webadmin/webadmin-protocols/pom.xml
+++ b/server/protocols/webadmin/webadmin-protocols/pom.xml
@@ -30,13 +30,57 @@
     <description>Finner grained management for protocols</description>
 
     <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-api</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-memory</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-memory</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>event-bus-api</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-core</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-protocols-imap4</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-protocols-library</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-protocols-library</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-testing</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
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 087c8b3efd..8bff24a53f 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
@@ -22,10 +22,12 @@ package org.apache.james.protocols.webadmin;
 import static 
org.apache.james.DisconnectorNotifier.AllUsersRequest.ALL_USERS_REQUEST;
 
 import java.time.Instant;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import jakarta.inject.Inject;
 
@@ -155,15 +157,19 @@ 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", (request, response) -> {
+            ChannelsQueryParameters params = 
ChannelsQueryParameters.from(request);
+            return 
OBJECT_MAPPER.writeValueAsString(params.apply(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()
+            ChannelsQueryParameters params = 
ChannelsQueryParameters.from(request);
+            return 
OBJECT_MAPPER.writeValueAsString(params.apply(connectionDescriptionSupplier.describeConnections()
                 .filter(connectionDescription -> 
connectionDescription.username().map(username::equals).orElse(false))
-                .map(ConnectionDescriptionDTO::from)
+                .map(ConnectionDescriptionDTO::from))
                 .toList());
         });
 
@@ -174,6 +180,100 @@ public class ProtocolServerRoutes implements Routes {
             .toList()));
     }
 
+    private static class ChannelsQueryParameters {
+        enum SortDirection {
+            ASC,
+            DESC
+        }
+
+        enum SortType {
+            NUMERICAL,
+            ALPHABETICAL
+        }
+
+        private final Optional<Long> limit;
+        private final long offset;
+        private final Optional<String> sortBy;
+        private final SortDirection sortDirection;
+        private final SortType sortType;
+
+        private ChannelsQueryParameters(Optional<Long> limit, long offset, 
Optional<String> sortBy,
+                                        SortDirection sortDirection, SortType 
sortType) {
+            this.limit = limit;
+            this.offset = offset;
+            this.sortBy = sortBy;
+            this.sortDirection = sortDirection;
+            this.sortType = sortType;
+        }
+
+        static ChannelsQueryParameters from(Request request) {
+            Optional<Long> limit = 
Optional.ofNullable(request.queryParams("limit")).map(Long::parseUnsignedLong);
+            long offset = 
Optional.ofNullable(request.queryParams("offset")).map(Long::parseUnsignedLong).orElse(0L);
+            Optional<String> sortBy = 
Optional.ofNullable(request.queryParams("sortBy"));
+            SortDirection sortDirection = 
Optional.ofNullable(request.queryParams("sortDirection"))
+                .map(s -> SortDirection.valueOf(s.toUpperCase()))
+                .orElse(SortDirection.ASC);
+            SortType sortType = 
Optional.ofNullable(request.queryParams("sortType"))
+                .map(s -> SortType.valueOf(s.toUpperCase()))
+                .orElse(SortType.ALPHABETICAL);
+            return new ChannelsQueryParameters(limit, offset, sortBy, 
sortDirection, sortType);
+        }
+
+        Stream<ConnectionDescriptionDTO> 
apply(Stream<ConnectionDescriptionDTO> stream) {
+            Stream<ConnectionDescriptionDTO> result = stream;
+            if (sortBy.isPresent()) {
+                result = result.sorted(buildComparator(sortBy.get()));
+            }
+            if (offset > 0) {
+                result = result.skip(offset);
+            }
+            if (limit.isPresent()) {
+                result = result.limit(limit.get());
+            }
+            return result;
+        }
+
+        private Comparator<ConnectionDescriptionDTO> buildComparator(String 
field) {
+            Comparator<ConnectionDescriptionDTO> comparator;
+            if (sortType == SortType.NUMERICAL) {
+                comparator = Comparator.comparingLong(a -> extractNumeric(a, 
field));
+            } else {
+                comparator = Comparator.comparing(a -> extractString(a, 
field));
+            }
+            if (sortDirection == SortDirection.DESC) {
+                comparator = comparator.reversed();
+            }
+            return comparator;
+        }
+
+        private static long extractNumeric(ConnectionDescriptionDTO dto, 
String field) {
+            try {
+                return Long.parseLong(extractString(dto, field));
+            } catch (NumberFormatException e) {
+                return 0L;
+            }
+        }
+
+        private static String extractString(ConnectionDescriptionDTO dto, 
String field) {
+            if (field.startsWith("protocolSpecificInformation.")) {
+                String key = 
field.substring("protocolSpecificInformation.".length());
+                return 
Optional.ofNullable(dto.protocolSpecificInformation().get(key)).orElse("");
+            }
+            return switch (field) {
+                case "protocol" -> dto.protocol();
+                case "endpoint" -> dto.endpoint();
+                case "remoteAddress" -> dto.remoteAddress().orElse("");
+                case "connectionDate" -> 
dto.connectionDate().map(Instant::toString).orElse("");
+                case "isActive" -> String.valueOf(dto.isActive());
+                case "isOpen" -> String.valueOf(dto.isOpen());
+                case "isWritable" -> String.valueOf(dto.isWritable());
+                case "isEncrypted" -> String.valueOf(dto.isEncrypted());
+                case "username" -> dto.username().orElse("");
+                default -> "";
+            };
+        }
+    }
+
     private Predicate<CertificateReloadable> filters(Request request) {
         Optional<Port> port = 
Optional.ofNullable(request.queryParams("port")).map(Integer::parseUnsignedInt).map(Port::of);
 
diff --git 
a/server/protocols/webadmin/webadmin-protocols/src/test/java/org/apache/james/protocols/webadmin/ProtocolServerRoutesChannelsTest.java
 
b/server/protocols/webadmin/webadmin-protocols/src/test/java/org/apache/james/protocols/webadmin/ProtocolServerRoutesChannelsTest.java
new file mode 100644
index 0000000000..c2c16beafa
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-protocols/src/test/java/org/apache/james/protocols/webadmin/ProtocolServerRoutesChannelsTest.java
@@ -0,0 +1,290 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.protocols.webadmin;
+
+import static io.restassured.RestAssured.given;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+
+import java.time.Duration;
+
+import org.apache.james.DisconnectorNotifier;
+import org.apache.james.core.Username;
+import org.apache.james.imap.encode.main.DefaultImapEncoderFactory;
+import org.apache.james.imap.main.DefaultImapDecoderFactory;
+import org.apache.james.imap.processor.fetch.FetchProcessor;
+import org.apache.james.imap.processor.main.DefaultImapProcessorFactory;
+import org.apache.james.imapserver.netty.IMAPServer;
+import org.apache.james.imapserver.netty.ImapMetrics;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.store.FakeAuthenticator;
+import org.apache.james.mailbox.store.FakeAuthorizator;
+import org.apache.james.mailbox.store.StoreSubscriptionManager;
+import org.apache.james.metrics.api.NoopGaugeRegistry;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.lib.mock.ConfigLoader;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+import org.apache.james.util.ClassLoaderUtils;
+import org.apache.james.utils.TestIMAPClient;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.google.common.collect.ImmutableSet;
+
+import io.restassured.specification.RequestSpecification;
+
+class ProtocolServerRoutesChannelsTest {
+    private static final Username ALICE = Username.of("[email protected]");
+    private static final Username BOB = Username.of("[email protected]");
+    private static final String PASSWORD = "secret";
+
+    @RegisterExtension
+    TestIMAPClient aliceClient = new TestIMAPClient();
+    @RegisterExtension
+    TestIMAPClient bobClient = new TestIMAPClient();
+
+    private IMAPServer imapServer;
+    private WebAdminServer webAdminServer;
+    private RequestSpecification spec;
+    private int imapPort;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        FakeAuthenticator authenticator = new FakeAuthenticator();
+        authenticator.addUser(ALICE, PASSWORD);
+        authenticator.addUser(BOB, PASSWORD);
+
+        InMemoryIntegrationResources resources = 
InMemoryIntegrationResources.builder()
+            .authenticator(authenticator)
+            .authorizator(FakeAuthorizator.defaultReject())
+            .inVmEventBus()
+            .defaultAnnotationLimits()
+            .defaultMessageParser()
+            .scanningSearchIndex()
+            .noPreDeletionHooks()
+            .storeQuotaManager()
+            .build();
+
+        RecordingMetricFactory metricFactory = new RecordingMetricFactory();
+        imapServer = new IMAPServer(
+            new DefaultImapDecoderFactory().buildImapDecoder(),
+            new DefaultImapEncoderFactory().buildImapEncoder(),
+            DefaultImapProcessorFactory.createXListSupportingProcessor(
+                resources.getMailboxManager(),
+                resources.getEventBus(),
+                new StoreSubscriptionManager(
+                    resources.getMailboxManager().getMapperFactory(),
+                    resources.getMailboxManager().getMapperFactory(),
+                    resources.getEventBus()),
+                null,
+                resources.getQuotaManager(),
+                resources.getQuotaRootResolver(),
+                metricFactory,
+                FetchProcessor.LocalCacheConfiguration.DEFAULT),
+            new ImapMetrics(metricFactory),
+            new NoopGaugeRegistry(),
+            ImmutableSet.of());
+
+        
imapServer.setFileSystem(FileSystemImpl.forTestingWithConfigurationFromClasspath());
+        
imapServer.configure(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("imapServer.xml")));
+        imapServer.init();
+
+        imapPort = imapServer.getListenAddresses().get(0).getPort();
+
+        DisconnectorNotifier disconnectorNotifier = new 
DisconnectorNotifier.InVMDisconnectorNotifier(imapServer);
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+            new ProtocolServerRoutes(ImmutableSet.of(), disconnectorNotifier, 
imapServer))
+            .start();
+        spec = WebAdminUtils.spec(webAdminServer.getPort());
+    }
+
+    @AfterEach
+    void tearDown() {
+        imapServer.destroy();
+        webAdminServer.destroy();
+    }
+
+    @Test
+    void getChannelsShouldReturnEmptyWhenNoConnections() {
+        given(spec).get("/servers/channels")
+            .then().statusCode(200).body("", hasSize(0));
+    }
+
+    @Test
+    void getChannelsShouldListLoggedInUser() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+
+        awaitChannelCount(1);
+
+        given(spec).get("/servers/channels")
+            .then().statusCode(200)
+            .body("[0].username", equalTo(ALICE.asString()))
+            .body("[0].protocol", equalTo("IMAP"));
+    }
+
+    @Test
+    void getChannelsByUserShouldFilterByUsername() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        given(spec).get("/servers/channels/" + ALICE.asString())
+            .then().statusCode(200)
+            .body("", hasSize(1))
+            .body("[0].username", equalTo(ALICE.asString()));
+
+        given(spec).get("/servers/channels/" + BOB.asString())
+            .then().statusCode(200)
+            .body("", hasSize(1))
+            .body("[0].username", equalTo(BOB.asString()));
+    }
+
+    @Test
+    void getConnectedUsersShouldListDistinctLoggedInUsers() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        given(spec).get("/servers/connectedUsers")
+            .then().statusCode(200)
+            .body("", hasSize(2))
+            .body("", hasItem(ALICE.asString()))
+            .body("", hasItem(BOB.asString()));
+    }
+
+    @Test
+    void deleteChannelsByUserShouldDisconnectUser() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        given(spec).delete("/servers/channels/" + ALICE.asString())
+            .then().statusCode(204);
+
+        Awaitility.await().atMost(Duration.ofSeconds(5))
+            .untilAsserted(() ->
+                given(spec).get("/servers/channels")
+                    .then().body("username", hasItem(BOB.asString()))
+                    .body("username.flatten()", 
org.hamcrest.Matchers.not(hasItem(ALICE.asString()))));
+    }
+
+    @Test
+    void deleteAllChannelsShouldDisconnectEveryone() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        given(spec).delete("/servers/channels")
+            .then().statusCode(204);
+
+        Awaitility.await().atMost(Duration.ofSeconds(5))
+            .untilAsserted(() ->
+                given(spec).get("/servers/channels")
+                    .then().body("", hasSize(0)));
+    }
+
+    @Test
+    void limitShouldRestrictNumberOfResults() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        given(spec).get("/servers/channels?limit=1")
+            .then().statusCode(200).body("", hasSize(1));
+    }
+
+    @Test
+    void offsetShouldSkipResults() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        given(spec).get("/servers/channels?offset=1")
+            .then().statusCode(200).body("", hasSize(1));
+    }
+
+    @Test
+    void sortByShouldOrderResultsAlphabetically() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        assertThat(
+            
given(spec).get("/servers/channels?sortBy=username&sortDirection=asc")
+                
.then().statusCode(200).extract().jsonPath().<String>getList("username"))
+            .isSortedAccordingTo(String::compareTo);
+    }
+
+    @Test
+    void sortByDescShouldReverseOrder() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        assertThat(
+            
given(spec).get("/servers/channels?sortBy=username&sortDirection=desc")
+                
.then().statusCode(200).extract().jsonPath().<String>getList("username"))
+            .isSortedAccordingTo((a, b) -> b.compareTo(a));
+    }
+
+    @Test
+    void sortByProtocolSpecificInformationShouldSortNumerically() throws 
Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+        bobClient.connect("127.0.0.1", imapPort).login(BOB, PASSWORD);
+
+        awaitChannelCount(2);
+
+        // cumulativeWrittenBytes is a numeric field in 
protocolSpecificInformation
+        
given(spec).get("/servers/channels?sortBy=protocolSpecificInformation.cumulativeWrittenBytes&sortType=numerical&sortDirection=asc")
+            .then().statusCode(200).body("", hasSize(2));
+    }
+
+    @Test
+    void unknownSortByShouldReturnResultsWithoutError() throws Exception {
+        aliceClient.connect("127.0.0.1", imapPort).login(ALICE, PASSWORD);
+
+        awaitChannelCount(1);
+
+        given(spec).get("/servers/channels?sortBy=unknownField")
+            .then().statusCode(200).body("", hasSize(1));
+    }
+
+    private void awaitChannelCount(int expected) {
+        Awaitility.await().atMost(Duration.ofSeconds(5))
+            .untilAsserted(() ->
+                given(spec).get("/servers/channels")
+                    .then().body("", hasSize(expected)));
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-protocols/src/test/resources/imapServer.xml
 
b/server/protocols/webadmin/webadmin-protocols/src/test/resources/imapServer.xml
new file mode 100644
index 0000000000..458288c3ce
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-protocols/src/test/resources/imapServer.xml
@@ -0,0 +1,16 @@
+
+<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>
+    <concurrentRequests>20</concurrentRequests>
+</imapserver>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to