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

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

commit 07981e23169a51197f240ea3901f8e6b77835084
Author: Quan Tran <[email protected]>
AuthorDate: Fri Jul 17 11:51:54 2026 +0700

    JAMES-4210 Wire POP3 AUTH and secure USER/PASS
    
    Register AUTH, configure PASS through the PLAIN SASL policy, and require 
TLS for plaintext USER/PASS by default.
---
 .../src/test/resources/pop3server.xml              |  3 +
 .../src/test/resources/pop3server.xml              |  3 +
 .../pop3server/core/CoreCmdHandlerLoader.java      |  5 +-
 .../james/pop3server/core/PassCmdHandler.java      | 77 ++++++++++------------
 .../apache/james/pop3server/netty/POP3Server.java  | 65 +++++++++++++++++-
 .../src/test/resources/pop3server.xml              |  3 +
 6 files changed, 108 insertions(+), 48 deletions(-)

diff --git a/server/apps/distributed-pop3-app/src/test/resources/pop3server.xml 
b/server/apps/distributed-pop3-app/src/test/resources/pop3server.xml
index 6e4473aae2..8b1a83fe32 100644
--- a/server/apps/distributed-pop3-app/src/test/resources/pop3server.xml
+++ b/server/apps/distributed-pop3-app/src/test/resources/pop3server.xml
@@ -35,6 +35,9 @@
         <connectiontimeout>1200</connectiontimeout>
         <connectionLimit>0</connectionLimit>
         <connectionLimitPerIP>0</connectionLimitPerIP>
+        <auth>
+            <requireSSL>false</requireSSL>
+        </auth>
         <handlerchain>
             <handler 
class="org.apache.james.pop3server.core.CoreCmdHandlerLoader"/>
         </handlerchain>
diff --git 
a/server/protocols/protocols-pop3-distributed/src/test/resources/pop3server.xml 
b/server/protocols/protocols-pop3-distributed/src/test/resources/pop3server.xml
index 6deb806e08..971e0eb464 100644
--- 
a/server/protocols/protocols-pop3-distributed/src/test/resources/pop3server.xml
+++ 
b/server/protocols/protocols-pop3-distributed/src/test/resources/pop3server.xml
@@ -9,6 +9,9 @@
         <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
     </tls>
     <connectiontimeout>3600</connectiontimeout>
+    <auth>
+        <requireSSL>false</requireSSL>
+    </auth>
     <handlerchain>
         <handler 
class="org.apache.james.pop3server.core.CoreCmdHandlerLoader"/>
     </handlerchain>
diff --git 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/CoreCmdHandlerLoader.java
 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/CoreCmdHandlerLoader.java
index 1f75f44a18..773ac9e6eb 100644
--- 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/CoreCmdHandlerLoader.java
+++ 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/CoreCmdHandlerLoader.java
@@ -21,13 +21,13 @@ package org.apache.james.pop3server.core;
 
 import java.util.List;
 
-import org.apache.james.protocols.api.handler.CommandDispatcher;
 import org.apache.james.protocols.api.handler.CommandHandlerResultLogger;
 import org.apache.james.protocols.lib.handler.HandlersPackage;
 import org.apache.james.protocols.pop3.core.CapaCmdHandler;
 import org.apache.james.protocols.pop3.core.DeleCmdHandler;
 import org.apache.james.protocols.pop3.core.ListCmdHandler;
 import org.apache.james.protocols.pop3.core.NoopCmdHandler;
+import org.apache.james.protocols.pop3.core.Pop3CommandDispatcher;
 import org.apache.james.protocols.pop3.core.QuitCmdHandler;
 import org.apache.james.protocols.pop3.core.RetrCmdHandler;
 import org.apache.james.protocols.pop3.core.RsetCmdHandler;
