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]
