Email/query `text`: words in different fields never match, and "Re:"/"Fwd:" in a subject query matches nothing

Issue Description

Two problems in Email/query full-text matching, both easy to hit from any client search box:

  1. A multi-word text condition only matches when all words are in the same field. A word from the subject plus a word from the body finds nothing, while the same two words as two text conditions under AND find the message.
  2. The subject is indexed without its reply/forward prefix, but the query keeps it. Any subject or text query that contains “Re:” or “Fwd:” (for example a user pasting a subject line) finds nothing.

Expected Behavior

RFC 8621, section 4.4.1:

  • text: “Looks for the text in Emails. The server MUST look up text in the From, To, Cc, Bcc, and Subject header fields of the message and SHOULD look inside any text/* … body parts”.
  • “Outside of a phrase, white space SHOULD be treated as dividing separate tokens that may be searched for separately but MUST all be present for the Email to match the filter.”

So "zq17925 quarterly" must match an Email whose subject has zq17925 and whose body has quarterly. And subject: "Re: Budget zq17925" should match the subject “Re: Budget zq17925 planning”.

Actual Behavior

Message used (imported with Email/import):

From: Probe <[email protected]>
To: [email protected]
Subject: Re: Budget zq17925 planning
Content-Type: text/plain; charset=utf-8

Please review the quarterly numbers before the meeting.
Filter (combined with inMailbox) Result
{"text": "zq17925"} found
{"text": "quarterly"} found
{"text": "zq17925 quarterly"} not found
{"operator": "AND", "conditions": [{"text": "zq17925"}, {"text": "quarterly"}]} found
{"subject": "Budget zq17925 planning"} found
{"subject": "Re: Budget zq17925"} not found
{"text": "Re: Budget zq17925"} not found
{"subject": "\"Re: Budget\""} not found
{"subject": "Fwd: Offsite xk17925"} (subject “Fwd: Offsite xk17925”) not found
{"subject": "Offsite xk17925"} found

Reproduction Steps

curl -s -u "[email protected]:$PASS1" -H 'Content-Type: application/json' https://mail.example.com/jmap/ -d '{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
  "methodCalls": [["Email/query", {"accountId": "c", "filter": {"operator": "AND", "conditions": [
    {"inMailbox": "a"}, {"text": "zq17925 quarterly"}]}}, "q"]]}'
{"methodResponses": [["Email/query", {"accountId": "c", "queryState": "...", "canCalculateChanges": true,
  "position": 0, "ids": []}, "q"]]}

With {"text": "zq17925"} instead, ids contains the message. The index was given 12 seconds before querying; single-word queries on the same message succeed, so indexing had finished.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

Where it seems to come from:

  1. crates/jmap/src/email/query.rs, EmailFilter::Text: the whole string goes into one has_text() per field inside a single Or (From, To, Cc, Bcc, Subject, Body, Attachment). Each has_text() requires all tokens in that one field. Splitting the string into tokens/phrases first and building AND(token1 in any field, token2 in any field, ...) would follow the RFC.
  2. crates/email/src/message/index/search.rs: the Subject is indexed as thread_name(subject), which strips “Re:”, “Fwd:” and similar. The subject and text query strings are not passed through thread_name(), so the “re”/“fwd” tokens can never match. Either index the full subject or strip the same prefixes from the query.

Stalwart v0.16.27, Docker image stalwartlabs/stalwart:v0.16-alpine, default setup from the bootstrap wizard.

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

This confirms the report’s technical details are accurate. This is a bug report, not a question I should answer with docs — I should acknowledge it and let it be triaged. Let me check if there’s existing related forum discussion, but given the nature (confirmed bug with code references), I’ll give a brief acknowledgment.

This confirms the report’s analysis precisely. The code reproduces exactly what’s described: the full text string is passed to has_text per field inside one Or block, without tokenizing, and the Subject is indexed via thread_name() which strips reply/forward prefixes while the query text is not. This is a genuine bug matching the RFC 8621 behavior described, not something documented as intended behavior, so I don’t need more searching — I should report this as confirmed and route to staff.

Both issues check out against the current source.

1. Multi-word text filter requires all words in the same field. In crates/jmap/src/email/query.rs, EmailFilter::Text(text) passes the entire query string as one has_text() call per field (From, To, Cc, Bcc, Subject, Body, Attachment), wrapped in a single Or. Since has_text() itself requires every token to be present in that one field, a query like "zq17925 quarterly" only matches if both words happen to live in the same field — it isn’t split into per-token OR(field matches, ...) conditions ANDed together as RFC 8621 §4.4.1 requires. Splitting on AND conditions manually (as you found) works around it.

2. “Re:”/“Fwd:” prefixes break subject/text matching. In crates/email/src/message/index/search.rs, the Subject header is indexed via thread_name(subject), which strips reply/forward prefixes before tokenizing. But the query-side code in query.rs never applies thread_name() to the incoming subject/text filter value, so a prefix like “Re:” or “Fwd:” in the query string produces tokens that can never appear in the index.

This matches the exact mechanism you identified. I don’t see any existing workaround or config flag to change this indexing/query behavior in the current docs. A staff or community member will follow up on this report, likely as a fix to tokenize text/subject filters per-word with OR-across-fields semantics, and to normalize the subject query through thread_name() the same way it’s indexed.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Fixed in v1.0.0 (expected next week).