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 88f762eedfe2b6d8d7e86a0b237f0b8c7cdc9c4f
Author: Quan Tran <[email protected]>
AuthorDate: Fri Jul 17 11:51:26 2026 +0700

    JAMES-4210 Implement POP3 AUTH command handler
    
    Implement AUTH parsing, transport availability checks, exchange lifecycle 
management, capability advertisement, and mailbox authentication.
---
 .../james/pop3server/core/AuthCmdHandler.java      | 402 +++++++++++++++++++++
 1 file changed, 402 insertions(+)

diff --git 
a/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/AuthCmdHandler.java
 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/AuthCmdHandler.java
new file mode 100644
index 0000000000..03473f5ebb
--- /dev/null
+++ 
b/server/protocols/protocols-pop3/src/main/java/org/apache/james/pop3server/core/AuthCmdHandler.java
@@ -0,0 +1,402 @@
+/****************************************************************
+ * 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.pop3server.core;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+
+import org.apache.james.core.Username;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.pop3server.mailbox.MailboxAdapterFactory;
+import org.apache.james.protocols.api.ProtocolSession.AttachmentKey;
+import org.apache.james.protocols.api.ProtocolSession.State;
+import org.apache.james.protocols.api.Request;
+import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.api.handler.DisconnectHandler;
+import org.apache.james.protocols.api.handler.LineHandler;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslFailure;
+import org.apache.james.protocols.api.sasl.SaslInitialRequest;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslStep;
+import org.apache.james.protocols.lib.POP3BeforeSMTPHelper;
+import org.apache.james.protocols.pop3.POP3Response;
+import org.apache.james.protocols.pop3.POP3Session;
+import org.apache.james.protocols.pop3.core.AbstractPOP3CommandHandler;
+import org.apache.james.protocols.pop3.core.CapaCapability;
+import org.apache.james.protocols.pop3.core.MDCConstants;
+import org.apache.james.protocols.pop3.mailbox.Mailbox;
+import org.apache.james.protocols.pop3.mailbox.MessageMetaData;
+import org.apache.james.protocols.pop3.sasl.Pop3SaslBridge;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
+import org.apache.james.util.MDCBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+public class AuthCmdHandler extends AbstractPOP3CommandHandler implements 
CapaCapability, DisconnectHandler<POP3Session> {
+    private record AuthRequest(String mechanismName, Optional<String> 
initialResponse) {
+    }
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(AuthCmdHandler.class);
+    private static final boolean CONTINUATION_ACTIVE = true;
+    private static final boolean CONTINUATION_INACTIVE = false;
+    private static final Collection<String> COMMANDS = ImmutableSet.of("AUTH");
+    private static final Response AUTH_FAILED = new 
POP3Response(POP3Response.ERR_RESPONSE, "Authentication failed.").immutable();
+    private static final Response AUTH_ABORTED = new 
POP3Response(POP3Response.ERR_RESPONSE, "Authentication aborted.").immutable();
+    public static final Response AUTH_REQUIRES_TLS = new 
POP3Response(POP3Response.ERR_RESPONSE, "Authentication requires 
TLS.").immutable();
+    private static final Response INVALID_AUTH_REQUEST = new 
POP3Response(POP3Response.ERR_RESPONSE, "Invalid AUTH request.").immutable();
+    private static final Response UNSUPPORTED_MECHANISM = new 
POP3Response(POP3Response.ERR_RESPONSE, "Unsupported authentication 
mechanism.").immutable();
+    private static final Response UNEXPECTED_ERROR = new 
POP3Response(POP3Response.ERR_RESPONSE, "Unexpected error accessing 
mailbox").immutable();
+    private static final Pop3SaslBridge SASL_BRIDGE = new Pop3SaslBridge();
+    private static final AttachmentKey<SaslExchange> ACTIVE_SASL_EXCHANGE = 
AttachmentKey.of("ACTIVE_SASL_EXCHANGE", SaslExchange.class);
+
+    private final Pop3MailboxProvider mailboxProvider;
+    private final SaslAuthenticator saslAuthenticator;
+    private final MetricFactory metricFactory;
+    private ImmutableList<SaslMechanism> saslMechanisms = ImmutableList.of();
+
+    @Inject
+    public AuthCmdHandler(@Named("mailboxmanager") MailboxManager 
mailboxManager, MailboxAdapterFactory mailboxAdapterFactory, MetricFactory 
metricFactory) {
+        this.mailboxProvider = new Pop3MailboxProvider(mailboxManager, 
mailboxAdapterFactory);
+        this.saslAuthenticator = 
JamesSaslAuthenticator.jamesSaslAuthenticator(mailboxManager);
+        this.metricFactory = metricFactory;
+    }
+
+    public void configureSaslMechanisms(ImmutableList<SaslMechanism> 
saslMechanisms) {
+        this.saslMechanisms = saslMechanisms;
+    }
+
+    @Override
+    public Response onCommand(POP3Session session, Request request) {
+        return metricFactory.decorateSupplierWithTimerMetric("pop3-auth", () ->
+            MDCBuilder.withMdc(
+                MDCBuilder.create()
+                    .addToContext(MDCBuilder.ACTION, "AUTH")
+                    .addToContext(MDCConstants.withSession(session)),
+                () -> authenticate(session, request)));
+    }
+
+    private Response authenticate(POP3Session session, Request request) {
+        if (session.getHandlerState() == POP3Session.TRANSACTION) {
+            return AUTH_FAILED;
+        }
+        try {
+            AuthRequest authRequest = parse(request);
+            return findMechanism(authRequest.mechanismName())
+                .map(mechanism -> authenticate(session, authRequest, 
mechanism))
+                .orElse(UNSUPPORTED_MECHANISM);
+        } catch (IllegalArgumentException e) {
+            LOGGER.info("Invalid POP3 AUTH request: {}", e.getMessage());
+            return INVALID_AUTH_REQUEST;
+        }
+    }
+
+    private AuthRequest parse(Request request) {
+        String argument = request.getArgument();
+        if (argument == null) {
+            throw new IllegalArgumentException("Invalid AUTH command");
+        }
+
+        String[] arguments = argument.trim().split(" +", 2);
+        if (arguments.length == 0 || arguments[0].isBlank()) {
+            throw new IllegalArgumentException("Missing SASL mechanism");
+        }
+        return new AuthRequest(arguments[0].toUpperCase(Locale.US), 
arguments.length == 2 ? Optional.of(arguments[1]) : Optional.empty());
+    }
+
+    private Optional<SaslMechanism> findMechanism(String mechanismName) {
+        return saslMechanisms.stream()
+            .filter(mechanism -> 
mechanism.name().equalsIgnoreCase(mechanismName))
+            .findFirst();
+    }
+
+    private Response authenticate(POP3Session session, AuthRequest request, 
SaslMechanism mechanism) {
+        if (!mechanism.isAvailableOnTransport(session.isTLSStarted())) {
+            if (!session.isTLSStarted() && 
mechanism.isAvailableOnTransport(true)) {
+                return AUTH_REQUIRES_TLS;
+            }
+            return UNSUPPORTED_MECHANISM;
+        }
+        return start(session, mechanism, 
SASL_BRIDGE.initialRequest(request.mechanismName(), request.initialResponse()));
+    }
+
+    private Response start(POP3Session session, SaslMechanism mechanism, 
SaslInitialRequest request) {
+        SaslExchange exchange = mechanism.start(request, saslAuthenticator);
+        registerExchange(session, exchange);
+        return handleStep(session, exchange, firstStep(session, exchange), 
CONTINUATION_INACTIVE);
+    }
+
+    private void registerExchange(POP3Session session, SaslExchange exchange) {
+        try {
+            session.setAttachment(ACTIVE_SASL_EXCHANGE, exchange, 
State.Connection)
+                .ifPresent(SASL_BRIDGE::close);
+        } catch (RuntimeException e) {
+            SASL_BRIDGE.close(exchange);
+            throw e;
+        }
+    }
+
+    private SaslStep firstStep(POP3Session session, SaslExchange exchange) {
+        try {
+            return exchange.firstStep();
+        } catch (RuntimeException e) {
+            closeExchange(session, exchange);
+            throw e;
+        }
+    }
+
+    private Response handleStep(POP3Session session, SaslExchange exchange, 
SaslStep step, boolean continuationActive) {
+        return switch (step) {
+            case SaslStep.Challenge challenge -> {
+                if (!continuationActive) {
+                    pushContinuationHandler(session, exchange);
+                }
+                yield respondActiveContinuation(session, exchange, () -> 
SASL_BRIDGE.challenge(challenge));
+            }
+            case SaslStep.Success success -> {
+                if (continuationActive) {
+                    popActiveContinuation(session, exchange);
+                }
+                yield handleSuccess(session, exchange, success);
+            }
+            case SaslStep.Failure failure -> {
+                if (continuationActive) {
+                    popActiveContinuation(session, exchange);
+                }
+                yield handleFailure(session, exchange, failure.failure());
+            }
+        };
+    }
+
+    private void pushContinuationHandler(POP3Session session, SaslExchange 
exchange) {
+        try {
+            session.pushLineHandler(continuationHandler(exchange));
+        } catch (RuntimeException e) {
+            closeExchange(session, exchange);
+            throw e;
+        }
+    }
+
+    private LineHandler<POP3Session> continuationHandler(SaslExchange 
exchange) {
+        return (session, line) -> {
+            if (isAbort(session, exchange, line)) {
+                abortActiveContinuation(session, exchange);
+                return AUTH_ABORTED;
+            }
+            return nextStep(session, exchange, line)
+                .map(step -> handleStep(session, exchange, step, 
CONTINUATION_ACTIVE))
+                .orElse(INVALID_AUTH_REQUEST);
+        };
+    }
+
+    private Optional<SaslStep> nextStep(POP3Session session, SaslExchange 
exchange, byte[] line) {
+        try {
+            return Optional.of(SASL_BRIDGE.onClientResponse(exchange, line));
+        } catch (IllegalArgumentException e) {
+            closeActiveContinuation(session, exchange);
+            return Optional.empty();
+        } catch (RuntimeException e) {
+            closeActiveContinuation(session, exchange);
+            throw e;
+        }
+    }
+
+    private Response handleSuccess(POP3Session session, SaslExchange exchange, 
SaslStep.Success success) {
+        if (success.serverData().isPresent()) {
+            pushSuccessDataAcknowledgementHandler(session, exchange, success);
+            return respondActiveContinuation(session, exchange, () -> 
SASL_BRIDGE.successData(success));
+        }
+        return completeAuthentication(session, exchange, success);
+    }
+
+    private void pushSuccessDataAcknowledgementHandler(POP3Session session, 
SaslExchange exchange, SaslStep.Success success) {
+        try {
+            
session.pushLineHandler(successDataAcknowledgementHandler(exchange, success));
+        } catch (RuntimeException e) {
+            closeExchange(session, exchange);
+            throw e;
+        }
+    }
+
+    private LineHandler<POP3Session> 
successDataAcknowledgementHandler(SaslExchange exchange, SaslStep.Success 
success) {
+        return (session, line) -> {
+            if (isAbort(session, exchange, line)) {
+                abortActiveContinuation(session, exchange);
+                return AUTH_ABORTED;
+            }
+            if (!isEmptyClientResponse(session, exchange, line)) {
+                closeActiveContinuation(session, exchange);
+                return INVALID_AUTH_REQUEST;
+            }
+            popActiveContinuation(session, exchange);
+            return completeAuthentication(session, exchange, success);
+        };
+    }
+
+    private boolean isAbort(POP3Session session, SaslExchange exchange, byte[] 
line) {
+        try {
+            return SASL_BRIDGE.isAbort(line);
+        } catch (RuntimeException e) {
+            closeActiveContinuation(session, exchange);
+            throw e;
+        }
+    }
+
+    private boolean isEmptyClientResponse(POP3Session session, SaslExchange 
exchange, byte[] line) {
+        try {
+            return SASL_BRIDGE.isEmptyClientResponse(line);
+        } catch (RuntimeException e) {
+            closeActiveContinuation(session, exchange);
+            throw e;
+        }
+    }
+
+    private Response respondActiveContinuation(POP3Session session, 
SaslExchange exchange, Supplier<Response> responseSupplier) {
+        try {
+            return responseSupplier.get();
+        } catch (RuntimeException e) {
+            closeActiveContinuation(session, exchange);
+            throw e;
+        }
+    }
+
+    private void popActiveContinuation(POP3Session session, SaslExchange 
exchange) {
+        try {
+            session.popLineHandler();
+        } catch (RuntimeException e) {
+            closeExchange(session, exchange);
+            throw e;
+        }
+    }
+
+    private void closeActiveContinuation(POP3Session session, SaslExchange 
exchange) {
+        try {
+            session.popLineHandler();
+        } finally {
+            closeExchange(session, exchange);
+        }
+    }
+
+    private void abortActiveContinuation(POP3Session session, SaslExchange 
exchange) {
+        try {
+            session.popLineHandler();
+        } finally {
+            abortExchange(session, exchange);
+        }
+    }
+
+    private Response completeAuthentication(POP3Session session, SaslExchange 
exchange, SaslStep.Success success) {
+        Username authorizationId = success.identity().authorizationId();
+        try {
+            Mailbox mailbox = mailboxProvider.open(session, authorizationId);
+            session.setUsername(authorizationId);
+            Response response = completeMailboxAuthentication(session, 
mailbox);
+            
POP3BeforeSMTPHelper.addIPAddress(session.getRemoteAddress().getAddress().getHostAddress());
+            return response;
+        } catch (IOException e) {
+            LOGGER.error("Unable to complete POP3 SASL authentication for {}", 
authorizationId, e);
+            return UNEXPECTED_ERROR;
+        } finally {
+            closeExchange(session, exchange);
+        }
+    }
+
+    private Response completeMailboxAuthentication(POP3Session session, 
Mailbox mailbox) {
+        session.setUserMailbox(mailbox);
+        try {
+            List<MessageMetaData> messages = mailbox.getMessages();
+            session.setAttachment(POP3Session.UID_LIST, messages, 
State.Transaction);
+            session.setAttachment(POP3Session.DELETED_UID_LIST, new 
ArrayList<>(), State.Transaction);
+        } catch (IOException e) {
+            LOGGER.error("Unable to initialize POP3 mailbox state", e);
+        }
+        session.setHandlerState(POP3Session.TRANSACTION);
+        return new POP3Response(POP3Response.OK_RESPONSE, "Welcome " + 
session.getUsername().asString());
+    }
+
+    private Response handleFailure(POP3Session session, SaslExchange exchange, 
SaslFailure failure) {
+        try {
+            return switch (failure.type()) {
+                case MALFORMED -> INVALID_AUTH_REQUEST;
+                case INVALID_CREDENTIALS, AUTHENTICATION_FAILED, 
USER_DOES_NOT_EXIST, DELEGATION_FORBIDDEN -> AUTH_FAILED;
+                case SERVER_ERROR -> {
+                    failure.cause()
+                        .ifPresentOrElse(
+                            cause -> LOGGER.error("POP3 SASL authentication 
failed", cause),
+                            () -> LOGGER.error("POP3 SASL authentication 
failed: {}", failure.reason()));
+                    yield UNEXPECTED_ERROR;
+                }
+            };
+        } finally {
+            closeExchange(session, exchange);
+        }
+    }
+
+    private void closeExchange(POP3Session session, SaslExchange exchange) {
+        session.removeAttachment(ACTIVE_SASL_EXCHANGE, State.Connection);
+        SASL_BRIDGE.close(exchange);
+    }
+
+    private void abortExchange(POP3Session session, SaslExchange exchange) {
+        session.removeAttachment(ACTIVE_SASL_EXCHANGE, State.Connection);
+        SASL_BRIDGE.abort(exchange);
+    }
+
+    @Override
+    public void onDisconnect(POP3Session session) {
+        if (session != null) {
+            session.removeAttachment(ACTIVE_SASL_EXCHANGE, State.Connection)
+                .ifPresent(SASL_BRIDGE::close);
+        }
+    }
+
+    @Override
+    public Set<String> getImplementedCapabilities(POP3Session session) {
+        ImmutableList<String> mechanisms = saslMechanisms.stream()
+            .filter(mechanism -> 
mechanism.isAvailableOnTransport(session.isTLSStarted()))
+            .map(SaslMechanism::name)
+            .distinct()
+            .collect(ImmutableList.toImmutableList());
+        if (mechanisms.isEmpty()) {
+            return ImmutableSet.of();
+        }
+        return ImmutableSet.of("SASL " + Joiner.on(' ').join(mechanisms));
+    }
+
+    @Override
+    public Collection<String> getImplCommands() {
+        return COMMANDS;
+    }
+}


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

Reply via email to