Fix JMAP references in nested objects not resolved

This commit is contained in:
Maurus Decimus
2026-05-04 16:45:37 +02:00
parent 68facdaba4
commit dc0f0c2879
3 changed files with 281 additions and 86 deletions

View File

@@ -16,6 +16,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
- `mail-parser` panic with certain messages containing corrupted attachments. - `mail-parser` panic with certain messages containing corrupted attachments.
- Pagination by anchor for queued messages, tasks and metrics. - Pagination by anchor for queued messages, tasks and metrics.
- Spam filter: Use original instead of rewritten `RCPT` on checks. - Spam filter: Use original instead of rewritten `RCPT` on checks.
- JMAP references in nested objects not resolved.
## [0.16.3] - 2026-04-30 ## [0.16.3] - 2026-04-30

View File

@@ -196,64 +196,73 @@ where
max_depth: usize, max_depth: usize,
eval_strings: bool, eval_strings: bool,
) -> trc::Result<()> { ) -> trc::Result<()> {
let Value::Object(obj) = self else { match self {
return Ok(()); Value::Element(element) => {
}; if let Some(id_ref) = element.as_id_ref() {
if let Some(id) = response.created_ids.get(id_ref) {
for (key, value) in obj.as_mut_vec() { if !element.try_set_id(id.clone()) {
// Resolve patch with references (e.g. mailboxIds/#idRef) return Err(trc::JmapEvent::InvalidResultReference
if depth == 0 .into_err()
&& let Key::Property(property) = key .details("Id reference points to invalid type."));
&& let Some(id_ref) = property.as_id_ref() }
{ } else if let Graph::Some { child_id, graph } = graph {
if let Some(id) = response.created_ids.get(id_ref) { graph
if !property.try_set_id(id.clone()) { .entry(child_id.to_string())
.or_insert_with(Vec::new)
.push(id_ref.to_string());
} else {
return Err(trc::JmapEvent::InvalidResultReference return Err(trc::JmapEvent::InvalidResultReference
.into_err() .into_err()
.details("Id reference points to invalid type.")); .details(format_compact!("Id reference {id_ref:?} not found.")));
} }
} else {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!("Id reference {id_ref:?} not found.")));
} }
} else if eval_strings
&& let Some(id) = key
.as_string_key()
.and_then(|k| k.strip_prefix('#'))
.and_then(|id_ref| response.created_ids.get(id_ref))
{
*key = Key::Owned(match id {
AnyId::Id(id) => id.to_string(),
AnyId::BlobId(id) => id.to_string(),
});
} }
Value::Array(items) if depth < max_depth => {
match value { // Resolve references in arrays (e.g. emailIds: [#idRef1, #idRef2])
Value::Element(element) => { for item in items {
if let Some(id_ref) = element.as_id_ref() { item.eval_object_references(
response,
graph,
depth + 1,
max_depth,
eval_strings,
)?;
}
}
Value::Object(items) if depth < max_depth => {
// Resolve references in JMAP sets (e.g. mailboxIds: { "#idRef1": true, "#idRef2": true })
for (key, value) in items.as_mut_vec() {
if let Key::Property(property) = key
&& let Some(id_ref) = property.as_id_ref()
{
if let Some(id) = response.created_ids.get(id_ref) { if let Some(id) = response.created_ids.get(id_ref) {
if !element.try_set_id(id.clone()) { if !property.try_set_id(id.clone()) {
return Err(trc::JmapEvent::InvalidResultReference return Err(trc::JmapEvent::InvalidResultReference
.into_err() .into_err()
.details("Id reference points to invalid type.")); .details("Id reference points to invalid type."));
} }
} else if let Graph::Some { child_id, graph } = graph {
graph
.entry(child_id.to_string())
.or_insert_with(Vec::new)
.push(id_ref.to_string());
} else { } else {
return Err(trc::JmapEvent::InvalidResultReference return Err(trc::JmapEvent::InvalidResultReference
.into_err() .into_err()
.details(format_compact!("Id reference {id_ref:?} not found."))); .details(format_compact!("Id reference {id_ref:?} not found.")));
} }
} else if eval_strings
&& let Some(id) = key
.as_string_key()
.and_then(|k| k.strip_prefix('#'))
.and_then(|id_ref| response.created_ids.get(id_ref))
{
*key = Key::Owned(match id {
AnyId::Id(id) => id.to_string(),
AnyId::BlobId(id) => id.to_string(),
});
} }
}
Value::Array(items) if depth < max_depth => { if matches!(
// Resolve references in arrays (e.g. emailIds: [#idRef1, #idRef2]) value,
for item in items { Value::Element(_) | Value::Array(_) | Value::Object(_)
item.eval_object_references( ) {
value.eval_object_references(
response, response,
graph, graph,
depth + 1, depth + 1,
@@ -262,51 +271,8 @@ where
)?; )?;
} }
} }
Value::Object(items) if depth < max_depth => {
// Resolve references in JMAP sets (e.g. mailboxIds: { "#idRef1": true, "#idRef2": true })
let visit_children = depth + 1 < max_depth;
for (key, value) in items.as_mut_vec() {
if let Key::Property(property) = key
&& let Some(id_ref) = property.as_id_ref()
{
if let Some(id) = response.created_ids.get(id_ref) {
if !property.try_set_id(id.clone()) {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details("Id reference points to invalid type."));
}
} else {
return Err(trc::JmapEvent::InvalidResultReference
.into_err()
.details(format_compact!(
"Id reference {id_ref:?} not found."
)));
}
} else if eval_strings
&& let Some(id) = key
.as_string_key()
.and_then(|k| k.strip_prefix('#'))
.and_then(|id_ref| response.created_ids.get(id_ref))
{
*key = Key::Owned(match id {
AnyId::Id(id) => id.to_string(),
AnyId::BlobId(id) => id.to_string(),
});
}
if visit_children && matches!(value, Value::Object(_)) {
value.eval_object_references(
response,
graph,
depth + 1,
max_depth,
eval_strings,
)?;
}
}
}
_ => {}
} }
_ => {}
} }
Ok(()) Ok(())

