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 31bc950aa9b839ef5a669241b13773ee3bf93486
Author: Quan Tran <[email protected]>
AuthorDate: Fri Jul 17 11:52:41 2026 +0700

    JAMES-4210 Test POP3 SASL production wiring
    
    Cover Guice mechanism configuration and distributed CAPA and AUTH PLAIN 
behavior.
---
 .../java/org/apache/james/Pop3ServerContract.java  | 71 +++++++++++++++++++++
 .../modules/protocols/POP3ServerModuleTest.java    | 72 ++++++++++++++++++++++
 2 files changed, 143 insertions(+)

diff --git 
a/server/apps/distributed-pop3-app/src/test/java/org/apache/james/Pop3ServerContract.java
 
b/server/apps/distributed-pop3-app/src/test/java/org/apache/james/Pop3ServerContract.java
index cce24282ba..5b5032f5fc 100644
--- 
a/server/apps/distributed-pop3-app/src/test/java/org/apache/james/Pop3ServerContract.java
+++ 
b/server/apps/distributed-pop3-app/src/test/java/org/apache/james/Pop3ServerContract.java
@@ -21,12 +21,21 @@ package org.apache.james;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.Reader;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 import java.util.List;
 import java.util.stream.IntStream;
 
 import org.apache.commons.net.pop3.POP3Client;
 import org.apache.commons.net.pop3.POP3MessageInfo;
+import org.apache.commons.net.pop3.POP3Reply;
 import org.apache.james.backends.cassandra.TestingSession;
 import 
org.apache.james.backends.cassandra.init.SessionWithInitializedTablesFactory;
 import org.apache.james.core.Username;
@@ -102,6 +111,68 @@ public interface Pop3ServerContract {
     String PASSWORD = "123456";
     String DOMAIN = "examplebis.local";
 
+    private static String plainInitialResponse(String username, String 
password) {
+        return Base64.getEncoder()
+            .encodeToString(("\0" + username + "\0" + 
password).getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static void send(BufferedWriter writer, String command) throws 
IOException {
+        writer.write(command + "\r\n");
+        writer.flush();
+    }
+
+    @Test
+    default void capaShouldAdvertisePlainSaslMechanism(GuiceJamesServer 
server) throws Exception {
+        POP3Client pop3Client = new POP3Client();
+        pop3Client.connect("127.0.0.1", 
server.getProbe(Pop3GuiceProbe.class).getPop3Port());
+
+        assertThat(pop3Client.sendCommand("CAPA")).isEqualTo(POP3Reply.OK);
+        pop3Client.getAdditionalReply();
+        assertThat(pop3Client.getReplyStrings()).contains("SASL PLAIN");
+
+        pop3Client.disconnect();
+    }
+
+    @Test
+    default void authPlainShouldSucceed(GuiceJamesServer server) throws 
Exception {
+        server.getProbe(DataProbeImpl.class).fluent()
+            .addDomain(DOMAIN)
+            .addUser(USER, PASSWORD);
+
+        try (Socket socket = new Socket("127.0.0.1", 
server.getProbe(Pop3GuiceProbe.class).getPop3Port());
+                BufferedReader reader = new BufferedReader(new 
InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
+                BufferedWriter writer = new BufferedWriter(new 
OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII))) {
+            assertThat(reader.readLine()).startsWith("+OK");
+
+            // WHEN authenticating with an RFC 5034 PLAIN initial response
+            send(writer, "AUTH PLAIN " + plainInitialResponse(USER, PASSWORD));
+
+            // THEN authentication succeeds and POP3 transaction commands are 
available
+            assertThat(reader.readLine()).isEqualTo("+OK Welcome " + USER);
+            send(writer, "STAT");
+            assertThat(reader.readLine()).matches("\\+OK \\d+ \\d+");
+        }
+    }
+
+    @Test
+    default void authPlainShouldRejectInvalidCredentials(GuiceJamesServer 
server) throws Exception {
+        server.getProbe(DataProbeImpl.class).fluent()
+            .addDomain(DOMAIN)
+            .addUser(USER, PASSWORD);
+
+        try (Socket socket = new Socket("127.0.0.1", 
server.getProbe(Pop3GuiceProbe.class).getPop3Port());
+                BufferedReader reader = new BufferedReader(new 
InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
+                BufferedWriter writer = new BufferedWriter(new 
OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII))) {
+            assertThat(reader.readLine()).startsWith("+OK");
+
+            send(writer, "AUTH PLAIN " + plainInitialResponse(USER, 
"invalid-password"));
+            assertThat(reader.readLine()).isEqualTo("-ERR Authentication 
failed.");
+
+            send(writer, "STAT");
+            assertThat(reader.readLine()).isEqualTo("-ERR");
+        }
+    }
+
     @Test
     default void mailsCanBeReadInPop3(GuiceJamesServer server) throws 
Exception {
         server.getProbe(DataProbeImpl.class).fluent()
diff --git 
a/server/container/guice/protocols/pop/src/test/java/org/apache/james/modules/protocols/POP3ServerModuleTest.java
 
b/server/container/guice/protocols/pop/src/test/java/org/apache/james/modules/protocols/POP3ServerModuleTest.java
new file mode 100644
index 0000000000..8a011cd9e3
--- /dev/null
+++ 
b/server/container/guice/protocols/pop/src/test/java/org/apache/james/modules/protocols/POP3ServerModuleTest.java
@@ -0,0 +1,72 @@
+/****************************************************************
+ * 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.modules.protocols;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+class POP3ServerModuleTest {
+    private final POP3ServerModule testee = new POP3ServerModule();
+
+    @Test
+    void retrieveSaslMechanismFactoryClassNamesShouldReturnEmptyWhenAbsent() 
throws Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+
+        ImmutableList<String> mechanismFactoryClassNames = 
testee.retrieveSaslMechanismFactoryClassNames(configuration);
+
+        assertThat(mechanismFactoryClassNames).isEmpty();
+    }
+
+    @Test
+    void retrieveSaslMechanismFactoryClassNamesShouldPreserveConfiguredOrder() 
throws Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms",
+            
"XOauth2SaslMechanismFactory,PlainSaslMechanismFactory,com.example.CustomSaslMechanismFactory");
+
+        ImmutableList<String> mechanismFactoryClassNames = 
testee.retrieveSaslMechanismFactoryClassNames(configuration);
+
+        assertThat(mechanismFactoryClassNames)
+            .containsExactly("XOauth2SaslMechanismFactory", 
"PlainSaslMechanismFactory", "com.example.CustomSaslMechanismFactory");
+    }
+
+    @Test
+    void 
retrieveSaslMechanismFactoryClassNamesShouldRejectBlankConfiguredList() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms", " ");
+
+        assertThatThrownBy(() -> 
testee.retrieveSaslMechanismFactoryClassNames(configuration))
+            .isInstanceOf(ConfigurationException.class);
+    }
+
+    @Test
+    void retrieveSaslMechanismFactoryClassNamesShouldRejectBlankEntry() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms", 
"PlainSaslMechanismFactory,,XOauth2SaslMechanismFactory");
+
+        assertThatThrownBy(() -> 
testee.retrieveSaslMechanismFactoryClassNames(configuration))
+            .isInstanceOf(ConfigurationException.class);
+    }
+}


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

Reply via email to