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

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

commit 7637b363b081222fc5f1860e703984d253955a26
Author: Benoit TELLIER <btell...@linagora.com>
AuthorDate: Thu Jun 19 17:30:54 2025 +0200

    [ENAHNCEMENT] Implement a MaxConnectionLifespanHandler SMTP handler
---
 .../servers/partials/configure/smtp-hooks.adoc     | 18 +++++
 .../james/protocols/api/ProtocolSession.java       |  9 +++
 .../james/protocols/api/ProtocolSessionImpl.java   | 10 +++
 .../james/protocols/api/ProtocolTransport.java     |  5 ++
 .../protocols/netty/NettyProtocolTransport.java    |  6 ++
 .../netty/MaxConnectionLifespanHandler.java        | 70 ++++++++++++++++
 .../MaxConnectionLifespanHandlerTest.java          | 92 ++++++++++++++++++++++
 .../src/test/resources/smtpserver-logout.xml       | 53 +++++++++++++
 8 files changed, 263 insertions(+)

diff --git a/docs/modules/servers/partials/configure/smtp-hooks.adoc 
b/docs/modules/servers/partials/configure/smtp-hooks.adoc
index f7d8fa7c18..f73df41cca 100644
--- a/docs/modules/servers/partials/configure/smtp-hooks.adoc
+++ b/docs/modules/servers/partials/configure/smtp-hooks.adoc
@@ -369,6 +369,24 @@ These hooks are designed to support the SMTP service 
extension, REQUIRETLS, and
 </smtpserver>
 ....
 
+== MaxConnectionLifespanHandler
+
+[source,xml]
+....
+<smtpserver enabled="true">
+ <handlerchain>
+        <handler 
class="org.apache.james.smtpserver.netty.MaxConnectionLifespanHandler">
+            <duration>5m</duration>
+        </handler>
+        <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
+    </handlerchain>
+</smtpserver>
+....
+
+will close SMTP connections after 5 minutes.
+
+This is useful for instance to force re-authentication and enforce password 
changes.
+
 == DKIM checks hooks
 
 Hook for verifying DKIM signatures of incoming mails.
diff --git 
a/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSession.java
 
b/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSession.java
index 142203977a..b41c054fe7 100644
--- 
a/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSession.java
+++ 
b/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSession.java
@@ -21,6 +21,7 @@ package org.apache.james.protocols.api;
 
 import java.net.InetSocketAddress;
 import java.nio.charset.Charset;
+import java.time.Duration;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -252,4 +253,12 @@ public interface ProtocolSession extends 
CommandDetectionSession {
      * Pop the last command handler 
      */
     void popLineHandler();
+
+    default void schedule(Runnable runnable, Duration waitDelay) {
+        throw new RuntimeException("Not supported");
+    }
+
+    default void close() {
+        throw new RuntimeException("Not supported");
+    }
 }
diff --git 
a/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSessionImpl.java
 
b/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSessionImpl.java
index ee711356b0..155fe481b5 100644
--- 
a/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSessionImpl.java
+++ 
b/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolSessionImpl.java
@@ -23,6 +23,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import java.net.InetSocketAddress;
 import java.nio.charset.Charset;
+import java.time.Duration;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
@@ -237,4 +238,13 @@ public class ProtocolSessionImpl implements 
ProtocolSession {
         transport.pushLineHandler(overrideCommandHandler, this);
     }
 
+    @Override
+    public void schedule(Runnable runnable, Duration waitDelay) {
+        transport.schedule(runnable, waitDelay);
+    }
+
+    @Override
+    public void close() {
+        transport.writeResponse(Response.DISCONNECT, this);
+    }
 }
diff --git 
a/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolTransport.java
 
b/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolTransport.java
index db4cc17917..3162cadb1b 100644
--- 
a/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolTransport.java
+++ 
b/protocols/api/src/main/java/org/apache/james/protocols/api/ProtocolTransport.java
@@ -20,6 +20,7 @@
 package org.apache.james.protocols.api;
 
 import java.net.InetSocketAddress;
+import java.time.Duration;
 import java.util.Optional;
 
 import javax.net.ssl.SSLSession;
@@ -97,4 +98,8 @@ public interface ProtocolTransport {
      * Return <code>true</code> if the channel is readable
      */
     boolean isReadable();
+
+    default void schedule(Runnable runnable, Duration waitDelay) {
+        throw new RuntimeException("Not supported");
+    }
 }
diff --git 
a/protocols/netty/src/main/java/org/apache/james/protocols/netty/NettyProtocolTransport.java
 
b/protocols/netty/src/main/java/org/apache/james/protocols/netty/NettyProtocolTransport.java
index 60b3191e92..621449c50c 100644
--- 
a/protocols/netty/src/main/java/org/apache/james/protocols/netty/NettyProtocolTransport.java
+++ 
b/protocols/netty/src/main/java/org/apache/james/protocols/netty/NettyProtocolTransport.java
@@ -24,7 +24,9 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.InetSocketAddress;
 import java.nio.channels.FileChannel;
+import java.time.Duration;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLSession;
@@ -184,4 +186,8 @@ public class NettyProtocolTransport extends 
AbstractProtocolTransport {
         
     }
 
+    @Override
+    public void schedule(Runnable runnable, Duration waitDelay) {
+        channel.eventLoop().schedule(runnable, waitDelay.toMillis(), 
TimeUnit.MILLISECONDS);
+    }
 }
diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/MaxConnectionLifespanHandler.java
 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/MaxConnectionLifespanHandler.java
