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]