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 6bc8a97e94456a69c4d4f8879288ab6d43471594
Author: Quan Tran <[email protected]>
AuthorDate: Mon Jul 20 20:37:47 2026 +0700

    JAMES-4210 POP3: reuse SaslCodex
---
 .../apache/james/protocols/api/sasl/SaslCodec.java |   2 +-
 .../james/protocols/pop3/sasl/Pop3SaslBridge.java  | 107 -------------
 .../protocols/pop3/sasl/Pop3SaslBridgeTest.java    | 166 ---------------------
 .../james/pop3server/core/AuthCmdHandler.java      |  29 ++--
 4 files changed, 17 insertions(+), 287 deletions(-)

diff --git 
a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslCodec.java
 
b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslCodec.java
index 6f1fdf2b3d..238186f344 100644
--- 
a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslCodec.java
+++ 
b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslCodec.java
@@ -26,7 +26,7 @@ import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
 
 /**
- * Wire encoding shared by the line based protocols carrying SASL exchanges 
(IMAP, SMTP).
+ * Wire encoding shared by the line based protocols carrying SASL exchanges 
(IMAP, SMTP, POP3).
  *
  * Those protocols agree on the encoding rules defined by RFC 4422: payloads 
travel base64
  * encoded on a single line, "*" cancels the exchange, and an empty line 
acknowledges final
diff --git 
a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/sasl/Pop3SaslBridge.java
 
b/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/sasl/Pop3SaslBridge.java
deleted file mode 100644
index ad9d1fffcf..0000000000
--- 
a/protocols/pop3/src/main/java/org/apache/james/protocols/pop3/sasl/Pop3SaslBridge.java
+++ /dev/null
@@ -1,107 +0,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.                                           *
- ****************************************************************/
-
-package org.apache.james.protocols.pop3.sasl;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Optional;
-import java.util.regex.Pattern;
-
-import org.apache.james.protocols.api.Response;
-import org.apache.james.protocols.api.sasl.SaslExchange;
-import org.apache.james.protocols.api.sasl.SaslInitialRequest;
-import org.apache.james.protocols.api.sasl.SaslStep;
-import org.apache.james.protocols.pop3.POP3Response;
-
-public class Pop3SaslBridge {
-    private static final Pattern CANONICAL_BASE64 = 
Pattern.compile("(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?");
-
-    /** Converts a POP3 AUTH command into a protocol-neutral SASL initial 
request. */
-    public SaslInitialRequest initialRequest(String mechanismName, 
Optional<String> initialClientResponse) {
-        return new SaslInitialRequest(mechanismName, 
initialClientResponse.map(this::decodeInitialClientResponse));
-    }
-
-    /** Encodes a SASL challenge as a POP3 continuation response. */
-    public Response challenge(SaslStep.Challenge challenge) {
-        return continuation(challenge.payload());
-    }
-
-    /** Encodes final SASL server data as a POP3 continuation response. */
-    public Response successData(SaslStep.Success success) {
-        return continuation(success.serverData());
-    }
-
-    /** Decodes a POP3 client continuation and forwards it to the SASL 
exchange. */
-    public SaslStep onClientResponse(SaslExchange exchange, byte[] line) {
-        return exchange.onResponse(decodeBase64(stripTrailingCrlf(line)));
-    }
-
-    /** Detects the RFC 5034 client cancellation marker. */
-    public boolean isAbort(byte[] line) {
-        return "*".equals(stripTrailingCrlf(line));
-    }
-
-    /** Detects the empty acknowledgement required after final SASL server 
data. */
-    public boolean isEmptyClientResponse(byte[] line) {
-        return stripTrailingCrlf(line).isEmpty();
-    }
-
-    /** Aborts and releases an active SASL exchange. */
-    public void abort(SaslExchange exchange) {
-        exchange.abort();
-    }
-
-    /** Releases an active SASL exchange. */
-    public void close(SaslExchange exchange) {
-        exchange.close();
-    }
-
-    private Response continuation(Optional<byte[]> payload) {
-        String encodedPayload = payload
-            .map(Base64.getEncoder()::encodeToString)
-            .orElse("");
-        return new POP3Response("+", encodedPayload).immutable();
-    }
-
-    private byte[] decodeInitialClientResponse(String value) {
-        if (value.equals("=")) {
-            return new byte[0];
-        }
-        return decodeBase64(value);
-    }
-
-    private byte[] decodeBase64(String value) {
-        if (!CANONICAL_BASE64.matcher(value).matches()) {
-            throw new IllegalArgumentException("Invalid Base64 value");
-        }
-        return Base64.getDecoder().decode(value);
-    }
-
-    private String stripTrailingCrlf(byte[] line) {
-        String value = new String(line, StandardCharsets.US_ASCII);
-        if (value.endsWith("\r\n")) {
-            return value.substring(0, value.length() - 2);
-        }
-        if (value.endsWith("\n")) {
-            return value.substring(0, value.length() - 1);
-        }
-        return value;
-    }
-}
diff --git 
a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/sasl/Pop3SaslBridgeTest.java
 
