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

commit ec89682b203bf30bf932c9d9c32427d9d26b15d6
Author: Benoit TELLIER <[email protected]>
AuthorDate: Tue Feb 3 14:54:04 2026 +0100

    MAILBOX-401 Allow search on mailing list subject prefix
---
 .../mailbox/opensearch/json/IndexableMessage.java  |  2 +-
 .../james/mailbox/opensearch/json/SubjectsDto.java | 61 ++++++++++++++++++++++
 .../opensearch/OpenSearchIntegrationTest.java      | 31 +++++++++++
 .../store/search/mime/HeaderCollection.java        |  3 +-
 .../store/search/mime/HeaderCollectionTest.java    |  4 +-
 mailbox/store/src/test/resources/eml/mail.json     |  3 +-
 .../src/test/resources/eml/pgpSignedMail.json      |  4 +-
 mailbox/store/src/test/resources/eml/spamMail.json |  3 +-
 .../src/test/resources/eml/spamMailNoHeaders.json  |  3 +-
 9 files changed, 105 insertions(+), 9 deletions(-)

diff --git 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/IndexableMessage.java
 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/IndexableMessage.java
index fdd78aa3b5..c11ef172bd 100644
--- 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/IndexableMessage.java
+++ 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/IndexableMessage.java
@@ -325,7 +325,7 @@ public class IndexableMessage {
         this.sentDate = sentDate;
         this.saveDate = saveDate;
         this.size = size;
-        this.subjects = new SubjectsDto(subjects.getSubjects());
+        this.subjects = SubjectsDto.from(subjects.getSubjects());
         this.subType = subType;
         this.to = EMailersDto.from(to);
         this.uid = uid;
diff --git 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/SubjectsDto.java
 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/SubjectsDto.java
index 365e1e0056..e4d237eb92 100644
--- 
a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/SubjectsDto.java
+++ 
b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/json/SubjectsDto.java
@@ -19,9 +19,70 @@
 
 package org.apache.james.mailbox.opensearch.json;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.apache.james.mailbox.store.search.SearchUtil;
 
 import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
 
 public record SubjectsDto(@JsonValue Set<String> subjects) {
+    public static SubjectsDto from(Set<String> subjects) {
+        return new SubjectsDto(subjects.stream()
+            .flatMap(SubjectsDto::withSubjectQualifiers)
+            .distinct()
+            .collect(ImmutableSet.toImmutableSet()));
+    }
+
+    public static Stream<String> withSubjectQualifiers(String subject) {
+        String baseSubject = SearchUtil.getBaseSubject(subject);
+
+        return ImmutableSet.<String>builder()
+            .add(baseSubject)
+            .addAll(extractQualifiers(subject))
+            .build()
+            .stream();
+    }
+
+    public static List<String> extractQualifiers(String subject) {
+        List<String> result = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        boolean inBrackets = false;
+
+        for (char c : subject.toCharArray()) {
+            if (c == '[') {
+                inBrackets = true;
+                current.setLength(0); // reset
+            } else if (c == ']') {
+                if (inBrackets) {
+                    result.add(current.toString());
+                    result.addAll(domainTokens(current.toString()));
+                    inBrackets = false;
+                }
+            } else if (inBrackets) {
+                current.append(c);
+            }
+        }
+        return result;
+    }
+
+    public static List<String> domainTokens(String domain) {
+        List<String> parts = Splitter.on('.')
+            .omitEmptyStrings()
+            .splitToList(domain);
+
+        return IntStream.range(0, parts.size() - 1)
+            .boxed()
+            .flatMap(i -> Stream.of(
+                Joiner.on('.').join(parts.subList(i, parts.size())),
+                parts.get(i)))
+            .distinct()
+            .toList();
+    }
 }
diff --git 
a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
 
b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
index 19e279f79b..036decb70a 100644
--- 
a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
+++ 
b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchIntegrationTest.java
@@ -72,6 +72,8 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.opensearch.client.opensearch._types.query_dsl.MatchAllQuery;
 import org.opensearch.client.opensearch._types.query_dsl.Query;
 import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
@@ -676,6 +678,35 @@ class OpenSearchIntegrationTest extends 
AbstractMessageSearchIndexTest {
             .containsOnly(messageId1.getUid());
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "example.com",
+        "nas-backup.example.com",
+        "[nas-backup.example.com]",
+        "nas",
+        "backup",
+    })
+    void mailingListPrefixShouldBePreservedInSearch(String subject) throws 
Exception {
+        MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
+        MailboxSession session = MailboxSessionUtil.create(USERNAME);
+        MessageManager messageManager = 
storeMailboxManager.getMailbox(mailboxPath, session);
+
+        ComposedMessageId messageId1 = messageManager.appendMessage(
+            MessageManager.AppendCommand.builder().build(
+                Message.Builder
+                    .of()
+                    .setBody("testmail", StandardCharsets.UTF_8)
+                    .setSubject("[nas-backup.example.com] Backup completed 
successfully")
+                    .build()),
+            session).getId();
+
+        awaitForOpenSearch(QueryBuilders.matchAll().build().toQuery(), 14);
+
+        
assertThat(Flux.from(messageManager.search(SearchQuery.of(SearchQuery.subject(subject)),
 session)).toStream())
+            .containsOnly(messageId1.getUid());
+    }
+
+
     @Test
     void searchDomainInSubject() throws Exception {
         MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, INBOX);
diff --git 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/mime/HeaderCollection.java
 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/mime/HeaderCollection.java
index 6066cf6e1f..97430b2a24 100644
--- 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/mime/HeaderCollection.java
+++ 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/search/mime/HeaderCollection.java
@@ -26,7 +26,6 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 
-import org.apache.james.mailbox.store.search.SearchUtil;
 import org.apache.james.mailbox.store.search.comparator.SentDateComparator;
 import org.apache.james.mime4j.field.address.LenientAddressParser;
 import org.apache.james.mime4j.stream.Field;
@@ -131,7 +130,7 @@ public class HeaderCollection {
                     manageAddressField(headerName, rawHeaderValue);
                     break;
                 case SUBJECT:
-                    subjectSet.add(SearchUtil.getBaseSubject(headerValue));
+                    subjectSet.add(headerValue);
                     break;
                 case DATE:
                     sentDate = SentDateComparator.toISODate(headerValue);
diff --git 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/mime/HeaderCollectionTest.java
 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/mime/HeaderCollectionTest.java
index 1c75f898f1..364c79e38b 100644
--- 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/mime/HeaderCollectionTest.java
+++ 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/mime/HeaderCollectionTest.java
@@ -85,13 +85,13 @@ class HeaderCollectionTest {
     }
 
     @Test
-    void shouldNormalizeSubject() {
+    void shouldNotNormalizeSubject() {
         HeaderCollection headerCollection = HeaderCollection.builder()
             .add(new FieldImpl("Subject", "Re: test"))
             .build();
 
         assertThat(headerCollection.getSubjectSet())
-            .containsOnly("test");
+            .containsOnly("Re: test");
     }
 
     @Test
diff --git a/mailbox/store/src/test/resources/eml/mail.json 
b/mailbox/store/src/test/resources/eml/mail.json
index 9d90ff358a..286aa75987 100644
--- a/mailbox/store/src/test/resources/eml/mail.json
+++ b/mailbox/store/src/test/resources/eml/mail.json
@@ -163,7 +163,8 @@
  "cc": [],
  "bcc": [],
  "subject": [
-  "[arch-general] Inkscape fails to open svg files"
+  "Inkscape fails to open svg files",
+   "arch-general"
  ],
  "sentDate": "2015-06-04T06:08:41+02:00",
  "attachments": [],
diff --git a/mailbox/store/src/test/resources/eml/pgpSignedMail.json 
b/mailbox/store/src/test/resources/eml/pgpSignedMail.json
index 139bd3638c..3395c05e4b 100644
--- a/mailbox/store/src/test/resources/eml/pgpSignedMail.json
+++ b/mailbox/store/src/test/resources/eml/pgpSignedMail.json
@@ -161,7 +161,9 @@
   "cc": [],
   "bcc": [],
   "subject": [
-       "libapache-mod-jk security update"
+       "libapache-mod-jk security update",
+    "SECURITY",
+    "DSA 3278-1"
   ],
   "sentDate": "2015-06-03T19:14:32+0000",
   "attachments": [],
diff --git a/mailbox/store/src/test/resources/eml/spamMail.json 
b/mailbox/store/src/test/resources/eml/spamMail.json
index d1e212770d..2d4eac4b35 100644
--- a/mailbox/store/src/test/resources/eml/spamMail.json
+++ b/mailbox/store/src/test/resources/eml/spamMail.json
@@ -116,7 +116,8 @@
   "cc": [],
   "bcc": [],
   "subject": [
-       "UNCHECKED contents in mail FROM <[email protected]>"
+       "UNCHECKED contents in mail FROM <[email protected]>",
+    "root"
   ],
   "sentDate": "2015-06-03T09:05:46+0000",
   "attachments": [
diff --git a/mailbox/store/src/test/resources/eml/spamMailNoHeaders.json 
b/mailbox/store/src/test/resources/eml/spamMailNoHeaders.json
index e0717172b3..8d4f8c35b3 100644
--- a/mailbox/store/src/test/resources/eml/spamMailNoHeaders.json
+++ b/mailbox/store/src/test/resources/eml/spamMailNoHeaders.json
@@ -29,7 +29,8 @@
   "cc": [],
   "bcc": [],
   "subject": [
-       "UNCHECKED contents in mail FROM <[email protected]>"
+       "UNCHECKED contents in mail FROM <[email protected]>",
+    "root"
   ],
   "sentDate": "2015-06-03T09:05:46+0000",
   "attachments": [],


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

Reply via email to