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 a1a6dedf4e99e15acbe02c96a9edee799f00d981
Author: Quan Tran <[email protected]>
AuthorDate: Tue Jul 7 09:21:53 2026 +0700

    JAMES-4210 Adapt IMAP authentication processors to SASL exchanges
    
    Drive IMAP AUTHENTICATE and LOGIN through the new SASL exchange model. Add 
IMAP SASL bridge encoding and decoding, final server-data handling, exchange 
cleanup, typed failure mapping, and preserve mailbox session semantics 
including delegation.
---
 protocols/imap/pom.xml                             |   4 +
 .../imap/encode/AuthenticateResponseEncoder.java   |   6 +-
 .../message/response/AuthenticateResponse.java     |  16 +-
 .../imap/processor/AbstractAuthProcessor.java      | 179 ++++------
 .../imap/processor/AuthenticateProcessor.java      | 393 ++++++++++++++-------
 .../james/imap/processor/DefaultProcessor.java     |  49 ++-
 .../james/imap/processor/LoginProcessor.java       |  39 +-
 .../james/imap/processor/sasl/ImapSaslBridge.java  | 104 ++++++
 .../imap/processor/AuthenticateProcessorTest.java  | 277 +++++++++++++++
 .../imap/processor/sasl/ImapSaslBridgeTest.java    | 240 +++++++++++++
 10 files changed, 1055 insertions(+), 252 deletions(-)

diff --git a/protocols/imap/pom.xml b/protocols/imap/pom.xml
index 1d4d6642d0..e1ec4819fb 100644
--- a/protocols/imap/pom.xml
+++ b/protocols/imap/pom.xml
@@ -91,6 +91,10 @@
             <groupId>${james.protocols.groupId}</groupId>
             <artifactId>protocols-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-sasl</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.beetstra.jutf7</groupId>
             <artifactId>jutf7</artifactId>
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/encode/AuthenticateResponseEncoder.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/encode/AuthenticateResponseEncoder.java
index a4dab679c8..3e77db72e6 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/encode/AuthenticateResponseEncoder.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/encode/AuthenticateResponseEncoder.java
@@ -30,6 +30,10 @@ public class AuthenticateResponseEncoder implements 
ImapResponseEncoder<Authenti
 
     @Override
     public void encode(AuthenticateResponse message, ImapResponseComposer 
composer) throws IOException {
-        composer.continuationResponse();
+        if (message.getResponse().filter(response -> 
!response.isEmpty()).isPresent()) {
+            composer.continuationResponse(message.getResponse().get());
+        } else {
+            composer.continuationResponse();
+        }
     }
 }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/message/response/AuthenticateResponse.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/message/response/AuthenticateResponse.java
index 345fc9e5f4..dcf4355e34 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/message/response/AuthenticateResponse.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/message/response/AuthenticateResponse.java
@@ -19,8 +19,22 @@
 
 package org.apache.james.imap.message.response;
 
+import java.util.Optional;
+
 import org.apache.james.imap.api.message.response.ImapResponseMessage;
 
-public class AuthenticateResponse implements ImapResponseMessage{
+public class AuthenticateResponse implements ImapResponseMessage {
+    private final Optional<String> response;
+
+    public AuthenticateResponse() {
+        this.response = Optional.empty();
+    }
+
+    public AuthenticateResponse(String response) {
+        this.response = Optional.of(response);
+    }
 
+    public Optional<String> getResponse() {
+        return response;
+    }
 }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
index 8786a6ec94..cad4359952 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
@@ -27,8 +27,6 @@ import org.apache.james.imap.api.message.request.ImapRequest;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.main.PathConverter;
-import org.apache.james.jwt.OidcJwtTokenVerifier;
-import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.mailbox.Authorizator;
 import org.apache.james.mailbox.DefaultMailboxes;
 import org.apache.james.mailbox.MailboxManager;
@@ -41,12 +39,13 @@ import 
org.apache.james.mailbox.exception.UserDoesNotExistException;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.metrics.api.MetricFactory;
-import org.apache.james.protocols.api.OIDCSASLParser;
+import org.apache.james.protocols.api.sasl.SaslFailure;
+import org.apache.james.protocols.api.sasl.SaslIdentity;
+import org.apache.james.protocols.api.sasl.SaslStep;
 import org.apache.james.util.AuditTrail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 
 import reactor.core.publisher.Mono;
@@ -80,54 +79,6 @@ public abstract class AbstractAuthProcessor<R extends 
ImapRequest> extends Abstr
         this.imapConfiguration = imapConfiguration;
     }
 