new file mode 100644
index 0000000000..a491725d92
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/MaxConnectionLifespanHandler.java
@@ -0,0 +1,70 @@
+/****************************************************************
+ * 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.smtpserver.netty;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.api.handler.ConnectHandler;
+import org.apache.james.protocols.smtp.SMTPSession;
+import org.apache.james.util.DurationParser;
+
+public class MaxConnectionLifespanHandler implements 
ConnectHandler<SMTPSession> {
+    public static final Response NOOP = new Response() {
+
+        @Override
+        public String getRetCode() {
+            return "";
+        }
+
+        @Override
+        public List<CharSequence> getLines() {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public boolean isEndSession() {
+            return false;
+        }
+
+    };
+
+    private Optional<Duration> connectionLifespan = Optional.empty();
+
+    @Override
+    public void init(Configuration config) throws ConfigurationException {
+        connectionLifespan = 
Optional.of(DurationParser.parse(Optional.ofNullable(config.getString("duration",
 null))
+            .orElseThrow(() -> new ConfigurationRuntimeException("'duration' 
configuration property is compulsary"))));
+    }
+
+    @Override
+    public Response onConnect(SMTPSession session) {
+        connectionLifespan.ifPresent(duration ->
+            session.schedule(session::close, duration));
+
+        return NOOP;
+    }
+}
diff --git 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/MaxConnectionLifespanHandlerTest.java
 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/MaxConnectionLifespanHandlerTest.java
new file mode 100644
index 0000000000..1dbadf65b8
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/MaxConnectionLifespanHandlerTest.java
@@ -0,0 +1,92 @@
+/****************************************************************
+ * 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.smtpserver;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.james.smtpserver.SMTPServerTestSystem.BOB;
+import static org.apache.james.smtpserver.SMTPServerTestSystem.PASSWORD;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Base64;
+
+import org.apache.commons.net.smtp.SMTPClient;
+import org.apache.commons.net.smtp.SMTPConnectionClosedException;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MaxConnectionLifespanHandlerTest {
+    protected Configuration configuration;
+
+    private final SMTPServerTestSystem testSystem = new SMTPServerTestSystem();
+
+    @BeforeEach
+    void setUp() throws Exception {
+        testSystem.preSetUp();
+    }
+
+    @AfterEach
+    void tearDown() {
+        testSystem.smtpServer.destroy();
+    }
+
+    @Test
+    void shouldLogoutAfterLifespan() throws Exception {
+        testSystem.smtpServer.configure(FileConfigurationProvider.getConfig(
+            ClassLoader.getSystemResourceAsStream("smtpserver-logout.xml")));
+        testSystem.smtpServer.init();
+
+        SMTPClient smtpProtocol = new SMTPClient();
+        InetSocketAddress bindedAddress = testSystem.getBindedAddress();
+        smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), 
bindedAddress.getPort());
+        authenticate(smtpProtocol);
+
+        Thread.sleep(2000);
+
+        assertThatThrownBy(() -> smtpProtocol.sendCommand("EHLO 
localhost")).isInstanceOf(SMTPConnectionClosedException.class);
+    }
+    
+    @Test
+    void shouldNotLogoutBeforeLifespan() throws Exception {
+        testSystem.smtpServer.configure(FileConfigurationProvider.getConfig(
+            ClassLoader.getSystemResourceAsStream("smtpserver-logout.xml")));
+        testSystem.smtpServer.init();
+
+        SMTPClient smtpProtocol = new SMTPClient();
+        InetSocketAddress bindedAddress = testSystem.getBindedAddress();
+        smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), 
bindedAddress.getPort());
+        authenticate(smtpProtocol);
+
+        assertThatCode(() -> smtpProtocol.sendCommand("EHLO 
localhost")).doesNotThrowAnyException();
+    }
+
+    private void authenticate(SMTPClient smtpProtocol) throws IOException {
+        smtpProtocol.sendCommand("AUTH PLAIN");
+        smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + 
BOB.asString() + "\0" + PASSWORD + "\0").getBytes(UTF_8)));
+        assertThat(smtpProtocol.getReplyCode())
+            .as("authenticated")
+            .isEqualTo(235);
+    }
+}
diff --git 
a/server/protocols/protocols-smtp/src/test/resources/smtpserver-logout.xml 
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-logout.xml
new file mode 100644
index 0000000000..c57b8a7033
--- /dev/null
+++ b/server/protocols/protocols-smtp/src/test/resources/smtpserver-logout.xml
@@ -0,0 +1,53 @@
+<?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.
+ -->
+
+<!-- Read 
https://james.apache.org/server/config-smtp-lmtp.html#SMTP_Configuration for 
further details -->
+
+<smtpserver enabled="true">
+    <bind>0.0.0.0:0</bind>
+    <connectionBacklog>200</connectionBacklog>
+    <tls socketTLS="false" startTLS="false">
+        <keystore>file://conf/keystore</keystore>
+        <secret>james72laBalle</secret>
+        <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+        <algorithm>SunX509</algorithm>
+    </tls>
+    <connectiontimeout>360</connectiontimeout>
+    <connectionLimit>0</connectionLimit>
+    <connectionLimitPerIP>0</connectionLimitPerIP>
+    <auth>
+        <announce>forUnauthorizedAddresses</announce>
+        <requireSSL>false</requireSSL>
+    </auth>
+    <verifyIdentity>true</verifyIdentity>
+    <maxmessagesize>0</maxmessagesize>
+    <addressBracketsEnforcement>true</addressBracketsEnforcement>
+    <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting>
+    <handlerchain>
+        <handler 
class="org.apache.james.smtpserver.netty.MaxConnectionLifespanHandler">
+            <duration>1s</duration>
+        </handler>
+        <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
+    </handlerchain>
+    <gracefulShutdown>false</gracefulShutdown>
+</smtpserver>
+
+


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

Reply via email to