diff --git a/crates/features/src/mailflow/rules.rs b/crates/features/src/mailflow/rules.rs index 23456e0..1816523 100644 --- a/crates/features/src/mailflow/rules.rs +++ b/crates/features/src/mailflow/rules.rs @@ -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(ids: &[u32], serializer: S) -> Result { + 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, D::Error> { + Vec::::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, }, SenderGroup { + #[serde(with = "jmap_ids")] groups: Vec, }, SenderTenant { + #[serde(with = "jmap_ids")] tenants: Vec, }, /// Any recipient is one of these. @@ -96,6 +133,7 @@ pub enum Condition { domains: Vec, }, RecipientGroup { + #[serde(with = "jmap_ids")] groups: Vec, }, /// 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::(r#"{"type":"senderTenant","tenants":["!!"]}"#) + .is_err() + ); + } + #[test] fn dlp_rules_have_one_dlp_action_on_outgoing_mail() { let block = Action::Block { diff --git a/tests/src/system/mail_rules.rs b/tests/src/system/mail_rules.rs index 856762f..1a38e75 100644 --- a/tests/src/system/mail_rules.rs +++ b/tests/src/system/mail_rules.rs @@ -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();