This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 440f7546405b81d99f239cbbcfab473e64122c2b
Author: Quan Tran <[email protected]>
AuthorDate: Tue Jun 30 09:39:46 2026 +0700

    JAMES-4210 Preserve legacy SMTP AuthHook behavior with SASL LOGIN
    
    Apply the legacy AuthHook SASL adapter to the effective PLAIN mechanism and 
to LOGIN's delegated PLAIN mechanism, preserving legacy AuthHook behavior for 
both AUTH PLAIN and AUTH LOGIN without leaking AuthHook-specific startup logic 
into AuthCmdHandler.
---
 .../smtp/core/esmtp/AuthHookSaslMechanism.java     | 30 ++++++++++++++----
 .../smtp/core/esmtp/AuthHookSaslMechanismTest.java | 36 ++++++++++++++--------
 2 files changed, 48 insertions(+), 18 deletions(-)

diff --git 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanism.java
 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanism.java
index 0c75392b88..7cc03f8554 100644
--- 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanism.java
+++ 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanism.java
@@ -53,17 +53,39 @@ class AuthHookSaslMechanism implements SaslMechanism {
         if (exchange instanceof AuthHookSaslMechanism.Exchange 
authHookExchange) {
             return authHookExchange.terminalResponse();
         }
-        return Optional.empty();
+        return LoginSaslMechanism.delegatedExchange(exchange)
+            .flatMap(AuthHookSaslMechanism::terminalResponse);
+    }
+
+    static ImmutableList<SaslMechanism> 
withLegacyAuthHooks(ImmutableList<SaslMechanism> mechanisms, List<AuthHook> 
authHooks,
+                                                            
List<HookResultHook> hookResultHooks, SMTPSession session) {
+        return mechanisms.stream()
+            .map(mechanism -> adaptMechanism(mechanism, authHooks, 
hookResultHooks, session))
+            .collect(ImmutableList.toImmutableList());
+    }
+
+    private static SaslMechanism adaptMechanism(SaslMechanism mechanism, 
List<AuthHook> authHooks,
+                                                List<HookResultHook> 
hookResultHooks, SMTPSession session) {
+        if (mechanism instanceof LoginSaslMechanism loginSaslMechanism) {
+            return new LoginSaslMechanism(new AuthHookSaslMechanism(
+                loginSaslMechanism.plainMechanism(), authHooks, 
hookResultHooks, session));
+        }
+        if (mechanism.name().equalsIgnoreCase(SaslMechanismNames.PLAIN)) {
+            return new AuthHookSaslMechanism(mechanism, authHooks, 
hookResultHooks, session);
+        }
+        return mechanism;
     }
 
     private final SaslMechanism plainMechanism;
     private final ImmutableList<AuthHook> authHooks;
     private final ImmutableList<HookResultHook> hookResultHooks;
+    private final SMTPSession session;
 
