use jmap_types::{GetObject, JmapObject, PatchObject, QueryObject, SetObject};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MailboxProperty {
Id,
Name,
ParentId,
Role,
SortOrder,
TotalEmails,
UnreadEmails,
TotalThreads,
UnreadThreads,
MyRights,
IsSubscribed,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ThreadProperty {
Id,
EmailIds,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EmailProperty {
Id,
BlobId,
ThreadId,
MailboxIds,
Keywords,
Size,
ReceivedAt,
MessageId,
InReplyTo,
References,
Subject,
From,
To,
Cc,
Bcc,
ReplyTo,
Sender,
SentAt,
HasAttachment,
Preview,
BodyStructure,
TextBody,
HtmlBody,
Attachments,
BodyValues,
Headers,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IdentityProperty {
Id,
Name,
Email,
ReplyTo,
Bcc,
TextSignature,
HtmlSignature,
MayDelete,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EmailSubmissionProperty {
Id,
IdentityId,
EmailId,
ThreadId,
Envelope,
SendAt,
UndoStatus,
DeliveryStatus,
DsnBlobIds,
MdnBlobIds,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum VacationResponseProperty {
Id,
IsEnabled,
FromDate,
ToDate,
Subject,
TextBody,
HtmlBody,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SearchSnippetProperty {
EmailId,
Subject,
Preview,
}
#[cfg(feature = "sieve")]
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SieveScriptProperty {
Id,
Name,
BlobId,
IsActive,
}
impl JmapObject for crate::Mailbox {
const TYPE_NAME: &'static str = "Mailbox";
type Property = MailboxProperty;
}
impl GetObject for crate::Mailbox {}
impl SetObject for crate::Mailbox {
type Patch = PatchObject;
const SERVER_SET: &'static [&'static str] = &[
"id",
"totalEmails",
"unreadEmails",
"totalThreads",
"unreadThreads",
"myRights",
];
const CLIENT_SETTABLE: &'static [&'static str] = &[
"name",
"parentId",
"role",
"sortOrder",
"isSubscribed",
"metadata",
"privateMetadata",
];
const IMMUTABLE: &'static [&'static str] = &[];
}
impl QueryObject for crate::Mailbox {
type Filter = crate::MailboxFilter;
type Comparator = crate::MailboxComparator;
}
impl JmapObject for crate::Thread {
const TYPE_NAME: &'static str = "Thread";
type Property = ThreadProperty;
}
impl GetObject for crate::Thread {}
impl JmapObject for crate::Email {
const TYPE_NAME: &'static str = "Email";
type Property = EmailProperty;
}
impl GetObject for crate::Email {}
impl SetObject for crate::Email {
type Patch = PatchObject;
const SERVER_SET: &'static [&'static str] = &[
"id",
"blobId",
"threadId",
"size",
"hasAttachment",
"preview",
];
const CLIENT_SETTABLE: &'static [&'static str] = &[
"mailboxIds",
"keywords",
"receivedAt",
"messageId",
"inReplyTo",
"references",
"sender",
"from",
"to",
"cc",
"bcc",
"replyTo",
"subject",
"sentAt",
"headers",
"bodyStructure",
"bodyValues",
"textBody",
"htmlBody",
"attachments",
"metadata",
"privateMetadata",
];
const IMMUTABLE: &'static [&'static str] = &[
"receivedAt",
"messageId",
"inReplyTo",
"references",
"sender",
"from",
"to",
"cc",
"bcc",
"replyTo",
"subject",
"sentAt",
"headers",
"bodyStructure",
"bodyValues",
"textBody",
"htmlBody",
"attachments",
];
}
impl QueryObject for crate::Email {
type Filter = crate::EmailFilter;
type Comparator = crate::EmailComparator;
}
impl JmapObject for crate::Identity {
const TYPE_NAME: &'static str = "Identity";
type Property = IdentityProperty;
}
impl GetObject for crate::Identity {}
impl SetObject for crate::Identity {
type Patch = PatchObject;
const SERVER_SET: &'static [&'static str] = &["id", "mayDelete"];
const CLIENT_SETTABLE: &'static [&'static str] = &[
"name",
"email",
"replyTo",
"bcc",
"textSignature",
"htmlSignature",
];
const IMMUTABLE: &'static [&'static str] = &["email"];
}
impl JmapObject for crate::EmailSubmission {
const TYPE_NAME: &'static str = "EmailSubmission";
type Property = EmailSubmissionProperty;
}
impl GetObject for crate::EmailSubmission {}
impl SetObject for crate::EmailSubmission {
type Patch = PatchObject;
const SERVER_SET: &'static [&'static str] = &[
"id",
"threadId",
"sendAt",
"deliveryStatus",
"dsnBlobIds",
"mdnBlobIds",
];
const CLIENT_SETTABLE: &'static [&'static str] =
&["identityId", "emailId", "envelope", "undoStatus"];
const IMMUTABLE: &'static [&'static str] = &["identityId", "emailId", "envelope"];
}
impl QueryObject for crate::EmailSubmission {
type Filter = crate::EmailSubmissionFilter;
type Comparator = crate::EmailSubmissionComparator;
}
impl JmapObject for crate::VacationResponse {
const TYPE_NAME: &'static str = "VacationResponse";
type Property = VacationResponseProperty;
}
impl GetObject for crate::VacationResponse {}
impl SetObject for crate::VacationResponse {
type Patch = PatchObject;
const SERVER_SET: &'static [&'static str] = &["id"];
const CLIENT_SETTABLE: &'static [&'static str] = &[
"isEnabled",
"fromDate",
"toDate",
"subject",
"textBody",
"htmlBody",
];
const IMMUTABLE: &'static [&'static str] = &[];
}
impl JmapObject for crate::SearchSnippet {
const TYPE_NAME: &'static str = "SearchSnippet";
type Property = SearchSnippetProperty;
}
#[cfg(feature = "sieve")]
impl JmapObject for crate::sieve::SieveScript {
const TYPE_NAME: &'static str = "SieveScript";
type Property = SieveScriptProperty;
}
#[cfg(feature = "sieve")]
impl GetObject for crate::sieve::SieveScript {}
#[cfg(feature = "sieve")]
impl SetObject for crate::sieve::SieveScript {
type Patch = PatchObject;
const SERVER_SET: &'static [&'static str] = &["id", "isActive"];
const CLIENT_SETTABLE: &'static [&'static str] = &["name", "blobId"];
const IMMUTABLE: &'static [&'static str] = &[];
}
#[cfg(test)]
mod tests {
use jmap_types::{Date, Id, UTCDate, check_property_classification};
use serde_json::{Map, Value, json};
use std::collections::BTreeMap;
fn de<T: serde::de::DeserializeOwned>(v: Value) -> T {
serde_json::from_value(v).expect("hand-written fixture deserializes")
}
fn addrs() -> Option<Vec<crate::EmailAddress>> {
Some(de(json!([{"name": "A", "email": "a@example.com"}])))
}
fn part() -> crate::EmailBodyPart {
de(json!({"partId": "1", "type": "text/plain"}))
}
#[test]
fn mailbox_properties_are_classified() {
let obj = crate::Mailbox {
id: Id::from("m1"),
name: "Inbox".into(),
parent_id: Some(Id::from("m0")),
role: Some(de(json!("inbox"))),
sort_order: 1,
total_emails: 1,
unread_emails: 1,
total_threads: 1,
unread_threads: 1,
my_rights: crate::MailboxRights::default(),
is_subscribed: true,
metadata: Some(de(json!({"acme.example.com": {"memo": "m"}}))),
private_metadata: Some(de(json!({"acme.example.com": {"memo": "p"}}))),
extra: Map::new(),
};
assert_eq!(check_property_classification(&obj), Ok(()));
}
#[test]
fn email_properties_are_classified() {
let obj = crate::Email {
id: Id::from("e1"),
blob_id: Id::from("b1"),
thread_id: Id::from("t1"),
mailbox_ids: BTreeMap::from([(Id::from("m1"), true)]),
keywords: de(json!({"$seen": true})),
size: 1,
received_at: UTCDate::from("2026-01-01T00:00:00Z"),
message_id: Some(vec!["a@example.com".into()]),
in_reply_to: Some(vec!["b@example.com".into()]),
references: Some(vec!["b@example.com".into()]),
sender: addrs(),
from: addrs(),
to: addrs(),
cc: addrs(),
bcc: addrs(),
reply_to: addrs(),
subject: Some("s".into()),
sent_at: Some(Date::from("2026-01-01T00:00:00Z")),
headers: de(json!([{"name": "Subject", "value": " s"}])),
body_values: de(json!({"1": {"value": "v"}})),
text_body: vec![part()],
html_body: vec![part()],
attachments: vec![part()],
body_structure: Some(part()),
has_attachment: true,
preview: Some("p".into()),
metadata: Some(de(json!({"acme.example.com": {"memo": "m"}}))),
private_metadata: Some(de(json!({"acme.example.com": {"memo": "p"}}))),
extra: Map::new(),
};
assert_eq!(check_property_classification(&obj), Ok(()));
}
#[test]
fn identity_properties_are_classified() {
let obj = crate::Identity {
id: Id::from("i1"),
name: "A".into(),
email: "a@example.com".into(),
reply_to: addrs(),
bcc: addrs(),
text_signature: "t".into(),
html_signature: "h".into(),
may_delete: true,
extra: Map::new(),
};
assert_eq!(check_property_classification(&obj), Ok(()));
}
#[test]
fn email_submission_properties_are_classified() {
let obj = crate::EmailSubmission {
id: Id::from("s1"),
identity_id: Id::from("i1"),
email_id: Id::from("e1"),
thread_id: Id::from("t1"),
envelope: Some(de(json!({
"mailFrom": {"email": "a@example.com"},
"rcptTo": [{"email": "b@example.com"}],
}))),
send_at: UTCDate::from("2026-01-01T00:00:00Z"),
undo_status: de(json!("pending")),
delivery_status: Some(de(json!({"b@example.com": {
"smtpReply": "250 OK",
"delivered": "queued",
"displayed": "unknown",
}}))),
dsn_blob_ids: vec![Id::from("d1")],
mdn_blob_ids: vec![Id::from("n1")],
extra: Map::new(),
};
assert_eq!(check_property_classification(&obj), Ok(()));
}
#[test]
fn vacation_response_properties_are_classified() {
let obj = crate::VacationResponse {
id: Id::from("singleton"),
is_enabled: true,
from_date: Some(UTCDate::from("2026-01-01T00:00:00Z")),
to_date: Some(UTCDate::from("2026-02-01T00:00:00Z")),
subject: Some("s".into()),
text_body: Some("t".into()),
html_body: Some("h".into()),
extra: Map::new(),
};
assert_eq!(check_property_classification(&obj), Ok(()));
}
#[cfg(feature = "sieve")]
#[test]
fn sieve_script_properties_are_classified() {
let obj = crate::SieveScript {
id: Id::from("s1"),
name: Some("n".into()),
blob_id: Id::from("b1"),
is_active: true,
extra: Map::new(),
};
assert_eq!(check_property_classification(&obj), Ok(()));
}
}