This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 3bba59d6af boyscoot: test Authentication parsing (#2906)
3bba59d6af is described below

commit 3bba59d6afa5d5f9f37c7293798de455e908d5c9
Author: Matthieu Baechler <[email protected]>
AuthorDate: Fri Jan 9 07:55:03 2026 +0000

    boyscoot: test Authentication parsing (#2906)
---
 .../protocols/smtp/core/esmtp/AuthCmdHandler.java  | 79 +++++++++++++---------
 .../smtp/core/esmtp/AuthCmdHandlerTest.java        | 68 +++++++++++++++++++
 2 files changed, 114 insertions(+), 33 deletions(-)

diff --git 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java
 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java
index 7cf0f14abc..95055945cd 100644
--- 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java
+++ 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java
@@ -18,12 +18,10 @@
  ****************************************************************/
 
 
-
 package org.apache.james.protocols.smtp.core.esmtp;
 
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collection;
 import java.util.Collections;
@@ -31,7 +29,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.james.core.Username;
@@ -55,8 +52,9 @@ import org.apache.james.util.AuditTrail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
-import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -268,44 +266,59 @@ public class AuthCmdHandler
      */
     private Response doPlainAuth(SMTPSession session, String line) {
         try {
-            List<String> tokens = Optional.ofNullable(decodeBase64(line))
-                .map(userpass1 -> Arrays.stream(userpass1.split("\0"))
-                    .filter(token -> !token.isBlank())
-                    .collect(Collectors.toList()))
-                .orElse(List.of());
-            Preconditions.checkArgument(tokens.size() == 1 || tokens.size() == 
2 || tokens.size() == 3);
-            Response response = null;
-
-            if (tokens.size() == 1) {
-                response = doDelegation(session, Username.of(tokens.get(0)));
-            } else if (tokens.size() == 2) {
-                // If we got here, this is what happened.  RFC 2595
-                // says that "the client may leave the authorization
-                // identity empty to indicate that it is the same as
-                // the authentication identity."  As noted above,
-                // that would be represented as a decoded string of
-                // the form: "\0authenticate-id\0password".  The
-                // first call to nextToken will skip the empty
-                // authorize-id, and give us the authenticate-id,
-                // which we would store as the authorize-id.  The
-                // second call will give us the password, which we
-                // think is the authenticate-id (user).  Then when
-                // we ask for the password, there are no more
-                // elements, leading to the exception we just
-                // caught.  So we need to move the user to the
-                // password, and the authorize_id to the user.
-                response = doAuthTest(session, 
Optional.of(Username.of(tokens.get(0))), Optional.of(tokens.get(1)), 
AUTH_TYPE_PLAIN);
+
+            AuthValues authValues =
+                    Optional.ofNullable(decodeBase64(line))
+                            .flatMap(AuthCmdHandler::parseAuthValues)
+                            .orElseThrow(() -> new 
IllegalArgumentException("Can't parse line as authentication values"));
+
+            Response response;
+
+            if (authValues.password.isEmpty()) {
+                response = doDelegation(session, authValues.username);
             } else {
-                response = doAuthTest(session, 
Optional.of(Username.of(tokens.get(1))), Optional.of(tokens.get(2)), 
AUTH_TYPE_PLAIN);
+                response = doAuthTest(session, 
Optional.of(authValues.username), authValues.password, AUTH_TYPE_PLAIN);
             }
+
             session.popLineHandler();
             return response;
         } catch (Exception e) {
             LOGGER.info("Could not decode parameters for AUTH PLAIN", e);
-            return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,"Could 
not decode parameters for AUTH PLAIN");
+            return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could 
not decode parameters for AUTH PLAIN");
         }
     }
 
+    @VisibleForTesting
+    static Optional<AuthValues> parseAuthValues(String input) {
+
+        List<String> parts = 
Splitter.on('\0').splitToStream(input).filter(token -> 
!token.isBlank()).toList();
+
+        return switch (parts.size()) {
+            case 1 -> Optional.of(new AuthValues(Username.of(parts.get(0)), 
Optional.empty()));
+            // If we got here, this is what happened.  RFC 2595
+            // says that "the client may leave the authorization
+            // identity empty to indicate that it is the same as
+            // the authentication identity."  As noted above,
+            // that would be represented as a decoded string of
+            // the form: "\0authenticate-id\0password".  The
+            // first call to nextToken will skip the empty
+            // authorize-id, and give us the authenticate-id,
+            // which we would store as the authorize-id.  The
+            // second call will give us the password, which we
+            // think is the authenticate-id (user).  Then when
+            // we ask for the password, there are no more
+            // elements, leading to the exception we just
+            // caught.  So we need to move the user to the
+            // password, and the authorize_id to the user.
+            case 2 -> Optional.of(new AuthValues(Username.of(parts.get(0)), 
Optional.of(parts.get(1))));
+            case 3 -> Optional.of(new AuthValues(Username.of(parts.get(1)), 
Optional.of(parts.get(2))));
+            default -> Optional.empty();
+        };
+    }
+
+    record AuthValues(Username username, Optional<String> password) {
+    }
+
     private String decodeBase64(String line) {
         if (line != null) {
             String lineWithoutTrailingCrLf = StringUtils.replace(line, "\r\n", 
"");
diff --git 
a/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandlerTest.java
 
b/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandlerTest.java
new file mode 100644
index 0000000000..edf407331d
--- /dev/null
+++ 
b/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandlerTest.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.smtp.core.esmtp;
+
+import java.util.Optional;
+
+import org.apache.james.core.Username;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class AuthCmdHandlerTest {
+
+    @Test
+    void shouldReturnEmptyWhenEmptyInput() {
+        Assertions.assertThat(AuthCmdHandler.parseAuthValues("")).isEmpty();
+    }
+
+    @Test
+    void shouldReturnEmptyWhenBlankInput() {
+        Assertions.assertThat(AuthCmdHandler.parseAuthValues(" 
\t\n")).isEmpty();
+    }
+
+    @Test
+    void shouldReturnEmptyWhenTwoBlankParts() {
+        Assertions.assertThat(AuthCmdHandler.parseAuthValues(" 
\0\t\n")).isEmpty();
+    }
+
+    @Test
+    void shouldReturnEmptyWhenThreeBlankParts() {
+        Assertions.assertThat(AuthCmdHandler.parseAuthValues(" \0\0 
")).isEmpty();
+    }
+
+    @Test
+    void shouldReturnUsernameWhenSinglePart() {
+        Assertions.assertThat(AuthCmdHandler.parseAuthValues("bob"))
+                .contains(new AuthCmdHandler.AuthValues(Username.of("bob"), 
Optional.empty()));
+    }
+
+    @Test
+    void shouldReturnUsernameAndPassWhenTwoParts() {
+        Assertions.assertThat(AuthCmdHandler.parseAuthValues("bob\0pass"))
+                .contains(new AuthCmdHandler.AuthValues(Username.of("bob"), 
Optional.of("pass")));
+    }
+
+    @Test
+    void shouldReturnUsernameAndPassWhenThreeParts() {
+        
Assertions.assertThat(AuthCmdHandler.parseAuthValues("something\0bob\0pass"))
+                .contains(new AuthCmdHandler.AuthValues(Username.of("bob"), 
Optional.of("pass")));
+    }
+
+
+}
\ No newline at end of file


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

Reply via email to