You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by hq...@apache.org on 2026/07/21 08:25:59 UTC
(james-project) 13/13: JAMES-4210 POP3: reuse SaslCodex
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 <hq...@linagora.com>
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("user@example.com");
- 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: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org