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 71f857b72b804f824f414905bfe63be6cd228d86
Author: Quan Tran <[email protected]>
AuthorDate: Tue Jul 7 09:22:50 2026 +0700

    JAMES-4210 Document custom IMAP SASL extension support
    
    Update the custom IMAP example to provide a configurable SASL mechanism 
factory through auth.saslMechanisms, covering SASL-IR, continuation, invalid 
token, and final server-data flows.
---
 examples/custom-imap/README.md                     |  75 ++++++++
 examples/custom-imap/pom.xml                       |   6 +
 .../sample-configuration/imapserver.xml            |   7 +
 .../imap/sasl/ExampleTokenSaslConfiguration.java   |  43 +++++
 .../imap/sasl/ExampleTokenSaslMechanism.java       |  99 ++++++++++
 .../sasl/ExampleTokenSaslMechanismFactory.java     |  33 ++++
 .../custom-imap/src/main/resources/imapserver.xml  |  16 +-
 .../examples/imap/CustomSaslMechanismTest.java     | 209 +++++++++++++++++++++
 8 files changed, 487 insertions(+), 1 deletion(-)

diff --git a/examples/custom-imap/README.md b/examples/custom-imap/README.md
index 7d0eee0325..750ce1b7be 100644
--- a/examples/custom-imap/README.md
+++ b/examples/custom-imap/README.md
@@ -14,6 +14,26 @@ Sample configure file: 
[imapserver.xml](./sample-configuration/imapserver.xml)
 Note that when `imapPackages` is not provided, James will implicit use
 `org.apache.James.modules.protocols.DefaultImapPackage`
 
+# Creating your own IMAP SASL mechanisms
+
+This example also demonstrates how to add a custom IMAP SASL mechanism.
+The `EXAMPLE-TOKEN` mechanism is declared through `auth.saslMechanisms`,
+while `auth.exampleToken` is a custom configuration block owned by the 
extension:
+
+```xml
+<auth>
+    
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
+    <exampleToken>
+        <expectedToken>secret-token</expectedToken>
+        <authorizedUser>[email protected]</authorizedUser>
+    </exampleToken>
+</auth>
+```
+
+`auth.saslMechanisms` lists SASL mechanism factory classes. Built-in factories
+can use simple names, while custom factories use their fully qualified class 
name.
+The factory reads that server's `auth.exampleToken` block.
+
 ## Running the example
 
 Build the project:
@@ -56,4 +76,59 @@ a02 OK LOGIN completed.
 A03 PING
 * PONG
 A03 OK PING completed.
