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


The following commit(s) were added to refs/heads/master by this push:
     new b77b67e8f3 [ENHANCEMENT] Enable Smtp technical accounts to optionally 
send as users (#3140)
b77b67e8f3 is described below

commit b77b67e8f3455c90a8513128c4bfd24184ce2322
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri Aug 28 09:27:06 2026 +0700

    [ENHANCEMENT] Enable Smtp technical accounts to optionally send as users 
(#3140)
---
 .../apache/james/protocols/smtp/SMTPSession.java   | 14 ++++
 ...AbstractSenderAuthIdentifyVerificationHook.java |  6 ++
 .../james/smtpserver/ConfigurationAuthHook.java    | 75 ++++++++++-------
 .../SenderAuthIdentifyVerificationHook.java        |  3 +
 .../ConfiguredAuthOtherIdentityTest.java           | 93 ++++++++++++++++++++++
 .../smtpserver-configured-auth-other-identity.xml  | 73 +++++++++++++++++
 src/site/xdoc/server/config-smtp-lmtp.xml          |  5 ++
 7 files changed, 239 insertions(+), 30 deletions(-)

diff --git 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java
index dfdefa0f7e..a4b540d632 100644
--- 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java
+++ 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java
@@ -44,6 +44,20 @@ public interface SMTPSession extends ProtocolSession {
     /** HELO or EHLO */
     AttachmentKey<String> CURRENT_HELO_MODE = 
AttachmentKey.of("CURRENT_HELO_MODE", String.class);
     AttachmentKey<String> CURRENT_HELO_NAME = 
AttachmentKey.of("CURRENT_HELO_NAME", String.class);
+    /** Set when the authenticated account was granted the right to use 
identities other than its own */
+    AttachmentKey<Boolean> ALLOW_USE_OTHER_IDENTITY = 
AttachmentKey.of("ALLOW_USE_OTHER_IDENTITY", Boolean.class);
+
+    /**
+     * Whether this session is allowed to use MAIL FROM / From identities 
other than the one it authenticated with.
+     *
+     * Set upon authentication for accounts explicitly granted that right, 
this bypasses the identity checks
+     * performed by the sender identity verification hook (see the 
<code>verifyIdentity</code> setting).
+     *
+     * @return true if identity verification is to be bypassed for this session
+     */
+    default boolean allowUseOtherIdentity() {
+        return getAttachment(ALLOW_USE_OTHER_IDENTITY, 
State.Connection).orElse(false);
+    }
 
     /**
      * Returns the service wide configuration
diff --git 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
index c5937e3548..e2488925c8 100644
--- 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
+++ 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
@@ -88,6 +88,9 @@ public abstract class 
AbstractSenderAuthIdentifyVerificationHook implements Mail
     
     @Override
     public HookResult doMail(SMTPSession session, MaybeSender sender) {
+        if (session.allowUseOtherIdentity()) {
+            return HookResult.DECLINED;
+        }
         return doCheck(session, sender);
     }
 
@@ -97,6 +100,9 @@ public abstract class 
AbstractSenderAuthIdentifyVerificationHook implements Mail
      */
     @Override
     public HookResult doRcpt(SMTPSession session, MaybeSender sender, 
MailAddress rcpt) {
+        if (session.allowUseOtherIdentity()) {
+            return HookResult.DECLINED;
+        }
         return doCheck(session, sender);
     }
 
diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/ConfigurationAuthHook.java
 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/ConfigurationAuthHook.java
index 22e575ddff..1d2788418f 100644
--- 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/ConfigurationAuthHook.java
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/ConfigurationAuthHook.java
@@ -30,6 +30,7 @@ import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.commons.lang3.NotImplementedException;
 import org.apache.james.core.Username;
 import org.apache.james.jwt.OidcSASLConfiguration;
+import org.apache.james.protocols.api.ProtocolSession;
 import org.apache.james.protocols.smtp.SMTPSession;
 import org.apache.james.protocols.smtp.hook.AuthHook;
 import org.apache.james.protocols.smtp.hook.HookResult;
@@ -38,12 +39,16 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.Multimap;
 
 /**
  * Declarative authentication.
  *
+ * <p>Each {@code account} supports an optional {@code allowUseOtherIdentity} 
flag (defaults to {@code false}).
+ * When set, the authenticated session bypasses the {@code verifyIdentity} 
checks and may thus use any
+ * MAIL FROM / From identity. This is intended for application accounts 
sending on behalf of end users
+ * (calendar invitations, notifications...), and should be granted only to 
accounts whose credentials are
+ * under the operator control.</p>
+ *
  * @deprecated Prefer implementing a SASL mechanism factory. Existing 
handler-chain registrations
  * are adapted by the SMTP AUTH handler during migration.
  */
@@ -51,7 +56,13 @@ import com.google.common.collect.Multimap;
 public class ConfigurationAuthHook implements AuthHook {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ConfigurationAuthHook.class);
 
-    private Multimap<Username, String> accounts = ImmutableListMultimap.of();
+    private record Account(Username username, List<String> passwords, boolean 
allowUseOtherIdentity) {
+        boolean matches(Username username, String password) {
+            return this.username.equals(username) && 
passwords.stream().anyMatch(password::equals);
+        }
+    }
+
+    private List<Account> accounts = ImmutableList.of();
 
     @Inject
     public ConfigurationAuthHook() {
@@ -62,40 +73,44 @@ public class ConfigurationAuthHook implements AuthHook {
     public void init(Configuration config) throws ConfigurationException {
         HierarchicalConfiguration<ImmutableNode> hierarchicalConfiguration = 
(HierarchicalConfiguration<ImmutableNode>) config;
 
-        ImmutableListMultimap.Builder<Username, String> builder = 
ImmutableListMultimap.builder();
-
-        for (HierarchicalConfiguration<ImmutableNode> accountNode : 
hierarchicalConfiguration.configurationAt("accounts")
-            .configurationsAt("account")) {
-            String username = accountNode.getString("username");
-            if (username != null) {
-                List<String> passwords = accountNode.getList(String.class, 
"passwords.password");
-                passwords.forEach(pw -> builder.put(Username.of(username), 
pw));
-            }
-        }
-        this.accounts = builder.build();
-
-        LOGGER.info("SMTP authentication enabled from configuration for users: 
{}", accounts.keySet()
+        this.accounts = hierarchicalConfiguration.configurationAt("accounts")
+            .configurationsAt("account")
             .stream()
-            .map(Username::asString)
+            .flatMap(accountNode -> parseAccount(accountNode).stream())
+            .collect(ImmutableList.toImmutableList());
+
+        LOGGER.info("SMTP authentication enabled from configuration for users: 
{}", accounts.stream()
+            .map(account -> account.username().asString())
             .collect(ImmutableList.toImmutableList()));
     }
 
+    private Optional<Account> 
parseAccount(HierarchicalConfiguration<ImmutableNode> accountNode) {
+        return Optional.ofNullable(accountNode.getString("username"))
+            .map(username -> new Account(Username.of(username),
+                accountNode.getList(String.class, "passwords.password", 
ImmutableList.of()),
+                accountNode.getBoolean("allowUseOtherIdentity", false)));
+    }
+
     @Override
     public HookResult doAuth(SMTPSession session, Username username, String 
password) {
-        Optional<Username> loggedInUser = 
Optional.ofNullable(accounts.get(username))
-            .filter(allowedsPass -> 
allowedsPass.stream().anyMatch(password::equals))
-            .map(any -> username);
-
-        if (loggedInUser.isPresent()) {
-            session.setUsername(loggedInUser.get());
-            session.setRelayingAllowed(true);
-
-            return HookResult.builder()
-                .hookReturnCode(HookReturnCode.ok())
-                .smtpDescription("Authentication Successful")
-                .build();
+        return accounts.stream()
+            .filter(account -> account.matches(username, password))
+            .findFirst()
+            .map(account -> authenticate(session, account))
+            .orElse(HookResult.DECLINED);
+    }
+
+    private HookResult authenticate(SMTPSession session, Account account) {
+        session.setUsername(account.username());
+        session.setRelayingAllowed(true);
+        if (account.allowUseOtherIdentity()) {
+            session.setAttachment(SMTPSession.ALLOW_USE_OTHER_IDENTITY, true, 
ProtocolSession.State.Connection);
         }
-        return HookResult.DECLINED;
+
+        return HookResult.builder()
+            .hookReturnCode(HookReturnCode.ok())
+            .smtpDescription("Authentication Successful")
+            .build();
     }
 
     @Override
diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
index 300b79bc25..d5d9e956b7 100644
--- 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
@@ -139,6 +139,9 @@ public class SenderAuthIdentifyVerificationHook extends 
AbstractSenderAuthIdenti
 
     @Override
     public HookResult onMessage(SMTPSession session, Mail mail) {
+        if (session.allowUseOtherIdentity()) {
+            return HookResult.DECLINED;
+        }
         ExtendedSMTPSession nSession = (ExtendedSMTPSession) session;
         boolean shouldCheck = 
nSession.senderVerificationConfiguration().mode() == 
SMTPConfiguration.SenderVerificationMode.STRICT ||
             (nSession.senderVerificationConfiguration().mode() == 
SMTPConfiguration.SenderVerificationMode.RELAXED && session.getUsername() != 
null);
diff --git 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ConfiguredAuthOtherIdentityTest.java
 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ConfiguredAuthOtherIdentityTest.java
new file mode 100644
index 0000000000..682d631d21
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ConfiguredAuthOtherIdentityTest.java
@@ -0,0 +1,93 @@
+/****************************************************************
+ * 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.smtpserver;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.InetSocketAddress;
+import java.util.Base64;
+
+import org.apache.commons.net.smtp.SMTPClient;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ConfiguredAuthOtherIdentityTest {
+    private static final String END_USER = "[email protected]";
+
+    private final SMTPServerTestSystem smtpServerTestSystem = new 
SMTPServerTestSystem();
+
+    @BeforeEach
+    void setUp() throws Exception {
+        
smtpServerTestSystem.setUp("smtpserver-configured-auth-other-identity.xml");
+    }
+
+    @AfterEach
+    void tearDown() {
+        smtpServerTestSystem.smtpServer.destroy();
+    }
+
+    private SMTPClient authenticate(String username, String password) throws 
Exception {
+        SMTPClient smtpProtocol = new SMTPClient();
+        InetSocketAddress bindedAddress = 
smtpServerTestSystem.getBindedAddress();
+        smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), 
bindedAddress.getPort());
+
+        smtpProtocol.sendCommand("AUTH PLAIN");
+        smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + 
username + "\0" + password + "\0").getBytes(UTF_8)));
+        assertThat(smtpProtocol.getReplyCode())
+            .as("authenticated")
+            .isEqualTo(235);
+        smtpProtocol.login("domain.tld");
+        return smtpProtocol;
+    }
+
+    @Test
+    void mailFromOtherIdentityShouldBeRejectedWhenNotAllowed() throws 
Exception {
+        SMTPClient smtpProtocol = authenticate("[email protected]", 
"secret123456");
+
+        smtpProtocol.setSender(END_USER);
+
+        assertThat(smtpProtocol.getReplyCode())
+            .isEqualTo(503);
+    }
+
+    @Test
+    void mailFromOtherIdentityShouldBeAcceptedWhenAllowed() throws Exception {
+        SMTPClient smtpProtocol = authenticate("[email protected]", 
"secret234567");
+
+        smtpProtocol.setSender(END_USER);
+
+        assertThat(smtpProtocol.getReplyCode())
+            .isEqualTo(250);
+    }
+
+    @Test
+    void headerFromOtherIdentityShouldBeAcceptedWhenAllowed() throws Exception 
{
+        SMTPClient smtpProtocol = authenticate("[email protected]", 
"secret234567");
+
+        smtpProtocol.setSender(END_USER);
+        smtpProtocol.addRecipient("[email protected]");
+        smtpProtocol.sendShortMessageData("From: " + END_USER + "\r\nSubject: 
test\r\n\r\nTest body\r\n.\r\n");
+        smtpProtocol.quit();
+
+        
assertThat(smtpServerTestSystem.queue.getLastMail().getMaybeSender().asString())
+            .isEqualTo(END_USER);
+    }
+}
diff --git 
a/server/protocols/protocols-smtp/src/test/resources/smtpserver-configured-auth-other-identity.xml
 
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-configured-auth-other-identity.xml
new file mode 100644
index 0000000000..1ed6feb6bf
--- /dev/null
+++ 
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-configured-auth-other-identity.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+
+<!--
+  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.
+ -->
+
+<!-- Read 
https://james.apache.org/server/config-smtp-lmtp.html#SMTP_Configuration for 
further details -->
+
+<smtpserver enabled="true">
+    <bind>0.0.0.0:0</bind>
+    <connectionBacklog>200</connectionBacklog>
+    <tls socketTLS="false" startTLS="false">
+        <keystore>file://conf/keystore</keystore>
+        <secret>james72laBalle</secret>
+        <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+        <algorithm>SunX509</algorithm>
+    </tls>
+    <connectiontimeout>360</connectiontimeout>
+    <connectionLimit>0</connectionLimit>
+    <connectionLimitPerIP>0</connectionLimitPerIP>
+    <auth>
+        <announce>forUnauthorizedAddresses</announce>
+        <requireSSL>false</requireSSL>
+    </auth>
+    <verifyIdentity>strict</verifyIdentity>
+    <maxmessagesize>0</maxmessagesize>
+    <addressBracketsEnforcement>true</addressBracketsEnforcement>
+    <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting>
+    <handlerchain 
coreHandlersPackage="org.apache.james.smtpserver.NoAuthCmdHandlerLoader" 
enableJmx="false">
+        <handler 
class="org.apache.james.protocols.smtp.core.esmtp.AuthCmdHandler" />
+        <handler 
class="org.apache.james.smtpserver.SetMailAttributeMessageHook" >
+            <name>technicaluser</name>
+            <value>true</value>
+        </handler>
+        <handler class="org.apache.james.smtpserver.ConfigurationAuthHook" >
+            <accounts>
+                <account>
+                    <username>[email protected]</username>
+                    <passwords>
+                        <password>secret123456</password>
+                        <password>here_to_ease_secret_rotation</password>
+                        
<password>here_to_give_different_creds_to_each_app</password>
+                    </passwords>
+                </account>
+                <!-- Allowed to send on behalf of end users: verifyIdentity is 
bypassed for this account. -->
+                <account>
+                    <username>[email protected]</username>
+                    <passwords>
+                        <password>secret234567</password>
+                    </passwords>
+                    <allowUseOtherIdentity>true</allowUseOtherIdentity>
+                </account>
+            </accounts>
+        </handler>
+    </handlerchain>
+    <gracefulShutdown>false</gracefulShutdown>
+    <disabledFeatures>ENHANCEDSTATUSCODES</disabledFeatures>
+</smtpserver>
\ No newline at end of file
diff --git a/src/site/xdoc/server/config-smtp-lmtp.xml 
b/src/site/xdoc/server/config-smtp-lmtp.xml
index d6b632734a..5d85cb3921 100644
--- a/src/site/xdoc/server/config-smtp-lmtp.xml
+++ b/src/site/xdoc/server/config-smtp-lmtp.xml
@@ -184,6 +184,11 @@
               <li><code>true</code>: act as <code>strict</code></li>
               <li><code>false</code>: act as <code>disabled</code></li>
           </ul>
+
+          Note that individual accounts declared on the 
<code>ConfigurationAuthHook</code> handler can be granted
+          <code>allowUseOtherIdentity</code> (defaults to <code>false</code>), 
which bypasses these checks for the
+          sessions they authenticate. This allows application accounts to send 
on behalf of end users
+          (calendar invitations, notifications...) without turning identity 
verification off for everybody else.
       </dd>
       <dt><strong>handler.maxmessagesize</strong></dt>
       <dd>This is an optional tag with a non-negative integer body.  It 
specifies the maximum 


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

Reply via email to