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 bc6b4e16c50cdfc6c24fa63a7663a42bca03f4d3
Author: Quan Tran <[email protected]>
AuthorDate: Wed Jun 24 13:53:34 2026 +0700

    JAMES-4210 Resolve SMTP SASL mechanisms per server configuration
    
    Wire SMTP Guice support for per-server SASL mechanism resolution, default 
SMTP SASL factories, probe cleanup, and module tests.
---
 server/container/guice/protocols/smtp/pom.xml      |  4 ++
 .../james/modules/protocols/SMTPServerModule.java  | 82 +++++++++++++++++++++-
 .../SmtpDefaultSaslMechanismFactories.java         | 31 ++++++++
 .../james/modules/protocols/SmtpGuiceProbe.java    |  7 ++
 .../modules/protocols/SMTPServerModuleTest.java    | 72 +++++++++++++++++++
 5 files changed, 194 insertions(+), 2 deletions(-)

diff --git a/server/container/guice/protocols/smtp/pom.xml 
b/server/container/guice/protocols/smtp/pom.xml
index 101df093cc..a58b6c5aaa 100644
--- a/server/container/guice/protocols/smtp/pom.xml
+++ b/server/container/guice/protocols/smtp/pom.xml
@@ -49,6 +49,10 @@
             <artifactId>testing-base</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-sasl</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.google.inject</groupId>
             <artifactId>guice</artifactId>
diff --git 
a/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SMTPServerModule.java
 
b/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SMTPServerModule.java
index b44afd9d81..d74147d30b 100644
--- 
a/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SMTPServerModule.java
+++ 
b/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SMTPServerModule.java
@@ -19,23 +19,46 @@
 
 package org.apache.james.modules.protocols;
 
+import java.util.Arrays;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.james.ProtocolConfigurationSanitizer;
 import org.apache.james.RunArguments;
 import org.apache.james.core.ConnectionDescriptionSupplier;
 import org.apache.james.core.Disconnector;
+import org.apache.james.dnsservice.api.DNSService;
 import org.apache.james.filesystem.api.FileSystem;
 import org.apache.james.lifecycle.api.ConfigurationSanitizer;
+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.SaslMechanismFactory;
+import org.apache.james.protocols.lib.handler.ProtocolHandlerLoader;
 import org.apache.james.protocols.lib.netty.CertificateReloadable;
+import org.apache.james.protocols.netty.Encryption;
+import org.apache.james.protocols.sasl.BuiltInSaslMechanismFactories;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
+import org.apache.james.protocols.sasl.OauthBearerSaslMechanismFactory;
+import org.apache.james.protocols.sasl.PlainSaslMechanismFactory;
+import org.apache.james.protocols.sasl.XOauth2SaslMechanismFactory;
 import org.apache.james.server.core.configuration.ConfigurationProvider;
 import org.apache.james.smtpserver.SendMailHandler;
+import 
org.apache.james.smtpserver.netty.SMTPServer.AuthAnnouncementConfiguration;
 import org.apache.james.smtpserver.netty.SMTPServerFactory;
+import 
org.apache.james.smtpserver.netty.SMTPServerFactory.SmtpSaslMechanismLoader;
 import org.apache.james.utils.GuiceProbe;
+import org.apache.james.utils.GuiceSaslMechanismResolver;
 import org.apache.james.utils.InitializationOperation;
 import org.apache.james.utils.InitilizationOperationBuilder;
 import org.apache.james.utils.KeystoreCreator;
 
+import com.google.common.collect.ImmutableList;
 import com.google.inject.AbstractModule;
-import com.google.inject.Scopes;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import com.google.inject.multibindings.Multibinder;
 import com.google.inject.multibindings.ProvidesIntoSet;
 
