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

btellier 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 c8d8456987 JAMES-4153 DKIMSign: signature template: allow for 
interpolating domain (#2861)
c8d8456987 is described below

commit c8d8456987996e7ed99fd52a982b41ed7d7b5c77
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Nov 24 08:56:32 2025 +0100

    JAMES-4153 DKIMSign: signature template: allow for interpolating domain 
(#2861)
---
 docs/modules/servers/partials/DKIMSign.adoc        |  18 +++
 .../org/apache/james/jdkim/mailets/DKIMSign.java   |  54 ++++++--
 .../apache/james/jdkim/mailets/DKIMSignTest.java   | 152 +++++++++++++++++++++
 3 files changed, 213 insertions(+), 11 deletions(-)

diff --git a/docs/modules/servers/partials/DKIMSign.adoc 
b/docs/modules/servers/partials/DKIMSign.adoc
index 4888ee45bc..33a3f419d6 100644
--- a/docs/modules/servers/partials/DKIMSign.adoc
+++ b/docs/modules/servers/partials/DKIMSign.adoc
@@ -38,6 +38,24 @@ Sample configuration with file-provided private key:
 </mailet>
 ....
 
+Sample configuration with domain interpolation for multi-domain support:
+
+....
+<mailet match=&quot;All&quot; class=&quot;DKIMSign&quot;>
+  <signatureTemplate>v=1; s=selector; d=%MAIL_FROM; 
h=from:to:received:received; a=rsa-sha256; bh=; b=;</signatureTemplate>
+  <privateKeyFilepath>dkim-signing.pem</privateKeyFilepath>
+  <defaultDomain>example.com</defaultDomain>
+</mailet>
+....
+
+Domain interpolation allows dynamic DKIM signing for multiple domains using a 
single configuration.
+The `%MAIL_FROM` placeholder in the signatureTemplate will be replaced with:
+
+1. The domain from the sender's email address (from `mail.getSender()`), or
+2. The `defaultDomain` if no sender is present or the sender has no domain
+
+This enables DMARC alignment for multi-domain deployments with a single DKIM 
private key.
+
 By default the mailet assume that Javamail will convert LF to CRLF when sending
 so will compute the hash using converted newlines. If you don't want this
 behaviour then set forceCRLF attribute to false.
\ No newline at end of file
diff --git 
a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java 
b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java
index 6753afdf96..096f664458 100644
--- 
a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java
+++ 
b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java
@@ -98,29 +98,41 @@ import com.github.fge.lambdas.Throwing;
  * &lt;/mailet&gt;
  * </code></pre>
  *
+ * Sample configuration with domain interpolation for multi-domain support:
+ *
+ * <pre><code>
+ * &lt;mailet match=&quot;All&quot; class=&quot;DKIMSign&quot;&gt;
+ *   &lt;signatureTemplate&gt;v=1; s=selector; d=%MAIL_FROM; 
h=from:to:received:received; a=rsa-sha256; bh=; b=;&lt;/signatureTemplate&gt;
+ *   
&lt;privateKeyFilepath&gt;conf://dkim-signing.pem&lt;/privateKeyFilepath&gt;
+ *   &lt;defaultDomain&gt;example.com&lt;/defaultDomain&gt;
+ * &lt;/mailet&gt;
+ * </code></pre>
+ *
+ * Domain interpolation allows dynamic DKIM signing for multiple domains using 
a single configuration.
+ * The %MAIL_FROM placeholder in the signatureTemplate will be replaced with:
+ * 1. The domain from the sender's email address (from mail.getSender()), or
+ * 2. The defaultDomain if no sender is present or the sender has no domain
+ *
+ * This enables DMARC alignment for multi-domain deployments with a single 
DKIM private key.
+ *
  * By default the mailet assume that Javamail will convert LF to CRLF when 
sending
  * so will compute the hash using converted newlines. If you don't want this
  * behaviour then set forceCRLF attribute to false.
  */
 public class DKIMSign extends GenericMailet {
 
+    public static final String MAIL_FROM_INTERPOLATION_PATTERN = "%MAIL_FROM";
     private final FileSystem fileSystem;
     private String signatureTemplate;
     private PrivateKey privateKey;
     private boolean forceCRLF;
+    private Optional<String> defaultDomain;
 
     @Inject
     public DKIMSign(FileSystem fileSystem) {
         this.fileSystem = fileSystem;
     }
 
-    /**
-     * @return the signatureTemplate
-     */
-    private String getSignatureTemplate() {
-        return signatureTemplate;
-    }
-
     /**
      * @return the privateKey
      */
@@ -132,13 +144,18 @@ public class DKIMSign extends GenericMailet {
         signatureTemplate = getInitParameter("signatureTemplate");
         Optional<String> privateKeyPassword = 
getInitParameterAsOptional("privateKeyPassword");
         forceCRLF = getInitParameter("forceCRLF", true);
+        defaultDomain = getInitParameterAsOptional("defaultDomain");
+
+        if (signatureTemplate.contains(MAIL_FROM_INTERPOLATION_PATTERN) && 
!defaultDomain.isPresent()) {
+            throw new IllegalStateException("signatureTemplate contains 
%MAIL_FROM placeholder but defaultDomain is not configured");
+        }
 
         try {
             char[] passphrase = 
privateKeyPassword.map(String::toCharArray).orElse(null);
             InputStream pem = getInitParameterAsOptional("privateKey")
                 .map(String::getBytes)
                 .map(ByteArrayInputStream::new)
-                .map(byteArrayInputStream -> (InputStream) 
byteArrayInputStream)
+                .map(InputStream.class::cast)
                 .orElseGet(Throwing.supplier(() -> 
fileSystem.getResource(getInitParameter("privateKeyFilepath"))).sneakyThrow());
 
             privateKey = extractPrivateKey(pem, passphrase);
@@ -152,9 +169,9 @@ public class DKIMSign extends GenericMailet {
     }
 
     public void service(Mail mail) throws MessagingException {
-        DKIMSigner signer = new DKIMSigner(getSignatureTemplate(), 
getPrivateKey());
-        SignatureRecord signRecord = signer
-                .newSignatureRecordTemplate(getSignatureTemplate());
+        String interpolatedTemplate = interpolateTemplate(mail);
+        DKIMSigner signer = new DKIMSigner(interpolatedTemplate, 
getPrivateKey());
+        SignatureRecord signRecord = 
signer.newSignatureRecordTemplate(interpolatedTemplate);
         try {
             BodyHasher bhj = signer.newBodyHasher(signRecord);
             MimeMessage message = mail.getMessage();
@@ -187,6 +204,21 @@ public class DKIMSign extends GenericMailet {
 
     }
 
+    private String interpolateTemplate(Mail mail) {
+        if (signatureTemplate.contains(MAIL_FROM_INTERPOLATION_PATTERN)) {
+            String domain = extractDomainFromSender(mail)
+                .orElseGet(() -> defaultDomain.orElse(""));
+            return signatureTemplate.replace(MAIL_FROM_INTERPOLATION_PATTERN, 
domain);
+        }
+        return signatureTemplate;
+    }
+
+    private Optional<String> extractDomainFromSender(Mail mail) {
+        return mail.getMaybeSender()
+            .asOptional()
+            .map(mailAddress -> mailAddress.getDomain().asString());
+    }
+
     private void prependHeader(MimeMessage message, String signatureHeader)
             throws MessagingException {
         List<String> prevHeader = 
Collections.list(message.getAllHeaderLines());
diff --git 
a/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java
 
b/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java
index 41535a167d..8d7a4114b5 100644
--- 
a/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java
+++ 
b/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java
@@ -20,6 +20,7 @@
 package org.apache.james.jdkim.mailets;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Fail.fail;
 
 import java.io.ByteArrayInputStream;
@@ -35,6 +36,7 @@ import jakarta.mail.internet.InternetAddress;
 import jakarta.mail.internet.MimeMessage;
 import jakarta.mail.internet.MimeMessage.RecipientType;
 
+import org.apache.james.core.MailAddress;
 import org.apache.james.filesystem.api.FileSystem;
 import org.apache.james.jdkim.api.SignatureRecord;
 import org.apache.james.jdkim.exceptions.FailException;
@@ -47,6 +49,7 @@ import org.apache.mailet.base.test.FakeMail;
 import org.apache.mailet.base.test.FakeMailContext;
 import org.apache.mailet.base.test.FakeMailetConfig;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
@@ -359,4 +362,153 @@ class DKIMSignTest {
         }
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {PKCS1_PEM_FILE, PKCS8_PEM_FILE})
+    void testDKIMSignWithDomainInterpolation(String pemFile) throws Exception {
+        String message = "Received: by 10.XX.XX.12 with SMTP id 
dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 
(PDT)\r\nReturn-Path: <[email protected]>\r\nReceived: from example.co.uk 
(example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id 
dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 
(PDT)\r\nFrom: [email protected]\r\nTo: 
[email protected]\r\n\r\nbody\r\nline1\r\nline2\r\n";
+
+        Mailet mailet = new DKIMSign(fileSystem);
+
+        FakeMailetConfig mci = FakeMailetConfig.builder()
+                .mailetName("Test")
+                .mailetContext(FAKE_MAIL_CONTEXT)
+                .setProperty(
+                        "signatureTemplate",
+                        "v=1; s=selector; d=%MAIL_FROM; 
h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+                .setProperty("defaultDomain", "linagora.com")
+                .setProperty("privateKeyFilepath", pemFile)
+                .build();
+
+        mailet.init(mci);
+
+        Mail mail = FakeMail.builder()
+            .name("test")
+            .sender(new MailAddress("[email protected]"))
+            .mimeMessage(new MimeMessage(Session
+                .getDefaultInstance(new Properties()),
+                new ByteArrayInputStream(message.getBytes())))
+            .build();
+
+        mailet.service(mail);
+
+        ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+        mail.getMessage().writeTo(rawMessage);
+
+        MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new 
MockPublicKeyRecordRetriever(
+                "v=DKIM1; k=rsa; 
p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+                "selector", "domain1.com");
+        verify(rawMessage, mockPublicKeyRecordRetriever);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {PKCS1_PEM_FILE, PKCS8_PEM_FILE})
+    void testDKIMSignWithDomainInterpolationAndDefaultDomain(String pemFile) 
throws Exception {
+        String message = "Received: by 10.XX.XX.12 with SMTP id 
dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 
(PDT)\r\nReturn-Path: <[email protected]>\r\nReceived: from example.co.uk 
(example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id 
dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 
(PDT)\r\nFrom: [email protected]\r\nTo: 
[email protected]\r\n\r\nbody\r\nline1\r\nline2\r\n";
+
+        Mailet mailet = new DKIMSign(fileSystem);
+
+        FakeMailetConfig mci = FakeMailetConfig.builder()
+                .mailetName("Test")
+                .mailetContext(FAKE_MAIL_CONTEXT)
+                .setProperty(
+                        "signatureTemplate",
+                        "v=1; s=selector; d=%MAIL_FROM; 
h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+                .setProperty("privateKeyFilepath", pemFile)
+                .setProperty("defaultDomain", "fallback.com")
+                .build();
+
+        mailet.init(mci);
+
+        Mail mail = FakeMail.builder()
+            .name("test")
+            .mimeMessage(new MimeMessage(Session
+                .getDefaultInstance(new Properties()),
+                new ByteArrayInputStream(message.getBytes())))
+            .build();
+
+        mailet.service(mail);
+
+        ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+        mail.getMessage().writeTo(rawMessage);
+
+        MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new 
MockPublicKeyRecordRetriever(
+                "v=DKIM1; k=rsa; 
p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+                "selector", "fallback.com");
+        verify(rawMessage, mockPublicKeyRecordRetriever);
+    }
+
+    @Test
+    void testDKIMSignWithDomainInterpolationMultipleDomains() throws Exception 
{
+        String message1 = "From: [email protected]\r\nTo: 
[email protected]\r\n\r\nbody\r\n";
+        String message2 = "From: [email protected]\r\nTo: 
[email protected]\r\n\r\nbody\r\n";
+
+        Mailet mailet = new DKIMSign(fileSystem);
+
+        FakeMailetConfig mci = FakeMailetConfig.builder()
+                .mailetName("Test")
+                .mailetContext(FAKE_MAIL_CONTEXT)
+                .setProperty(
+                        "signatureTemplate",
+                        "v=1; s=selector; d=%MAIL_FROM; h=from:to; 
a=rsa-sha256; bh=; b=;")
+                .setProperty("defaultDomain", "linagora.com")
+                .setProperty("privateKeyFilepath", PKCS1_PEM_FILE)
+                .build();
+
+        mailet.init(mci);
+
+        Mail mail1 = FakeMail.builder()
+            .name("test1")
+            .sender(new MailAddress("[email protected]"))
+            .mimeMessage(new MimeMessage(Session
+                .getDefaultInstance(new Properties()),
+                new ByteArrayInputStream(message1.getBytes())))
+            .build();
+
+        mailet.service(mail1);
+
+        ByteArrayOutputStream rawMessage1 = new ByteArrayOutputStream();
+        mail1.getMessage().writeTo(rawMessage1);
+
+        MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever1 = new 
MockPublicKeyRecordRetriever(
+                "v=DKIM1; k=rsa; 
p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+                "selector", "domain1.com");
+        verify(rawMessage1, mockPublicKeyRecordRetriever1);
+
+        Mail mail2 = FakeMail.builder()
+            .name("test2")
+            .sender(new MailAddress("[email protected]"))
+            .mimeMessage(new MimeMessage(Session
+                .getDefaultInstance(new Properties()),
+                new ByteArrayInputStream(message2.getBytes())))
+            .build();
+
+        mailet.service(mail2);
+
+        ByteArrayOutputStream rawMessage2 = new ByteArrayOutputStream();
+        mail2.getMessage().writeTo(rawMessage2);
+
+        MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever2 = new 
MockPublicKeyRecordRetriever(
+                "v=DKIM1; k=rsa; 
p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+                "selector", "domain2.com");
+        verify(rawMessage2, mockPublicKeyRecordRetriever2);
+    }
+
+    @Test
+    void testDKIMSignWithMailFromPlaceholderButNoDefaultDomainShouldFail() {
+        Mailet mailet = new DKIMSign(fileSystem);
+
+        FakeMailetConfig mci = FakeMailetConfig.builder()
+                .mailetName("Test")
+                .mailetContext(FAKE_MAIL_CONTEXT)
+                .setProperty(
+                        "signatureTemplate",
+                        "v=1; s=selector; d=%MAIL_FROM; 
h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+                .setProperty("privateKeyFilepath", PKCS1_PEM_FILE)
+                .build();
+
+        assertThatThrownBy(() -> mailet.init(mci))
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessageContaining("signatureTemplate contains %MAIL_FROM 
placeholder but defaultDomain is not configured");
+    }
+
 }


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

Reply via email to