This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new e9a402638f [ENAHNCEMENT] Implement a MaxConnectionLifespanHandler SMTP
handler
e9a402638f is described below
commit e9a402638fc7325ca7de10f9e731bd169e6e764b
Author: Benoit TELLIER <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]