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 c49ef4d1ba863b235e1995b89df7175b93e127e4 Author: Quan Tran <[email protected]> AuthorDate: Wed Jul 29 08:54:53 2026 +0700 JAMES-4210 IMAP: close SASL exchanges upon disconnection Track the active SASL exchange as an IMAP session attachment and close it when the connection terminates or encounters a transport exception. IMAP authentication processing can run asynchronously. A disconnect may therefore race with SASL exchange creation and registration. Keep the tracker sealed after disconnect so delayed registrations are rejected and immediately closed. Synchronize tracker lookup and creation on the individual IMAP session because getAttribute and setAttribute are not atomic together. This ensures authentication and disconnect paths use the same tracker without introducing cross-session contention. Ensure normal completion, cancellation, failure, and disconnect paths release each exchange at most once. Add real IMAP server lifecycle tests for disconnect, cancellation, and terminal exchanges. --- .../imap/api/process/ImapSaslExchangeTracker.java | 115 +++++++++++++++ .../imap/processor/AuthenticateProcessor.java | 22 +-- .../netty/ImapChannelUpstreamHandler.java | 12 ++ .../imapserver/netty/AbstractIMAPServerTest.java | 94 ++++++++++-- .../netty/IMAPServerSaslExchangeLifecycleTest.java | 164 +++++++++++++++++++++ 5 files changed, 382 insertions(+), 25 deletions(-) diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSaslExchangeTracker.java b/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSaslExchangeTracker.java new file mode 100644 index 0000000000..4ba7ac0dd2 --- /dev/null +++ b/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSaslExchangeTracker.java @@ -0,0 +1,115 @@ +/**************************************************************** + * 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.imap.api.process; + +import org.apache.james.protocols.api.sasl.SaslExchange; + +/** + * Owns the active SASL exchange for an IMAP session. Once closed on disconnect, + * the tracker stays attached and rejects delayed asynchronous registrations. + */ +public class ImapSaslExchangeTracker { + private static final String ATTRIBUTE_KEY = ImapSaslExchangeTracker.class.getName(); + + public static ImapSaslExchangeTracker forSession(ImapSession session) { + // Make tracker initialization atomic with disconnect sealing for this session. + synchronized (session) { + Object value = session.getAttribute(ATTRIBUTE_KEY); + if (value instanceof ImapSaslExchangeTracker tracker) { + return tracker; + } + + ImapSaslExchangeTracker tracker = new ImapSaslExchangeTracker(); + session.setAttribute(ATTRIBUTE_KEY, tracker); + return tracker; + } + } + + public static void closeForSession(ImapSession session) { + forSession(session).close(); + } + + private static IllegalStateException closeRejectedExchange(SaslExchange exchange) { + IllegalStateException failure = new IllegalStateException("IMAP SASL exchange cannot be registered"); + try { + exchange.close(); + } catch (RuntimeException e) { + failure.addSuppressed(e); + } + return failure; + } + + private SaslExchange activeExchange; + private boolean closed; + + private ImapSaslExchangeTracker() { + } + + public SaslExchange register(SaslExchange exchange) { + if (tryRegister(exchange)) { + return exchange; + } + throw closeRejectedExchange(exchange); + } + + private synchronized boolean tryRegister(SaslExchange exchange) { + if (closed || activeExchange != null) { + return false; + } + activeExchange = exchange; + return true; + } + + public void closeExchange(SaslExchange exchange) { + if (release(exchange)) { + exchange.close(); + } + } + + public void abortExchange(SaslExchange exchange) { + if (release(exchange)) { + exchange.abort(); + } + } + + public void close() { + SaslExchange exchange; + synchronized (this) { + if (closed) { + return; + } + closed = true; + exchange = activeExchange; + activeExchange = null; + } + + if (exchange != null) { + exchange.close(); + } + } + + private synchronized boolean release(SaslExchange exchange) { + if (activeExchange != exchange) { + return false; + } + activeExchange = null; + return true; + } +} diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java index 833fa95247..7acb7b1de2 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java @@ -29,6 +29,7 @@ import jakarta.inject.Inject; import org.apache.james.imap.api.display.HumanReadableText; import org.apache.james.imap.api.message.Capability; import org.apache.james.imap.api.message.response.StatusResponseFactory; +import org.apache.james.imap.api.process.ImapSaslExchangeTracker; import org.apache.james.imap.api.process.ImapSession; import org.apache.james.imap.main.PathConverter; import org.apache.james.imap.message.request.AuthenticateRequest; @@ -97,8 +98,9 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq try { SaslInitialRequest initialRequest = SaslCodec.initialRequest(request.getAuthType(), initialClientResponse(request)); SaslAuthenticator authenticator = jamesSaslAuthenticator.withExtraAuthorizator(withAdminUsers()); - SaslExchange exchange = mechanism.get().start(initialRequest, authenticator); - handleFirstStep(exchange, firstStep(exchange), session, request, responder); + SaslExchange exchange = ImapSaslExchangeTracker.forSession(session) + .register(mechanism.get().start(initialRequest, authenticator)); + handleFirstStep(exchange, firstStep(exchange, session), session, request, responder); } catch (IllegalArgumentException e) { LOGGER.info("Invalid syntax in AUTHENTICATE initial client response", e); authFailure(session, request, responder, HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(), @@ -136,11 +138,11 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq return Optional.empty(); } - private SaslStep firstStep(SaslExchange exchange) { + private SaslStep firstStep(SaslExchange exchange, ImapSession session) { try { return exchange.firstStep(); } catch (RuntimeException e) { - exchange.close(); + ImapSaslExchangeTracker.forSession(session).closeExchange(exchange); throw e; } } @@ -195,7 +197,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER) .then()); } catch (RuntimeException e) { - exchange.close(); + ImapSaslExchangeTracker.forSession(session).closeExchange(exchange); throw e; } } @@ -281,7 +283,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq try { session.popLineHandler(); } finally { - exchange.close(); + ImapSaslExchangeTracker.forSession(session).closeExchange(exchange); } } @@ -289,7 +291,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq try { session.popLineHandler(); } finally { - exchange.abort(); + ImapSaslExchangeTracker.forSession(session).abortExchange(exchange); } } @@ -297,7 +299,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq try { session.popLineHandler(); } catch (RuntimeException e) { - exchange.close(); + ImapSaslExchangeTracker.forSession(session).closeExchange(exchange); throw e; } } @@ -318,7 +320,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER) .then()); } catch (RuntimeException e) { - exchange.close(); + ImapSaslExchangeTracker.forSession(session).closeExchange(exchange); throw e; } } @@ -348,7 +350,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq try { handleSaslStep(step, session, request, responder, successLog(request)); } finally { - exchange.close(); + ImapSaslExchangeTracker.forSession(session).closeExchange(exchange); } } diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java index 800fecc6a8..323745e4a0 100644 --- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java +++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java @@ -43,6 +43,7 @@ import org.apache.james.imap.api.ImapSessionState; import org.apache.james.imap.api.display.HumanReadableText; import org.apache.james.imap.api.message.response.StatusResponse; import org.apache.james.imap.api.process.ImapProcessor; +import org.apache.james.imap.api.process.ImapSaslExchangeTracker; import org.apache.james.imap.api.process.ImapSession; import org.apache.james.imap.api.process.ImapSession.SessionId; import org.apache.james.imap.encode.ImapEncoder; @@ -274,6 +275,7 @@ public class ImapChannelUpstreamHandler extends ChannelInboundHandlerAdapter imp InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress(); LOGGER.info("Connection closed for {} and user {}", address.getAddress().getHostAddress(), retrieveUsername(imapSession)); + closeSaslExchange(imapSession); Optional.ofNullable(imapSession).ifPresent(ImapSession::cancelOngoingProcessing); Optional.ofNullable(imapSession) .map(ImapSession::logout) @@ -300,6 +302,7 @@ public class ImapChannelUpstreamHandler extends ChannelInboundHandlerAdapter imp ImapSession imapSession = ctx.channel().attr(IMAP_SESSION_ATTRIBUTE_KEY).getAndSet(null); String username = retrieveUsername(imapSession); try (Closeable closeable = mdc(imapSession).build()) { + closeSaslExchange(imapSession); if (cause instanceof SocketException) { logExpectedException("Socket exception encountered for user " + username, cause); } else if (isSslHandshkeException(cause)) { @@ -339,6 +342,15 @@ public class ImapChannelUpstreamHandler extends ChannelInboundHandlerAdapter imp } } + private void closeSaslExchange(ImapSession imapSession) { + try { + // Fallback when the connection terminates or encounters an unexpected transport exception during a pending exchange. + Optional.ofNullable(imapSession).ifPresent(ImapSaslExchangeTracker::closeForSession); + } catch (RuntimeException e) { + LOGGER.warn("Failed to close the active IMAP SASL exchange", e); + } + } + private void logExpectedException(String message, Throwable cause) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(message, cause); diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/AbstractIMAPServerTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/AbstractIMAPServerTest.java index a4c6471f4c..0eed3b718b 100644 --- a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/AbstractIMAPServerTest.java +++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/AbstractIMAPServerTest.java @@ -27,6 +27,7 @@ import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -37,10 +38,16 @@ import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.commons.net.imap.AuthenticatingIMAPClient; import org.apache.james.core.Username; import org.apache.james.imap.api.ConnectionCheck; +import org.apache.james.imap.api.message.response.StatusResponseFactory; +import org.apache.james.imap.api.process.ImapProcessor; import org.apache.james.imap.encode.main.DefaultImapEncoderFactory; import org.apache.james.imap.main.DefaultImapDecoderFactory; +import org.apache.james.imap.message.response.UnpooledStatusResponseFactory; +import org.apache.james.imap.processor.DefaultProcessor; +import org.apache.james.imap.processor.base.UnknownRequestProcessor; import org.apache.james.imap.processor.fetch.FetchProcessor; import org.apache.james.imap.processor.main.DefaultImapProcessorFactory; +import org.apache.james.mailbox.MailboxCounterCorrector; import org.apache.james.mailbox.inmemory.InMemoryMailboxManager; import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources; import org.apache.james.mailbox.store.FakeAuthenticator; @@ -48,6 +55,7 @@ import org.apache.james.mailbox.store.FakeAuthorizator; import org.apache.james.mailbox.store.StoreSubscriptionManager; import org.apache.james.metrics.api.NoopGaugeRegistry; import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.apache.james.protocols.api.sasl.SaslMechanism; import org.apache.james.protocols.api.utils.BogusSslContextFactory; import org.apache.james.protocols.api.utils.BogusTrustManagerFactory; import org.apache.james.protocols.lib.LegacyJavaEncryptionFactory; @@ -102,26 +110,33 @@ abstract class AbstractIMAPServerTest { protected IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config, InMemoryIntegrationResources inMemoryIntegrationResources, FetchProcessor.LocalCacheConfiguration localCacheConfiguration) throws Exception { + return createImapServer(config, inMemoryIntegrationResources, localCacheConfiguration, Optional.empty()); + } + + protected IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config, + InMemoryIntegrationResources inMemoryIntegrationResources, + FetchProcessor.LocalCacheConfiguration localCacheConfiguration, + ImmutableList<SaslMechanism> saslMechanisms) throws Exception { + return createImapServer(config, inMemoryIntegrationResources, localCacheConfiguration, Optional.of(saslMechanisms)); + } + + private IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config, + InMemoryIntegrationResources inMemoryIntegrationResources, + FetchProcessor.LocalCacheConfiguration localCacheConfiguration, + Optional<ImmutableList<SaslMechanism>> saslMechanisms) throws Exception { memoryIntegrationResources = inMemoryIntegrationResources; RecordingMetricFactory metricFactory = new RecordingMetricFactory(); Set<ConnectionCheck> connectionChecks = defaultConnectionChecks(); mailboxManager = spy(memoryIntegrationResources.getMailboxManager()); + StoreSubscriptionManager subscriptionManager = new StoreSubscriptionManager(mailboxManager.getMapperFactory(), + mailboxManager.getMapperFactory(), + mailboxManager.getEventBus()); + ImapProcessor processor = createProcessor(config, localCacheConfiguration, metricFactory, subscriptionManager, saslMechanisms); IMAPServer imapServer = new IMAPServer( new DefaultImapDecoderFactory().buildImapDecoder(), new DefaultImapEncoderFactory().buildImapEncoder(), - DefaultImapProcessorFactory.createXListSupportingProcessor( - mailboxManager, - memoryIntegrationResources.getEventBus(), - new StoreSubscriptionManager(mailboxManager.getMapperFactory(), - mailboxManager.getMapperFactory(), - mailboxManager.getEventBus()), - null, - memoryIntegrationResources.getQuotaManager(), - memoryIntegrationResources.getQuotaRootResolver(), - metricFactory, - localCacheConfiguration, - config), + processor, new ImapMetrics(metricFactory), new NoopGaugeRegistry(), connectionChecks); @@ -135,13 +150,57 @@ abstract class AbstractIMAPServerTest { return imapServer; } + private ImapProcessor createProcessor(HierarchicalConfiguration<ImmutableNode> config, + FetchProcessor.LocalCacheConfiguration localCacheConfiguration, + RecordingMetricFactory metricFactory, + StoreSubscriptionManager subscriptionManager, + Optional<ImmutableList<SaslMechanism>> saslMechanisms) throws Exception { + if (saslMechanisms.isEmpty()) { + return DefaultImapProcessorFactory.createXListSupportingProcessor( + mailboxManager, + memoryIntegrationResources.getEventBus(), + subscriptionManager, + null, + memoryIntegrationResources.getQuotaManager(), + memoryIntegrationResources.getQuotaRootResolver(), + metricFactory, + localCacheConfiguration, + config); + } + + StatusResponseFactory statusResponseFactory = new UnpooledStatusResponseFactory(); + return DefaultProcessor.createDefaultProcessor( + new UnknownRequestProcessor(statusResponseFactory), + mailboxManager, + memoryIntegrationResources.getEventBus(), + subscriptionManager, + statusResponseFactory, + null, + memoryIntegrationResources.getQuotaManager(), + memoryIntegrationResources.getQuotaRootResolver(), + MailboxCounterCorrector.DEFAULT, + metricFactory, + localCacheConfiguration, + saslMechanisms.orElseThrow()); + } + protected IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config, FetchProcessor.LocalCacheConfiguration localCacheConfiguration) throws Exception { + return createImapServer(config, defaultIntegrationResources(), localCacheConfiguration); + } + + protected IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config, + FetchProcessor.LocalCacheConfiguration localCacheConfiguration, + ImmutableList<SaslMechanism> saslMechanisms) throws Exception { + return createImapServer(config, defaultIntegrationResources(), localCacheConfiguration, saslMechanisms); + } + + private InMemoryIntegrationResources defaultIntegrationResources() { authenticator = new FakeAuthenticator(); authenticator.addUser(USER, USER_PASS); authenticator.addUser(USER2, USER_PASS); authenticator.addUser(USER3, USER_PASS); - memoryIntegrationResources = InMemoryIntegrationResources.builder() + return InMemoryIntegrationResources.builder() .authenticator(authenticator) .authorizator(FakeAuthorizator.defaultReject()) .inVmEventBus() @@ -151,8 +210,6 @@ abstract class AbstractIMAPServerTest { .noPreDeletionHooks() .storeQuotaManager() .build(); - - return createImapServer(config, memoryIntegrationResources, localCacheConfiguration); } protected IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config) throws Exception { @@ -163,6 +220,13 @@ abstract class AbstractIMAPServerTest { return createImapServer(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)), localCacheConfiguration); } + protected IMAPServer createImapServer(String configurationFile, ImmutableList<SaslMechanism> saslMechanisms) throws Exception { + return createImapServer( + ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)), + FetchProcessor.LocalCacheConfiguration.DEFAULT, + saslMechanisms); + } + protected IMAPServer createImapServer(String configurationFile) throws Exception { return createImapServer(configurationFile, FetchProcessor.LocalCacheConfiguration.DEFAULT); } diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSaslExchangeLifecycleTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSaslExchangeLifecycleTest.java new file mode 100644 index 0000000000..4a3c8f8443 --- /dev/null +++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerSaslExchangeLifecycleTest.java @@ -0,0 +1,164 @@ +/**************************************************************** + * 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.imapserver.netty; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.net.imap.IMAPClient; +import org.apache.commons.net.imap.IMAPReply; +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.SaslInitialRequest; +import org.apache.james.protocols.api.sasl.SaslMechanism; +import org.apache.james.protocols.api.sasl.SaslStep; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +class IMAPServerSaslExchangeLifecycleTest extends AbstractIMAPServerTest { + private static final String CHALLENGE = "Y2hhbGxlbmdl"; + private static final String CLIENT_RESPONSE = "cmVzcG9uc2U="; + + private static class RecordingSaslMechanism implements SaslMechanism { + private final AtomicInteger closeCount; + private final AtomicInteger abortCount; + + private RecordingSaslMechanism(AtomicInteger closeCount, AtomicInteger abortCount) { + this.closeCount = closeCount; + this.abortCount = abortCount; + } + + @Override + public String name() { + return "RECORDING"; + } + + @Override + public SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator) { + return new SaslExchange() { + @Override + public SaslStep firstStep() { + return new SaslStep.Challenge(Optional.of("challenge".getBytes(US_ASCII))); + } + + @Override + public SaslStep onResponse(byte[] clientResponse) { + return new SaslStep.Failure(SaslFailure.authenticationFailed( + Optional.empty(), Optional.empty(), "Test-only mechanism")); + } + + @Override + public void abort() { + abortCount.incrementAndGet(); + } + + @Override + public void close() { + closeCount.incrementAndGet(); + } + }; + } + } + + private final AtomicInteger closeCount = new AtomicInteger(); + private final AtomicInteger abortCount = new AtomicInteger(); + + private IMAPServer imapServer; + private int port; + + @BeforeEach + void setUp() throws Exception { + imapServer = createImapServer("imapServer.xml", + ImmutableList.of(new RecordingSaslMechanism(closeCount, abortCount))); + port = imapServer.getListenAddresses().get(0).getPort(); + } + + @AfterEach + void tearDown() { + if (imapServer != null) { + imapServer.destroy(); + } + } + + @Test + void disconnectDuringSaslContinuationShouldCloseExchangeOnce() throws Exception { + IMAPClient client = connectedClient(); + try { + assertThat(client.sendCommand("AUTHENTICATE RECORDING")).isEqualTo(IMAPReply.CONT); + assertThat(client.getReplyString()).contains("+ " + CHALLENGE); + } finally { + client.disconnect(); + } + + Awaitility.await().atMost(Duration.ofSeconds(2)) + .untilAsserted(() -> assertThat(closeCount.get()).isEqualTo(1)); + } + + @Test + void abortDuringSaslContinuationShouldAbortExchangeOnce() throws Exception { + IMAPClient client = connectedClient(); + try { + assertThat(client.sendCommand("AUTHENTICATE RECORDING")).isEqualTo(IMAPReply.CONT); + assertThat(client.getReplyString()).contains("+ " + CHALLENGE); + assertThat(client.sendData("*")).isEqualTo(IMAPReply.NO); + assertThat(client.getReplyString()).contains("NO AUTHENTICATE failed."); + } finally { + client.disconnect(); + } + + Awaitility.await().during(Duration.ofMillis(100)).atMost(Duration.ofSeconds(2)) + .untilAsserted(() -> { + assertThat(abortCount.get()).isEqualTo(1); + assertThat(closeCount.get()).isZero(); + }); + } + + @Test + void disconnectAfterTerminalSaslStepShouldNotCloseExchangeAgain() throws Exception { + IMAPClient client = connectedClient(); + try { + assertThat(client.sendCommand("AUTHENTICATE RECORDING")).isEqualTo(IMAPReply.CONT); + assertThat(client.getReplyString()).contains("+ " + CHALLENGE); + assertThat(client.sendData(CLIENT_RESPONSE)).isEqualTo(IMAPReply.NO); + assertThat(client.getReplyString()).contains("NO AUTHENTICATE failed."); + } finally { + client.disconnect(); + } + + Awaitility.await().during(Duration.ofMillis(100)).atMost(Duration.ofSeconds(2)) + .untilAsserted(() -> assertThat(closeCount.get()).isEqualTo(1)); + } + + private IMAPClient connectedClient() throws IOException { + IMAPClient client = new IMAPClient(); + client.connect("127.0.0.1", port); + return client; + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