@@ -45,8 +45,9 @@ public class CoreCmdHandlerLoader implements HandlersPackage {
     private static final List<String> commands = List.of(
             // Insert the base commands in the Map
             WelcomeMessageHandler.class.getName(),
-            CommandDispatcher.class.getName(),
+            Pop3CommandDispatcher.class.getName(),
             CapaCmdHandler.class.getName(),
+            AuthCmdHandler.class.getName(),
             UserCmdHandler.class.getName(),
             PassCmdHandler.class.getName(),
             ListCmdHandler.class.getName(),
diff --git 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/PassCmdHandler.java
 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/PassCmdHandler.java
index 93ea6a573d..9ee5112a5a 100644
--- 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/PassCmdHandler.java
+++ 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/PassCmdHandler.java
@@ -18,8 +18,7 @@
  ****************************************************************/
 package org.apache.james.pop3server.core;
 
-import static org.apache.james.protocols.sasl.plain.PlainSaslMechanism.ENABLED;
-import static 
org.apache.james.protocols.sasl.plain.PlainSaslMechanism.REQUIRE_SSL;
+import static 
org.apache.james.pop3server.core.AuthCmdHandler.AUTH_REQUIRES_TLS;
 
 import java.io.IOException;
 import java.util.Optional;
@@ -29,11 +28,6 @@ import jakarta.inject.Named;
 
 import org.apache.james.core.Username;
 import org.apache.james.mailbox.MailboxManager;
-import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.mailbox.MessageManager;
-import org.apache.james.mailbox.exception.MailboxException;
-import org.apache.james.mailbox.model.MailboxId;
-import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.pop3server.mailbox.MailboxAdapterFactory;
 import org.apache.james.protocols.api.Request;
@@ -54,37 +48,50 @@ import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
 
-import reactor.core.publisher.Mono;
-
 /**
  * {@link PassCmdHandler} which also handles POP3 Before SMTP
  * 
  */
 public class PassCmdHandler extends AbstractPassCmdHandler  {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(PassCmdHandler.class);
-    private static final PlainSaslMechanism PLAIN_SASL_MECHANISM = new 
PlainSaslMechanism(ENABLED, !REQUIRE_SSL);
 
-    private final MailboxManager manager;
-    private final MailboxAdapterFactory mailboxAdapterFactory;
+    private final Pop3MailboxProvider mailboxProvider;
     private final SaslAuthenticator saslAuthenticator;
+    private Optional<PlainSaslMechanism> plainSaslMechanism;
 
     @Inject
     public PassCmdHandler(@Named("mailboxmanager") MailboxManager manager, 
MailboxAdapterFactory mailboxAdapterFactory, MetricFactory metricFactory) {
         super(metricFactory);
-        this.manager = manager;
-        this.mailboxAdapterFactory = mailboxAdapterFactory;
+        this.mailboxProvider = new Pop3MailboxProvider(manager, 
mailboxAdapterFactory);
         this.saslAuthenticator = 
JamesSaslAuthenticator.jamesSaslAuthenticator(manager);
+        this.plainSaslMechanism = Optional.empty();
+    }
+
+    public void configurePlainSaslMechanism(PlainSaslMechanism 
plainSaslMechanism) {
+        this.plainSaslMechanism = Optional.of(plainSaslMechanism);
     }
 
     @Override
     public Response onCommand(POP3Session session, Request request) {
+        boolean authenticationRequiresTls = authenticationRequiresTls(session, 
request);
         Response response =  super.onCommand(session, request);
+        if (authenticationRequiresTls) {
+            response = AUTH_REQUIRES_TLS;
+        }
         if (POP3Response.OK_RESPONSE.equals(response.getRetCode())) {
             
POP3BeforeSMTPHelper.addIPAddress(session.getRemoteAddress().getAddress().getHostAddress());
         }
         return response;
     }
 
+    private boolean authenticationRequiresTls(POP3Session session, Request 
request) {
+        return session.getHandlerState() == POP3Session.AUTHENTICATION_USERSET
+            && request.getArgument() != null
+            && !session.isTLSStarted()
+            && availablePlainSaslMechanism(session).isEmpty()
+            && plainSaslMechanism.filter(mechanism -> 
mechanism.isAvailableOnTransport(true)).isPresent();
+    }
+
 
     @Override
     protected Mailbox auth(POP3Session session, Username username, String 
password) throws Exception {
@@ -95,13 +102,24 @@ public class PassCmdHandler extends AbstractPassCmdHandler 
 {
     }
 
     private Mailbox authenticate(POP3Session session, Username username, 
String password) throws IOException {
-        return switch (PLAIN_SASL_MECHANISM.authenticate(username, password, 
saslAuthenticator)) {
-            case SaslStep.Success success -> openMailbox(session, 
success.identity().authorizationId());
+        Optional<PlainSaslMechanism> availablePlainSaslMechanism = 
availablePlainSaslMechanism(session);
+        if (availablePlainSaslMechanism.isEmpty()) {
+            LOGGER.warn("PASS rejected because authentication is unavailable 
on the current transport");
+            return null;
+        }
+
+        return switch 
(availablePlainSaslMechanism.orElseThrow().authenticate(username, password, 
saslAuthenticator)) {
+            case SaslStep.Success success -> mailboxProvider.open(session, 
success.identity().authorizationId());
             case SaslStep.Failure failure -> 
handleAuthenticationFailure(session, failure.failure());
             case SaslStep.Challenge ignored -> throw new 
IllegalStateException("Direct PLAIN authentication must be terminal");
         };
     }
 
+    private Optional<PlainSaslMechanism> 
availablePlainSaslMechanism(POP3Session session) {
+        return plainSaslMechanism
+            .filter(mechanism -> 
mechanism.isAvailableOnTransport(session.isTLSStarted()));
+    }
+
     private Mailbox handleAuthenticationFailure(POP3Session session, 
SaslFailure failure) throws IOException {
         if (failure.type() == SaslFailure.Type.SERVER_ERROR) {
             throw failure.cause()
@@ -115,31 +133,4 @@ public class PassCmdHandler extends AbstractPassCmdHandler 
 {
         return null;
     }
 
-    private Mailbox openMailbox(POP3Session session, Username username) throws 
IOException {
-        MailboxSession mSession = null;
-        try {
-            mSession = manager.authenticate(username).withoutDelegation();
-            session.stopDetectingCommandInjection();
-            manager.startProcessingRequest(mSession);
-            MailboxPath inbox = MailboxPath.inbox(mSession);
-            
-            // check if the mailbox exists, if not create it
-            if (!Mono.from(manager.mailboxExists(inbox, mSession)).block()) {
-                Optional<MailboxId> mailboxId = manager.createMailbox(inbox, 
mSession);
-                LOGGER.info("Provisioning INBOX. {} created.", mailboxId);
-            }
-            MessageManager mailbox = 
manager.getMailbox(MailboxPath.inbox(mSession), mSession);
-            LOGGER.info("Opening mailbox {} {} with mailbox session {}",
-                mailbox.getId().serialize(),
-                mailbox.getMailboxPath().asString(),
-                mSession.getSessionId().getValue());
-            return mailboxAdapterFactory.create(mailbox, mSession);
-        } catch (MailboxException e) {
-            throw new IOException("Unable to access mailbox for user " + 
session.getUsername().asString(), e);
-        } finally {
-            if (mSession != null) {
-                manager.endProcessingRequest(mSession);
-            }
-        }
-    }
 }
diff --git 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/netty/POP3Server.java
 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/netty/POP3Server.java
index 87bc8341f8..99d7bba205 100644
--- 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/netty/POP3Server.java
+++ 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/netty/POP3Server.java
@@ -18,30 +18,72 @@
  ****************************************************************/
 package org.apache.james.pop3server.netty;
 
+import static org.apache.james.protocols.sasl.plain.PlainSaslMechanism.ENABLED;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.pop3server.core.AuthCmdHandler;
 import org.apache.james.pop3server.core.CoreCmdHandlerLoader;
+import org.apache.james.pop3server.core.PassCmdHandler;
 import org.apache.james.pop3server.jmx.JMXHandlersLoader;
 import org.apache.james.protocols.api.ProtocolConfiguration;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
 import org.apache.james.protocols.lib.handler.HandlersPackage;
 import org.apache.james.protocols.lib.netty.AbstractProtocolAsyncServer;
-import org.apache.james.protocols.netty.AbstractChannelPipelineFactory;
 import 
org.apache.james.protocols.netty.AllButStartTlsLineChannelHandlerFactory;
 import org.apache.james.protocols.netty.BasicChannelInboundHandler;
 import org.apache.james.protocols.netty.ChannelHandlerFactory;
 import org.apache.james.protocols.netty.ProtocolMDCContextFactory;
 import org.apache.james.protocols.pop3.POP3Protocol;
+import org.apache.james.protocols.sasl.plain.PlainSaslMechanism;
+
+import com.google.common.collect.ImmutableList;
 
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.TooLongFrameException;
 
 /**
  * NIO POP3 Server which use Netty
  */
 public class POP3Server extends AbstractProtocolAsyncServer implements 
POP3ServerMBean {
+    // RFC 5034 section 4 exempts SASL continuation responses from regular 
POP3 command limits.
+    private static final int MAXIMUM_SASL_CONTINUATION_LINE_LENGTH = 65536;
+    private static final boolean REQUIRE_SSL_DEFAULT = true;
 
     /**
      * The configuration data to be passed to the handler
      */
     private final ProtocolConfiguration theConfigData = new 
POP3Configuration();
     private POP3Protocol protocol;
+    private ImmutableList<SaslMechanism> saslMechanisms = ImmutableList.of();
+    private PlainSaslMechanism passSaslMechanism = new 
PlainSaslMechanism(true, REQUIRE_SSL_DEFAULT);
+
+    private class Pop3ChannelInboundHandler extends BasicChannelInboundHandler 
{
+        private Pop3ChannelInboundHandler() {
+            super(new ProtocolMDCContextFactory.Standard(), 
POP3Server.this.protocol, POP3Server.this.getEncryption(), false);
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext context, Throwable 
cause) throws Exception {
+            try {
+                super.exceptionCaught(context, cause);
+            } finally {
+                if (cause instanceof TooLongFrameException) {
+                    // Flush the line-length error before closing. Channel 
teardown then releases
+                    // any active SASL exchange instead of leaving its 
continuation installed.
+                    
context.channel().writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
+                }
+            }
+        }
+    }
+
+    public void setSaslMechanisms(ImmutableList<SaslMechanism> saslMechanisms) 
{
+        this.saslMechanisms = saslMechanisms;
+    }
 
     @Override
     protected int getDefaultPort() {
@@ -73,9 +115,21 @@ public class POP3Server extends AbstractProtocolAsyncServer 
implements POP3Serve
         }
     }
 
+    @Override
+    protected void doConfigure(HierarchicalConfiguration<ImmutableNode> 
config) throws ConfigurationException {
+        super.doConfigure(config);
+        passSaslMechanism = new PlainSaslMechanism(ENABLED, 
config.getBoolean("auth.requireSSL", REQUIRE_SSL_DEFAULT));
+    }
+
     @Override
     protected void preInit() throws Exception {
         super.preInit();
+        getProtocolHandlerChain()
+            .getHandlers(AuthCmdHandler.class)
+            .forEach(handler -> 
handler.configureSaslMechanisms(saslMechanisms));
+        getProtocolHandlerChain()
+            .getHandlers(PassCmdHandler.class)
+            .forEach(handler -> 
handler.configurePlainSaslMechanism(passSaslMechanism));
         protocol = new POP3Protocol(getProtocolHandlerChain(), theConfigData);
     }
 
@@ -86,7 +140,9 @@ public class POP3Server extends AbstractProtocolAsyncServer 
implements POP3Serve
 
     @Override
     protected ChannelInboundHandlerAdapter createCoreHandler() {
-        return new BasicChannelInboundHandler(new 
ProtocolMDCContextFactory.Standard(), protocol, getEncryption(), false);
+        // Unlike the basic handler, close after reporting an oversized SASL 
continuation so
+        // channel teardown releases the active SASL exchange.
+        return new Pop3ChannelInboundHandler();
     }
 
     @Override
@@ -101,7 +157,10 @@ public class POP3Server extends 
AbstractProtocolAsyncServer implements POP3Serve
 
     @Override
     protected ChannelHandlerFactory createFrameHandlerFactory() {
-        return new AllButStartTlsLineChannelHandlerFactory("stls", 
AbstractChannelPipelineFactory.MAX_LINE_LENGTH);
+        // RFC 5034 section 4 excludes SASL response lines from regular POP3 
line-length limits because
+        // they can carry larger encoded mechanism payloads. 
Pop3CommandDispatcher still enforces the
+        // regular POP3 command limit and the RFC 5034 limit for the initial 
AUTH command.
+        return new AllButStartTlsLineChannelHandlerFactory("stls", 
MAXIMUM_SASL_CONTINUATION_LINE_LENGTH);
     }
 
 }
diff --git a/server/protocols/protocols-pop3/src/test/resources/pop3server.xml 
b/server/protocols/protocols-pop3/src/test/resources/pop3server.xml
index 6deb806e08..971e0eb464 100644
--- a/server/protocols/protocols-pop3/src/test/resources/pop3server.xml
+++ b/server/protocols/protocols-pop3/src/test/resources/pop3server.xml
@@ -9,6 +9,9 @@
         <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
     </tls>
     <connectiontimeout>3600</connectiontimeout>
+    <auth>
+        <requireSSL>false</requireSSL>
+    </auth>
     <handlerchain>
         <handler 
class="org.apache.james.pop3server.core.CoreCmdHandlerLoader"/>
     </handlerchain>


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

Reply via email to