use jmap_types::Id;
use serde::{Deserialize, Serialize};
pub const JMAP_SIEVE_SCRIPTS_URI: &str = "urn:ietf:params:jmap:sieve";
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SieveScript {
pub id: Id,
pub name: Option<String>,
pub blob_id: Id,
pub is_active: bool,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl SieveScript {
pub fn new(id: Id, blob_id: Id, is_active: bool) -> Self {
Self {
id,
name: None,
blob_id,
is_active,
extra: serde_json::Map::new(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SieveCapability {
pub implementation: String,
}
impl SieveCapability {
pub fn new(implementation: String) -> Self {
Self { implementation }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SieveAccountCapability {
pub max_size_script_name: u64,
pub max_size_script: Option<u64>,
pub max_number_scripts: Option<u64>,
pub max_number_redirects: Option<u64>,
pub sieve_extensions: Vec<String>,
pub notification_methods: Option<Vec<String>>,
pub external_lists: Option<Vec<String>>,
}
impl SieveAccountCapability {
pub fn new(max_size_script_name: u64, sieve_extensions: Vec<String>) -> Self {
Self {
max_size_script_name,
max_size_script: None,
max_number_scripts: None,
max_number_redirects: None,
sieve_extensions,
notification_methods: None,
external_lists: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sieve_script_full_roundtrip() {
let json = r#"{"id":"s1","name":"vacation","blobId":"b1","isActive":true}"#;
let script: SieveScript = serde_json::from_str(json).expect("must parse");
assert_eq!(script.id.as_ref(), "s1");
assert_eq!(script.name.as_deref(), Some("vacation"));
assert_eq!(script.blob_id.as_ref(), "b1");
assert!(script.is_active);
let back = serde_json::to_string(&script).expect("serialize");
assert_eq!(back, json);
}
#[test]
fn sieve_script_null_name_serializes_as_null() {
let json_in = r#"{"id":"s2","name":null,"blobId":"b2","isActive":false}"#;
let script: SieveScript = serde_json::from_str(json_in).expect("must parse");
assert!(script.name.is_none());
let back = serde_json::to_string(&script).expect("serialize");
assert_eq!(back, json_in);
}
#[test]
fn sieve_script_new_defaults() {
let script = SieveScript::new(Id::from("s3"), Id::from("b3"), true);
assert_eq!(script.id.as_ref(), "s3");
assert!(script.name.is_none());
assert_eq!(script.blob_id.as_ref(), "b3");
assert!(script.is_active);
let json = serde_json::to_string(&script).expect("serialize");
assert!(
json.contains("\"name\":null"),
"name must be null when None, not absent"
);
}
#[test]
fn sieve_account_capability_roundtrip() {
let json = r#"{"maxSizeScriptName":512,"maxSizeScript":65536,"maxNumberScripts":5,"maxNumberRedirects":null,"sieveExtensions":["fileinto","reject"],"notificationMethods":null,"externalLists":null}"#;
let cap: SieveAccountCapability = serde_json::from_str(json).expect("must parse");
assert_eq!(cap.max_size_script_name, 512);
assert_eq!(cap.max_size_script, Some(65536));
assert_eq!(cap.max_number_scripts, Some(5));
assert!(cap.max_number_redirects.is_none());
assert_eq!(cap.sieve_extensions, vec!["fileinto", "reject"]);
assert!(cap.notification_methods.is_none());
assert!(cap.external_lists.is_none());
let back = serde_json::to_string(&cap).expect("serialize");
assert_eq!(back, json);
}
#[test]
fn sieve_account_capability_minimal() {
let cap = SieveAccountCapability::new(256, vec!["fileinto".to_owned()]);
let json = serde_json::to_string(&cap).expect("serialize");
assert!(json.contains("\"maxSizeScriptName\""));
assert!(json.contains("\"sieveExtensions\""));
assert!(
json.contains("\"maxSizeScript\":null"),
"must be null when None"
);
assert!(
json.contains("\"maxNumberScripts\":null"),
"must be null when None"
);
assert!(
json.contains("\"maxNumberRedirects\":null"),
"must be null when None"
);
assert!(
json.contains("\"notificationMethods\":null"),
"must be null when None"
);
assert!(
json.contains("\"externalLists\":null"),
"must be null when None"
);
}
#[test]
fn sieve_script_preserves_vendor_extras() {
let raw = serde_json::json!({
"id": "s1",
"name": "vacation",
"blobId": "b1",
"isActive": true,
"acmeCorpSyntaxValidated": true
});
let script: SieveScript = serde_json::from_value(raw).unwrap();
assert_eq!(
script
.extra
.get("acmeCorpSyntaxValidated")
.and_then(|v| v.as_bool()),
Some(true)
);
let back = serde_json::to_value(&script).unwrap();
assert_eq!(back["acmeCorpSyntaxValidated"], true);
}
#[test]
fn sieve_capability_roundtrip() {
let json = r#"{"implementation":"Cyrus Sieve 3.0"}"#;
let cap: SieveCapability = serde_json::from_str(json).expect("must parse");
assert_eq!(cap.implementation, "Cyrus Sieve 3.0");
let back = serde_json::to_string(&cap).expect("serialize");
assert_eq!(back, json);
}
}