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]
