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

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

commit ddf1f266cf179a6e619fce34a7acace5645546ac
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Oct 29 12:31:05 2026 +0100

    [FIX] Header hashing: convert each field once per message
    
    signatureCheck fetched the fields once per h= entry, and every
    Message#getFields call built a new mime4j MessageServiceFactory and
    re-serialized all same-name fields. The cost grew with the product of
    h= entries and same-name fields: a 256 KB message took over 2 s.
    
    Message now keeps its factory, serializes all fields once (lazily) and
    indexes them by lower-cased name in ArrayLists. signatureCheck fetches
    each distinct name once per signature.
---
 .../java/org/apache/james/jdkim/DKIMCommon.java    |  7 ++--
 .../java/org/apache/james/jdkim/impl/Message.java  | 47 ++++++++++++++--------
 .../test/java/org/apache/james/jdkim/DKIMTest.java | 47 +++++++++++++++++++++-
 3 files changed, 81 insertions(+), 20 deletions(-)

diff --git a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java 
b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java
index 919a679..2435db8 100644
--- a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java
+++ b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java
@@ -85,12 +85,13 @@ public abstract class DKIMCommon {
         // NOTE: this relies on the list returned by Message being in insertion
         // order
         Map<String, Integer> processedHeader = new HashMap<>();
+        // Each distinct name is fetched once: h= may repeat a name many times.
+        Map<String, List<String>> fieldsByName = new HashMap<>();
 
         for (CharSequence header : headers) {
-            // NOTE check this getter is case insensitive
-            List<String> hl = h.getFields(header.toString());
+            String lowerCaseHeader = header.toString().toLowerCase(Locale.US);
+            List<String> hl = fieldsByName.computeIfAbsent(lowerCaseHeader, 
h::getFields);
             if (hl != null && !hl.isEmpty()) {
-                String lowerCaseHeader = 
header.toString().toLowerCase(Locale.US);
                 Integer done = processedHeader.get(lowerCaseHeader);
                 if (done == null)
                     done = 0;
diff --git a/main/src/main/java/org/apache/james/jdkim/impl/Message.java 
b/main/src/main/java/org/apache/james/jdkim/impl/Message.java
index b41de81..d59ca40 100644
--- a/main/src/main/java/org/apache/james/jdkim/impl/Message.java
+++ b/main/src/main/java/org/apache/james/jdkim/impl/Message.java
@@ -34,8 +34,12 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.LinkedList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 
 /**
  * The header of an entity (see RFC 2045).
@@ -43,6 +47,9 @@ import java.util.List;
 public class Message implements Headers {
 
     private final org.apache.james.mime4j.dom.Message message;
+    private final MessageServiceFactory messageServiceFactory;
+    private List<String> fields;
+    private Map<String, List<String>> fieldsByName;
 
     /**
      * Creates a new <code>Header</code> from the specified stream.
@@ -52,7 +59,8 @@ public class Message implements Headers {
      * @throws MimeIOException on MIME protocol violations.
      */
     public Message(InputStream is) throws IOException, MimeException {
-        MessageBuilder mb = newMessageBuilder().newMessageBuilder();
+        this.messageServiceFactory = newMessageBuilder();
+        MessageBuilder mb = messageServiceFactory.newMessageBuilder();
 
         this.message = mb.parseMessage(new EOLConvertingInputStream(is));
     }
@@ -83,20 +91,23 @@ public class Message implements Headers {
      * @see org.apache.james.jdkim.api.Headers#getFields()
      */
     public List<String> getFields() {
-        List<Field> res = message.getHeader().getFields();
-        return convertFields(res);
+        convertFields();
+        return fields;
     }
 
-    private List<String> convertFields(List<Field> res) {
-        List<String> res2 = new LinkedList<>();
-        MessageWriter mw;
-        try {
-            mw = newMessageBuilder().newMessageWriter();
-        } catch (MimeException e1) {
-            return res2;
+    /**
+     * Serializes every field once, so that lookups by name do not convert 
them again.
+     */
+    private void convertFields() {
+        if (fields != null) {
+            return;
         }
-        for (Field f : res) {
-            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        List<String> all = new ArrayList<>();
+        Map<String, List<String>> byName = new HashMap<>();
+        MessageWriter mw = messageServiceFactory.newMessageWriter();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        for (Field f : message.getHeader().getFields()) {
+            bos.reset();
             String field = null;
             try {
                 mw.writeField(f, bos);
@@ -106,16 +117,20 @@ public class Message implements Headers {
                 field = new String(fieldbytes, 0, fieldbytes.length - 2, 
StandardCharsets.ISO_8859_1);
             } catch (IOException ignored) {
             }
-            res2.add(field);
+            all.add(field);
+            byName.computeIfAbsent(f.getName().toLowerCase(Locale.US), name -> 
new ArrayList<>()).add(field);
         }
-        return res2;
+        byName.replaceAll((name, list) -> Collections.unmodifiableList(list));
+        fields = Collections.unmodifiableList(all);
+        fieldsByName = byName;
     }
 
     /**
      * @see org.apache.james.jdkim.api.Headers#getFields(java.lang.String)
      */
     public List<String> getFields(final String name) {
-        return convertFields(message.getHeader().getFields(name));
+        convertFields();
+        return fieldsByName.getOrDefault(name.toLowerCase(Locale.US), 
Collections.emptyList());
     }
 
     /**
diff --git a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java 
b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
index 5a176eb..bbf1933 100644
--- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
@@ -33,9 +33,12 @@ import java.io.SequenceInputStream;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.security.Signature;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.james.jdkim.MockPublicKeyRecordRetriever.Record;
 import org.apache.james.jdkim.api.Headers;
@@ -44,6 +47,7 @@ import org.apache.james.jdkim.api.SignatureRecord;
 import org.apache.james.jdkim.api.VerifierOptions;
 import org.apache.james.jdkim.exceptions.PermFailException;
 import org.apache.james.jdkim.impl.Message;
+import org.apache.james.jdkim.tagvalue.SignatureRecordImpl;
 import org.junit.Test;
 
 public class DKIMTest {
@@ -218,4 +222,45 @@ public class DKIMTest {
             return headers.getBodyInputStream();
         }
     }
-}
\ No newline at end of file
+    @Test
+    public void signatureCheckShouldConvertEachFieldAtMostOnce() throws 
Exception {
+        StringBuilder mail = new StringBuilder();
+        for (int i = 0; i < 500; i++) {
+            mail.append("From: x").append(i).append("@example.com\r\n");
+        }
+        mail.append("\r\n");
+        Message message = new Message(new 
ByteArrayInputStream(mail.toString().getBytes(StandardCharsets.US_ASCII)));
+        AtomicInteger converted = new AtomicInteger();
+        Headers counting = new Headers() {
+            public List<String> getFields() {
+                return message.getFields();
+            }
+
+            public List<String> getFields(String name) {
+                List<String> l = message.getFields(name);
+                converted.addAndGet(l.size());
+                return l;
+            }
+        };
+        SignatureRecord sign = new SignatureRecordImpl("v=1; a=rsa-sha256; 
d=example.com; s=s; bh=AAAA; b=AAAA; h="
+                + String.join(":", Collections.nCopies(500, "from")));
+        Signature sig = Signature.getInstance("SHA256withRSA");
+        sig.initSign(TestKeys.privateKey);
+
+        DKIMCommon.signatureCheck(counting, sign, sign.getHeaders(), sig);
+
+        assertThat(converted.get()).isLessThanOrEqualTo(1000);
+    }
+
+    @Test
+    public void messageShouldConvertFieldsOnce() throws Exception {
+        String mail = "From: [email protected]\r\nSubject: test\r\nfrom: 
[email protected]\r\n\r\nbody\r\n";
+        Message message = new Message(new 
ByteArrayInputStream(mail.getBytes(StandardCharsets.US_ASCII)));
+
+        assertThat(message.getFields("FROM")).containsExactly("From: 
[email protected]", "from: [email protected]");
+        
assertThat(message.getFields("from")).isSameAs(message.getFields("From"));
+        assertThat(message.getFields()).containsExactly("From: 
[email protected]", "Subject: test", "from: [email protected]");
+        assertThat(message.getFields("missing")).isEmpty();
+    }
+
+}


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

Reply via email to