From 313e6e41ad5ffc499bf24efd86581be2a458fb6e Mon Sep 17 00:00:00 2001 From: onevcat Date: Mon, 9 Feb 2026 10:29:53 +0900 Subject: [PATCH] Fix hasAttachment query for binary attachments --- crates/email/src/message/index/search.rs | 10 +++--- tests/src/jmap/mail/query.rs | 45 +++++++++++++++++++++++- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/email/src/message/index/search.rs b/crates/email/src/message/index/search.rs index 81f9f983..c093f007 100644 --- a/crates/email/src/message/index/search.rs +++ b/crates/email/src/message/index/search.rs @@ -8,7 +8,8 @@ use crate::message::{ index::{MAX_MESSAGE_PARTS, extractors::VisitTextArchived}, metadata::{ ArchivedMessageMetadata, ArchivedMetadataHeaderName, ArchivedMetadataHeaderValue, - ArchivedMetadataPartType, DecodedPartContent, MESSAGE_RECEIVED_MASK, MetadataHeaderName, + ArchivedMetadataPartType, DecodedPartContent, MESSAGE_HAS_ATTACHMENT, + MESSAGE_RECEIVED_MASK, MetadataHeaderName, }, }; use mail_parser::{DateTime, decoders::html::html_to_text, parsers::fields::thread::thread_name}; @@ -339,10 +340,11 @@ impl ArchivedMessageMetadata { #[cfg(feature = "test_mode")] document.set_unknown_language(default_language); - let has_attachment = - document.has_field(&(SearchField::Email(EmailSearchField::Attachment))); - + // Email.hasAttachment should reflect the presence of attachments, not whether attachment + // *text* was indexed. + let has_attachment = (self.rcvd_attach.to_native() & MESSAGE_HAS_ATTACHMENT) != 0; document.index_bool(EmailSearchField::HasAttachment, has_attachment); + document } } diff --git a/tests/src/jmap/mail/query.rs b/tests/src/jmap/mail/query.rs index 51dd5e0a..5e3e5c16 100644 --- a/tests/src/jmap/mail/query.rs +++ b/tests/src/jmap/mail/query.rs @@ -88,8 +88,51 @@ pub async fn test(params: &mut JMAPTest, insert: bool) { MAX_THREADS ); - // Wait for indexing to complete + // Regression test: Email/query filter {hasAttachment:true} should match emails with binary + // attachments (not just those with attachment text indexed). + let sent_at = now(); + let binary_anchor = "stalwart-hasattachment-binary"; + let binary_message = MessageBuilder::new() + .from(("Sender", "sender@domain.com")) + .to(("Recipient", "rcpt@domain.com")) + .subject("Binary attachment regression") + .date(Date::new(sent_at as i64 + 10_000)) + .message_id(binary_anchor) + .text_body("binary attachment") + .attachment("application/octet-stream", "file.bin", &[0u8; 8][..]) + .write_to_vec() + .unwrap(); + + client + .email_import( + binary_message, + [Id::new(2000u64).to_string()], + Vec::::new().into(), + Some(sent_at as i64 + 10_000), + ) + .await + .unwrap(); + + // Wait for indexing to complete (covers both the bulk import and the regression message). wait_for_index(&server).await; + + let binary_email_id = get_anchor(client, binary_anchor) + .await + .expect("binary regression email should be findable by Message-Id"); + + let ids = client + .email_query( + email::query::Filter::has_attachment(true).into(), + None::>, + ) + .await + .unwrap() + .take_ids(); + + assert!( + ids.iter().any(|id| id.to_string() == binary_email_id), + "expected hasAttachment query to include imported binary attachment message" + ); } let can_stem = !params.server.search_store().is_mysql();