b/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/sasl/Pop3SaslBridgeTest.java
deleted file mode 100644
index d56c78ce77..0000000000
--- 
a/protocols/pop3/src/test/java/org/apache/james/protocols/pop3/sasl/Pop3SaslBridgeTest.java
+++ /dev/null
@@ -1,166 +0,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.                                           *
- ****************************************************************/
-
-package org.apache.james.protocols.pop3.sasl;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Optional;
-
-import org.apache.james.core.Username;
-import org.apache.james.protocols.api.Response;
-import org.apache.james.protocols.api.sasl.SaslExchange;
-import org.apache.james.protocols.api.sasl.SaslIdentity;
-import org.apache.james.protocols.api.sasl.SaslInitialRequest;
-import org.apache.james.protocols.api.sasl.SaslStep;
-import org.junit.jupiter.api.Test;
-
-class Pop3SaslBridgeTest {
-    private static final Username USER = Username.of("[email protected]");
-    private static final SaslIdentity IDENTITY = new SaslIdentity(USER, USER);
-
-    private final Pop3SaslBridge testee = new Pop3SaslBridge();
-
-    private static class RecordingExchange implements SaslExchange {
-        private byte[] lastClientResponse;
-        private boolean closed;
-
-        @Override
-        public SaslStep firstStep() {
-            return new SaslStep.Challenge(Optional.empty());
-        }
-
-        @Override
-        public SaslStep onResponse(byte[] clientResponse) {
-            lastClientResponse = clientResponse.clone();
-            return new SaslStep.Success(IDENTITY, Optional.empty());
-        }
-
-        @Override
-        public void close() {
-            closed = true;
-        }
-    }
-
-    @Test
-    void initialRequestShouldDecodeCanonicalBase64() {
-        SaslInitialRequest request = testee.initialRequest("PLAIN", 
Optional.of(encoded("initial")));
-
-        assertThat(request.initialResponse()).hasValueSatisfying(value -> 
assertThat(value).containsExactly(bytes("initial")));
-    }
-
-    @Test
-    void initialRequestShouldDecodeEqualSignAsEmptyResponse() {
-        SaslInitialRequest request = testee.initialRequest("PLAIN", 
Optional.of("="));
-
-        assertThat(request.initialResponse()).hasValueSatisfying(value -> 
assertThat(value).isEmpty());
-    }
-
-    @Test
-    void initialRequestShouldRejectMissingPadding() {
-        assertThatThrownBy(() -> testee.initialRequest("PLAIN", 
Optional.of("YQ")))
-            .isInstanceOf(IllegalArgumentException.class);
-    }
-
-    @Test
-    void initialRequestShouldRejectMisplacedPadding() {
-        assertThatThrownBy(() -> testee.initialRequest("PLAIN", 
Optional.of("Y=Q=")))
-            .isInstanceOf(IllegalArgumentException.class);
-    }
-
-    @Test
-    void challengeShouldEncodePop3Continuation() {
-        Response response = testee.challenge(new 
SaslStep.Challenge(Optional.of(bytes("challenge"))));
-
-        assertThat(response.getLines()).containsExactly("+ " + 
encoded("challenge"));
-    }
-
-    @Test
-    void challengeShouldPreserveSpaceForEmptyPayload() {
-        Response response = testee.challenge(new 
SaslStep.Challenge(Optional.empty()));
-
-        assertThat(response.getLines()).containsExactly("+ ");
-    }
-
-    @Test
-    void successDataShouldEncodePop3Continuation() {
-        Response response = testee.successData(new SaslStep.Success(IDENTITY, 
Optional.of(bytes("server-data"))));
-
-        assertThat(response.getLines()).containsExactly("+ " + 
encoded("server-data"));
-    }
-
-    @Test
-    void onClientResponseShouldDecodeContinuation() {
-        RecordingExchange exchange = new RecordingExchange();
-
-        SaslStep result = testee.onClientResponse(exchange, 
(encoded("response") + "\r\n").getBytes(StandardCharsets.US_ASCII));
-
-        assertThat(result).isInstanceOf(SaslStep.Success.class);
-        
assertThat(exchange.lastClientResponse).containsExactly(bytes("response"));
-    }
-
-    @Test
-    void onClientResponseShouldAcceptEmptyResponse() {
-        RecordingExchange exchange = new RecordingExchange();
-
-        testee.onClientResponse(exchange, 
"\r\n".getBytes(StandardCharsets.US_ASCII));
-
-        assertThat(exchange.lastClientResponse).isEmpty();
-    }
-
-    @Test
-    void onClientResponseShouldRejectMissingPadding() {
-        RecordingExchange exchange = new RecordingExchange();
-
-        assertThatThrownBy(() -> testee.onClientResponse(exchange, 
"YQ\r\n".getBytes(StandardCharsets.US_ASCII)))
-            .isInstanceOf(IllegalArgumentException.class);
-    }
-
-    @Test
-    void isAbortShouldOnlyAcceptSingleAsterisk() {
-        
assertThat(testee.isAbort("*\r\n".getBytes(StandardCharsets.US_ASCII))).isTrue();
-        
assertThat(testee.isAbort("**\r\n".getBytes(StandardCharsets.US_ASCII))).isFalse();
-    }
-
-    @Test
-    void isEmptyClientResponseShouldAcceptEmptyLine() {
-        
assertThat(testee.isEmptyClientResponse("\r\n".getBytes(StandardCharsets.US_ASCII))).isTrue();
-        
assertThat(testee.isEmptyClientResponse("=\r\n".getBytes(StandardCharsets.US_ASCII))).isFalse();
-    }
-
-    @Test
-    void abortShouldUseCombinedExchangeLifecycleContract() {
-        RecordingExchange exchange = new RecordingExchange();
-
-        testee.abort(exchange);
-
-        assertThat(exchange.closed).isTrue();
-    }
-
-    private static String encoded(String value) {
-        return Base64.getEncoder().encodeToString(bytes(value));
-    }
-
-    private static byte[] bytes(String value) {
-        return value.getBytes(StandardCharsets.UTF_8);
-    }
-}
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
index 03473f5ebb..1fb760d7bc 100644
--- 
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
@@ -42,6 +42,7 @@ 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.SaslCodec;
 import org.apache.james.protocols.api.sasl.SaslExchange;
 import org.apache.james.protocols.api.sasl.SaslFailure;
 import org.apache.james.protocols.api.sasl.SaslInitialRequest;