-    protected void doPasswordAuth(AuthenticationAttempt authenticationAttempt, 
ImapSession session, ImapRequest request, Responder responder) {
-        Preconditions.checkArgument(!authenticationAttempt.isDelegation());
-
-        if (authenticationAttempt.getAuthenticationId() == null || 
authenticationAttempt.getPassword() == null) {
-            authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(), Optional.empty(),
-                "Malformed authentication command."
-            );
-        } else {
-            try {
-                final MailboxSession mailboxSession = 
getMailboxManager().authenticate(
-                    authenticationAttempt.getAuthenticationId(),
-                    authenticationAttempt.getPassword()
-                ).withoutDelegation();
-                authSuccess(session, mailboxSession, request, responder, 
"Password authentication succeeded.");
-            } catch (BadCredentialsException e) {
-                authFailure(session, request, responder, 
HumanReadableText.INVALID_CREDENTIALS,
-                    Optional.of(authenticationAttempt.getAuthenticationId()),
-                    Optional.empty(),
-                    "Password authentication failed because of bad 
credentials."
-                );
-            } catch (MailboxException e) {
-                // This is probably not a user error, so we do not increase 
the failure count or add the
-                // event to the audit log.
-                LOGGER.error("Authentication failed", e);
-                no(request, responder, 
HumanReadableText.GENERIC_FAILURE_DURING_PROCESSING);
-            }
-        }
-    }
-
-    protected void doPasswordAuthWithDelegation(AuthenticationAttempt 
authenticationAttempt, ImapSession session, ImapRequest request, Responder 
responder) {
-        Preconditions.checkArgument(authenticationAttempt.isDelegation());
-        Username otherUser = 
authenticationAttempt.getDelegateUserName().orElseThrow();
-
-        Username givenUser = authenticationAttempt.getAuthenticationId();
-        if (givenUser == null) {
-            authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED,
-                Optional.empty(), Optional.of(otherUser), "Malformed 
authentication command.");
-        } else {
-            doAuthWithDelegation(() -> getMailboxManager()
-                    .withExtraAuthorizator(withAdminUsers())
-                    .authenticate(givenUser, 
authenticationAttempt.getPassword())
-                    .as(otherUser),
-                session,
-                request, responder,
-                givenUser, otherUser);
-        }
-    }
-
     protected Authorizator withAdminUsers() {
         return (userId, otherUserId) -> {
             if (imapConfiguration.getAdminUsers().contains(userId.asString())) 
{
@@ -140,8 +91,14 @@ public abstract class AbstractAuthProcessor<R extends 
ImapRequest> extends Abstr
     protected void 
doAuthWithDelegation(MailboxSessionAuthWithDelegationSupplier 
mailboxSessionSupplier,
                                         ImapSession session, ImapRequest 
request, Responder responder,
                                         Username authenticateUser, Username 
delegatorUser) {
+        doAuth(mailboxSessionSupplier, session, request, responder, 
authenticateUser, delegatorUser, "Authentication with delegation succeeded.");
+    }
+
+    protected void doAuth(MailboxSessionAuthWithDelegationSupplier 
mailboxSessionSupplier,
+                          ImapSession session, ImapRequest request, Responder 
responder,
+                          Username authenticateUser, Username delegatorUser, 
String successLog) {
         try {
-            authSuccess(session, mailboxSessionSupplier.get(), request, 
responder, "Authentication with delegation succeeded.");
+            authSuccess(session, mailboxSessionSupplier.get(), request, 
responder, successLog);
         } catch (BadCredentialsException e) {
             authFailure(session, request, responder, 
HumanReadableText.INVALID_CREDENTIALS, Optional.of(authenticateUser),
                 Optional.of(delegatorUser), "Password authentication with 
delegation failed because of bad credentials.");
@@ -150,37 +107,58 @@ public abstract class AbstractAuthProcessor<R extends 
ImapRequest> extends Abstr
                 Optional.of(delegatorUser), "Delegation target user does not 
exist.");
         } catch (ForbiddenDelegationException e) {
             authFailure(session, request, responder, 
HumanReadableText.DELEGATION_FORBIDDEN, Optional.of(authenticateUser),
-            Optional.of(delegatorUser), "Requested delegation is forbidden.");
+                Optional.of(delegatorUser), "Requested delegation is 
forbidden.");
         } catch (MailboxException e) {
-            // This is probably not a user error, so we do not increase the 
failure count or add the
-            // event to the audit log.
             LOGGER.info("Authentication failed", e);
             no(request, responder, 
HumanReadableText.GENERIC_FAILURE_DURING_PROCESSING);
         }
     }
 
-    protected void doOAuth(OIDCSASLParser.OIDCInitialResponse 
oidcInitialResponse, OidcSASLConfiguration oidcSASLConfiguration,
-                         ImapSession session, ImapRequest request, Responder 
responder) {
-        new 
OidcJwtTokenVerifier(oidcSASLConfiguration).validateToken(oidcInitialResponse.getToken())
-            .ifPresentOrElse(authenticatedUser -> {
-                Username associatedUser = 
Username.of(oidcInitialResponse.getAssociatedUser());
-                if (!associatedUser.equals(authenticatedUser)) {
-                    doAuthWithDelegation(() -> getMailboxManager()
-                            .withExtraAuthorizator(withAdminUsers())
-                            .authenticate(authenticatedUser)
-                            .as(associatedUser),
-                        session, request, responder, authenticatedUser, 
associatedUser);
-                } else {
-                    authSuccess(session, 
getMailboxManager().createSystemSession(authenticatedUser), request, responder,
-                        "OAuth authentication succeeded."
-                    );
-                }
-            }, () -> {
-                authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(),
-                    
Optional.of(Username.of(oidcInitialResponse.getAssociatedUser())),
-                    "OAuth authentication failed."
-                );
-            });
+    protected void handleSaslStep(SaslStep step, ImapSession session, 
ImapRequest request, Responder responder, String successLog) {
+        switch (step) {
+            case SaslStep.Success success -> handleSaslSuccess(success, 
session, request, responder, successLog);
+            case SaslStep.Failure failure -> 
handleSaslFailure(failure.failure(), session, request, responder);
+            case SaslStep.Challenge ignored -> throw new 
IllegalStateException("Challenge SASL step cannot be applied as authentication 
result");
+        }
+    }
+
+    protected void handleSaslSuccess(SaslStep.Success success, ImapSession 
session, ImapRequest request, Responder responder, String successLog) {
+        SaslIdentity identity = success.identity();
+        if (!identity.authenticationId().equals(identity.authorizationId())) {
+            doAuthWithDelegation(() -> getMailboxManager()
+                    .withExtraAuthorizator(withAdminUsers())
+                    .authenticate(identity.authenticationId())
+                    .as(identity.authorizationId()),
+                session, request, responder, identity.authenticationId(), 
identity.authorizationId());
+            return;
+        }
+
+        doAuth(() -> getMailboxManager()
+                .authenticate(identity.authenticationId())
+                .withoutDelegation(),
+            session, request, responder, identity.authenticationId(), 
identity.authorizationId(), successLog);
+    }
+
+    protected void handleSaslFailure(SaslFailure failure, ImapSession session, 
ImapRequest request, Responder responder) {
+        switch (failure.type()) {
+            case MALFORMED -> authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED,
+                failure.authenticationId(), failure.authorizationId(), 
failure.reason());
+            case AUTHENTICATION_FAILED -> authFailure(session, request, 
responder, HumanReadableText.AUTHENTICATION_FAILED,
+                failure.authenticationId(), failure.authorizationId(), 
failure.reason());
+            case INVALID_CREDENTIALS -> authFailure(session, request, 
responder, HumanReadableText.INVALID_CREDENTIALS,
+                failure.authenticationId(), failure.authorizationId(), 
failure.reason());
+            case USER_DOES_NOT_EXIST -> authFailure(session, request, 
responder, HumanReadableText.USER_DOES_NOT_EXIST,
+                failure.authenticationId(), failure.authorizationId(), 
failure.reason());
+            case DELEGATION_FORBIDDEN -> authFailure(session, request, 
responder, HumanReadableText.DELEGATION_FORBIDDEN,
+                failure.authenticationId(), failure.authorizationId(), 
failure.reason());
+            case SERVER_ERROR -> {
+                failure.cause()
+                    .ifPresentOrElse(
+                        cause -> LOGGER.error("Authentication failed: {}", 
failure.reason(), cause),
+                        () -> LOGGER.error("Authentication failed: {}", 
failure.reason()));
+                no(request, responder, 
HumanReadableText.GENERIC_FAILURE_DURING_PROCESSING);
+            }
+        }
     }
 
     protected void provisionInbox(ImapSession session, MailboxManager 
mailboxManager, MailboxSession mailboxSession) throws MailboxException {
@@ -233,15 +211,7 @@ public abstract class AbstractAuthProcessor<R extends 
ImapRequest> extends Abstr
         }
     }
 
-    protected static AuthenticationAttempt delegation(Username authorizeId, 
Username authenticationId, String password) {
-        return new AuthenticationAttempt(Optional.of(authorizeId), 
authenticationId, password);
-    }
-
-    protected static AuthenticationAttempt noDelegation(Username 
authenticationId, String password) {
-        return new AuthenticationAttempt(Optional.empty(), authenticationId, 
password);
-    }
-
-    protected void authSuccess(ImapSession session, MailboxSession 
mailboxSession, ImapRequest request, Responder responder, String log) {
+    protected void authSuccess(ImapSession session, MailboxSession 
mailboxSession, ImapRequest request, Responder responder, String successLog) {
         session.authenticated();
         session.setMailboxSession(mailboxSession);
         try {
@@ -260,12 +230,13 @@ public abstract class AbstractAuthProcessor<R extends 
ImapRequest> extends Abstr
         if (assumedUser.isPresent()) {
             entry = entry.parameters(() -> ImmutableMap.of("delegatorUser", 
assumedUser.get().asString()));
         }
-        entry.log(log);
+        entry.log(successLog);
         okComplete(request, responder);
         session.stopDetectingCommandInjection();
     }
 
-    protected void authFailure(ImapSession session, ImapRequest request, 
Responder responder, HumanReadableText failed, Optional<Username> username, 
Optional<Username> assumedUser, String log) {
+    protected void authFailure(ImapSession session, ImapRequest request, 
Responder responder, HumanReadableText failed, Optional<Username> username,
+                               Optional<Username> assumedUser, String 
failureReason) {
         AuditTrail.Entry entry = AuditTrail.entry()
             .username(() -> username.map(name -> name.asString()).orElse(null))
             .sessionId(() -> session.sessionId().asString())
@@ -275,35 +246,7 @@ public abstract class AbstractAuthProcessor<R extends 
ImapRequest> extends Abstr
         if (assumedUser.isPresent()) {
             entry = entry.parameters(() -> ImmutableMap.of("delegatorUser", 
assumedUser.get().asString()));
         }
-        entry.log(log);
+        entry.log(failureReason);
         manageFailureCount(session, request, responder, failed);
     }
-
-    protected static class AuthenticationAttempt {
-        private final Optional<Username> delegateUserName;
-        private final Username authenticationId;
-        private final String password;
-
-        public AuthenticationAttempt(Optional<Username> delegateUserName, 
Username authenticationId, String password) {
-            this.delegateUserName = delegateUserName;
-            this.authenticationId = authenticationId;
-            this.password = password;
-        }
-
-        public boolean isDelegation() {
-            return delegateUserName.isPresent() && 
!delegateUserName.get().equals(authenticationId);
-        }
-
-        public Optional<Username> getDelegateUserName() {
-            return delegateUserName;
-        }
-
-        public Username getAuthenticationId() {
-            return authenticationId;
-        }
-
-        public String getPassword() {
-            return password;
-        }
-    }
 }
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 cfae43c6f3..8a9d87fbd3 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
@@ -19,36 +19,36 @@
 
 package org.apache.james.imap.processor;
 
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Base64;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
-import java.util.stream.Collectors;
 
 import jakarta.inject.Inject;
 
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.james.core.Username;
 import org.apache.james.imap.api.display.HumanReadableText;
 import org.apache.james.imap.api.message.Capability;
-import org.apache.james.imap.api.message.request.ImapRequest;
 import org.apache.james.imap.api.message.response.StatusResponseFactory;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.main.PathConverter;
 import org.apache.james.imap.message.request.AuthenticateRequest;
 import org.apache.james.imap.message.request.IRAuthenticateRequest;
 import org.apache.james.imap.message.response.AuthenticateResponse;
+import org.apache.james.imap.processor.sasl.ImapSaslBridge;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.metrics.api.MetricFactory;
-import org.apache.james.protocols.api.OIDCSASLParser;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslInitialRequest;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismNames;
+import org.apache.james.protocols.api.sasl.SaslStep;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
 import org.apache.james.util.MDCBuilder;
 import org.apache.james.util.ReactorUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 import reactor.core.publisher.Mono;
@@ -59,17 +59,21 @@ import reactor.core.publisher.Mono;
 public class AuthenticateProcessor extends 
AbstractAuthProcessor<AuthenticateRequest> implements 
CapabilityImplementingProcessor {
     public static final String AUTH_PLAIN = "AUTH=PLAIN";
     public static final Capability AUTH_PLAIN_CAPABILITY = 
Capability.of(AUTH_PLAIN);
-    private static final Logger LOGGER = 
LoggerFactory.getLogger(AuthenticateProcessor.class);
-    private static final String AUTH_TYPE_PLAIN = "PLAIN";
-    private static final String AUTH_TYPE_OAUTHBEARER = "OAUTHBEARER";
-    private static final String AUTH_TYPE_XOAUTH2 = "XOAUTH2";
-    private static final List<Capability> OAUTH_CAPABILITIES = 
ImmutableList.of(Capability.of("AUTH=" + AUTH_TYPE_OAUTHBEARER), 
Capability.of("AUTH=" + AUTH_TYPE_XOAUTH2));
     public static final Capability SASL_CAPABILITY = Capability.of("SASL-IR");
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(AuthenticateProcessor.class);
+
+    private final ImapSaslBridge saslBridge;
+    private final JamesSaslAuthenticator jamesSaslAuthenticator;
+    private ImmutableList<SaslMechanism> saslMechanisms;
 
     @Inject
     public AuthenticateProcessor(MailboxManager mailboxManager, 
StatusResponseFactory factory,
-                                 MetricFactory metricFactory, 
PathConverter.Factory pathConverterFactory) {
+                                 MetricFactory metricFactory, 
PathConverter.Factory pathConverterFactory,
+                                 JamesSaslAuthenticator 
jamesSaslAuthenticator) {
         super(AuthenticateRequest.class, mailboxManager, factory, 
metricFactory, pathConverterFactory);
+        this.saslBridge = new ImapSaslBridge();
+        this.jamesSaslAuthenticator = jamesSaslAuthenticator;
+        this.saslMechanisms = ImmutableList.of();
     }
 
     @Override
@@ -79,127 +83,40 @@ public class AuthenticateProcessor extends 
AbstractAuthProcessor<AuthenticateReq
 
     @Override
     protected void processRequest(AuthenticateRequest request, ImapSession 
session, final Responder responder) {
-        final String authType = request.getAuthType();
-
-        if (authType.equalsIgnoreCase(AUTH_TYPE_PLAIN)) {
-            // See if AUTH=PLAIN is allowed. See IMAP-304
-            if (session.isPlainAuthDisallowed()) {
-                LOGGER.warn("Plain authentication rejected because it is 
disabled or not allowed over insecure channel");
-                no(request, responder, HumanReadableText.DISABLED_LOGIN);
-            } else {
-                if (request instanceof IRAuthenticateRequest) {
-                    IRAuthenticateRequest irRequest = (IRAuthenticateRequest) 
request;
-                    parseAndDoPlainAuth(irRequest.getInitialClientResponse(), 
session, request, responder);
-                } else {
-                    session.executeSafely(() -> {
-                        responder.respond(new AuthenticateResponse());
-                        responder.flush();
-                        session.pushLineHandler((requestSession, data) -> 
Mono.fromRunnable(() -> {
-                            
parseAndDoPlainAuth(extractInitialClientResponse(data), requestSession, 
request, responder);
-                            // remove the handler now
-                            requestSession.popLineHandler();
-                            responder.flush();
-                        
}).subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER).then());
-                    });
-                }
-            }
-        } else if (authType.equalsIgnoreCase(AUTH_TYPE_OAUTHBEARER) || 
authType.equalsIgnoreCase(AUTH_TYPE_XOAUTH2)) {
-            if (!session.supportsOAuth()) {
-                LOGGER.warn("OAuth authentication rejected because it is 
disabled");
-                no(request, responder, 
HumanReadableText.UNSUPPORTED_AUTHENTICATION_MECHANISM);
-            } else {
-                if (request instanceof IRAuthenticateRequest) {
-                    IRAuthenticateRequest irRequest = (IRAuthenticateRequest) 
request;
-                    parseAndDoOAuth(irRequest.getInitialClientResponse(), 
session, request, responder);
-                } else {
-                    session.executeSafely(() -> {
-                        responder.respond(new AuthenticateResponse());
-                        responder.flush();
-                        session.pushLineHandler((requestSession, data) -> 
Mono.fromRunnable(() -> {
-                            
parseAndDoOAuth(extractInitialClientResponse(data), requestSession, request, 
responder);
-                            requestSession.popLineHandler();
-                            responder.flush();
-                        
}).subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER).then());
-                    });
-                }
-            }
-        } else {
-            LOGGER.debug("Unsupported authentication mechanism '{}'", 
authType);
+        Optional<SaslMechanism> mechanism = 
findMechanism(request.getAuthType());
+
+        if (mechanism.isEmpty()) {
+            LOGGER.debug("Unsupported authentication mechanism '{}'", 
request.getAuthType());
             no(request, responder, 
HumanReadableText.UNSUPPORTED_AUTHENTICATION_MECHANISM);
+            return;
         }
-    }
 
-    /**
-     * Parse the initial client response and start plain authentication.
-     */
-    protected void parseAndDoPlainAuth(String initialClientResponse, 
ImapSession session, ImapRequest request, Responder responder) {
-        AuthenticationAttempt authenticationAttempt = 
parseDelegationAttempt(initialClientResponse);
-        if (authenticationAttempt.isDelegation()) {
-            doPasswordAuthWithDelegation(authenticationAttempt, session, 
request, responder);
-        } else {
-            doPasswordAuth(authenticationAttempt, session, request, responder);
+        if (!isAvailable(mechanism.get(), session)) {
+            rejectUnavailable(request, responder, mechanism.get());
+            return;
         }
-    }
-
-    /**
-     * Parse the initial client response and start oauth authentication.
-     */
-    protected void parseAndDoOAuth(String initialResponse, ImapSession 
session, ImapRequest request, Responder responder) {
-        OIDCSASLParser.parse(initialResponse)
-            .flatMap(oidcInitialResponseValue -> 
session.oidcSaslConfiguration().map(configure -> 
Pair.of(oidcInitialResponseValue, configure)))
-            .ifPresentOrElse(pair -> doOAuth(pair.getLeft(), pair.getRight(), 
session, request, responder),
-                () -> authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(),
-                    Optional.empty(), "Malformed authentication command."));
-    }
 
-    private AuthenticationAttempt parseDelegationAttempt(String 
initialClientResponse) {
         try {
-            String userpass = new 
String(Base64.getDecoder().decode(initialClientResponse));
-            List<String> tokens = Arrays.stream(userpass.split("\0"))
-                .filter(token -> !token.isBlank())
-                .collect(Collectors.toList());
-            Preconditions.checkArgument(tokens.size() == 2 || tokens.size() == 
3);
-            if (tokens.size() == 2) {
-                // If we got here, this is what happened.  RFC 2595
-                // says that "the client may leave the authorization
-                // identity empty to indicate that it is the same as
-                // the authentication identity."  As noted above,
-                // that would be represented as a decoded string of
-                // the form: "\0authenticate-id\0password".  The
-                // first call to nextToken will skip the empty
-                // authorize-id, and give us the authenticate-id,
-                // which we would store as the authorize-id.  The
-                // second call will give us the password, which we
-                // think is the authenticate-id (user).  Then when
-                // we ask for the password, there are no more
-                // elements, leading to the exception we just
-                // caught.  So we need to move the user to the
-                // password, and the authorize_id to the user.
-                return noDelegation(Username.of(tokens.get(0)), tokens.get(1));
-            } else {
-                return delegation(Username.of(tokens.get(0)), 
Username.of(tokens.get(1)), tokens.get(2));
-            }
-        } catch (Exception e) {
-            // Ignored - this exception in parsing will be dealt
-            // with in the if clause below
+            SaslInitialRequest initialRequest = 
saslBridge.initialRequest(request.getAuthType(), 
initialClientResponse(request));
+            SaslAuthenticator authenticator = 
jamesSaslAuthenticator.withExtraAuthorizator(withAdminUsers());
+            SaslExchange exchange = mechanism.get().start(initialRequest, 
authenticator);
+            handleFirstStep(exchange, firstStep(exchange), session, request, 
responder);
+        } catch (IllegalArgumentException e) {
             LOGGER.info("Invalid syntax in AUTHENTICATE initial client 
response", e);
-            return noDelegation(null, null);
+            authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(),
+                Optional.empty(), "Malformed authentication command.");
         }
     }
 
     @Override
     public List<Capability> getImplementedCapabilities(ImapSession session) {
-        List<Capability> caps = new ArrayList<>();
-        // Only ounce AUTH=PLAIN if the session does allow plain auth or TLS 
is active.
-        // See IMAP-304
-        if (!session.isPlainAuthDisallowed()) {
-            caps.add(AUTH_PLAIN_CAPABILITY);
-        }
+        List<Capability> caps = saslMechanisms.stream()
+            .filter(mechanism -> isAvailable(mechanism, session))
+            .map(mechanism -> Capability.of("AUTH=" + mechanism.name()))
+            .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+
         // Support for SASL-IR. See RFC4959
         caps.add(SASL_CAPABILITY);
-        if (session.supportsOAuth()) {
-            caps.addAll(OAUTH_CAPABILITIES);
-        }
         return ImmutableList.copyOf(caps);
     }
 
@@ -210,9 +127,235 @@ public class AuthenticateProcessor extends 
AbstractAuthProcessor<AuthenticateReq
             .addToContext("authType", request.getAuthType());
     }
 
-    private static String extractInitialClientResponse(byte[] data) {
-        // cut of the CRLF
-        return new String(data, 0, data.length - 2, StandardCharsets.US_ASCII);
+    public void configureSaslMechanisms(ImmutableList<SaslMechanism> 
saslMechanisms) {
+        this.saslMechanisms = saslMechanisms;
+    }
+
+    private Optional<String> initialClientResponse(AuthenticateRequest 
request) {
+        if (request instanceof IRAuthenticateRequest irAuthenticateRequest) {
+            return 
Optional.of(irAuthenticateRequest.getInitialClientResponse());
+        }
+        return Optional.empty();
+    }
+
+    private SaslStep firstStep(SaslExchange exchange) {
+        try {
+            return exchange.firstStep();
+        } catch (RuntimeException e) {
+            saslBridge.close(exchange);
+            throw e;
+        }
+    }
+
+    private Optional<SaslMechanism> findMechanism(String mechanismName) {
+        String normalizedName = mechanismName.toUpperCase(Locale.US);
+        return saslMechanisms.stream()
+            .filter(mechanism -> 
mechanism.name().toUpperCase(Locale.US).equals(normalizedName))
+            .findFirst();
+    }
+
+    private boolean isAvailable(SaslMechanism mechanism, ImapSession session) {
+        return mechanism.isAvailableOnTransport(session.isTLSActive());
+    }
+
+    private void rejectUnavailable(AuthenticateRequest request, Responder 
responder, SaslMechanism mechanism) {
+        LOGGER.warn("{} authentication rejected because it is not allowed over 
current transport", mechanism.name());
+        if (SaslMechanismNames.PLAIN.equalsIgnoreCase(mechanism.name())) {
+            no(request, responder, HumanReadableText.DISABLED_LOGIN);
+            return;
+        }
+        no(request, responder, 
HumanReadableText.UNSUPPORTED_AUTHENTICATION_MECHANISM);
+    }
+
+    private void handleFirstStep(SaslExchange exchange, SaslStep step, 
ImapSession session, AuthenticateRequest request, Responder responder) {
+        if (step instanceof SaslStep.Challenge challenge) {
+            handleInitialChallenge(exchange, challenge, session, request, 
responder);
+            return;
+        }
+        if (step instanceof SaslStep.Success success && 
success.serverData().isPresent()) {
+            handleSuccessWithServerData(exchange, success, session, request, 
responder);
+            return;
+        }
+        handleTerminalStep(exchange, step, session, request, responder);
+    }
+
+    private void handleInitialChallenge(SaslExchange exchange, 
SaslStep.Challenge challenge,
+                                        ImapSession session, 
AuthenticateRequest request, Responder responder) {
+        pushContinuationHandlerAndRespond(exchange, challenge, session, 
request, responder);
+    }
+
+    private void pushContinuationHandlerAndRespond(SaslExchange exchange, 
SaslStep.Challenge challenge,
+                                                   ImapSession session, 
AuthenticateRequest request, Responder responder) {
+        pushContinuationHandler(exchange, session, request, responder);
+        respondActiveContinuation(exchange, session, () ->
+            responder.respond(new 
AuthenticateResponse(saslBridge.continuation(challenge))));
+    }
+
+    private void pushContinuationHandler(SaslExchange exchange, ImapSession 
session, AuthenticateRequest request, Responder responder) {
+        try {
+            session.pushLineHandler((requestSession, data) -> 
Mono.fromRunnable(() -> handleContinuationLine(exchange, requestSession, 
request, responder, data))
+                .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER)
+                .then());
+        } catch (RuntimeException e) {
+            saslBridge.close(exchange);
+            throw e;
+        }
+    }
+
+    private void handleContinuationLine(SaslExchange exchange, ImapSession 
session, AuthenticateRequest request, Responder responder, byte[] data) {
+        if (isAbort(exchange, session, data)) {
+            abortActiveContinuation(exchange, session);
+            no(request, responder, HumanReadableText.AUTHENTICATION_FAILED);
+            responder.flush();
+            return;
+        }
+
+        nextStep(exchange, session, request, responder, data)
+            .ifPresent(step -> handleContinuationStep(exchange, step, session, 
request, responder));
+    }
+
+    private Optional<SaslStep> nextStep(SaslExchange exchange, ImapSession 
session, AuthenticateRequest request, Responder responder, byte[] data) {
+        try {
+            return Optional.of(saslBridge.onClientResponse(exchange, data));
+        } catch (IllegalArgumentException e) {
+            LOGGER.info("Invalid syntax in AUTHENTICATE client response", e);
+            closeActiveContinuation(exchange, session);
+            authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(),
+                Optional.empty(), "Malformed authentication command.");
+            responder.flush();
+            return Optional.empty();
+        } catch (RuntimeException e) {
+            closeActiveContinuation(exchange, session);
+            throw e;
+        }
     }
 
+    private void handleContinuationStep(SaslExchange exchange, SaslStep step, 
ImapSession session, AuthenticateRequest request, Responder responder) {
+        if (step instanceof SaslStep.Challenge challenge) {
+            try {
+                responder.respond(new 
AuthenticateResponse(saslBridge.continuation(challenge)));
+                responder.flush();
+            } catch (RuntimeException e) {
+                closeActiveContinuation(exchange, session);
+                throw e;
+            }
+            return;
+        }
+
+        popActiveContinuation(exchange, session);
+        if (step instanceof SaslStep.Success success && 
success.serverData().isPresent()) {
+            handleSuccessWithServerData(exchange, success, session, request, 
responder);
+            return;
+        }
+
+        handleTerminalStep(exchange, step, session, request, responder);
+        responder.flush();
+    }
+
+    private void respondActiveContinuation(SaslExchange exchange, ImapSession 
session, Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (RuntimeException e) {
+            closeActiveContinuation(exchange, session);
+            throw e;
+        }
+    }
+
+    private boolean isAbort(SaslExchange exchange, ImapSession session, byte[] 
data) {
+        try {
+            return saslBridge.isAbort(data);
+        } catch (RuntimeException e) {
+            closeActiveContinuation(exchange, session);
+            throw e;
+        }
+    }
+
+    private boolean isEmptyClientResponse(SaslExchange exchange, ImapSession 
session, byte[] data) {
+        try {
+            return saslBridge.isEmptyClientResponse(data);
+        } catch (RuntimeException e) {
+            closeActiveContinuation(exchange, session);
+            throw e;
+        }
+    }
+
+    private void closeActiveContinuation(SaslExchange exchange, ImapSession 
session) {
+        try {
+            session.popLineHandler();
+        } finally {
+            saslBridge.close(exchange);
+        }
+    }
+
+    private void abortActiveContinuation(SaslExchange exchange, ImapSession 
session) {
+        try {
+            session.popLineHandler();
+        } finally {
+            saslBridge.abort(exchange);
+        }
+    }
+
+    private void popActiveContinuation(SaslExchange exchange, ImapSession 
session) {
+        try {
+            session.popLineHandler();
+        } catch (RuntimeException e) {
+            saslBridge.close(exchange);
+            throw e;
+        }
+    }
+
+    private void handleSuccessWithServerData(SaslExchange exchange, 
SaslStep.Success success, ImapSession session,
+                                             AuthenticateRequest request, 
Responder responder) {
+        pushSuccessDataAcknowledgementHandler(exchange, success, session, 
request, responder);
+        respondActiveContinuation(exchange, session, () -> {
+            responder.respond(new 
AuthenticateResponse(saslBridge.successData(success)));
+            responder.flush();
+        });
+    }
+
+    private void pushSuccessDataAcknowledgementHandler(SaslExchange exchange, 
SaslStep.Success success, ImapSession session,
+                                                       AuthenticateRequest 
request, Responder responder) {
+        try {
+            session.pushLineHandler((requestSession, data) -> 
Mono.fromRunnable(() -> handleSuccessDataAcknowledgement(exchange, success, 
requestSession, request, responder, data))
+                .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER)
+                .then());
+        } catch (RuntimeException e) {
+            saslBridge.close(exchange);
+            throw e;
+        }
+    }
+
+    private void handleSuccessDataAcknowledgement(SaslExchange exchange, 
SaslStep.Success success, ImapSession session,
+                                                  AuthenticateRequest request, 
Responder responder, byte[] data) {
+        if (isAbort(exchange, session, data)) {
+            abortActiveContinuation(exchange, session);
+            no(request, responder, HumanReadableText.AUTHENTICATION_FAILED);
+            responder.flush();
+            return;
+        }
+        if (!isEmptyClientResponse(exchange, session, data)) {
+            closeActiveContinuation(exchange, session);
+            authFailure(session, request, responder, 
HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(),
+                Optional.empty(), "Malformed authentication command.");
+            responder.flush();
+            return;
+        }
+
+        popActiveContinuation(exchange, session);
+        handleTerminalStep(exchange, success, session, request, responder);
+        responder.flush();
+    }
+
+    private void handleTerminalStep(SaslExchange exchange, SaslStep step, 
ImapSession session, AuthenticateRequest request, Responder responder) {
+        try {
+            handleSaslStep(step, session, request, responder, 
successLog(request));
+        } finally {
+            saslBridge.close(exchange);
+        }
+    }
+
+    private String successLog(AuthenticateRequest request) {
+        String authType = request.getAuthType().toUpperCase(Locale.US);
+        return authType + " authentication succeeded.";
+    }
 }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
index 794954c85d..be56964b79 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.imap.processor;
 
+import static 
org.apache.james.protocols.sasl.JamesSaslAuthenticator.jamesSaslAuthenticator;
+
 import java.util.Map;
 import java.util.stream.Stream;
 
@@ -40,6 +42,9 @@ import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.quota.QuotaManager;
 import org.apache.james.mailbox.quota.QuotaRootResolver;
 import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
+import org.apache.james.protocols.sasl.plain.PlainSaslMechanism;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -59,16 +64,56 @@ public class DefaultProcessor implements ImapProcessor {
                                                        MailboxCounterCorrector 
mailboxCounterCorrector,
                                                        MetricFactory 
metricFactory,
                                                        
FetchProcessor.LocalCacheConfiguration localCacheConfiguration) {
+        return createDefaultProcessor(chainEndProcessor, mailboxManager, 
eventBus, subscriptionManager,
+            statusResponseFactory, mailboxTyper, quotaManager, 
quotaRootResolver, mailboxCounterCorrector,
+            metricFactory, localCacheConfiguration, ImmutableList.of(new 
PlainSaslMechanism()));
+    }
+
+    public static ImapProcessor createDefaultProcessor(ImapProcessor 
chainEndProcessor,
+                                                       MailboxManager 
mailboxManager,
+                                                       EventBus eventBus,
+                                                       SubscriptionManager 
subscriptionManager,
+                                                       StatusResponseFactory 
statusResponseFactory,
+                                                       MailboxTyper 
mailboxTyper,
+                                                       QuotaManager 
quotaManager,
+                                                       QuotaRootResolver 
quotaRootResolver,
+                                                       MailboxCounterCorrector 
mailboxCounterCorrector,
+                                                       MetricFactory 
metricFactory,
+                                                       
FetchProcessor.LocalCacheConfiguration localCacheConfiguration,
+                                                       
ImmutableList<SaslMechanism> defaultSaslMechanisms) {
+        return createDefaultProcessor(chainEndProcessor, mailboxManager, 
eventBus, subscriptionManager,
+            statusResponseFactory, mailboxTyper, quotaManager, 
quotaRootResolver, mailboxCounterCorrector,
+            metricFactory, localCacheConfiguration, defaultSaslMechanisms, 
jamesSaslAuthenticator(mailboxManager));
+    }
+
+    public static ImapProcessor createDefaultProcessor(ImapProcessor 
chainEndProcessor,
+                                                       MailboxManager 
mailboxManager,
+                                                       EventBus eventBus,
+                                                       SubscriptionManager 
subscriptionManager,
+                                                       StatusResponseFactory 
statusResponseFactory,
+                                                       MailboxTyper 
mailboxTyper,
+                                                       QuotaManager 
quotaManager,
+                                                       QuotaRootResolver 
quotaRootResolver,
+                                                       MailboxCounterCorrector 
mailboxCounterCorrector,
+                                                       MetricFactory 
metricFactory,
+                                                       
FetchProcessor.LocalCacheConfiguration localCacheConfiguration,
+                                                       
ImmutableList<SaslMechanism> defaultSaslMechanisms,
+                                                       JamesSaslAuthenticator 
saslAuthenticator) {
         PathConverter.Factory pathConverterFactory = 
PathConverter.Factory.DEFAULT;
 
         ImmutableList.Builder<AbstractProcessor> builder = 
ImmutableList.builder();
         CapabilityProcessor capabilityProcessor = new 
CapabilityProcessor(mailboxManager, statusResponseFactory, metricFactory);
+        LoginProcessor loginProcessor = new LoginProcessor(mailboxManager, 
statusResponseFactory, metricFactory, pathConverterFactory, saslAuthenticator);
+        loginProcessor.configureSaslMechanisms(defaultSaslMechanisms);
+        AuthenticateProcessor authenticateProcessor = new 
AuthenticateProcessor(mailboxManager, statusResponseFactory, metricFactory, 
pathConverterFactory, saslAuthenticator);
+        authenticateProcessor.configureSaslMechanisms(defaultSaslMechanisms);
+
         builder.add(new SystemMessageProcessor());
         builder.add(new LogoutProcessor(mailboxManager, statusResponseFactory, 
metricFactory));
         builder.add(capabilityProcessor);
         builder.add(new IdProcessor(mailboxManager, statusResponseFactory, 
metricFactory));
         builder.add(new CheckProcessor(mailboxManager, statusResponseFactory, 
metricFactory));
-        builder.add(new LoginProcessor(mailboxManager, statusResponseFactory, 
metricFactory, pathConverterFactory));
+        builder.add(loginProcessor);
         builder.add(new RenameProcessor(mailboxManager, statusResponseFactory, 
metricFactory, pathConverterFactory));
         builder.add(new DeleteProcessor(mailboxManager, statusResponseFactory, 
metricFactory, pathConverterFactory));
         builder.add(new CreateProcessor(mailboxManager, statusResponseFactory, 
metricFactory, pathConverterFactory));
@@ -76,7 +121,7 @@ public class DefaultProcessor implements ImapProcessor {
         builder.add(new UnsubscribeProcessor(mailboxManager, 
subscriptionManager, statusResponseFactory, metricFactory, 
pathConverterFactory));
         builder.add(new SubscribeProcessor(mailboxManager, 
subscriptionManager, statusResponseFactory, metricFactory, 
pathConverterFactory));
         builder.add(new CopyProcessor(mailboxManager, statusResponseFactory, 
metricFactory, pathConverterFactory));
-        builder.add(new AuthenticateProcessor(mailboxManager, 
statusResponseFactory, metricFactory, pathConverterFactory));
+        builder.add(authenticateProcessor);
         builder.add(new ExpungeProcessor(mailboxManager, 
statusResponseFactory, metricFactory));
         builder.add(new ReplaceProcessor(mailboxManager, 
statusResponseFactory, metricFactory, pathConverterFactory));
         builder.add(new ExamineProcessor(mailboxManager, eventBus, 
statusResponseFactory, metricFactory, pathConverterFactory, 
mailboxCounterCorrector));
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/LoginProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/LoginProcessor.java
index c7c2a845b3..2f2ba5d168 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/LoginProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/LoginProcessor.java
@@ -21,6 +21,7 @@ package org.apache.james.imap.processor;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 import jakarta.inject.Inject;
 
@@ -32,6 +33,11 @@ import org.apache.james.imap.main.PathConverter;
 import org.apache.james.imap.message.request.LoginRequest;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismNames;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
+import org.apache.james.protocols.sasl.plain.PlainSaslMechanism;
 import org.apache.james.util.MDCBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,9 +51,15 @@ public class LoginProcessor extends 
AbstractAuthProcessor<LoginRequest> implemen
     private static final List<Capability> LOGINDISABLED_CAPS = 
ImmutableList.of(Capability.of("LOGINDISABLED"));
     private static final Logger LOGGER = 
LoggerFactory.getLogger(LoginProcessor.class);
 
+    private final JamesSaslAuthenticator jamesSaslAuthenticator;
+    private Optional<PlainSaslMechanism> plainSaslMechanism;
+
     @Inject
-    public LoginProcessor(MailboxManager mailboxManager, StatusResponseFactory 
factory, MetricFactory metricFactory, PathConverter.Factory 
pathConverterFactory) {
+    public LoginProcessor(MailboxManager mailboxManager, StatusResponseFactory 
factory, MetricFactory metricFactory, PathConverter.Factory 
pathConverterFactory,
+                          JamesSaslAuthenticator jamesSaslAuthenticator) {
         super(LoginRequest.class, mailboxManager, factory, metricFactory, 
pathConverterFactory);
+        this.jamesSaslAuthenticator = jamesSaslAuthenticator;
+        this.plainSaslMechanism = Optional.empty();
     }
 
     /**
@@ -55,12 +67,16 @@ public class LoginProcessor extends 
AbstractAuthProcessor<LoginRequest> implemen
      */
     @Override
     protected void processRequest(LoginRequest request, ImapSession session, 
Responder responder) {
+        Optional<PlainSaslMechanism> plainSaslMechanism = 
availablePlainSaslMechanism(session);
+
         // check if the login is allowed with LOGIN command. See IMAP-304
-        if (session.isPlainAuthDisallowed()) {
-            LOGGER.warn("Login rejected because it is disabled or not allowed 
over insecure channel");
+        if (plainSaslMechanism.isEmpty()) {
+            LOGGER.warn("Login rejected because PLAIN SASL mechanism is 
disabled");
             no(request, responder, HumanReadableText.DISABLED_LOGIN);
         } else {
-            doPasswordAuth(noDelegation(request.getUserid(), 
request.getPassword()), session, request, responder);
+            SaslAuthenticator authenticator = 
jamesSaslAuthenticator.withExtraAuthorizator(withAdminUsers());
+            
handleSaslStep(plainSaslMechanism.orElseThrow().authenticate(request.getUserid(),
 request.getPassword(), authenticator),
+                session, request, responder, "Password authentication 
succeeded.");
         }
     }
 
@@ -68,12 +84,25 @@ public class LoginProcessor extends 
AbstractAuthProcessor<LoginRequest> implemen
     public List<Capability> getImplementedCapabilities(ImapSession session) {
         // Announce LOGINDISABLED if plain auth / login is deactivated and the 
session is not using
         // TLS. See IMAP-304
-        if (session.isPlainAuthDisallowed()) {
+        if (availablePlainSaslMechanism(session).isEmpty()) {
             return LOGINDISABLED_CAPS;
         }
         return Collections.emptyList();
     }
 
+    public void configureSaslMechanisms(ImmutableList<SaslMechanism> 
saslMechanisms) {
+        this.plainSaslMechanism = saslMechanisms.stream()
+            .filter(mechanism -> 
SaslMechanismNames.PLAIN.equalsIgnoreCase(mechanism.name()))
+            .filter(PlainSaslMechanism.class::isInstance)
+            .map(PlainSaslMechanism.class::cast)
+            .findFirst();
+    }
+
+    private Optional<PlainSaslMechanism> 
availablePlainSaslMechanism(ImapSession session) {
+        return plainSaslMechanism
+            .filter(mechanism -> 
mechanism.isAvailableOnTransport(session.isTLSActive()));
+    }
+
     @Override
     protected MDCBuilder mdc(LoginRequest request) {
         return MDCBuilder.create()
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/sasl/ImapSaslBridge.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/sasl/ImapSaslBridge.java
new file mode 100644
index 0000000000..7e5a8217fe
--- /dev/null
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/sasl/ImapSaslBridge.java
@@ -0,0 +1,104 @@
+/****************************************************************
+ * 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.processor.sasl;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Optional;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslInitialRequest;
+import org.apache.james.protocols.api.sasl.SaslStep;
+
+public class ImapSaslBridge {
+    /**
+     * Converts an IMAP AUTHENTICATE request into a protocol-neutral SASL 
initial request.
+     */
+    public SaslInitialRequest initialRequest(String mechanismName, 
Optional<String> initialClientResponse) {
+        return new SaslInitialRequest(mechanismName, 
initialClientResponse.map(this::decodeInitialClientResponse));
+    }
+
+    /**
+     * Encodes a SASL challenge payload as an IMAP continuation payload.
+     */
+    public String continuation(SaslStep.Challenge challenge) {
+        return challenge.payload()
+            .map(Base64.getEncoder()::encodeToString)
+            .orElse("");
+    }
+
+    /**
+     * Encodes final SASL server data as an IMAP continuation payload.
+     */
+    public String successData(SaslStep.Success success) {
+        return success.serverData()
+            .map(Base64.getEncoder()::encodeToString)
+            .orElse("");
+    }
+
+    /**
+     * Decodes an IMAP client continuation line and forwards it to the SASL 
exchange.
+     */
+    public SaslStep onClientResponse(SaslExchange exchange, byte[] line) {
+        return exchange.onResponse(decodeBase64(stripTrailingCrlf(line)));
+    }
+
+    public boolean isAbort(byte[] line) {
+        return "*".equals(stripTrailingCrlf(line));
+    }
+
+    /**
+     * Detects the empty IMAP client response used to acknowledge final SASL 
server data.
+     */
+    public boolean isEmptyClientResponse(byte[] line) {
+        return stripTrailingCrlf(line).isEmpty();
+    }
+
+    /**
+     * Aborts an active SASL exchange.
+     */
+    public void abort(SaslExchange exchange) {
+        exchange.abort();
+    }
+
+    /**
+     * Closes an active SASL exchange.
+     */
+    public void close(SaslExchange exchange) {
+        exchange.close();
+    }
+
+    private byte[] decodeBase64(String value) {
+        return Base64.getDecoder().decode(value);
+    }
+
+    private byte[] decodeInitialClientResponse(String value) {
+        if (value.equals("=")) {
+            return new byte[0];
+        }
+        return decodeBase64(value);
+    }
+
+    private String stripTrailingCrlf(byte[] line) {
+        String value = new String(line, StandardCharsets.US_ASCII);
+        return StringUtils.stripEnd(value, "\r\n");
+    }
+}
diff --git 
a/protocols/imap/src/test/java/org/apache/james/imap/processor/AuthenticateProcessorTest.java
 
b/protocols/imap/src/test/java/org/apache/james/imap/processor/AuthenticateProcessorTest.java
new file mode 100644
index 0000000000..1d66db3437
--- /dev/null
+++ 
b/protocols/imap/src/test/java/org/apache/james/imap/processor/AuthenticateProcessorTest.java
@@ -0,0 +1,277 @@
+/****************************************************************
+ * 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.processor;
+
+import static org.apache.james.imap.ImapFixture.TAG;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Optional;
+
+import org.apache.james.core.Username;
+import org.apache.james.imap.api.message.response.ImapResponseMessage;
+import org.apache.james.imap.api.process.ImapLineHandler;
+import org.apache.james.imap.api.process.ImapProcessor;
+import org.apache.james.imap.encode.FakeImapSession;
+import org.apache.james.imap.main.PathConverter;
+import org.apache.james.imap.message.request.AuthenticateRequest;
+import org.apache.james.imap.message.response.UnpooledStatusResponseFactory;
+import org.apache.james.mailbox.Authenticator;
+import org.apache.james.mailbox.Authorizator;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+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;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import reactor.core.publisher.Mono;
+
+class AuthenticateProcessorTest {
+    private static final String BROKEN_MECHANISM = "BROKEN";
+    private static final Username USER = Username.of("[email protected]");
+    private static final SaslIdentity IDENTITY = new SaslIdentity(USER, USER);
+
+    private static class TestSaslMechanism implements SaslMechanism {
+        private final SaslExchange exchange;
+
+        private TestSaslMechanism(SaslExchange exchange) {
+            this.exchange = exchange;
+        }
+
+        @Override
+        public String name() {
+            return BROKEN_MECHANISM;
+        }
+
+        @Override
+        public SaslExchange start(SaslInitialRequest request, 
SaslAuthenticator authenticator) {
+            return exchange;
+        }
+    }
+
+    private static class RecordingLineHandlerImapSession extends 
FakeImapSession {
+        private ImapLineHandler lineHandler;
+        private boolean throwOnPop;
+        private int popCount;
+
+        @Override
+        public void pushLineHandler(ImapLineHandler lineHandler) {
+            this.lineHandler = lineHandler;
+        }
+
+        @Override
+        public void popLineHandler() {
+            popCount++;
+            if (throwOnPop) {
+                throw new IllegalStateException("boom");
+            }
+            lineHandler = null;
+        }
+    }
+
+    private static class ThrowingResponder implements ImapProcessor.Responder {
+        @Override
+        public void respond(ImapResponseMessage message) {
+            throw new IllegalStateException("boom");
+        }
+
+        @Override
+        public void flush() {
+        }
+    }
+
+    private static class ThrowingFirstStepExchange implements SaslExchange {
+        private boolean closed;
+
+        @Override
+        public SaslStep firstStep() {
+            throw new IllegalArgumentException("boom");
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void abort() {
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+    }
+
+    private static class ThrowingContinuationExchange implements SaslExchange {
+        private boolean closed;
+
+        @Override
+        public SaslStep firstStep() {
+            return new SaslStep.Challenge(Optional.of(bytes("challenge")));
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            throw new IllegalStateException("boom");
+        }
+
+        @Override
+        public void abort() {
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+    }
+
+    private static class SuccessDataExchange implements SaslExchange {
+        private boolean closed;
+
+        @Override
+        public SaslStep firstStep() {
+            return new SaslStep.Success(IDENTITY, 
Optional.of(bytes("server-data")));
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void abort() {
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+    }
+
+    private final AuthenticateProcessor testee = new AuthenticateProcessor(
+        mock(MailboxManager.class),
+        new UnpooledStatusResponseFactory(),
+        new RecordingMetricFactory(),
+        PathConverter.Factory.DEFAULT,
+        new JamesSaslAuthenticator(mock(Authenticator.class), 
mock(Authorizator.class)));
+
+    @Test
+    void processRequestShouldCloseExchangeWhenFirstStepThrows() {
+        // GIVEN a mechanism whose exchange fails while computing the first 
SASL step
+        ThrowingFirstStepExchange exchange = new ThrowingFirstStepExchange();
+        testee.configureSaslMechanisms(ImmutableList.of(new 
TestSaslMechanism(exchange)));
+
+        // WHEN the processor handles the malformed exchange
+        testee.processRequest(new AuthenticateRequest(BROKEN_MECHANISM, TAG), 
new FakeImapSession(), mock(ImapProcessor.Responder.class));
+
+        // THEN it closes the exchange even though the terminal step handling 
was not reached
+        assertThat(exchange.closed).isTrue();
+    }
+
+    @Test
+    void processRequestShouldCloseExchangeWhenInitialChallengeWriteThrows() {
+        // GIVEN a first challenge that fails while being written to the client
+        ThrowingContinuationExchange exchange = new 
ThrowingContinuationExchange();
+        testee.configureSaslMechanisms(ImmutableList.of(new 
TestSaslMechanism(exchange)));
+
+        // WHEN the processor handles the initial challenge
+        assertThatThrownBy(() -> testee.processRequest(new 
AuthenticateRequest(BROKEN_MECHANISM, TAG), new FakeImapSession(), new 
ThrowingResponder()))
+            .isInstanceOf(IllegalStateException.class);
+
+        // THEN it closes the exchange even though no continuation handler was 
installed
+        assertThat(exchange.closed).isTrue();
+    }
+
+    @Test
+    void processRequestShouldCloseExchangeWhenInitialSuccessDataWriteThrows() {
+        // GIVEN a first successful SASL step with final server data that 
fails while being written to the client
+        SuccessDataExchange exchange = new SuccessDataExchange();
+        testee.configureSaslMechanisms(ImmutableList.of(new 
TestSaslMechanism(exchange)));
+
+        // WHEN the processor handles the initial success data
+        assertThatThrownBy(() -> testee.processRequest(new 
AuthenticateRequest(BROKEN_MECHANISM, TAG), new FakeImapSession(), new 
ThrowingResponder()))
+            .isInstanceOf(IllegalStateException.class);
+
+        // THEN it closes the exchange because terminal handling did not own 
it yet
+        assertThat(exchange.closed).isTrue();
+    }
+
+    @Test
+    void continuationShouldCloseExchangeWhenOnResponseThrows() {
+        // GIVEN an active continuation whose mechanism fails unexpectedly 
while processing the client response
+        ThrowingContinuationExchange exchange = new 
ThrowingContinuationExchange();
+        RecordingLineHandlerImapSession session = new 
RecordingLineHandlerImapSession();
+        testee.configureSaslMechanisms(ImmutableList.of(new 
TestSaslMechanism(exchange)));
+        testee.processRequest(new AuthenticateRequest(BROKEN_MECHANISM, TAG), 
session, mock(ImapProcessor.Responder.class));
+        ImapLineHandler lineHandler = session.lineHandler;
+        assertThat(lineHandler).isNotNull();
+
+        // WHEN the continuation line is processed
+        assertThatThrownBy(() -> Mono.from(lineHandler.onLine(session, 
imapLine("response"))).block())
+            .isInstanceOf(IllegalStateException.class);
+
+        // THEN the active line handler is removed and the exchange is closed 
before rethrowing
+        assertThat(session.popCount).isEqualTo(1);
+        assertThat(exchange.closed).isTrue();
+    }
+
+    @Test
+    void 
successDataAcknowledgementShouldCloseExchangeWhenPopLineHandlerThrows() {
+        // GIVEN an active final server-data acknowledgement handler
+        SuccessDataExchange exchange = new SuccessDataExchange();
+        RecordingLineHandlerImapSession session = new 
RecordingLineHandlerImapSession();
+        testee.configureSaslMechanisms(ImmutableList.of(new 
TestSaslMechanism(exchange)));
+        testee.processRequest(new AuthenticateRequest(BROKEN_MECHANISM, TAG), 
session, mock(ImapProcessor.Responder.class));
+        ImapLineHandler lineHandler = session.lineHandler;
+        assertThat(lineHandler).isNotNull();
+        session.throwOnPop = true;
+
+        // WHEN the acknowledgement line reaches a failing line-handler cleanup
+        assertThatThrownBy(() -> Mono.from(lineHandler.onLine(session, 
emptyLine())).block())
+            .isInstanceOf(IllegalStateException.class);
+
+        // THEN the exchange is still closed before the failure is rethrown
+        assertThat(session.popCount).isEqualTo(1);
+        assertThat(exchange.closed).isTrue();
+    }
+
+    private static byte[] imapLine(String value) {
+        return (Base64.getEncoder().encodeToString(bytes(value)) + 
"\r\n").getBytes(StandardCharsets.US_ASCII);
+    }
+
+    private static byte[] emptyLine() {
+        return "\r\n".getBytes(StandardCharsets.US_ASCII);
+    }
+
+    private static byte[] bytes(String value) {
+        return value.getBytes(StandardCharsets.UTF_8);
+    }
+}
diff --git 
a/protocols/imap/src/test/java/org/apache/james/imap/processor/sasl/ImapSaslBridgeTest.java
 
b/protocols/imap/src/test/java/org/apache/james/imap/processor/sasl/ImapSaslBridgeTest.java
new file mode 100644
index 0000000000..63053ee11c
--- /dev/null
+++ 
b/protocols/imap/src/test/java/org/apache/james/imap/processor/sasl/ImapSaslBridgeTest.java
@@ -0,0 +1,240 @@
+/****************************************************************
+ * 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.processor.sasl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.core.Username;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslIdentity;
+import org.apache.james.protocols.api.sasl.SaslInitialRequest;
+import org.apache.james.protocols.api.sasl.SaslStep;
+import org.junit.jupiter.api.Test;
+
+class ImapSaslBridgeTest {
+    private static final Username USER = Username.of("[email protected]");
+    private static final SaslIdentity IDENTITY = new SaslIdentity(USER, USER);
+
+    private final ImapSaslBridge testee = new ImapSaslBridge();
+
+    private static class RecordingExchange implements SaslExchange {
+        protected final List<String> lifecycleEvents;
+        private byte[] lastClientResponse;
+
+        private RecordingExchange() {
+            this.lifecycleEvents = new ArrayList<>();
+        }
+
+        @Override
+        public SaslStep firstStep() {
+            return new SaslStep.Challenge(Optional.of(bytes("challenge")));
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            lastClientResponse = clientResponse.clone();
+            return new SaslStep.Success(IDENTITY, Optional.empty());
+        }
+
+        @Override
+        public void close() {
+            lifecycleEvents.add("close");
+        }
+    }
+
+    private static class RecordingAbortExchange extends RecordingExchange {
+        @Override
+        public void abort() {
+            lifecycleEvents.add("abort");
+            close();
+        }
+    }
+
+    private static class ThrowingAbortExchange implements SaslExchange {
+        private final List<String> lifecycleEvents;
+
+        private ThrowingAbortExchange() {
+            this.lifecycleEvents = new ArrayList<>();
+        }
+
+        @Override
+        public SaslStep firstStep() {
+            return new SaslStep.Challenge(Optional.of(bytes("challenge")));
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            return new SaslStep.Success(IDENTITY, Optional.empty());
+        }
+
+        @Override
+        public void abort() {
+            lifecycleEvents.add("abort");
+            close();
+            throw new IllegalStateException("boom");
+        }
+
+        @Override
+        public void close() {
+            lifecycleEvents.add("close");
+        }
+    }
+
+    @Test
+    void initialRequestShouldDecodeInitialClientResponse() {
+        String encodedInitialResponse = 
Base64.getEncoder().encodeToString(bytes("initial"));
+
+        SaslInitialRequest request = testee.initialRequest("PLAIN", 
Optional.of(encodedInitialResponse));
+
+        assertThat(request.mechanismName()).isEqualTo("PLAIN");
+        assertThat(request.initialResponse()).hasValueSatisfying(value -> 
assertThat(value).containsExactly(bytes("initial")));
+    }
+
+    @Test
+    void initialRequestShouldDecodeEqualSignAsEmptyInitialClientResponse() {
+        SaslInitialRequest request = testee.initialRequest("PLAIN", 
Optional.of("="));
+
+        assertThat(request.initialResponse()).hasValueSatisfying(value -> 
assertThat(value).isEmpty());
+    }
+
+    @Test
+    void continuationShouldBase64EncodeChallengePayload() {
+        SaslStep.Challenge challenge = new 
SaslStep.Challenge(Optional.of(bytes("challenge")));
+
+        String continuation = testee.continuation(challenge);
+
+        
assertThat(continuation).isEqualTo(Base64.getEncoder().encodeToString(bytes("challenge")));
+    }
+
+    @Test
+    void continuationShouldReturnEmptyStringWhenChallengeHasNoPayload() {
+        SaslStep.Challenge challenge = new 
SaslStep.Challenge(Optional.empty());
+
+        String continuation = testee.continuation(challenge);
+
+        assertThat(continuation).isEmpty();
+    }
+
+    @Test
+    void successDataShouldBase64EncodePayload() {
+        SaslStep.Success success = new SaslStep.Success(IDENTITY, 
Optional.of(bytes("server-data")));
+
+        String successData = testee.successData(success);
+
+        
assertThat(successData).isEqualTo(Base64.getEncoder().encodeToString(bytes("server-data")));
+    }
+
+    @Test
+    void isEmptyClientResponseShouldDetectEmptyLine() {
+        
assertThat(testee.isEmptyClientResponse("\r\n".getBytes(StandardCharsets.US_ASCII))).isTrue();
+    }
+
+    @Test
+    void isEmptyClientResponseShouldRejectNonEmptyLine() {
+        
assertThat(testee.isEmptyClientResponse("data\r\n".getBytes(StandardCharsets.US_ASCII))).isFalse();
+    }
+
+    @Test
+    void onClientResponseShouldDecodeLineAndContinueExchange() {
+        RecordingExchange exchange = new RecordingExchange();
+        byte[] line = (Base64.getEncoder().encodeToString(bytes("response")) + 
"\r\n").getBytes(StandardCharsets.US_ASCII);
+
+        SaslStep step = testee.onClientResponse(exchange, line);
+
+        assertThat(((SaslStep.Success) step).identity()).isEqualTo(IDENTITY);
+        
assertThat(exchange.lastClientResponse).containsExactly(bytes("response"));
+    }
+
+    @Test
+    void onClientResponseShouldDecodeLineWithLfOnly() {
+        RecordingExchange exchange = new RecordingExchange();
+        byte[] line = (Base64.getEncoder().encodeToString(bytes("response")) + 
"\n").getBytes(StandardCharsets.US_ASCII);
+
+        SaslStep step = testee.onClientResponse(exchange, line);
+
+        assertThat(((SaslStep.Success) step).identity()).isEqualTo(IDENTITY);
+        
assertThat(exchange.lastClientResponse).containsExactly(bytes("response"));
+    }
+
+    @Test
+    void isAbortShouldDetectImapSaslAbortLine() {
+        
assertThat(testee.isAbort("*\r\n".getBytes(StandardCharsets.US_ASCII))).isTrue();
+    }
+
+    @Test
+    void isAbortShouldDetectImapSaslAbortLineWithLfOnly() {
+        
assertThat(testee.isAbort("*\n".getBytes(StandardCharsets.US_ASCII))).isTrue();
+    }
+
+    @Test
+    void isAbortShouldRejectRegularClientResponse() {
+        byte[] line = (Base64.getEncoder().encodeToString(bytes("response")) + 
"\r\n").getBytes(StandardCharsets.US_ASCII);
+
+        assertThat(testee.isAbort(line)).isFalse();
+    }
+
+    @Test
+    void abortShouldCloseExchangeByDefault() {
+        RecordingExchange exchange = new RecordingExchange();
+
+        testee.abort(exchange);
+
+        assertThat(exchange.lifecycleEvents).containsExactly("close");
+    }
+
+    @Test
+    void abortShouldUseExchangeSpecificAbortWhenOverridden() {
+        RecordingAbortExchange exchange = new RecordingAbortExchange();
+
+        testee.abort(exchange);
+
+        assertThat(exchange.lifecycleEvents).containsExactly("abort", "close");
+    }
+
+    @Test
+    void abortShouldPropagateExchangeSpecificAbortFailure() {
+        ThrowingAbortExchange exchange = new ThrowingAbortExchange();
+
+        assertThatThrownBy(() -> testee.abort(exchange))
+            .isInstanceOf(IllegalStateException.class);
+
+        assertThat(exchange.lifecycleEvents).containsExactly("abort", "close");
+    }
+
+    @Test
+    void closeShouldCloseExchange() {
+        RecordingExchange exchange = new RecordingExchange();
+
+        testee.close(exchange);
+
+        assertThat(exchange.lifecycleEvents).containsExactly("close");
+    }
+
+    private static byte[] bytes(String value) {
+        return value.getBytes(StandardCharsets.UTF_8);
+    }
+}


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

Reply via email to