From 3217aae4e8ae19d4627b4b4a01f6a1babbdc4800 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sun, 27 Sep 2026 18:41:37 -0700 Subject: [PATCH] LegalHold/get takes coveringAccount Only the active holds covering one account, live or deleted and kept, through any route: for the console's Held badge (LH-14). --- .../src/object/inbuxa_legal_hold.rs | 23 ++++++++++++++++++- crates/jmap/src/inbuxa/legal_hold.rs | 19 +++++++++++++++ tests/src/system/legal_hold.rs | 13 +++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/jmap-proto/src/object/inbuxa_legal_hold.rs b/crates/jmap-proto/src/object/inbuxa_legal_hold.rs index 8f9b9bd..aa2b1fc 100644 --- a/crates/jmap-proto/src/object/inbuxa_legal_hold.rs +++ b/crates/jmap-proto/src/object/inbuxa_legal_hold.rs @@ -139,6 +139,27 @@ impl Element for LegalHoldValue { } } +/// The get call's own argument: only the active holds covering an account, +/// through any route (LH-2), for the console's Held badge (LH-14). +#[derive(Debug, Clone, Default)] +pub struct LegalHoldGetArguments { + pub covering_account: Option, +} + +impl<'de> DeserializeArguments<'de> for LegalHoldGetArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "coveringAccount" { + self.covering_account = map.next_value()?; + } else { + let _ = map.next_value::()?; + } + Ok(()) + } +} + /// The set call's own arguments: why (AU-12). #[derive(Debug, Clone, Default)] pub struct LegalHoldSetArguments { @@ -170,7 +191,7 @@ impl JmapObject for LegalHold { type Comparator = (); - type GetArguments = (); + type GetArguments = LegalHoldGetArguments; type SetArguments<'de> = LegalHoldSetArguments; diff --git a/crates/jmap/src/inbuxa/legal_hold.rs b/crates/jmap/src/inbuxa/legal_hold.rs index b4fc1bb..3cce717 100644 --- a/crates/jmap/src/inbuxa/legal_hold.rs +++ b/crates/jmap/src/inbuxa/legal_hold.rs @@ -131,6 +131,25 @@ pub async fn get( } else { Default::default() }; + // LH-14: the holds on one account, whether it's live or deleted and kept + if let Some(account) = request.arguments.covering_account.take() { + let account_id = account.document_id(); + let covering = match server.member_of(account_id).await { + Some(member) => hold::covering(data, &member).await?, + None => match inbuxa_features::undelete::data::kept_account(data, account_id).await? { + Some(kept) => { + hold::covering(data, &common::hold::kept_member(account_id, &kept)).await? + } + None => Vec::new(), + }, + }; + for current in covering { + response + .list + .push(to_value(¤t, &properties, summaries.get(¤t.id))); + } + return Ok(response); + } match ids { None => { for current in hold::all(data).await? { diff --git a/tests/src/system/legal_hold.rs b/tests/src/system/legal_hold.rs index 7e697b1..e7716df 100644 --- a/tests/src/system/legal_hold.rs +++ b/tests/src/system/legal_hold.rs @@ -422,6 +422,19 @@ pub async fn test(test: &mut TestServer) { assert_eq!(summary["accountsCovered"], 1, "LH-9: {response}"); assert_eq!(summary["itemsHeld"], 1, "LH-9: {response}"); assert!(summary["sizeHeld"].as_u64().is_some_and(|s| s > 0), "LH-9: {response}"); + // LH-14: the holds on one account, for the console's Held badge + let (_, response) = admin + .hold_call( + "inbuxa:LegalHold/get", + json!({"coveringAccount": frozen.id_string(), "properties": ["name"]}), + ) + .await; + let mut names = response["list"] + .as_array() + .map(|l| l.iter().filter_map(|h| h["name"].as_str()).collect::>()) + .unwrap_or_default(); + names.sort_unstable(); + assert_eq!(names, vec!["Matter 7001", "Matter 7002"], "LH-14: {response}"); let (_, response) = frozen .hold_call("x:ArchivedItem/set", json!({"destroy": [item_id]}))