+A04 LOGOUT
+```
+
+Test the custom SASL mechanism:
+
+```bash
+telnet localhost 143
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+* OK JAMES IMAP4rev1 Server james.local is ready.
+A01 CAPABILITY
+* CAPABILITY IMAP4rev1 AUTH=PLAIN SASL-IR AUTH=EXAMPLE-TOKEN PING
+A01 OK CAPABILITY completed.
+A02 AUTHENTICATE EXAMPLE-TOKEN c2VjcmV0LXRva2Vu
+A02 OK AUTHENTICATE completed.
+A03 PING
+* PONG
+A03 OK PING completed.
+```
+
+The custom SASL mechanism also supports a continuation when the client does 
not send
+the initial response in the `AUTHENTICATE` command. The continuation payload is
+base64-encoded by IMAP, so `R28gYWhlYWQ` decodes to `Go ahead`:
+
+```bash
+telnet localhost 143
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+* OK JAMES IMAP4rev1 Server james.local is ready.
+A01 AUTHENTICATE EXAMPLE-TOKEN
++ R28gYWhlYWQ
+c2VjcmV0LXRva2Vu
+A01 OK AUTHENTICATE completed.
+A02 PING
+* PONG
+A02 OK PING completed.
+```
+
+The mechanism can also return final server data on success. The client 
acknowledges
+that final data with an empty line before James sends the tagged `OK`:
+
+```bash
+telnet localhost 143
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+* OK JAMES IMAP4rev1 Server james.local is ready.
+A01 AUTHENTICATE EXAMPLE-TOKEN
++ R28gYWhlYWQ
+c2VjcmV0LXRva2VuOnNlcnZlci1kYXRh
++ VG9rZW4gYWNjZXB0ZWQ=
+
+A01 OK AUTHENTICATE completed.
 ```
diff --git a/examples/custom-imap/pom.xml b/examples/custom-imap/pom.xml
index bbdfc87b08..a4665f87da 100644
--- a/examples/custom-imap/pom.xml
+++ b/examples/custom-imap/pom.xml
@@ -67,6 +67,12 @@
       <version>${james.baseVersion}</version>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>${james.protocols.groupId}</groupId>
+      <artifactId>protocols-api</artifactId>
+      <version>${james.baseVersion}</version>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
diff --git a/examples/custom-imap/sample-configuration/imapserver.xml 
b/examples/custom-imap/sample-configuration/imapserver.xml
index 3333910586..f1df338c0d 100644
--- a/examples/custom-imap/sample-configuration/imapserver.xml
+++ b/examples/custom-imap/sample-configuration/imapserver.xml
@@ -33,6 +33,13 @@ under the License.
         <connectionLimitPerIP>0</connectionLimitPerIP>
         <plainAuthDisallowed>false</plainAuthDisallowed>
         <gracefulShutdown>false</gracefulShutdown>
+        <auth>
+            
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
+            <exampleToken>
+                <expectedToken>secret-token</expectedToken>
+                <authorizedUser>[email protected]</authorizedUser>
+            </exampleToken>
+        </auth>
         
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
         
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
     </imapserver>
diff --git 
a/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslConfiguration.java
 
b/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslConfiguration.java
new file mode 100644
index 0000000000..6bf0c219cc
--- /dev/null
+++ 
b/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslConfiguration.java
@@ -0,0 +1,43 @@
+/****************************************************************
+ * 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.examples.imap.sasl;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.core.Username;
+
+public record ExampleTokenSaslConfiguration(String expectedToken, Username 
authorizedUser) {
+    private static final String EXPECTED_TOKEN_PROPERTY = 
"auth.exampleToken.expectedToken";
+    private static final String AUTHORIZED_USER_PROPERTY = 
"auth.exampleToken.authorizedUser";
+
+    public static ExampleTokenSaslConfiguration 
from(HierarchicalConfiguration<ImmutableNode> configuration) throws 
ConfigurationException {
+        if (!configuration.containsKey(EXPECTED_TOKEN_PROPERTY)) {
+            throw new ConfigurationException(EXPECTED_TOKEN_PROPERTY + " is 
mandatory");
+        }
+        if (!configuration.containsKey(AUTHORIZED_USER_PROPERTY)) {
+            throw new ConfigurationException(AUTHORIZED_USER_PROPERTY + " is 
mandatory");
+        }
+
+        return new ExampleTokenSaslConfiguration(
+            configuration.getString(EXPECTED_TOKEN_PROPERTY),
+            Username.of(configuration.getString(AUTHORIZED_USER_PROPERTY)));
+    }
+}
diff --git 
a/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslMechanism.java
 
b/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslMechanism.java
new file mode 100644
index 0000000000..6a8943a01b
--- /dev/null
+++ 
b/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslMechanism.java
@@ -0,0 +1,99 @@
+/****************************************************************
+ * 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.examples.imap.sasl;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslFailure;
+import org.apache.james.protocols.api.sasl.SaslIdentity;
+import org.apache.james.protocols.api.sasl.SaslInitialRequest;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslStep;
+
+public class ExampleTokenSaslMechanism implements SaslMechanism {
+    public static final String NAME = "EXAMPLE-TOKEN";
+    public static final String CONTINUATION_PROMPT = "Go ahead";
+    public static final String SUCCESS_DATA_TOKEN_SUFFIX = ":server-data";
+    public static final String SUCCESS_DATA = "Token accepted";
+
+    private final ExampleTokenSaslConfiguration configuration;
+
+    public ExampleTokenSaslMechanism(ExampleTokenSaslConfiguration 
configuration) {
+        this.configuration = configuration;
+    }
+
+    @Override
+    public String name() {
+        return NAME;
+    }
+
+    @Override
+    public SaslExchange start(SaslInitialRequest request, SaslAuthenticator 
authenticator) {
+        Optional<byte[]> initialResponse = request.initialResponse();
+        return new ExampleTokenSaslExchange(initialResponse, configuration);
+    }
+
+    private static class ExampleTokenSaslExchange implements SaslExchange {
+        private final Optional<byte[]> initialResponse;
+        private final ExampleTokenSaslConfiguration configuration;
+
+        private ExampleTokenSaslExchange(Optional<byte[]> initialResponse, 
ExampleTokenSaslConfiguration configuration) {
+            this.initialResponse = initialResponse;
+            this.configuration = configuration;
+        }
+
+        @Override
+        public SaslStep firstStep() {
+            return initialResponse
+                .map(this::authenticate)
+                .orElseGet(() -> new 
SaslStep.Challenge(Optional.of(CONTINUATION_PROMPT
+                    .getBytes(StandardCharsets.UTF_8))));
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            return authenticate(clientResponse);
+        }
+
+        @Override
+        public void close() {
+        }
+
+        private SaslStep authenticate(byte[] clientResponse) {
+            String token = new String(clientResponse, StandardCharsets.UTF_8);
+            if (configuration.expectedToken().equals(token)) {
+                return success(Optional.empty());
+            }
+            // allow client to request server to return data on success 
message, which may be used by Kerberos auth
+            if ((configuration.expectedToken() + 
SUCCESS_DATA_TOKEN_SUFFIX).equals(token)) {
+                return 
success(Optional.of(SUCCESS_DATA.getBytes(StandardCharsets.UTF_8)));
+            }
+            return new 
SaslStep.Failure(SaslFailure.authenticationFailed(Optional.empty(), 
Optional.of(configuration.authorizedUser()),
+                "EXAMPLE-TOKEN authentication failed."));
+        }
+
+        private SaslStep success(Optional<byte[]> serverData) {
+            return new SaslStep.Success(new 
SaslIdentity(configuration.authorizedUser(), configuration.authorizedUser()), 
serverData);
+        }
+    }
+}
diff --git 
a/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslMechanismFactory.java
 
b/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslMechanismFactory.java
new file mode 100644
index 0000000000..57895b789d
--- /dev/null
+++ 
b/examples/custom-imap/src/main/java/org/apache/james/examples/imap/sasl/ExampleTokenSaslMechanismFactory.java
@@ -0,0 +1,33 @@
+/****************************************************************
+ * 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.examples.imap.sasl;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
+
+public class ExampleTokenSaslMechanismFactory implements SaslMechanismFactory {
+    @Override
+    public SaslMechanism create(HierarchicalConfiguration<ImmutableNode> 
serverConfiguration) throws ConfigurationException {
+        return new 
ExampleTokenSaslMechanism(ExampleTokenSaslConfiguration.from(serverConfiguration));
+    }
+}
diff --git a/examples/custom-imap/src/main/resources/imapserver.xml 
b/examples/custom-imap/src/main/resources/imapserver.xml
index 9f5ec0e98b..3ffc60b05a 100644
--- a/examples/custom-imap/src/main/resources/imapserver.xml
+++ b/examples/custom-imap/src/main/resources/imapserver.xml
@@ -36,6 +36,13 @@ under the License.
         <customProperties>pong.response=customImapParameter</customProperties>
         <customProperties>prop.b=anotherValue</customProperties>
         <gracefulShutdown>false</gracefulShutdown>
+        <auth>
+            
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
+            <exampleToken>
+                <expectedToken>secret-token</expectedToken>
+                <authorizedUser>[email protected]</authorizedUser>
+            </exampleToken>
+        </auth>
         
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
         
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
     </imapserver>
@@ -55,7 +62,14 @@ under the License.
         <customProperties>pong.response=bad</customProperties>
         <customProperties>prop.b=baad</customProperties>
         <gracefulShutdown>false</gracefulShutdown>
+        <auth>
+            
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
+            <exampleToken>
+                <expectedToken>secret-token</expectedToken>
+                <authorizedUser>[email protected]</authorizedUser>
+            </exampleToken>
+        </auth>
         
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
         
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
     </imapserver>
-</imapservers>
\ No newline at end of file
+</imapservers>
diff --git 
a/examples/custom-imap/src/test/java/org/apache/james/examples/imap/CustomSaslMechanismTest.java
 
b/examples/custom-imap/src/test/java/org/apache/james/examples/imap/CustomSaslMechanismTest.java
new file mode 100644
index 0000000000..41fb770b2c
--- /dev/null
+++ 
b/examples/custom-imap/src/test/java/org/apache/james/examples/imap/CustomSaslMechanismTest.java
@@ -0,0 +1,209 @@
+/****************************************************************
+ * 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.examples.imap;
+
+import static 
org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+import static org.apache.james.jmap.JMAPTestingConstants.BOB;
+import static org.apache.james.jmap.JMAPTestingConstants.BOB_PASSWORD;
+import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.function.Predicate;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.MemoryJamesConfiguration;
+import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanism;
+import org.apache.james.modules.protocols.ImapGuiceProbe;
+import org.apache.james.utils.DataProbeImpl;
+import org.apache.james.utils.TestIMAPClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class CustomSaslMechanismTest {
+    private static class ClientConnection implements AutoCloseable {
+        private final Socket socket;
+        private final BufferedReader reader;
+        private final BufferedWriter writer;
+
+        private ClientConnection(String host, int port) throws IOException {
+            socket = new Socket();
+            socket.connect(new InetSocketAddress(host, port));
+            socket.setSoTimeout(5_000);
+            reader = new BufferedReader(new 
InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
+            writer = new BufferedWriter(new 
OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII));
+        }
+
+        private void writeLine(String line) throws IOException {
+            writer.write(line);
+            writer.write("\r\n");
+            writer.flush();
+        }
+
+        private String readUntil(Predicate<String> condition) throws 
IOException {
+            StringBuilder response = new StringBuilder();
+            while (true) {
+                String line = reader.readLine();
+                if (line == null) {
+                    throw new EOFException("Connection closed while waiting 
for IMAP response");
+                }
+                response.append(line).append("\n");
+                if (condition.test(line)) {
+                    return response.toString();
+                }
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            socket.close();
+        }
+    }
+
+    private static final String EXPECTED_TOKEN = "secret-token";
+    private static final String LOCALHOST_IP = "127.0.0.1";
+
+    @RegisterExtension
+    static JamesServerExtension jamesServerExtension = new 
JamesServerBuilder<MemoryJamesConfiguration>(tmpDir ->
+        MemoryJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
+        .server(MemoryJamesServerMain::createServer)
+        .build();
+
+    @BeforeEach
+    void setup(GuiceJamesServer server) throws Exception {
+        server.getProbe(DataProbeImpl.class).fluent()
+            .addDomain(DOMAIN)
+            .addUser(BOB.asString(), BOB_PASSWORD);
+    }
+
+    @Test
+    void imapServerShouldAdvertiseCustomSaslMechanism(GuiceJamesServer server) 
throws IOException {
+        assertThat(new TestIMAPClient().connect("127.0.0.1", imapPort(server))
+            .sendCommand("CAPABILITY"))
+            .contains("AUTH=PLAIN", "AUTH=EXAMPLE-TOKEN");
+    }
+
+    @Test
+    void 
imapServerShouldAuthenticateCustomSaslMechanismUsingOwnConfiguration(GuiceJamesServer
 server) throws IOException {
+        TestIMAPClient client = new TestIMAPClient().connect("127.0.0.1", 
imapPort(server));
+
+        assertThat(client.sendCommand("AUTHENTICATE EXAMPLE-TOKEN " + 
encode(EXPECTED_TOKEN)))
+            .contains("OK AUTHENTICATE completed.");
+        assertThat(client.sendCommand("PING"))
+            .contains("PONG");
+    }
+
+    @Test
+    void 
imapServerShouldAuthenticateCustomSaslMechanismUsingContinuation(GuiceJamesServer
 server) throws IOException {
+        try (ClientConnection client = clientConnection(server)) {
+            client.readUntil(line -> line.startsWith("* OK"));
+
+            client.writeLine("A01 AUTHENTICATE EXAMPLE-TOKEN");
+            assertThat(client.readUntil(line -> line.startsWith("+")))
+                .contains("+ " + 
encode(ExampleTokenSaslMechanism.CONTINUATION_PROMPT));
+
+            client.writeLine(encode(EXPECTED_TOKEN));
+            String authenticationResponse = client.readUntil(line -> 
line.startsWith("A01"));
+            assertThat(authenticationResponse)
+                .contains("OK AUTHENTICATE completed.")
+                .doesNotContain("+ " + 
encode(ExampleTokenSaslMechanism.CONTINUATION_PROMPT));
+
+            client.writeLine("A02 PING");
+            assertThat(client.readUntil(line -> line.startsWith("A02")))
+                .contains("PONG");
+        }
+    }
+
+    @Test
+    void 
imapServerShouldAuthenticateCustomSaslMechanismReturningServerDataOnSuccess(GuiceJamesServer
 server) throws IOException {
+        try (ClientConnection client = clientConnection(server)) {
+            // GIVEN a custom SASL exchange started without SASL-IR
+            client.readUntil(line -> line.startsWith("* OK"));
+            client.writeLine("A01 AUTHENTICATE EXAMPLE-TOKEN");
+            assertThat(client.readUntil(line -> line.startsWith("+")))
+                .contains("+ " + 
encode(ExampleTokenSaslMechanism.CONTINUATION_PROMPT));
+
+            // WHEN the mechanism succeeds with final server data, as 
GSSAPI/Kerberos-like SASL mechanisms may require
+            client.writeLine(encode(EXPECTED_TOKEN + 
ExampleTokenSaslMechanism.SUCCESS_DATA_TOKEN_SUFFIX));
+            assertThat(client.readUntil(line -> line.startsWith("+") || 
line.startsWith("A01")))
+                .contains("+ " + 
encode(ExampleTokenSaslMechanism.SUCCESS_DATA));
+
+            // THEN the client acknowledges the final server data before IMAP 
completes authentication
+            client.writeLine("");
+            assertThat(client.readUntil(line -> line.startsWith("A01")))
+                .contains("OK AUTHENTICATE completed.");
+
+            // THEN the authenticated IMAP session remains usable
+            client.writeLine("A02 PING");
+            assertThat(client.readUntil(line -> line.startsWith("A02")))
+                .contains("PONG");
+        }
+    }
+
+    @Test
+    void plainSaslAuthenticationShouldStillWork(GuiceJamesServer server) 
throws IOException {
+        TestIMAPClient client = new TestIMAPClient().connect("127.0.0.1", 
imapPort(server));
+
+        assertThat(client.sendCommand("AUTHENTICATE PLAIN " + 
encodePlainInitialResponse()))
+            .contains("OK AUTHENTICATE completed.");
+        assertThat(client.sendCommand("PING"))
+            .contains("PONG");
+    }
+
+    @Test
+    void imapServerShouldRejectInvalidCustomSaslToken(GuiceJamesServer server) 
throws IOException {
+        assertThat(new TestIMAPClient().connect("127.0.0.1", imapPort(server))
+            .sendCommand("AUTHENTICATE EXAMPLE-TOKEN " + 
encode("invalid-token")))
+            .contains("NO AUTHENTICATE failed.");
+    }
+
+    private int imapPort(GuiceJamesServer server) {
+        return server.getProbe(ImapGuiceProbe.class).getImapPort();
+    }
+
+    private ClientConnection clientConnection(GuiceJamesServer server) throws 
IOException {
+        return new ClientConnection(LOCALHOST_IP, imapPort(server));
+    }
+
+    private String encode(String token) {
+        return 
Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private String encodePlainInitialResponse() {
+        return encode(BOB.asString() + "\0" + BOB.asString() + "\0" + 
BOB_PASSWORD);
+    }
+}


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

Reply via email to