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]
