Fix JMAP: SearchSnippet/get response structure

This commit is contained in:
Maurus Decimus
2026-06-18 16:07:59 +02:00
parent 5bb4ff5995
commit aaee070a2e
4 changed files with 32 additions and 10 deletions

View File

@@ -20,6 +20,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
- Unchanged immutable `id` property is rejected on `/set`. - Unchanged immutable `id` property is rejected on `/set`.
- `filter: null` rejected as `notRequest` on `/query` and `/queryChanges`. - `filter: null` rejected as `notRequest` on `/query` and `/queryChanges`.
- `Email/query` total miscount when `collapseThreads` is enabled. - `Email/query` total miscount when `collapseThreads` is enabled.
- `SearchSnippet/get` response structure.
- OIDC: Add default domain name to groups that are not email addresses. - OIDC: Add default domain name to groups that are not email addresses.
- RocksDB: Enable blob garbage collection to reclaim disk space from deleted blobs. - RocksDB: Enable blob garbage collection to reclaim disk space from deleted blobs.

View File

@@ -33,8 +33,7 @@ pub struct GetSearchSnippetResponse {
pub list: Vec<SearchSnippet>, pub list: Vec<SearchSnippet>,
#[serde(rename = "notFound")] #[serde(rename = "notFound")]
#[serde(skip_serializing_if = "Vec::is_empty")] pub not_found: Option<Vec<MaybeInvalid<Id>>>,
pub not_found: Vec<Id>,
} }
#[derive(serde::Serialize, Clone, Debug)] #[derive(serde::Serialize, Clone, Debug)]
@@ -42,10 +41,8 @@ pub struct SearchSnippet {
#[serde(rename = "emailId")] #[serde(rename = "emailId")]
pub email_id: Id, pub email_id: Id,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>, pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<String>, pub preview: Option<String>,
} }

View File

@@ -189,6 +189,18 @@ impl<'de, V: FromStr> serde::Deserialize<'de> for MaybeInvalid<V> {
} }
} }
impl<V: FromStr + serde::Serialize> serde::Serialize for MaybeInvalid<V> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
MaybeInvalid::Value(v) => v.serialize(serializer),
MaybeInvalid::Invalid(s) => serializer.serialize_str(s),
}
}
}
impl<V: FromStr> Default for MaybeInvalid<V> { impl<V: FromStr> Default for MaybeInvalid<V> {
fn default() -> Self { fn default() -> Self {
MaybeInvalid::Invalid("".to_string()) MaybeInvalid::Invalid("".to_string())

View File

@@ -17,7 +17,7 @@ use jmap_proto::{
search_snippet::{GetSearchSnippetRequest, GetSearchSnippetResponse, SearchSnippet}, search_snippet::{GetSearchSnippetRequest, GetSearchSnippetResponse, SearchSnippet},
}, },
object::email::EmailFilter, object::email::EmailFilter,
request::IntoValid, request::MaybeInvalid,
}; };
use mail_parser::decoders::html::html_to_text; use mail_parser::decoders::html::html_to_text;
use nlp::language::{Language, search_snippet::generate_snippet, stemmer::Stemmer}; use nlp::language::{Language, search_snippet::generate_snippet, stemmer::Stemmer};
@@ -108,14 +108,22 @@ impl EmailSearchSnippet for Server {
let mut response = GetSearchSnippetResponse { let mut response = GetSearchSnippetResponse {
account_id: request.account_id, account_id: request.account_id,
list: Vec::with_capacity(email_ids.len()), list: Vec::with_capacity(email_ids.len()),
not_found: vec![], not_found: None,
}; };
let mut not_found = Vec::new();
if email_ids.len() > self.core.jmap.snippet_max_results { if email_ids.len() > self.core.jmap.snippet_max_results {
return Err(trc::JmapEvent::RequestTooLarge.into_err()); return Err(trc::JmapEvent::RequestTooLarge.into_err());
} }
for email_id in email_ids.into_valid() { for email_id in email_ids {
let email_id = match email_id {
MaybeInvalid::Value(email_id) => email_id,
invalid => {
not_found.push(invalid);
continue;
}
};
let document_id = email_id.document_id(); let document_id = email_id.document_id();
let mut snippet = SearchSnippet { let mut snippet = SearchSnippet {
email_id, email_id,
@@ -123,7 +131,7 @@ impl EmailSearchSnippet for Server {
preview: None, preview: None,
}; };
if !document_ids.contains(document_id) { if !document_ids.contains(document_id) {
response.not_found.push(email_id); not_found.push(MaybeInvalid::Value(email_id));
continue; continue;
} else if terms.is_empty() { } else if terms.is_empty() {
response.list.push(snippet); response.list.push(snippet);
@@ -141,7 +149,7 @@ impl EmailSearchSnippet for Server {
{ {
Some(metadata) => metadata, Some(metadata) => metadata,
None => { None => {
response.not_found.push(email_id); not_found.push(MaybeInvalid::Value(email_id));
continue; continue;
} }
}; };
@@ -178,7 +186,7 @@ impl EmailSearchSnippet for Server {
CausedBy = trc::location!(), CausedBy = trc::location!(),
); );
response.not_found.push(email_id); not_found.push(MaybeInvalid::Value(email_id));
continue; continue;
}; };
let raw_message = ChainedBytes::new(metadata.raw_headers.as_ref()).with_last( let raw_message = ChainedBytes::new(metadata.raw_headers.as_ref()).with_last(
@@ -246,6 +254,10 @@ impl EmailSearchSnippet for Server {
response.list.push(snippet); response.list.push(snippet);
} }
if !not_found.is_empty() {
response.not_found = Some(not_found);
}
Ok(response) Ok(response)
} }
} }