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]
