use jmap_types::{impl_string_enum, Id};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MailboxRole {
Inbox,
Trash,
Sent,
Drafts,
Junk,
Archive,
Flagged,
Important,
All,
Other(String),
}
impl_string_enum!(MailboxRole, "a JMAP Mailbox role string",
"inbox" => Inbox,
"trash" => Trash,
"sent" => Sent,
"drafts" => Drafts,
"junk" => Junk,
"archive" => Archive,
"flagged" => Flagged,
"important" => Important,
"all" => All,
);
impl MailboxRole {
pub fn to_wire_str(&self) -> &str {
match self {
Self::Inbox => "inbox",
Self::Trash => "trash",
Self::Sent => "sent",
Self::Drafts => "drafts",
Self::Junk => "junk",
Self::Archive => "archive",
Self::Flagged => "flagged",
Self::Important => "important",
Self::All => "all",
Self::Other(s) => s.as_str(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MailboxRights {
pub may_read_items: bool,
pub may_add_items: bool,
pub may_remove_items: bool,
pub may_set_seen: bool,
pub may_set_keywords: bool,
pub may_create_child: bool,
pub may_rename: bool,
pub may_delete: bool,
pub may_submit: bool,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Mailbox {
pub id: Id,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<MailboxRole>,
pub sort_order: u32,
pub total_emails: u32,
pub unread_emails: u32,
pub total_threads: u32,
pub unread_threads: u32,
pub my_rights: MailboxRights,
pub is_subscribed: bool,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl Mailbox {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: Id,
name: impl Into<String>,
sort_order: u32,
total_emails: u32,
unread_emails: u32,
total_threads: u32,
unread_threads: u32,
my_rights: MailboxRights,
is_subscribed: bool,
) -> Self {
Self {
id,
name: name.into(),
sort_order,
total_emails,
unread_emails,
total_threads,
unread_threads,
my_rights,
is_subscribed,
parent_id: None,
role: None,
extra: serde_json::Map::new(),
}
}
}
fn deserialize_parent_id_three_way<'de, D>(
deserializer: D,
) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
serde_json::Value::deserialize(deserializer).map(Some)
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MailboxFilterCondition {
#[serde(
default,
deserialize_with = "deserialize_parent_id_three_way",
skip_serializing_if = "Option::is_none"
)]
pub parent_id: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_any_role: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_subscribed: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn mailbox_rights_preserves_vendor_extras() {
let raw = json!({
"mayReadItems": true,
"mayAddItems": true,
"mayRemoveItems": false,
"maySetSeen": true,
"maySetKeywords": false,
"mayCreateChild": false,
"mayRename": false,
"mayDelete": false,
"maySubmit": false,
"acmeCorpMayPin": true
});
let rights: MailboxRights = serde_json::from_value(raw).unwrap();
assert_eq!(
rights.extra.get("acmeCorpMayPin").and_then(|v| v.as_bool()),
Some(true)
);
let back = serde_json::to_value(&rights).unwrap();
assert_eq!(back["acmeCorpMayPin"], true);
}
#[test]
fn mailbox_preserves_vendor_extras() {
let raw = json!({
"id": "m1",
"name": "Inbox",
"sortOrder": 0,
"totalEmails": 0,
"unreadEmails": 0,
"totalThreads": 0,
"unreadThreads": 0,
"myRights": {
"mayReadItems": true, "mayAddItems": true, "mayRemoveItems": true,
"maySetSeen": true, "maySetKeywords": true, "mayCreateChild": true,
"mayRename": true, "mayDelete": false, "maySubmit": true
},
"isSubscribed": true,
"acmeCorpColor": "#ff0000"
});
let mbox: Mailbox = serde_json::from_value(raw).unwrap();
assert_eq!(
mbox.extra.get("acmeCorpColor").and_then(|v| v.as_str()),
Some("#ff0000")
);
let back = serde_json::to_value(&mbox).unwrap();
assert_eq!(back["acmeCorpColor"], "#ff0000");
}
#[test]
fn mailbox_filter_parent_id_absent_round_trips() {
let cond: MailboxFilterCondition = serde_json::from_value(json!({})).unwrap();
assert!(cond.parent_id.is_none(), "absent must deserialize as None");
let back = serde_json::to_value(&cond).unwrap();
assert!(
back.get("parentId").is_none(),
"absent parentId must not appear in serialized output"
);
}
#[test]
fn mailbox_filter_parent_id_null_round_trips() {
let cond: MailboxFilterCondition =
serde_json::from_value(json!({"parentId": null})).unwrap();
assert!(
matches!(cond.parent_id, Some(serde_json::Value::Null)),
"explicit null must deserialize as Some(Value::Null), got {:?}",
cond.parent_id
);
let back = serde_json::to_value(&cond).unwrap();
assert_eq!(
back["parentId"],
serde_json::Value::Null,
"Some(Value::Null) must serialize back as null"
);
}
#[test]
fn mailbox_filter_parent_id_string_round_trips() {
let cond: MailboxFilterCondition =
serde_json::from_value(json!({"parentId": "mbox-42"})).unwrap();
match cond.parent_id {
Some(serde_json::Value::String(ref s)) => assert_eq!(s, "mbox-42"),
other => panic!("expected Some(String), got {other:?}"),
}
let back = serde_json::to_value(&cond).unwrap();
assert_eq!(back["parentId"], "mbox-42");
}
}