Mail rules: group and tenant ids in their JMAP form
ci / fork-checks (pull_request) Successful in 2m26s
ci / build (pull_request) Successful in 5m47s

senderGroup, senderTenant and recipientGroup conditions now read and write group and tenant ids as JMAP ids ("b", "c"…), like legal hold scopes and the rest of the API, so the console can use its object pickers; plain numbers are still read. Held as numbers for matching. Unit test for both forms and a bad id; mail_rules_tests round-trips a tenant condition over JMAP.
This commit is contained in:
2026-09-28 19:30:28 -07:00
parent de514115dd
commit 823d42d528
2 changed files with 80 additions and 0 deletions
+53
View File
@@ -60,6 +60,41 @@ fn one() -> u32 {
1
}
/// Group and tenant ids in the JMAP form clients use (`"b"`, `"c"`…), held
/// as numbers for matching. Plain numbers are read too.
mod jmap_ids {
use serde::{Deserialize, Deserializer, Serializer, de::Error, ser::SerializeSeq};
use std::str::FromStr;
use types::id::Id;
pub fn serialize<S: Serializer>(ids: &[u32], serializer: S) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(ids.len()))?;
for id in ids {
seq.serialize_element(&Id::from(*id).to_string())?;
}
seq.end()
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Either {
Text(String),
Number(u32),
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u32>, D::Error> {
Vec::<Either>::deserialize(deserializer)?
.into_iter()
.map(|id| match id {
Either::Number(n) => Ok(n),
Either::Text(text) => Id::from_str(&text)
.map(|id| id.document_id())
.map_err(|_| D::Error::custom(format!("\"{text}\" isn't an id"))),
})
.collect()
}
}
/// A detector and the least it must find.
#[derive(Debug, Clone, PartialEq, Eq, SerdeSerialize, SerdeDeserialize)]
#[serde(rename_all = "camelCase")]
@@ -83,9 +118,11 @@ pub enum Condition {
domains: Vec<String>,
},
SenderGroup {
#[serde(with = "jmap_ids")]
groups: Vec<u32>,
},
SenderTenant {
#[serde(with = "jmap_ids")]
tenants: Vec<u32>,
},
/// Any recipient is one of these.
@@ -96,6 +133,7 @@ pub enum Condition {
domains: Vec<String>,
},
RecipientGroup {
#[serde(with = "jmap_ids")]
groups: Vec<u32>,
},
/// Any recipient isn't at a domain this server hosts.
@@ -609,6 +647,21 @@ mod tests {
assert_eq!(back["actions"][0]["notifySender"], true);
}
#[test]
fn group_and_tenant_ids_are_jmap_ids() {
let condition: Condition =
serde_json::from_str(r#"{"type":"senderGroup","groups":["b", 7]}"#).unwrap();
assert_eq!(condition, Condition::SenderGroup { groups: vec![1, 7] });
assert_eq!(
serde_json::to_value(&condition).unwrap()["groups"],
serde_json::json!(["b", "h"])
);
assert!(
serde_json::from_str::<Condition>(r#"{"type":"senderTenant","tenants":["!!"]}"#)
.is_err()
);
}
#[test]
fn dlp_rules_have_one_dlp_action_on_outgoing_mail() {
let block = Action::Block {
+27
View File
@@ -114,6 +114,33 @@ pub async fn test(test: &mut TestServer) {
"{rule}"
);
// Group and tenant ids travel in their JMAP form
let mut by_tenant = transport_rule();
by_tenant["name"] = "By tenant".into();
by_tenant["conditions"] = json!([{"type": "senderTenant", "tenants": ["b"]}]);
let (_, response) = call(
&admin,
"inbuxa:MailRule/set",
json!({"create": {"g": by_tenant}}),
)
.await;
let tenant_rule = response["created"]["g"]["id"]
.as_str()
.unwrap_or_else(|| panic!("{response}"))
.to_string();
let (_, response) = call(&admin, "inbuxa:MailRule/get", json!({"ids": [tenant_rule]})).await;
assert_eq!(
response["list"][0]["conditions"][0]["tenants"],
json!(["b"]),
"{response}"
);
call(
&admin,
"inbuxa:MailRule/set",
json!({"destroy": [tenant_rule]}),
)
.await;
// Checked when written
let mut inbound = dlp_rule();
inbound["direction"] = "incoming".into();