@@ -55,7 +56,6 @@ 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;
@@ -79,7 +79,6 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
     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;
@@ -149,7 +148,7 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
             }
             return UNSUPPORTED_MECHANISM;
         }
-        return start(session, mechanism, 
SASL_BRIDGE.initialRequest(request.mechanismName(), request.initialResponse()));
+        return start(session, mechanism, 
SaslCodec.initialRequest(request.mechanismName(), request.initialResponse()));
     }
 
     private Response start(POP3Session session, SaslMechanism mechanism, 
SaslInitialRequest request) {
@@ -161,9 +160,9 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
     private void registerExchange(POP3Session session, SaslExchange exchange) {
         try {
             session.setAttachment(ACTIVE_SASL_EXCHANGE, exchange, 
State.Connection)
-                .ifPresent(SASL_BRIDGE::close);
+                .ifPresent(SaslExchange::close);
         } catch (RuntimeException e) {
-            SASL_BRIDGE.close(exchange);
+            exchange.close();
             throw e;
         }
     }
@@ -183,7 +182,7 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
                 if (!continuationActive) {
                     pushContinuationHandler(session, exchange);
                 }
-                yield respondActiveContinuation(session, exchange, () -> 
SASL_BRIDGE.challenge(challenge));
+                yield respondActiveContinuation(session, exchange, () -> 
challengeResponse(challenge.payload()));
             }
             case SaslStep.Success success -> {
                 if (continuationActive) {
@@ -223,7 +222,7 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
 
     private Optional<SaslStep> nextStep(POP3Session session, SaslExchange 
exchange, byte[] line) {
         try {
-            return Optional.of(SASL_BRIDGE.onClientResponse(exchange, line));
+            return 
Optional.of(exchange.onResponse(SaslCodec.decodeClientResponse(line)));
         } catch (IllegalArgumentException e) {
             closeActiveContinuation(session, exchange);
             return Optional.empty();
@@ -236,7 +235,7 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
     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 respondActiveContinuation(session, exchange, () -> 
challengeResponse(success.serverData()));
         }
         return completeAuthentication(session, exchange, success);
     }
@@ -267,7 +266,7 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
 
     private boolean isAbort(POP3Session session, SaslExchange exchange, byte[] 
line) {
         try {
-            return SASL_BRIDGE.isAbort(line);
+            return SaslCodec.isAbort(line);
         } catch (RuntimeException e) {
             closeActiveContinuation(session, exchange);
             throw e;
@@ -276,7 +275,7 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
 
     private boolean isEmptyClientResponse(POP3Session session, SaslExchange 
exchange, byte[] line) {
         try {
-            return SASL_BRIDGE.isEmptyClientResponse(line);
+            return SaslCodec.isEmptyClientResponse(line);
         } catch (RuntimeException e) {
             closeActiveContinuation(session, exchange);
             throw e;
@@ -292,6 +291,10 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
         }
     }
 
+    private Response challengeResponse(Optional<byte[]> payload) {
+        return new POP3Response("+", SaslCodec.encode(payload)).immutable();
+    }
+
     private void popActiveContinuation(POP3Session session, SaslExchange 
exchange) {
         try {
             session.popLineHandler();
@@ -366,19 +369,19 @@ public class AuthCmdHandler extends 
AbstractPOP3CommandHandler implements CapaCa
 
     private void closeExchange(POP3Session session, SaslExchange exchange) {
         session.removeAttachment(ACTIVE_SASL_EXCHANGE, State.Connection);
-        SASL_BRIDGE.close(exchange);
+        exchange.close();
     }
 
     private void abortExchange(POP3Session session, SaslExchange exchange) {
         session.removeAttachment(ACTIVE_SASL_EXCHANGE, State.Connection);
-        SASL_BRIDGE.abort(exchange);
+        exchange.abort();
     }
 
     @Override
     public void onDisconnect(POP3Session session) {
         if (session != null) {
             session.removeAttachment(ACTIVE_SASL_EXCHANGE, State.Connection)
-                .ifPresent(SASL_BRIDGE::close);
+                .ifPresent(SaslExchange::close);
         }
     }
 


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

Reply via email to