-    AuthHookSaslMechanism(SaslMechanism plainMechanism, List<AuthHook> 
authHooks, List<HookResultHook> hookResultHooks) {
+    AuthHookSaslMechanism(SaslMechanism plainMechanism, List<AuthHook> 
authHooks, List<HookResultHook> hookResultHooks, SMTPSession session) {
         this.plainMechanism = plainMechanism;
         this.authHooks = ImmutableList.copyOf(authHooks);
         this.hookResultHooks = ImmutableList.copyOf(hookResultHooks);
+        this.session = session;
     }
 
     @Override
@@ -78,10 +100,6 @@ class AuthHookSaslMechanism implements SaslMechanism {
 
     @Override
     public SaslExchange start(SaslInitialRequest request, SaslAuthenticator 
authenticator) {
-        throw new IllegalStateException("Legacy SMTP AuthHook adapter requires 
an SMTP session");
-    }
-
-    Exchange start(SaslInitialRequest request, SMTPSession session) {
         return new Exchange(request.initialResponse(), session);
     }
 
diff --git 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanismTest.java
 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanismTest.java
index 5d8265031e..22472544b5 100644
--- 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanismTest.java
+++ 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthHookSaslMechanismTest.java
@@ -31,6 +31,7 @@ import java.util.Optional;
 
 import org.apache.james.core.Username;
 import org.apache.james.protocols.api.Response;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
 import org.apache.james.protocols.api.sasl.SaslFailure;
 import org.apache.james.protocols.api.sasl.SaslIdentity;
 import org.apache.james.protocols.api.sasl.SaslInitialRequest;
@@ -50,6 +51,7 @@ class AuthHookSaslMechanismTest {
     private static final Username USERNAME = Username.of("[email protected]");
 
     private final SMTPSession session = mock(SMTPSession.class);
+    private final SaslAuthenticator authenticator = 
mock(SaslAuthenticator.class);
 
     @Test
     void shouldDelegatePlainAvailabilityToWrappedMechanism() {
@@ -57,7 +59,8 @@ class AuthHookSaslMechanismTest {
         AuthHookSaslMechanism mechanism = new AuthHookSaslMechanism(
             new PlainSaslMechanism(false, true),
             ImmutableList.of(mock(AuthHook.class)),
-            ImmutableList.of());
+            ImmutableList.of(),
+            session);
 
         // WHEN checking whether it is available on either transport
         // THEN the legacy adapter preserves the configured policy
@@ -70,14 +73,16 @@ class AuthHookSaslMechanismTest {
         // GIVEN a legacy hook declining the provided credentials
         AuthHook authHook = mock(AuthHook.class);
         when(authHook.doAuth(any(SMTPSession.class), eq(USERNAME), 
eq("password"))).thenReturn(HookResult.DECLINED);
-        SMTPSession session = mock(SMTPSession.class);
         AuthHookSaslMechanism mechanism = new AuthHookSaslMechanism(
             new PlainSaslMechanism(),
             ImmutableList.of(authHook),
-            ImmutableList.of());
+            ImmutableList.of(),
+            session);
 
         // WHEN authenticating through the adapter
-        SaslStep step = 
mechanism.start(initialRequest("\[email protected]\0password"), 
session).firstStep();
+        SaslStep step = mechanism
+            .start(initialRequest("\[email protected]\0password"), 
authenticator)
+            .firstStep();
 
         // THEN the legacy terminal failure is preserved without a fallback 
login
         assertThat(step).isEqualTo(new 
SaslStep.Failure(SaslFailure.invalidCredentials(
@@ -96,10 +101,13 @@ class AuthHookSaslMechanismTest {
         AuthHookSaslMechanism mechanism = new AuthHookSaslMechanism(
             new PlainSaslMechanism(),
             ImmutableList.of(authHook),
-            ImmutableList.of());
+            ImmutableList.of(),
+            session);
 
         // WHEN authenticating through the adapter
-        SaslStep step = mechanism.start(initialRequest("\[email protected]\0"), 
session).firstStep();
+        SaslStep step = mechanism
+            .start(initialRequest("\[email protected]\0"), authenticator)
+            .firstStep();
 
         // THEN the legacy hook receives the delegation call
         assertThat(step).isEqualTo(new SaslStep.Success(
@@ -120,15 +128,17 @@ class AuthHookSaslMechanismTest {
         AuthHookSaslMechanism mechanism = new AuthHookSaslMechanism(
             new PlainSaslMechanism(),
             ImmutableList.of(authHook),
-            ImmutableList.of());
+            ImmutableList.of(),
+            session);
 
         // WHEN the adapter processes the PLAIN credentials
-        AuthHookSaslMechanism.Exchange exchange = 
mechanism.start(initialRequest("\[email protected]\0password"), session);
+        AuthHookSaslMechanism.Exchange exchange = 
(AuthHookSaslMechanism.Exchange) mechanism
+            .start(initialRequest("\[email protected]\0password"), 
authenticator);
         SaslStep step = exchange.firstStep();
 
         // THEN the SASL failure keeps the legacy SMTP response for the 
protocol driver
         assertThat(step).isInstanceOf(SaslStep.Failure.class);
-        Response response = 
AuthHookSaslMechanism.terminalResponse(exchange).orElseThrow();
+        Response response = exchange.terminalResponse().orElseThrow();
         assertThat(response.getRetCode()).isEqualTo("421");
         assertThat(response.getLines()).containsExactly("421 Too many 
authentication attempts");
         assertThat(response.isEndSession()).isTrue();
@@ -146,15 +156,17 @@ class AuthHookSaslMechanismTest {
         AuthHookSaslMechanism mechanism = new AuthHookSaslMechanism(
             new PlainSaslMechanism(),
             ImmutableList.of(authHook),
-            ImmutableList.of());
+            ImmutableList.of(),
+            session);
 
         // WHEN the adapter processes the PLAIN credentials
-        AuthHookSaslMechanism.Exchange exchange = 
mechanism.start(initialRequest("\[email protected]\0password"), session);
+        AuthHookSaslMechanism.Exchange exchange = 
(AuthHookSaslMechanism.Exchange) mechanism
+            .start(initialRequest("\[email protected]\0password"), 
authenticator);
         SaslStep step = exchange.firstStep();
 
         // THEN SMTP success keeps the connection open
         assertThat(step).isInstanceOf(SaslStep.Success.class);
-        
assertThat(AuthHookSaslMechanism.terminalResponse(exchange).orElseThrow().isEndSession()).isFalse();
+        
assertThat(exchange.terminalResponse().orElseThrow().isEndSession()).isFalse();
     }
 
     private SaslInitialRequest initialRequest(String value) {


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

Reply via email to