Mail rules: group and tenant ids in their JMAP form #112

Merged
jcoffey-dev merged 1 commits from feature/rule-ids-as-jmap-ids into main 2026-09-29 02:36:50 +00:00
2 changed files with 80 additions and 0 deletions
Showing only changes of commit 823d42d528 - Show all commits
+53
View File
@@ -60,6 +60,41 @@ fn one() -> u32 {
1 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. /// A detector and the least it must find.
#[derive(Debug, Clone, PartialEq, Eq, SerdeSerialize, SerdeDeserialize)] #[derive(Debug, Clone, PartialEq, Eq, SerdeSerialize, SerdeDeserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -83,9 +118,11 @@ pub enum Condition {
domains: Vec<String>, domains: Vec<String>,
}, },
SenderGroup { SenderGroup {
#[serde(with = "jmap_ids")]
groups: Vec<u32>, groups: Vec<u32>,
}, },
SenderTenant { SenderTenant {
#[serde(with = "jmap_ids")]
tenants: Vec<u32>, tenants: Vec<u32>,
}, },
/// Any recipient is one of these. /// Any recipient is one of these.
@@ -96,6 +133,7 @@ pub enum Condition {
domains: Vec<String>, domains: Vec<String>,
}, },
RecipientGroup { RecipientGroup {
#[serde(with = "jmap_ids")]
groups: Vec<u32>, groups: Vec<u32>,
}, },
/// Any recipient isn't at a domain this server hosts. /// Any recipient isn't at a domain this server hosts.
@@ -609,6 +647,21 @@ mod tests {
assert_eq!(back["actions"][0]["notifySender"], true); 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] #[test]
fn dlp_rules_have_one_dlp_action_on_outgoing_mail() { fn dlp_rules_have_one_dlp_action_on_outgoing_mail() {
let block = Action::Block { let block = Action::Block {
+27
View File
@@ -114,6 +114,33 @@ pub async fn test(test: &mut TestServer) {
"{rule}" "{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 // Checked when written
let mut inbound = dlp_rule(); let mut inbound = dlp_rule();
inbound["direction"] = "incoming".into(); inbound["direction"] = "incoming".into();