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]

Reply via email to