@@ -43,7 +66,7 @@ public class SMTPServerModule extends AbstractModule {
     @Override
     protected void configure() {
         install(new JSPFModule());
-        bind(SMTPServerFactory.class).in(Scopes.SINGLETON);
+        bind(SaslAuthenticator.class).to(JamesSaslAuthenticator.class);
 
         Multibinder.newSetBinder(binder(), 
GuiceProbe.class).addBinding().to(SmtpGuiceProbe.class);
 
@@ -52,6 +75,61 @@ public class SMTPServerModule extends AbstractModule {
         Multibinder.newSetBinder(binder(), 
ConnectionDescriptionSupplier.class).addBinding().to(SMTPServerFactory.class);
     }
 
+    @Provides
+    @Singleton
+    SMTPServerFactory provideSmtpServerFactory(DNSService dns,
+                                               ProtocolHandlerLoader 
protocolHandlerLoader,
+                                               FileSystem fileSystem,
+                                               MetricFactory metricFactory,
+                                               SmtpSaslMechanismLoader 
saslMechanismLoader,
+                                               SaslAuthenticator 
saslAuthenticator,
+                                               Encryption.Factory 
encryptionFactory) {
+        SMTPServerFactory smtpServerFactory = new SMTPServerFactory(dns, 
protocolHandlerLoader, fileSystem,
+            metricFactory, saslMechanismLoader, saslAuthenticator);
+        smtpServerFactory.setEncryptionFactory(encryptionFactory);
+        return smtpServerFactory;
+    }
+
+    @Provides
+    @Singleton
+    @SmtpDefaultSaslMechanismFactories
+    ImmutableList<SaslMechanismFactory> 
provideDefaultSmtpSaslMechanismFactories(OauthBearerSaslMechanismFactory 
oauthBearer,
+                                                                               
  XOauth2SaslMechanismFactory xoauth2) {
+        return ImmutableList.of(new 
PlainSaslMechanismFactory(AuthAnnouncementConfiguration.REQUIRE_SSL_DEFAULT), 
oauthBearer, xoauth2);
+    }
+
+    @Provides
+    @Singleton
+    SmtpSaslMechanismLoader 
provideSmtpSaslMechanismLoader(GuiceSaslMechanismResolver saslMechanismResolver,
+                                                           
@SmtpDefaultSaslMechanismFactories ImmutableList<SaslMechanismFactory> 
defaultSaslMechanismFactories) {
+        return configuration -> retrieveSaslMechanisms(saslMechanismResolver, 
defaultSaslMechanismFactories, configuration);
+    }
+
+    private ImmutableList<SaslMechanism> 
retrieveSaslMechanisms(GuiceSaslMechanismResolver saslMechanismResolver,
+                                                                
ImmutableList<SaslMechanismFactory> defaultSaslMechanismFactories,
+                                                                
HierarchicalConfiguration<ImmutableNode> configuration) throws 
ConfigurationException {
+        ImmutableList<String> mechanismFactoryClassNames = 
retrieveSaslMechanismFactoryClassNames(configuration);
+        ImmutableList<SaslMechanismFactory> enabledDefaultFactories =
+            
BuiltInSaslMechanismFactories.enabledForServer(defaultSaslMechanismFactories, 
configuration);
+        return saslMechanismResolver.resolve(mechanismFactoryClassNames, 
enabledDefaultFactories, configuration);
+    }
+
+    ImmutableList<String> 
retrieveSaslMechanismFactoryClassNames(HierarchicalConfiguration<ImmutableNode> 
configuration) throws ConfigurationException {
+        if (!configuration.containsKey("auth.saslMechanisms")) {
+            return ImmutableList.of();
+        }
+
+        ImmutableList<String> mechanismFactoryClassNames = 
Arrays.stream(configuration.getStringArray("auth.saslMechanisms"))
+            .flatMap(value -> Arrays.stream(value.split(",")))
+            .map(String::trim)
+            .collect(ImmutableList.toImmutableList());
+
+        if (mechanismFactoryClassNames.isEmpty() || 
mechanismFactoryClassNames.stream().anyMatch(StringUtils::isBlank)) {
+            throw new ConfigurationException("auth.saslMechanisms must not be 
blank when configured");
+        }
+        return mechanismFactoryClassNames;
+    }
+
     @ProvidesIntoSet
     InitializationOperation configureSmtp(ConfigurationProvider 
configurationProvider,
                                         SMTPServerFactory smtpServerFactory,
diff --git 
a/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpDefaultSaslMechanismFactories.java
 
b/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpDefaultSaslMechanismFactories.java
new file mode 100644
index 0000000000..a6cea4fd32
--- /dev/null
+++ 
b/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpDefaultSaslMechanismFactories.java
@@ -0,0 +1,31 @@
+/****************************************************************
+ * 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.modules.protocols;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.google.inject.BindingAnnotation;
+
+@BindingAnnotation
+@Retention(RUNTIME)
+public @interface SmtpDefaultSaslMechanismFactories {
+}
diff --git 
a/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpGuiceProbe.java
 
b/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpGuiceProbe.java
index 9c67ced130..e96153afbc 100644
--- 
a/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpGuiceProbe.java
+++ 
b/server/container/guice/protocols/smtp/src/main/java/org/apache/james/modules/protocols/SmtpGuiceProbe.java
@@ -24,6 +24,7 @@ import java.net.InetSocketAddress;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import jakarta.annotation.PreDestroy;
 import jakarta.inject.Inject;
 
 import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
@@ -56,6 +57,12 @@ public class SmtpGuiceProbe implements GuiceProbe {
         this.smtpServerFactory = smtpServerFactory;
     }
 
+    @PreDestroy
+    void destroy() {
+        // SMTPServerFactory is provided through a factory method; dispose it 
explicitly on Guice shutdown.
+        smtpServerFactory.destroy();
+    }
+
     public Port getSmtpPort() {
         return getPort(server -> true);
     }
diff --git 
a/server/container/guice/protocols/smtp/src/test/java/org/apache/james/modules/protocols/SMTPServerModuleTest.java
 
b/server/container/guice/protocols/smtp/src/test/java/org/apache/james/modules/protocols/SMTPServerModuleTest.java
new file mode 100644
index 0000000000..f88e3d3f0d
--- /dev/null
+++ 
b/server/container/guice/protocols/smtp/src/test/java/org/apache/james/modules/protocols/SMTPServerModuleTest.java
@@ -0,0 +1,72 @@
+/****************************************************************
+ * 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.modules.protocols;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+class SMTPServerModuleTest {
+    private final SMTPServerModule testee = new SMTPServerModule();
+
+    @Test
+    void retrieveSaslMechanismFactoryClassNamesShouldReturnEmptyWhenAbsent() 
throws Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+
+        ImmutableList<String> mechanismFactoryClassNames = 
testee.retrieveSaslMechanismFactoryClassNames(configuration);
+
+        assertThat(mechanismFactoryClassNames).isEmpty();
+    }
+
+    @Test
+    void 
retrieveSaslMechanismFactoryClassNamesShouldReturnConfiguredSaslFactoryList() 
throws Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms",
+            
"PlainSaslMechanismFactory,com.example.CustomSaslMechanismFactory,PlainSaslMechanismFactory");
+
+        ImmutableList<String> mechanismFactoryClassNames = 
testee.retrieveSaslMechanismFactoryClassNames(configuration);
+
+        assertThat(mechanismFactoryClassNames)
+            .containsExactly("PlainSaslMechanismFactory", 
"com.example.CustomSaslMechanismFactory", "PlainSaslMechanismFactory");
+    }
+
+    @Test
+    void 
retrieveSaslMechanismFactoryClassNamesShouldRejectBlankConfiguredList() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms", " ");
+
+        assertThatThrownBy(() -> 
testee.retrieveSaslMechanismFactoryClassNames(configuration))
+            .isInstanceOf(ConfigurationException.class);
+    }
+
+    @Test
+    void retrieveSaslMechanismFactoryClassNamesShouldRejectBlankEntry() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms", 
"PlainSaslMechanismFactory,,XOauth2SaslMechanismFactory");
+
+        assertThatThrownBy(() -> 
testee.retrieveSaslMechanismFactoryClassNames(configuration))
+            .isInstanceOf(ConfigurationException.class);
+    }
+}


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

Reply via email to