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 d99592f9a6a4bbdd6d102c64a34bb11e370831c7
Author: Thanhbv <[email protected]>
AuthorDate: Mon Nov 6 10:34:51 2023 +0700

    JAMES 3897: CrowdsecImapConnectionCheck
---
 .../org/apache/james/imap/api/ConnectionCheck.java |  29 ++++++
 .../james/imap/api/ConnectionCheckFactory.java     |  27 +++++
 .../apache/james/imap/api/ImapConfiguration.java   |  24 ++++-
 .../docs/modules/ROOT/pages/configure/imap.adoc    |   2 +
 .../docs/modules/ROOT/pages/extending/imap.adoc    |  23 ++++-
 .../protocols/ConnectionCheckFactoryImpl.java      |  54 ++++++++++
 .../james/modules/protocols/IMAPServerModule.java  |   7 +-
 .../imapserver/netty/HAProxyMessageHandler.java    |  13 ++-
 .../apache/james/imapserver/netty/IMAPServer.java  |  14 ++-
 .../james/imapserver/netty/IMAPServerFactory.java  |  19 +++-
 .../netty/ImapChannelUpstreamHandler.java          |  27 ++++-
 .../james/imapserver/netty/IMAPServerTest.java     | 113 ++++++++++++++++++++-
 .../james/imapserver/netty/IpConnectionCheck.java  |  47 +++++++++
 .../test/resources/imapServerImapConnectCheck.xml  |  16 +++
 third-party/crowdsec/docker-compose.yml            |   5 +-
 third-party/crowdsec/pom.xml                       |  27 +++++
 .../crowdsec/sample-configuration/imapserver.xml   |  41 ++++++++
 .../apache/james/CrowdsecImapConnectionCheck.java  |  67 ++++++++++++
 .../apache/james/exception/CrowdsecException.java  |  26 +++++
 .../java/org/apache/james/CrowdsecContract.java    |  38 +++++++
 .../james/CrowdsecImapConnectionCheckTest.java     |  97 ++++++++++++++++++
 .../java/org/apache/james/MemoryCrowdsecTest.java  |  58 +++++++++++
 .../crowdsec/src/test/resources/logback.xml        |  48 +++++++++
 23 files changed, 800 insertions(+), 22 deletions(-)

diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/api/ConnectionCheck.java 
b/protocols/imap/src/main/java/org/apache/james/imap/api/ConnectionCheck.java
new file mode 100644
index 0000000000..dbc2700106
--- /dev/null
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/api/ConnectionCheck.java
@@ -0,0 +1,29 @@
+/****************************************************************
+ * 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.imap.api;
+
+import java.net.InetSocketAddress;
+
+import org.reactivestreams.Publisher;
+
+@FunctionalInterface
+public interface ConnectionCheck {
+    Publisher<Void> validate(InetSocketAddress remoteAddress);
+}
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/api/ConnectionCheckFactory.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/api/ConnectionCheckFactory.java
new file mode 100644
index 0000000000..aa5dcded5c
--- /dev/null
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/api/ConnectionCheckFactory.java
@@ -0,0 +1,27 @@
+/****************************************************************
+ * 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.imap.api;
+
+import java.util.Set;
+
+@FunctionalInterface
+public interface ConnectionCheckFactory {
+    Set<ConnectionCheck> create(ImapConfiguration imapConfiguration);
+}
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 41fc4c26b9..7522eaa6e2 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
@@ -62,6 +62,7 @@ public class ImapConfiguration {
         private Optional<Boolean> isCondstoreEnable;
         private Optional<Boolean> provisionDefaultMailboxes;
         private Optional<Properties> customProperties;
+        private ImmutableSet<String> additionalConnectionChecks;
 
         private Builder() {
             this.appendLimit = Optional.empty();
@@ -74,6 +75,7 @@ public class ImapConfiguration {
             this.isCondstoreEnable = Optional.empty();
             this.provisionDefaultMailboxes = Optional.empty();
             this.customProperties = Optional.empty();
+            this.additionalConnectionChecks = ImmutableSet.of();
         }
 
         public Builder idleTimeInterval(long idleTimeInterval) {
@@ -143,6 +145,11 @@ public class ImapConfiguration {
             return this;
         }
 
+        public Builder connectionChecks(ImmutableSet<String> 
additionalConnectionChecks) {
+            this.additionalConnectionChecks = additionalConnectionChecks;
+            return this;
+        }
+
         public ImapConfiguration build() {
             ImmutableSet<Capability> normalizeDisableCaps = 
disabledCaps.stream()
                     .filter(Builder::noBlankString)
@@ -159,7 +166,8 @@ public class ImapConfiguration {
                     normalizeDisableCaps,
                     isCondstoreEnable.orElse(DEFAULT_CONDSTORE_DISABLE),
                     
provisionDefaultMailboxes.orElse(DEFAULT_PROVISION_DEFAULT_MAILBOXES),
-                    customProperties.orElseGet(Properties::new));
+                    customProperties.orElseGet(Properties::new),
+                    additionalConnectionChecks);
         }
     }
 
@@ -173,8 +181,9 @@ public class ImapConfiguration {
     private final boolean isCondstoreEnable;
     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) {
+    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) {
         this.appendLimit = appendLimit;
         this.enableIdle = enableIdle;
         this.idleTimeInterval = idleTimeInterval;
@@ -185,6 +194,7 @@ public class ImapConfiguration {
         this.isCondstoreEnable = isCondstoreEnable;
         this.provisionDefaultMailboxes = provisionDefaultMailboxes;
         this.customProperties = customProperties;
+        this.additionalConnectionChecks = additionalConnectionChecks;
     }
 
     public Optional<Long> getAppendLimit() {
@@ -231,6 +241,10 @@ public class ImapConfiguration {
         return customProperties;
     }
 
+    public ImmutableSet<String> getAdditionalConnectionChecks() {
+        return additionalConnectionChecks;
+    }
+
     @Override
     public final boolean equals(Object obj) {
         if (obj instanceof ImapConfiguration) {
@@ -244,7 +258,8 @@ public class ImapConfiguration {
                 && Objects.equal(that.getDisabledCaps(), disabledCaps)
                 && Objects.equal(that.isProvisionDefaultMailboxes(), 
provisionDefaultMailboxes)
                 && Objects.equal(that.getCustomProperties(), customProperties)
-                && Objects.equal(that.isCondstoreEnable(), isCondstoreEnable);
+                && Objects.equal(that.isCondstoreEnable(), isCondstoreEnable)
+                && Objects.equal(that.getAdditionalConnectionChecks(), 
additionalConnectionChecks);
         }
         return false;
     }
@@ -252,7 +267,7 @@ public class ImapConfiguration {
     @Override
     public final int hashCode() {
         return Objects.hashCode(enableIdle, idleTimeInterval, 
idleTimeIntervalUnit, disabledCaps, isCondstoreEnable,
-            concurrentRequests, maxQueueSize, appendLimit, 
provisionDefaultMailboxes, customProperties);
+            concurrentRequests, maxQueueSize, appendLimit, 
provisionDefaultMailboxes, customProperties, additionalConnectionChecks);
     }
 
     @Override
@@ -268,6 +283,7 @@ public class ImapConfiguration {
                 .add("maxQueueSize", maxQueueSize)
                 .add("provisionDefaultMailboxes", provisionDefaultMailboxes)
                 .add("customProperties", customProperties)
+                .add("additionalConnectionChecks", additionalConnectionChecks)
                 .toString();
     }
 }
diff --git 
a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/imap.adoc 
b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/imap.adoc
index 6b8f6d68a1..f99b27ef74 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/imap.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/imap.adoc
@@ -159,6 +159,8 @@ The following configuration properties are available for 
extensions:
 thus enable implementing new IMAP commands or replace existing IMAP 
processors. List of FQDNs, which can be located in
 James extensions.
 
+| additionalConnectionChecks
+| Configure (union) of additional connection checks. ConnectionCheck will 
check if the connection IP is secure or not.
 | customProperties
 | Properties for custom extension. Each tag is a property entry, and holds a 
string under the form key=value.
 |===
diff --git 
a/server/apps/distributed-app/docs/modules/ROOT/pages/extending/imap.adoc 
b/server/apps/distributed-app/docs/modules/ROOT/pages/extending/imap.adoc
index 76c9a5fef9..c0c89808ac 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/extending/imap.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/extending/imap.adoc
@@ -22,4 +22,25 @@ Custom configuration can be obtained through 
`ImapConfiguration` class via the `
 
 A full working example is available 
link:https://github.com/apache/james-project/tree/master/examples/custom-imap[here].
 
-See this page for xref:configure/imap.adoc#_extending_imap[more details on 
configuring IMAP extensions].
\ No newline at end of file
+See this page for xref:configure/imap.adoc#_extending_imap[more details on 
configuring IMAP extensions].
+
+== IMAP additional Connection Checks
+
+James allows defining your own additional connection checks to guarantee that 
the connecting IP is secured.
+
+A custom connection check should implement the following functional interface:
+```
+@FunctionalInterface
+public interface ConnectionCheck {
+    Publisher<Void> validate(InetSocketAddress remoteAddress);
+}
+```
+
+- `validate` method is used to check the connecting IP is secured.
+
+Then the custom defined ConnectionCheck can be added in `imapserver.xml` file:
+```
+<additionalConnectionChecks>org.apache.james.CrowdsecImapConnectionCheck</additionalConnectionChecks>
+```
+
+An example for configuration is available 
link:https://github.com/apache/james-project/blob/master/third-party/crowdsec/sample-configuration/imapserver.xml[here].
\ No newline at end of file
diff --git 
a/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/ConnectionCheckFactoryImpl.java
 
b/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/ConnectionCheckFactoryImpl.java
new file mode 100644
index 0000000000..99cecbd97c
--- /dev/null
+++ 
b/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/ConnectionCheckFactoryImpl.java
@@ -0,0 +1,54 @@
+/****************************************************************
+ * 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.modules.protocols;
+
+import java.util.Optional;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.apache.james.imap.api.ConnectionCheck;
+import org.apache.james.imap.api.ConnectionCheckFactory;
+import org.apache.james.imap.api.ImapConfiguration;
+import org.apache.james.utils.ClassName;
+import org.apache.james.utils.GuiceGenericLoader;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableSet;
+
+public class ConnectionCheckFactoryImpl implements ConnectionCheckFactory {
+    private final GuiceGenericLoader loader;
+
+    @Inject
+    public ConnectionCheckFactoryImpl(GuiceGenericLoader loader) {
+        this.loader = loader;
+    }
+
+    @Override
+    public Set<ConnectionCheck> create(ImapConfiguration imapConfiguration) {
+        return 
Optional.ofNullable(imapConfiguration.getAdditionalConnectionChecks())
+            .orElse(ImmutableSet.of())
+            .stream()
+            .map(ClassName::new)
+            .map(Throwing.function(loader::instantiate))
+            .map(ConnectionCheck.class::cast)
+            .collect(ImmutableSet.toImmutableSet());
+    }
+}
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 75a2fa690e..52e17e7c0e 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
@@ -30,6 +30,7 @@ import org.apache.james.ProtocolConfigurationSanitizer;
 import org.apache.james.RunArguments;
 import org.apache.james.filesystem.api.FileSystem;
 import org.apache.james.imap.ImapSuite;
+import org.apache.james.imap.api.ConnectionCheckFactory;
 import org.apache.james.imap.api.display.Localizer;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.DefaultMailboxTyper;
@@ -103,6 +104,7 @@ public class IMAPServerModule extends AbstractModule {
         Multibinder.newSetBinder(binder(), 
GuiceProbe.class).addBinding().to(ImapGuiceProbe.class);
 
         Multibinder.newSetBinder(binder(), 
CertificateReloadable.Factory.class).addBinding().to(IMAPServerFactory.class);
+        
bind(ConnectionCheckFactory.class).to(ConnectionCheckFactoryImpl.class);
     }
 
     @Provides
@@ -111,8 +113,9 @@ public class IMAPServerModule extends AbstractModule {
                                            GuiceGenericLoader loader,
                                            StatusResponseFactory 
statusResponseFactory,
                                            MetricFactory metricFactory,
-                                           GaugeRegistry gaugeRegistry) {
-        return new IMAPServerFactory(fileSystem, imapSuiteLoader(loader, 
statusResponseFactory), metricFactory, gaugeRegistry);
+                                           GaugeRegistry gaugeRegistry,
+                                           ConnectionCheckFactory 
connectionCheckFactory) {
+        return new IMAPServerFactory(fileSystem, imapSuiteLoader(loader, 
statusResponseFactory), metricFactory, gaugeRegistry, connectionCheckFactory);
     }
 
     DefaultProcessor provideClassImapProcessors(ImapPackage imapPackage, 
GuiceGenericLoader loader, StatusResponseFactory statusResponseFactory) {
diff --git 
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/HAProxyMessageHandler.java
 
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/HAProxyMessageHandler.java
index 7de5436dfe..3308f6caf0 100644
--- 
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/HAProxyMessageHandler.java
+++ 
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/HAProxyMessageHandler.java
@@ -22,7 +22,9 @@ package org.apache.james.imapserver.netty;
 import static 
org.apache.james.imapserver.netty.ImapChannelUpstreamHandler.MDC_KEY;
 
 import java.net.InetSocketAddress;
+import java.util.Set;
 
+import org.apache.james.imap.api.ConnectionCheck;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.protocols.api.CommandDetectionSession;
 import org.apache.james.protocols.api.ProxyInformation;
@@ -36,12 +38,19 @@ import io.netty.channel.ChannelPipeline;
 import io.netty.handler.codec.haproxy.HAProxyMessage;
 import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
 import io.netty.util.AttributeKey;
+import reactor.core.publisher.Flux;
 
 public class HAProxyMessageHandler extends ChannelInboundHandlerAdapter {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(HAProxyMessageHandler.class);
     private static final AttributeKey<CommandDetectionSession> 
SESSION_ATTRIBUTE_KEY = AttributeKey.valueOf("ImapSession");
     public static final AttributeKey<ProxyInformation> PROXY_INFO = 
AttributeKey.valueOf("proxyInfo");
 
+    private final Set<ConnectionCheck> connectionChecks;
+
+    public HAProxyMessageHandler(Set<ConnectionCheck> connectionChecks) {
+        this.connectionChecks = connectionChecks;
+    }
+
     @Override
     public void channelRead(ChannelHandlerContext ctx, Object msg) throws 
Exception {
         if (msg instanceof HAProxyMessage) {
@@ -51,14 +60,16 @@ public class HAProxyMessageHandler extends 
ChannelInboundHandlerAdapter {
             ImapSession imapSession = (ImapSession) 
pipeline.channel().attr(SESSION_ATTRIBUTE_KEY).get();
             if 
(haproxyMsg.proxiedProtocol().equals(HAProxyProxiedProtocol.TCP4) || 
haproxyMsg.proxiedProtocol().equals(HAProxyProxiedProtocol.TCP6)) {
 
+                InetSocketAddress sourceIP = new 
InetSocketAddress(haproxyMsg.sourceAddress(), haproxyMsg.sourcePort());
                 ctx.channel().attr(PROXY_INFO).set(
                     new ProxyInformation(
-                        new InetSocketAddress(haproxyMsg.sourceAddress(), 
haproxyMsg.sourcePort()),
+                        sourceIP,
                         new InetSocketAddress(haproxyMsg.destinationAddress(), 
haproxyMsg.destinationPort())));
 
                 LOGGER.info("Connection from {} runs through {} proxy", 
haproxyMsg.sourceAddress(), haproxyMsg.destinationAddress());
                 // Refresh MDC info to account for proxying
                 MDCBuilder boundMDC = IMAPMDCContext.boundMDC(ctx);
+                Flux.fromIterable(connectionChecks).concatMap(connectionCheck 
-> connectionCheck.validate(sourceIP)).then().block();
 
                 if (imapSession != null) {
                     imapSession.setAttribute(MDC_KEY, boundMDC);
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 5ded55125b..f4fa0c5412 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,11 +21,13 @@ package org.apache.james.imapserver.netty;
 import java.net.MalformedURLException;
 import java.time.Duration;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.tree.ImmutableNode;
+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;
@@ -132,6 +134,7 @@ public class IMAPServer extends 
AbstractConfigurableAsyncServer implements ImapC
     private final ImapDecoder decoder;
     private final ImapMetrics imapMetrics;
     private final GaugeRegistry gaugeRegistry;
+    private final Set<ConnectionCheck> connectionChecks;
 
     private String hello;
     private boolean compress;
@@ -146,13 +149,13 @@ public class IMAPServer extends 
AbstractConfigurableAsyncServer implements ImapC
     private Duration heartbeatInterval;
     private ReactiveThrottler reactiveThrottler;
 
-
-    public IMAPServer(ImapDecoder decoder, ImapEncoder encoder, ImapProcessor 
processor, ImapMetrics imapMetrics, GaugeRegistry gaugeRegistry) {
+    public IMAPServer(ImapDecoder decoder, ImapEncoder encoder, ImapProcessor 
processor, ImapMetrics imapMetrics, GaugeRegistry gaugeRegistry, 
Set<ConnectionCheck> connectionChecks) {
         this.processor = processor;
         this.encoder = encoder;
         this.decoder = decoder;
         this.imapMetrics = imapMetrics;
         this.gaugeRegistry = gaugeRegistry;
+        this.connectionChecks = connectionChecks;
     }
 
     @Override
@@ -247,7 +250,7 @@ public class IMAPServer extends 
AbstractConfigurableAsyncServer implements ImapC
 
                 if (proxyRequired) {
                     pipeline.addLast(HandlerConstants.PROXY_HANDLER, new 
HAProxyMessageDecoder());
-                    pipeline.addLast("proxyInformationHandler", new 
HAProxyMessageHandler());
+                    pipeline.addLast("proxyInformationHandler", new 
HAProxyMessageHandler(connectionChecks));
                 }
 
                 // Add the text line decoder which limit the max line length,
@@ -290,10 +293,12 @@ public class IMAPServer extends 
AbstractConfigurableAsyncServer implements ImapC
             .encoder(encoder)
             .compress(compress)
             .authenticationConfiguration(authenticationConfiguration)
+            .connectionChecks(connectionChecks)
             .secure(secure)
             .imapMetrics(imapMetrics)
             .heartbeatInterval(heartbeatInterval)
             .ignoreIDLEUponProcessing(ignoreIDLEUponProcessing)
+            .proxyRequired(proxyRequired)
             .build();
     }
 
@@ -302,4 +307,7 @@ public class IMAPServer extends 
AbstractConfigurableAsyncServer implements ImapC
         return new SwitchableLineBasedFrameDecoderFactory(maxLineLength);
     }
 
+    public Set<ConnectionCheck> getConnectionChecks() {
+        return this.connectionChecks;
+    }
 }
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 b0830f7986..3d0a2ad488 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
@@ -19,6 +19,7 @@
 package org.apache.james.imapserver.netty;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -27,6 +28,8 @@ import 
org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.james.filesystem.api.FileSystem;
 import org.apache.james.imap.ImapSuite;
+import org.apache.james.imap.api.ConnectionCheckFactory;
+import org.apache.james.imap.api.ImapConfiguration;
 import org.apache.james.imap.api.process.ImapProcessor;
 import org.apache.james.imap.decode.ImapDecoder;
 import org.apache.james.imap.encode.ImapEncoder;
@@ -36,6 +39,7 @@ import 
org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
 import org.apache.james.protocols.lib.netty.AbstractServerFactory;
 
 import com.github.fge.lambdas.functions.ThrowingFunction;
+import com.google.common.collect.ImmutableSet;
 
 public class IMAPServerFactory extends AbstractServerFactory {
 
@@ -43,30 +47,37 @@ public class IMAPServerFactory extends 
AbstractServerFactory {
     protected final ThrowingFunction<HierarchicalConfiguration<ImmutableNode>, 
ImapSuite> imapSuiteProvider;
     protected final ImapMetrics imapMetrics;
     protected final GaugeRegistry gaugeRegistry;
+    protected final ConnectionCheckFactory connectionCheckFactory;
 
     @Inject
     @Deprecated
     public IMAPServerFactory(FileSystem fileSystem, ImapDecoder decoder, 
ImapEncoder encoder, ImapProcessor processor,
-                             MetricFactory metricFactory, GaugeRegistry 
gaugeRegistry) {
+                             MetricFactory metricFactory, GaugeRegistry 
gaugeRegistry, ConnectionCheckFactory connectionCheckFactory) {
         this.fileSystem = fileSystem;
+        this.connectionCheckFactory = connectionCheckFactory;
         this.imapSuiteProvider = any -> new ImapSuite(decoder, encoder, 
processor);
         this.imapMetrics = new ImapMetrics(metricFactory);
         this.gaugeRegistry = gaugeRegistry;
     }
 
     public IMAPServerFactory(FileSystem fileSystem, 
ThrowingFunction<HierarchicalConfiguration<ImmutableNode>, ImapSuite> 
imapSuiteProvider,
-                             MetricFactory metricFactory, GaugeRegistry 
gaugeRegistry) {
+                             MetricFactory metricFactory, GaugeRegistry 
gaugeRegistry, ConnectionCheckFactory connectionCheckFactory) {
         this.fileSystem = fileSystem;
         this.imapSuiteProvider = imapSuiteProvider;
         this.imapMetrics = new ImapMetrics(metricFactory);
         this.gaugeRegistry = gaugeRegistry;
+        this.connectionCheckFactory = connectionCheckFactory;
     }
 
     protected IMAPServer createServer(HierarchicalConfiguration<ImmutableNode> 
config) {
         ImapSuite imapSuite = imapSuiteProvider.apply(config);
-        return new IMAPServer(imapSuite.getDecoder(), imapSuite.getEncoder(), 
imapSuite.getProcessor(), imapMetrics, gaugeRegistry);
+        ImmutableSet<String> connectionChecks = 
Arrays.stream(config.getStringArray("additionalConnectionChecks")).collect(ImmutableSet.toImmutableSet());
+
+        return new IMAPServer(imapSuite.getDecoder(), imapSuite.getEncoder(), 
imapSuite.getProcessor(), imapMetrics, gaugeRegistry, 
connectionCheckFactory.create(ImapConfiguration.builder()
+            .connectionChecks(connectionChecks)
+            .build()));
     }
-    
+
     @Override
     protected List<AbstractConfigurableAsyncServer> 
createServers(HierarchicalConfiguration<ImmutableNode> config) throws Exception 
{
         List<AbstractConfigurableAsyncServer> servers = new ArrayList<>();
diff --git 
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
 
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
index ff487c8fb1..32e66a7f6b 100644
--- 
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
+++ 
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
@@ -26,7 +26,9 @@ import java.net.InetSocketAddress;
 import java.time.Duration;
 import java.util.NoSuchElementException;
 import java.util.Optional;
+import java.util.Set;
 
+import org.apache.james.imap.api.ConnectionCheck;
 import org.apache.james.imap.api.ImapConstants;
 import org.apache.james.imap.api.ImapMessage;
 import org.apache.james.imap.api.ImapSessionState;
@@ -58,6 +60,7 @@ import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.handler.codec.TooLongFrameException;
 import io.netty.util.Attribute;
 import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 /**
@@ -79,6 +82,8 @@ public class ImapChannelUpstreamHandler extends 
ChannelInboundHandlerAdapter imp
         private boolean ignoreIDLEUponProcessing;
         private Duration heartbeatInterval;
         private ReactiveThrottler reactiveThrottler;
+        private Set<ConnectionCheck> connectionChecks;
+        private boolean proxyRequired;
 
         public ImapChannelUpstreamHandlerBuilder 
reactiveThrottler(ReactiveThrottler reactiveThrottler) {
             this.reactiveThrottler = reactiveThrottler;
@@ -115,6 +120,11 @@ public class ImapChannelUpstreamHandler extends 
ChannelInboundHandlerAdapter imp
             return this;
         }
 
+        public ImapChannelUpstreamHandlerBuilder 
connectionChecks(Set<ConnectionCheck> connectionChecks) {
+            this.connectionChecks = connectionChecks;
+            return this;
+        }
+
         public ImapChannelUpstreamHandlerBuilder imapMetrics(ImapMetrics 
imapMetrics) {
             this.imapMetrics = imapMetrics;
             return this;
@@ -130,8 +140,13 @@ public class ImapChannelUpstreamHandler extends 
ChannelInboundHandlerAdapter imp
             return this;
         }
 
+        public ImapChannelUpstreamHandlerBuilder proxyRequired(boolean 
proxyRequired) {
+            this.proxyRequired = proxyRequired;
+            return this;
+        }
+
         public ImapChannelUpstreamHandler build() {
-            return new ImapChannelUpstreamHandler(hello, processor, encoder, 
compress, secure, imapMetrics, authenticationConfiguration, 
ignoreIDLEUponProcessing, (int) heartbeatInterval.toSeconds(), 
reactiveThrottler);
+            return new ImapChannelUpstreamHandler(hello, processor, encoder, 
compress, secure, imapMetrics, authenticationConfiguration, 
ignoreIDLEUponProcessing, (int) heartbeatInterval.toSeconds(), 
reactiveThrottler, connectionChecks, proxyRequired);
         }
     }
 
@@ -150,10 +165,13 @@ public class ImapChannelUpstreamHandler extends 
ChannelInboundHandlerAdapter imp
     private final Metric imapCommandsMetric;
     private final boolean ignoreIDLEUponProcessing;
     private final ReactiveThrottler reactiveThrottler;
+    private final Set<ConnectionCheck> connectionChecks;
+    private final boolean proxyRequired;
 
     public ImapChannelUpstreamHandler(String hello, ImapProcessor processor, 
ImapEncoder encoder, boolean compress,
                                       Encryption secure, ImapMetrics 
imapMetrics, AuthenticationConfiguration authenticationConfiguration,
-                                      boolean ignoreIDLEUponProcessing, int 
heartbeatIntervalSeconds, ReactiveThrottler reactiveThrottler) {
+                                      boolean ignoreIDLEUponProcessing, int 
heartbeatIntervalSeconds, ReactiveThrottler reactiveThrottler,
+                                      Set<ConnectionCheck> connectionChecks, 
boolean proxyRequired) {
         this.hello = hello;
         this.processor = processor;
         this.encoder = encoder;
@@ -165,6 +183,8 @@ public class ImapChannelUpstreamHandler extends 
ChannelInboundHandlerAdapter imp
         this.ignoreIDLEUponProcessing = ignoreIDLEUponProcessing;
         this.heartbeatHandler = new 
ImapHeartbeatHandler(heartbeatIntervalSeconds, heartbeatIntervalSeconds, 
heartbeatIntervalSeconds);
         this.reactiveThrottler = reactiveThrottler;
+        this.connectionChecks = connectionChecks;
+        this.proxyRequired = proxyRequired;
     }
 
     @Override
@@ -177,6 +197,9 @@ public class ImapChannelUpstreamHandler extends 
ChannelInboundHandlerAdapter imp
         ctx.channel().attr(LINEARALIZER_ATTRIBUTE_KEY).set(new Linearalizer());
         MDCBuilder boundMDC = IMAPMDCContext.boundMDC(ctx)
             .addToContext(MDCBuilder.SESSION_ID, sessionId.asString());
+        if (!proxyRequired) {
+            Flux.fromIterable(connectionChecks).concatMap(connectionCheck -> 
connectionCheck.validate(imapsession.getRemoteAddress())).then().block();
+        }
         imapsession.setAttribute(MDC_KEY, boundMDC);
         try (Closeable closeable = mdc(imapsession).build()) {
             InetSocketAddress address = (InetSocketAddress) 
ctx.channel().remoteAddress();
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 5380d28dce..d7871a0f35 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
@@ -42,6 +42,7 @@ import java.security.cert.X509Certificate;
 import java.time.Duration;
 import java.util.List;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.function.Predicate;
 import java.util.stream.IntStream;
@@ -69,6 +70,7 @@ import org.apache.commons.net.imap.AuthenticatingIMAPClient;
 import org.apache.commons.net.imap.IMAPReply;
 import org.apache.commons.net.imap.IMAPSClient;
 import org.apache.james.core.Username;
+import org.apache.james.imap.api.ConnectionCheck;
 import org.apache.james.imap.encode.main.DefaultImapEncoderFactory;
 import org.apache.james.imap.main.DefaultImapDecoderFactory;
 import org.apache.james.imap.processor.base.AbstractProcessor;
@@ -111,6 +113,7 @@ import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.sun.mail.imap.IMAPFolder;
 
 import ch.qos.logback.classic.Logger;
@@ -148,6 +151,7 @@ class IMAPServerTest {
         memoryIntegrationResources = inMemoryIntegrationResources;
 
         RecordingMetricFactory metricFactory = new RecordingMetricFactory();
+        Set<ConnectionCheck> connectionChecks = defaultConnectionChecks();
         IMAPServer imapServer = new IMAPServer(
             DefaultImapDecoderFactory.createDecoder(),
             new DefaultImapEncoderFactory().buildImapEncoder(),
@@ -162,7 +166,7 @@ class IMAPServerTest {
                 memoryIntegrationResources.getQuotaRootResolver(),
                 metricFactory),
             new ImapMetrics(metricFactory),
-            new NoopGaugeRegistry());
+            new NoopGaugeRegistry(), connectionChecks);
 
         FileSystemImpl fileSystem = 
FileSystemImpl.forTestingWithConfigurationFromClasspath();
         imapServer.setFileSystem(fileSystem);
@@ -172,7 +176,6 @@ class IMAPServerTest {
 
         return imapServer;
     }
-
     private IMAPServer 
createImapServer(HierarchicalConfiguration<ImmutableNode> config) throws 
Exception {
         authenticator = new FakeAuthenticator();
         authenticator.addUser(USER, USER_PASS);
@@ -197,6 +200,59 @@ class IMAPServerTest {
         return 
createImapServer(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)));
     }
 
+    private Set<ConnectionCheck> defaultConnectionChecks() {
+        return ImmutableSet.of(new IpConnectionCheck());
+    }
+
+    @Nested
+    class ConnectionCheckTest {
+
+        IMAPServer imapServer;
+        private final IpConnectionCheck ipConnectionCheck = new 
IpConnectionCheck();
+        private int port;
+
+        @BeforeEach
+        void beforeEach() throws Exception {
+            HierarchicalConfiguration<ImmutableNode> config = 
ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("imapServerImapConnectCheck.xml"));
+            imapServer = createImapServer(config);
+            port = imapServer.getListenAddresses().get(0).getPort();
+        }
+
+        @AfterEach
+        void tearDown() {
+            imapServer.destroy();
+        }
+
+        @Test
+        void banIpWhenBannedIpConnect() {
+            imapServer.getConnectionChecks().stream()
+                .filter(check -> check instanceof IpConnectionCheck)
+                .map(check -> (IpConnectionCheck) check)
+                .forEach(ipCheck -> ipCheck.setBannedIps(Set.of("127.0.0.1")));
+
+            assertThatThrownBy(() -> testIMAPClient.connect("127.0.0.1", port)
+                .login(USER.asString(), USER_PASS)
+                .append("INBOX", SMALL_MESSAGE));
+        }
+
+        @Test
+        void allowConnectWithUnbannedIp() throws IOException {
+            imapServer.getConnectionChecks().stream()
+                .filter(check -> check instanceof IpConnectionCheck)
+                .map(check -> (IpConnectionCheck) check)
+                .forEach(ipCheck -> ipCheck.setBannedIps(Set.of("127.0.0.2")));
+
+            testIMAPClient.connect("127.0.0.1", port)
+                .login(USER.asString(), USER_PASS)
+                .append("INBOX", SMALL_MESSAGE);
+
+            assertThat(testIMAPClient
+                .select("INBOX")
+                .readFirstMessage())
+                .contains("* 1 FETCH (FLAGS (\\Recent \\Seen) BODY[] 
{21}\r\nheader: value\r\n\r\nBODY)\r\n");
+        }
+    }
+
     @Nested
     class PartialFetch {
         IMAPServer imapServer;
@@ -492,6 +548,10 @@ class IMAPServerTest {
 
     @Nested
     class Proxy {
+        private static final String CLIENT_IP = "255.255.255.254";
+        private static final String PROXY_IP = "255.255.255.255";
+        private static final String RANDOM_IP = "127.0.0.2";
+
         IMAPServer imapServer;
         private SocketChannel clientConnection;
 
@@ -512,10 +572,57 @@ class IMAPServerTest {
             imapServer.destroy();
         }
 
+        private void addBannedIps(String clientIp) {
+            imapServer.getConnectionChecks().stream()
+                .filter(check -> check instanceof IpConnectionCheck)
+                .map(check -> (IpConnectionCheck) check)
+                .forEach(ipCheck -> ipCheck.setBannedIps(Set.of(clientIp)));
+        }
+
         @Test
         void shouldNotFailOnProxyInformation() throws Exception {
             clientConnection.write(ByteBuffer.wrap(String.format("PROXY %s %s 
%s %d %d\r\na0 LOGIN %s %s\r\n",
-                "TCP4", "255.255.255.254", "255.255.255.255", 65535, 65535,
+                "TCP4", CLIENT_IP, PROXY_IP, 65535, 65535,
+                USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+
+            assertThat(new String(readBytes(clientConnection), 
StandardCharsets.US_ASCII))
+                .startsWith("a0 OK");
+        }
+
+        @Test
+        void shouldDetectAndBanByClientIP() throws IOException {
+            addBannedIps(CLIENT_IP);
+
+            // WHEN connect as CLIENT_IP to PROXY_DESTINATION via PROXY_IP
+            clientConnection.write(ByteBuffer.wrap(String.format("PROXY %s %s 
%s %d %d\r\na0 LOGIN %s %s\r\n",
+                "TCP4", CLIENT_IP, PROXY_IP, 65535, 65535,
+                USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+
+            // THEN LOGIN should be rejected
+            assertThat(new String(readBytes(clientConnection), 
StandardCharsets.US_ASCII))
+                .doesNotStartWith("a0 OK");
+        }
+
+        @Test
+        void shouldNotBanByProxyIP() throws IOException {
+            // GIVEN somehow PROXY_IP has been banned by mistake
+            addBannedIps(PROXY_IP);
+
+            clientConnection.write(ByteBuffer.wrap(String.format("PROXY %s %s 
%s %d %d\r\na0 LOGIN %s %s\r\n",
+                "TCP4", CLIENT_IP, PROXY_IP, 65535, 65535,
+                USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
+
+            // THEN CLIENT_IP still can connect
+            assertThat(new String(readBytes(clientConnection), 
StandardCharsets.US_ASCII))
+                .startsWith("a0 OK");
+        }
+
+        @Test
+        void clientUsageShouldBeNormalWhenClientIPIsNotBanned() throws 
IOException {
+            addBannedIps(RANDOM_IP);
+
+            clientConnection.write(ByteBuffer.wrap(String.format("PROXY %s %s 
%s %d %d\r\na0 LOGIN %s %s\r\n",
+                "TCP4", CLIENT_IP, PROXY_IP, 65535, 65535,
                 USER.asString(), USER_PASS).getBytes(StandardCharsets.UTF_8)));
 
             assertThat(new String(readBytes(clientConnection), 
StandardCharsets.US_ASCII))
diff --git 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IpConnectionCheck.java
 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IpConnectionCheck.java
new file mode 100644
index 0000000000..23e61a0432
--- /dev/null
+++ 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IpConnectionCheck.java
@@ -0,0 +1,47 @@
+/****************************************************************
+ * 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.imapserver.netty;
+
+import java.net.InetSocketAddress;
+import java.util.Set;
+
+import org.apache.james.imap.api.ConnectionCheck;
+import org.reactivestreams.Publisher;
+
+import reactor.core.publisher.Mono;
+
+public class IpConnectionCheck implements ConnectionCheck {
+    private Set<String> bannedIps = Set.of();
+
+    @Override
+    public Publisher<Void> validate(InetSocketAddress remoteAddress) {
+        String ip = remoteAddress.getHostName();
+        // check against bannedIps
+        if (bannedIps.stream().anyMatch(bannedIp -> bannedIp.equals(ip))) {
+            return Mono.error(() -> new RuntimeException("Banned"));
+        } else {
+            return Mono.empty();
+        }
+    }
+
+    public void setBannedIps(Set<String> bannedIps) {
+        this.bannedIps = bannedIps;
+    }
+}
diff --git 
a/server/protocols/protocols-imap4/src/test/resources/imapServerImapConnectCheck.xml
 
b/server/protocols/protocols-imap4/src/test/resources/imapServerImapConnectCheck.xml
new file mode 100644
index 0000000000..4a8f945041
--- /dev/null
+++ 
b/server/protocols/protocols-imap4/src/test/resources/imapServerImapConnectCheck.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>
+    
<additionalConnectionChecks>org.apache.james.imapserver.netty.IpConnectionCheck</additionalConnectionChecks>
+</imapserver>
\ No newline at end of file
diff --git a/third-party/crowdsec/docker-compose.yml 
b/third-party/crowdsec/docker-compose.yml
index 8a6bf728ac..53e3e938c3 100644
--- a/third-party/crowdsec/docker-compose.yml
+++ b/third-party/crowdsec/docker-compose.yml
@@ -14,6 +14,7 @@ services:
       - 
./sample-configuration/extensions.properties:/root/conf/extensions.properties
       - ./sample-configuration/smtpserver.xml:/root/conf/smtpserver.xml
       - 
./sample-configuration/crowdsec.properties:/root/conf/crowdsec.properties
+      - ./sample-configuration/imapserver.xml:/root/conf/imapserver.xml
     networks:
       - james
     ports:
@@ -37,8 +38,8 @@ services:
       - ./sample-configuration/collections:/etc/crowdsec/collections
       - /var/run/docker.sock:/var/run/docker.sock
     ports:
-      - "8082:8080"
-      - "6061:6060"
+      - "8080:8080"
+      - "6060:6060"
     networks:
       - james
 networks:
diff --git a/third-party/crowdsec/pom.xml b/third-party/crowdsec/pom.xml
index b552553773..e0415847a5 100644
--- a/third-party/crowdsec/pom.xml
+++ b/third-party/crowdsec/pom.xml
@@ -52,6 +52,11 @@
             <artifactId>testing-base</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-imap</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${james.protocols.groupId}</groupId>
             <artifactId>protocols-smtp</artifactId>
@@ -92,6 +97,7 @@
         <dependency>
             <groupId>commons-net</groupId>
             <artifactId>commons-net</artifactId>
+            <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>io.projectreactor.netty</groupId>
@@ -129,4 +135,25 @@
             <scope>test</scope>
         </dependency>
     </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                    <finalName>apache-james-crowdsec</finalName>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <phase>package</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
 </project>
diff --git a/third-party/crowdsec/sample-configuration/imapserver.xml 
b/third-party/crowdsec/sample-configuration/imapserver.xml
new file mode 100644
index 0000000000..8af6179bdb
--- /dev/null
+++ b/third-party/crowdsec/sample-configuration/imapserver.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+
+<!--
+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.
+-->
+
+<imapservers>
+    <imapserver enabled="true">
+        <jmxName>imapserver</jmxName>
+        <bind>0.0.0.0:143</bind>
+        <connectionBacklog>200</connectionBacklog>
+        <tls socketTLS="false" startTLS="false">
+            <keystore>classpath://keystore</keystore>
+            <secret>james72laBalle</secret>
+            
<provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+        </tls>
+        <connectionLimit>0</connectionLimit>
+        <connectionLimitPerIP>0</connectionLimitPerIP>
+        <plainAuthDisallowed>false</plainAuthDisallowed>
+        <gracefulShutdown>false</gracefulShutdown>
+        <customProperties>pong.response=customImapParameter</customProperties>
+        <customProperties>prop.b=anotherValue</customProperties>
+        <gracefulShutdown>false</gracefulShutdown>
+        
<additionalConnectionChecks>org.apache.james.CrowdsecImapConnectionCheck</additionalConnectionChecks>
+    </imapserver>
+</imapservers>
\ No newline at end of file
diff --git 
a/third-party/crowdsec/src/main/java/org/apache/james/CrowdsecImapConnectionCheck.java
 
b/third-party/crowdsec/src/main/java/org/apache/james/CrowdsecImapConnectionCheck.java
new file mode 100644
index 0000000000..270445645e
--- /dev/null
+++ 
b/third-party/crowdsec/src/main/java/org/apache/james/CrowdsecImapConnectionCheck.java
@@ -0,0 +1,67 @@
+/****************************************************************
+ * 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;
+
+import java.net.InetSocketAddress;
+
+import javax.inject.Inject;
+
+import org.apache.commons.net.util.SubnetUtils;
+import org.apache.james.exception.CrowdsecException;
+import org.apache.james.imap.api.ConnectionCheck;
+import org.apache.james.model.CrowdsecClientConfiguration;
+import org.apache.james.model.CrowdsecDecision;
+import org.apache.james.model.CrowdsecHttpClient;
+import org.reactivestreams.Publisher;
+
+public class CrowdsecImapConnectionCheck implements ConnectionCheck {
+    private final CrowdsecClientConfiguration crowdsecClientConfiguration;
+
+    @Inject
+    public CrowdsecImapConnectionCheck(CrowdsecClientConfiguration 
crowdsecClientConfiguration) {
+        this.crowdsecClientConfiguration = crowdsecClientConfiguration;
+    }
+
+    @Override
+    public Publisher<Void> validate(InetSocketAddress remoteAddress) {
+        String ip = remoteAddress.getHostName();
+        CrowdsecHttpClient client = new 
CrowdsecHttpClient(crowdsecClientConfiguration);
+        return client.getCrowdsecDecisions()
+            .filter(decisions -> decisions.stream().anyMatch(decision -> 
isBanned(decision, ip)))
+            .handle((crowdsecDecisions, synchronousSink) -> 
synchronousSink.error(new CrowdsecException("Ip " + ip + " is not allowed to 
connect to IMAP server by Crowdsec")));
+    }
+
+    private boolean isBanned(CrowdsecDecision decision, String ip) {
+        if (decision.getScope().equals("Ip") && 
ip.contains(decision.getValue())) {
+            return true;
+        }
+        if (decision.getScope().equals("Range") && 
belongToNetwork(decision.getValue(), ip)) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean belongToNetwork(String value, String ip) {
+        SubnetUtils subnetUtils = new SubnetUtils(value);
+        subnetUtils.setInclusiveHostCount(true);
+
+        return subnetUtils.getInfo().isInRange(ip);
+    }
+}
diff --git 
a/third-party/crowdsec/src/main/java/org/apache/james/exception/CrowdsecException.java
 
b/third-party/crowdsec/src/main/java/org/apache/james/exception/CrowdsecException.java
new file mode 100644
index 0000000000..38688be3c1
--- /dev/null
+++ 
b/third-party/crowdsec/src/main/java/org/apache/james/exception/CrowdsecException.java
@@ -0,0 +1,26 @@
+/****************************************************************
+ * 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.exception;
+
+public class CrowdsecException extends RuntimeException {
+    public CrowdsecException(String message) {
+        super(message);
+    }
+}
diff --git 
a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecContract.java 
b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecContract.java
new file mode 100644
index 0000000000..2ca5d6a2ae
--- /dev/null
+++ b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecContract.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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;
+
+
+import org.apache.james.utils.DataProbeImpl;
+import org.junit.jupiter.api.BeforeEach;
+
+public interface CrowdsecContract {
+
+    String DOMAIN = "domain.tld";
+    String BOB = "bob@" + DOMAIN;
+    String BOB_PASSWORD = "bobPassword";
+
+    @BeforeEach
+    default void setup(GuiceJamesServer server) throws Exception {
+        server.getProbe(DataProbeImpl.class).fluent()
+            .addDomain(DOMAIN)
+            .addUser(BOB, BOB_PASSWORD);
+    }
+}
diff --git 
a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecImapConnectionCheckTest.java
 
b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecImapConnectionCheckTest.java
new file mode 100644
index 0000000000..df8287cb94
--- /dev/null
+++ 
b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecImapConnectionCheckTest.java
@@ -0,0 +1,97 @@
+/****************************************************************
+ * 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;
+
+import static org.apache.james.CrowdsecExtension.CROWDSEC_PORT;
+import static 
org.apache.james.model.CrowdsecClientConfiguration.DEFAULT_API_KEY;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URL;
+
+import org.apache.james.model.CrowdsecClientConfiguration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import reactor.core.publisher.Mono;
+
+public class CrowdsecImapConnectionCheckTest {
+    @RegisterExtension
+    static CrowdsecExtension crowdsecExtension = new CrowdsecExtension();
+
+    @BeforeAll
+    static void setUp() throws IOException, InterruptedException {
+        crowdsecExtension.getCrowdsecContainer().execInContainer("cscli", 
"bouncer", "add", "bouncer", "-k", DEFAULT_API_KEY);
+    }
+
+    @Test
+    void givenIPBannedByCrowdsecDecisionIp() throws IOException, 
InterruptedException {
+        banIP("--ip", "127.0.0.3");
+        int port = 
crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new 
CrowdsecClientConfiguration(new URL("http://localhost:"; + port + "/v1"), 
DEFAULT_API_KEY);
+
+        CrowdsecImapConnectionCheck connectionCheck = new 
CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
+        connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
+        assertThatThrownBy(() -> Mono.from(connectionCheck.validate(new 
InetSocketAddress("127.0.0.3", 8800))).block())
+            .hasMessage("Ip 127.0.0.3 is not allowed to connect to IMAP server 
by Crowdsec");
+    }
+
+    @Test
+    void givenIPBannedByCrowdsecDecisionIpRange() throws IOException, 
InterruptedException {
+        banIP("--range", "127.0.0.1/24");
+        int port = 
crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new 
CrowdsecClientConfiguration(new URL("http://localhost:"; + port + "/v1"), 
DEFAULT_API_KEY);
+
+        CrowdsecImapConnectionCheck connectionCheck = new 
CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
+        connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
+        assertThatThrownBy(() -> Mono.from(connectionCheck.validate(new 
InetSocketAddress("127.0.0.3", 8800))).block())
+            .hasMessage("Ip 127.0.0.3 is not allowed to connect to IMAP server 
by Crowdsec");
+    }
+
+    @Test
+    void givenIPNotBannedByCrowdsecDecisionIp() throws IOException, 
InterruptedException {
+        banIP("--ip", "192.182.39.2");
+
+        int port = 
crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new 
CrowdsecClientConfiguration(new URL("http://localhost:"; + port + "/v1"), 
DEFAULT_API_KEY);
+
+        CrowdsecImapConnectionCheck connectionCheck = new 
CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
+        connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
+        Mono.from(connectionCheck.validate(new InetSocketAddress("127.0.0.3", 
8800))).block();
+    }
+
+    @Test
+    void givenIPNotBannedByCrowdsecDecisionIpRange() throws IOException, 
InterruptedException {
+        banIP("--range", "192.182.39.2/24");
+
+        int port = 
crowdsecExtension.getCrowdsecContainer().getMappedPort(CROWDSEC_PORT);
+        CrowdsecClientConfiguration crowdsecClientConfiguration = new 
CrowdsecClientConfiguration(new URL("http://localhost:"; + port + "/v1"), 
DEFAULT_API_KEY);
+
+        CrowdsecImapConnectionCheck connectionCheck = new 
CrowdsecImapConnectionCheck(crowdsecClientConfiguration);
+        connectionCheck.validate(new InetSocketAddress("127.0.0.3", 8800));
+        Mono.from(connectionCheck.validate(new InetSocketAddress("127.0.0.3", 
8800))).block();
+    }
+
+    private static void banIP(String type, String value) throws IOException, 
InterruptedException {
+        crowdsecExtension.getCrowdsecContainer().execInContainer("cscli", 
"decision", "add", type, value);
+    }
+}
diff --git 
a/third-party/crowdsec/src/test/java/org/apache/james/MemoryCrowdsecTest.java 
b/third-party/crowdsec/src/test/java/org/apache/james/MemoryCrowdsecTest.java
new file mode 100644
index 0000000000..3155bbbf07
--- /dev/null
+++ 
b/third-party/crowdsec/src/test/java/org/apache/james/MemoryCrowdsecTest.java
@@ -0,0 +1,58 @@
+/****************************************************************
+ * 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;
+
+import static 
org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+import static 
org.apache.james.model.CrowdsecClientConfiguration.DEFAULT_API_KEY;
+
+import java.io.IOException;
+
+import org.apache.james.utils.TestIMAPClient;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryCrowdsecTest implements CrowdsecContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new 
JamesServerBuilder<MemoryJamesConfiguration>(tmpDir ->
+        MemoryJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
+        .server(MemoryJamesServerMain::createServer)
+        .build();
+
+    @RegisterExtension
+    static CrowdsecExtension crowdsecExtension = new CrowdsecExtension();
+
+    @RegisterExtension
+    public TestIMAPClient testIMAPClient = new TestIMAPClient();
+
+    @BeforeAll
+    static void setUp() throws IOException, InterruptedException {
+        crowdsecExtension.getCrowdsecContainer().execInContainer("cscli", 
"bouncer", "add", "bouncer", "-k", DEFAULT_API_KEY);
+    }
+
+    @Test
+    void IPShouldBeBannedByCrowdsecWhenFailingToLoginThreeTimes() {
+        // TODO client login failed 3 times in short period, James will ban 
this IP based on Crowdsec decision
+    }
+}
diff --git a/third-party/crowdsec/src/test/resources/logback.xml 
b/third-party/crowdsec/src/test/resources/logback.xml
new file mode 100644
index 0000000000..3d56ea2e15
--- /dev/null
+++ b/third-party/crowdsec/src/test/resources/logback.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+ -->
+
+<configuration>
+
+    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
+        <resetJUL>true</resetJUL>
+    </contextListener>
+
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
+                <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>
+                <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>
+
+                <!-- Importance for handling multiple lines log -->
+                <appendLineSeparator>true</appendLineSeparator>
+
+                <jsonFormatter 
class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
+                    <prettyPrint>false</prettyPrint>
+                </jsonFormatter>
+            </layout>
+        </encoder>
+        <immediateFlush>false</immediateFlush>
+    </appender>
+    <root level="WARN">
+        <appender-ref ref="CONSOLE" />
+    </root>
+
+    <logger name="org.apache.james" level="INFO" />
+</configuration>
\ No newline at end of file


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

Reply via email to