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]
