Fix JMAP: Email/query: Improper anchor handling

This commit is contained in:
Maurus Decimus
2026-06-18 19:16:22 +02:00
parent 18411f8e9e
commit d174e415fb
6 changed files with 70 additions and 49 deletions

View File

@@ -23,6 +23,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
- `*/set`: Unchanged immutable `id` property is rejected on update.
- `*/query` and `*/queryChanges`: null` rejected as `notRequest`.
- `Email/query`:
* Improper `anchor` handling.
* Total miscount when `collapseThreads` is enabled.
* Wrong sort order on `hasKeyword`, `allInThreadHaveKeyword`, and `someInThreadHaveKeyword` conditions.
* Non-standard header values are not searchable.

View File

@@ -102,7 +102,9 @@ impl<'de, T: JmapObject> DeserializeArguments<'de> for QueryRequest<T> {
self.position = map.next_value()?;
},
b"anchor" => {
self.anchor = map.next_value()?;
self.anchor = map
.next_value::<Option<crate::request::MaybeInvalid<Id>>>()?
.map(|anchor| anchor.try_unwrap().unwrap_or(Id::from(u64::MAX)));
},
b"anchorOffset" => {
self.anchor_offset = map.next_value()?;

View File

@@ -10,8 +10,7 @@ use super::{
};
use crate::request::{
CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod,
QueryRequestMethod, SetRequestMethod,
deserialize::DeserializeArguments,
QueryRequestMethod, SetRequestMethod, deserialize::DeserializeArguments,
};
use serde::{
Deserialize, Deserializer,

View File

@@ -19,6 +19,7 @@ pub struct QueryResponseBuilder {
anchor_offset: i32,
pub has_anchor: bool,
pub anchor_found: bool,
index: i32,
pub response: QueryResponse,
}
@@ -49,6 +50,7 @@ impl QueryResponseBuilder {
anchor: request.anchor.map(|anchor| anchor.id()).unwrap_or(0),
anchor_offset: request.anchor_offset.unwrap_or(0),
anchor_found: false,
index: 0,
response: QueryResponse {
account_id: request.account_id,
query_state,
@@ -91,30 +93,31 @@ impl QueryResponseBuilder {
} else {
self.response.ids.push(id);
}
} else if self.anchor_offset >= 0 {
if !self.anchor_found {
if id_u64 != self.anchor {
return true;
}
} else {
let current_index = self.index;
self.index += 1;
if id_u64 == self.anchor {
self.anchor_found = true;
self.position = (current_index + self.anchor_offset).max(0);
}
if self.anchor_offset > 0 {
self.anchor_offset -= 1;
if self.anchor_offset >= 0 {
if self.anchor_found && current_index >= self.position {
self.response.ids.push(id);
if self.limit > 0 && self.response.ids.len() == self.limit {
return false;
}
}
} else {
self.response.ids.push(id);
if self.response.ids.len() == self.limit {
if self.anchor_found
&& self.limit > 0
&& self.response.ids.len() >= self.position as usize + self.limit
{
return false;
}
}
} else {
self.anchor_found = id_u64 == self.anchor;
self.response.ids.push(id);
if self.anchor_found {
self.position = self.anchor_offset;
return false;
}
}
true
@@ -125,35 +128,49 @@ impl QueryResponseBuilder {
}
pub fn build(mut self) -> trc::Result<QueryResponse> {
if !self.has_anchor || self.anchor_found {
if !self.has_anchor && self.requested_position >= 0 {
self.response.position = if self.position == 0 {
self.requested_position
} else {
0
};
} else if self.position >= 0 {
self.response.position = self.position;
} else {
let position = self.position.unsigned_abs() as usize;
let start_offset = if position < self.response.ids.len() {
self.response.ids.len() - position
} else {
0
};
self.response.position = start_offset as i32;
let end_offset = if self.limit > 0 {
std::cmp::min(start_offset + self.limit, self.response.ids.len())
if self.has_anchor {
if !self.anchor_found {
return Err(trc::JmapEvent::AnchorNotFound.into_err());
}
let start = self.position.max(0) as usize;
if self.anchor_offset < 0 {
let start = start.min(self.response.ids.len());
let end = if self.limit > 0 {
std::cmp::min(start + self.limit, self.response.ids.len())
} else {
self.response.ids.len()
};
self.response.ids = self.response.ids[start_offset..end_offset].to_vec()
self.response.ids = self.response.ids[start..end].to_vec();
}
self.response.position = start as i32;
Ok(self.response)
} else {
Err(trc::JmapEvent::AnchorNotFound.into_err())
return Ok(self.response);
}
if self.requested_position >= 0 {
self.response.position = if self.position == 0 {
self.requested_position
} else {
0
};
} else {
let position = self.position.unsigned_abs() as usize;
let start_offset = if position < self.response.ids.len() {
self.response.ids.len() - position
} else {
0
};
self.response.position = start_offset as i32;
let end_offset = if self.limit > 0 {
std::cmp::min(start_offset + self.limit, self.response.ids.len())
} else {
self.response.ids.len()
};
self.response.ids = self.response.ids[start_offset..end_offset].to_vec();
}
Ok(self.response)
}
}

View File

@@ -151,7 +151,11 @@ impl QueryChanges for Server {
}
QueryChangesRequestMethod::FileNode(mut request) => {
// Query changes
resolve_account_id(&mut request.account_id, MethodObject::FileNode, access_token)?;
resolve_account_id(
&mut request.account_id,
MethodObject::FileNode,
access_token,
)?;
changes = self
.changes(
build_changes_request(&request),

View File

@@ -199,11 +199,9 @@ impl QueryResults {
results.sort_by(|a, b| {
for comparator in &comparators {
let (a, b, is_ascending) = match comparator {
SearchComparator::DocumentSet { set, ascending } => (
set.contains(*a) as u32,
set.contains(*b) as u32,
*ascending,
),
SearchComparator::DocumentSet { set, ascending } => {
(set.contains(*a) as u32, set.contains(*b) as u32, *ascending)
}
SearchComparator::SortedSet { set, ascending } => {
let missing = if *ascending { u32::MAX } else { 0 };
(