jmap_mail_types/mailbox.rs
1//! RFC 8621 §2 Mailbox object and its component types.
2//!
3//! Provides [`Mailbox`], [`MailboxRole`], and [`MailboxRights`].
4//! Mailboxes are the folder containers for [`crate::Email`] objects.
5
6use jmap_types::Id;
7use serde::{Deserialize, Serialize};
8
9/// The role of a Mailbox, identifying its common purpose (RFC 8621 §2).
10///
11/// Values correspond to IMAP Mailbox Name Attributes (RFC 8457), converted to
12/// lowercase. An account is not required to have Mailboxes with any particular
13/// role, and at most one Mailbox per account may hold each role.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum MailboxRole {
17 /// The primary inbox for new incoming messages.
18 Inbox,
19 /// Deleted messages.
20 Trash,
21 /// Sent messages.
22 Sent,
23 /// Unsent draft messages.
24 Drafts,
25 /// Messages identified as likely spam.
26 Junk,
27 /// Archived messages.
28 Archive,
29 /// Messages flagged for follow-up.
30 Flagged,
31 /// Messages considered important.
32 Important,
33 /// Virtual mailbox containing all messages.
34 All,
35 /// Any role string not recognized by this implementation.
36 ///
37 /// RFC 8621 §2: "An unrecognized role SHOULD be treated as if no role were set."
38 ///
39 /// The inner string retains the original value received from the server, so
40 /// this variant round-trips correctly. When sending a `Mailbox/set` request
41 /// for a mailbox whose role came from the server, it is safe to echo the role
42 /// back — or omit it by setting `role` to `None`.
43 Other(String),
44}
45
46impl_string_enum!(MailboxRole, "a JMAP Mailbox role string",
47 "inbox" => Inbox,
48 "trash" => Trash,
49 "sent" => Sent,
50 "drafts" => Drafts,
51 "junk" => Junk,
52 "archive" => Archive,
53 "flagged" => Flagged,
54 "important" => Important,
55 "all" => All,
56);
57
58impl MailboxRole {
59 /// Return the RFC 8621 wire-format string for this role.
60 ///
61 /// Because this method is defined inside the crate that owns `MailboxRole`,
62 /// the match is exhaustive even though the enum is `#[non_exhaustive]`.
63 /// Adding a new variant without updating this method is a compile error.
64 pub fn to_wire_str(&self) -> &str {
65 match self {
66 Self::Inbox => "inbox",
67 Self::Trash => "trash",
68 Self::Sent => "sent",
69 Self::Drafts => "drafts",
70 Self::Junk => "junk",
71 Self::Archive => "archive",
72 Self::Flagged => "flagged",
73 Self::Important => "important",
74 Self::All => "all",
75 Self::Other(s) => s.as_str(),
76 }
77 }
78}
79
80/// Access control rights the authenticated user holds for a Mailbox (RFC 8621 §2).
81///
82/// Backwards compatible with IMAP ACLs (RFC 4314).
83///
84/// `Default` produces all-false (no access), which is the most restrictive valid value
85/// and a safe starting point when constructing rights in tests or server code.
86#[non_exhaustive]
87#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct MailboxRights {
90 /// User may use this Mailbox in Email/query filters and read its emails.
91 pub may_read_items: bool,
92 /// User may add mail to this Mailbox.
93 pub may_add_items: bool,
94 /// User may remove mail from this Mailbox.
95 pub may_remove_items: bool,
96 /// User may add or remove the `$seen` keyword on emails in this Mailbox.
97 pub may_set_seen: bool,
98 /// User may add or remove keywords other than `$seen` on emails.
99 pub may_set_keywords: bool,
100 /// User may create a child Mailbox under this one.
101 pub may_create_child: bool,
102 /// User may rename this Mailbox or move it under another parent.
103 pub may_rename: bool,
104 /// User may delete this Mailbox.
105 pub may_delete: bool,
106 /// Messages may be submitted directly to this Mailbox.
107 pub may_submit: bool,
108}
109
110/// A JMAP Mailbox object (RFC 8621 §2).
111///
112/// Mailboxes are the containers for Email objects. Each Email must belong to
113/// at least one Mailbox. Mailboxes form an acyclic forest via `parent_id`.
114#[non_exhaustive]
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct Mailbox {
118 /// Server-assigned immutable identifier.
119 pub id: Id,
120 /// User-visible name for this Mailbox.
121 pub name: String,
122 /// Id of the parent Mailbox, or `None` if top-level.
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub parent_id: Option<Id>,
125 /// Well-known role identifying the Mailbox's common purpose.
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub role: Option<MailboxRole>,
128 /// Client UI sort position; lower values sort first among siblings.
129 pub sort_order: u32,
130 /// Total number of Emails in this Mailbox (server-set).
131 pub total_emails: u32,
132 /// Number of Emails without `$seen` or `$draft` (server-set).
133 pub unread_emails: u32,
134 /// Number of Threads with at least one Email in this Mailbox (server-set).
135 pub total_threads: u32,
136 /// Number of unread Threads in this Mailbox (server-set).
137 pub unread_threads: u32,
138 /// ACL rights the authenticated user has on this Mailbox (server-set).
139 pub my_rights: MailboxRights,
140 /// Whether the user has subscribed to this Mailbox.
141 pub is_subscribed: bool,
142}
143
144impl Mailbox {
145 /// Construct a [`Mailbox`] from its required fields.
146 ///
147 /// `parent_id` and `role` default to `None`.
148 // Nine arguments because Mailbox has nine required RFC 8621 properties; all
149 // are needed for construction since #[non_exhaustive] prevents struct
150 // literals outside this crate.
151 #[allow(clippy::too_many_arguments)]
152 pub fn new(
153 id: Id,
154 name: impl Into<String>,
155 sort_order: u32,
156 total_emails: u32,
157 unread_emails: u32,
158 total_threads: u32,
159 unread_threads: u32,
160 my_rights: MailboxRights,
161 is_subscribed: bool,
162 ) -> Self {
163 Self {
164 id,
165 name: name.into(),
166 sort_order,
167 total_emails,
168 unread_emails,
169 total_threads,
170 unread_threads,
171 my_rights,
172 is_subscribed,
173 parent_id: None,
174 role: None,
175 }
176 }
177}
178
179/// Filter condition for `Mailbox/query` (RFC 8621 §2.3).
180///
181/// All fields are optional; a condition with no fields set matches every Mailbox.
182///
183/// ## `parentId` semantics
184///
185/// The `parentId` field has three distinct states that must be preserved:
186/// - **absent** (`None`) — do not filter by parent; return mailboxes at any level.
187/// - **`null`** (`Some(serde_json::Value::Null)`) — return only top-level mailboxes
188/// (those with no parent).
189/// - **`"<id>"`** (`Some(serde_json::Value::String(...))`) — return only mailboxes
190/// whose `parentId` equals the given `Id`.
191///
192/// `Option<serde_json::Value>` (with `#[serde(default)]`) preserves this three-way
193/// distinction without a custom deserializer: absent fields deserialize as `None`,
194/// and `null` deserializes as `Some(Value::Null)`.
195#[non_exhaustive]
196#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct MailboxFilterCondition {
199 /// See type-level docs for three-way semantics.
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub parent_id: Option<serde_json::Value>,
202
203 /// Mailbox name must contain this string (case-sensitive substring match).
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub name: Option<String>,
206
207 /// Mailbox role must equal this string (e.g. `"inbox"`, `"trash"`).
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub role: Option<String>,
210
211 /// If `true`, only mailboxes with a non-null role are returned.
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub has_any_role: Option<bool>,
214
215 /// If `true`, only subscribed mailboxes are returned.
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub is_subscribed: Option<bool>,
218}