View File

@@ -98,6 +98,8 @@ mod tests {
}, },
response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod}, response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod},
}; };
use crate::references::Graph;
use crate::references::eval::EvalObjectReferences;
use jmap_tools::{Key, Map, Value}; use jmap_tools::{Key, Map, Value};
use std::collections::HashMap; use std::collections::HashMap;
use types::id::Id; use types::id::Id;
@@ -721,4 +723,230 @@ mod tests {
panic!("Expected Mailbox Set Request"); panic!("Expected Mailbox Set Request");
} }
} }
#[test]
fn eval_nested_element_ref() {
let mut created_ids = HashMap::new();
created_ids.insert("server-1".to_string(), Id::new(42).into());
let response = Response::new(0, created_ids, 0);
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![
(
Key::Property(MailboxProperty::Name),
Value::Str("inbox".into()),
),
(
Key::Property(MailboxProperty::ParentId),
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Element(MailboxValue::IdReference("server-1".into())),
)])),
),
]));
value
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
.unwrap();
let nested = value
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap()
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap();
assert_eq!(nested, &Value::Element(MailboxValue::Id(Id::new(42))));
}
#[test]
fn eval_array_element_ref() {
let mut created_ids = HashMap::new();
created_ids.insert("a".to_string(), Id::new(1).into());
created_ids.insert("b".to_string(), Id::new(2).into());
let response = Response::new(0, created_ids, 0);
let mut value: Value<'_, MailboxProperty, MailboxValue> =
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Array(vec![
Value::Element(MailboxValue::IdReference("a".into())),
Value::Element(MailboxValue::IdReference("b".into())),
]),
)]));
value
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
.unwrap();
let arr = value
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap()
.as_array()
.unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], Value::Element(MailboxValue::Id(Id::new(1))));
assert_eq!(arr[1], Value::Element(MailboxValue::Id(Id::new(2))));
}
#[test]
fn eval_nested_array_of_objects_with_element_ref() {
let mut created_ids = HashMap::new();
created_ids.insert("a".to_string(), Id::new(7).into());
let response = Response::new(0, created_ids, 0);
let mut value: Value<'_, MailboxProperty, MailboxValue> =
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Array(vec![Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Element(MailboxValue::IdReference("a".into())),
)]))]),
)]));
value
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
.unwrap();
let resolved = value
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap()
.as_array()
.unwrap()[0]
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap();
assert_eq!(resolved, &Value::Element(MailboxValue::Id(Id::new(7))));
}
#[test]
fn eval_graph_collects_nested_ref() {
let response = Response::new(0, HashMap::new(), 0);
let mut graph_map: HashMap<String, Vec<String>> = HashMap::new();
let child_id = "outer".to_string();
let mut value: Value<'_, MailboxProperty, MailboxValue> =
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Element(MailboxValue::IdReference("inner".into())),
)])),
)]));
{
let mut graph = Graph::Some {
child_id: &child_id,
graph: &mut graph_map,
};
value
.eval_object_references(&response, &mut graph, 0, 5, true)
.unwrap();
}
assert_eq!(graph_map.get("outer"), Some(&vec!["inner".to_string()]));
}
#[test]
fn eval_unresolved_nested_ref_errors_without_graph() {
let response = Response::new(0, HashMap::new(), 0);
let mut value: Value<'_, MailboxProperty, MailboxValue> =
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Element(MailboxValue::IdReference("missing".into())),
)])),
)]));
let err = value
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
.unwrap_err();
assert!(
err.matches(trc::EventType::Jmap(
trc::JmapEvent::InvalidResultReference
)),
"{:?}",
err
);
}
#[test]
fn eval_depth_limit_blocks_walk_into_inner_object() {
let mut created_ids = HashMap::new();
created_ids.insert("inner".to_string(), Id::new(99).into());
let response = Response::new(0, created_ids, 0);
let mut value: Value<'_, MailboxProperty, MailboxValue> =
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Element(MailboxValue::IdReference("inner".into())),
)])),
)])),
)]));
value
.eval_object_references(&response, &mut Graph::None, 0, 2, true)
.unwrap();
let deepest = value
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap()
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap()
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap();
assert_eq!(
deepest,
&Value::Element(MailboxValue::IdReference("inner".into()))
);
}
#[test]
fn eval_depth_limit_substitutes_element_at_max_depth() {
let mut created_ids = HashMap::new();
created_ids.insert("inner".to_string(), Id::new(99).into());
let response = Response::new(0, created_ids, 0);
let mut value: Value<'_, MailboxProperty, MailboxValue> =
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Object(Map::from(vec![(
Key::Property(MailboxProperty::ParentId),
Value::Element(MailboxValue::IdReference("inner".into())),
)])),
)]));
value
.eval_object_references(&response, &mut Graph::None, 0, 2, true)
.unwrap();
let resolved = value
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap()
.as_object()
.unwrap()
.get(&Key::Property(MailboxProperty::ParentId))
.unwrap();
assert_eq!(resolved, &Value::Element(MailboxValue::Id(Id::new(99))));
}
} }