Skip to main content

rusmes_jmap/
types.rs

1//! JMAP type definitions
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// JMAP request
8#[derive(Debug, Clone, Deserialize, Serialize)]
9#[serde(rename_all = "camelCase")]
10pub struct JmapRequest {
11    pub using: Vec<String>,
12    pub method_calls: Vec<JmapMethodCall>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub created_ids: Option<serde_json::Value>,
15}
16
17/// JMAP method call
18#[derive(Debug, Clone, Deserialize, Serialize)]
19pub struct JmapMethodCall(pub String, pub serde_json::Value, pub String);
20
21/// JMAP response
22#[derive(Debug, Clone, Default, Deserialize, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct JmapResponse {
25    pub method_responses: Vec<JmapMethodResponse>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub session_state: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub created_ids: Option<serde_json::Value>,
30}
31
32/// JMAP method response
33#[derive(Debug, Clone, Deserialize, Serialize)]
34pub struct JmapMethodResponse(pub String, pub serde_json::Value, pub String);
35
36/// JMAP methods
37#[derive(Debug, Clone)]
38pub enum JmapMethod {
39    /// Email/get
40    EmailGet,
41    /// Email/set
42    EmailSet,
43    /// Email/query
44    EmailQuery,
45    /// Mailbox/get
46    MailboxGet,
47    /// Mailbox/set
48    MailboxSet,
49    /// Mailbox/query
50    MailboxQuery,
51}
52
53/// JMAP error types as defined in RFC 8620 Section 3.6
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum JmapErrorType {
56    /// The content type of the request was not "application/json" or the request did not parse as I-JSON.
57    NotJson,
58    /// The request parsed as JSON but did not match the structure defined in RFC 8620 Section 3.3.
59    NotRequest,
60    /// The server has a limit on the number of calls in a single request
61    Limit,
62    /// Unknown capability in "using" property
63    UnknownCapability,
64    /// Unknown method
65    UnknownMethod,
66    /// Invalid arguments to method
67    InvalidArguments,
68    /// Account not found or does not support this data type
69    AccountNotFound,
70    /// Account not supported by this method
71    AccountNotSupportedByMethod,
72    /// Account is read-only
73    AccountReadOnly,
74    /// Server error
75    ServerFail,
76    /// Server is unavailable
77    ServerUnavailable,
78    /// Server has a hard limit on the number of objects
79    ServerPartialFailure,
80    /// The authenticated principal is not permitted to access the requested account.
81    ///
82    /// Per RFC 8620 §3.6 there is no top-level `forbidden` error code (the closest
83    /// standardized concept is the `forbidden` `setError` in §5.3); we use this
84    /// dedicated method-level error so that ownership-mismatch responses are
85    /// distinguishable from `accountNotFound` (which would also be RFC-defensible
86    /// because it does not reveal whether the foreign account exists).
87    Forbidden,
88}
89
90impl JmapErrorType {
91    /// Get the string representation of the error type
92    pub fn as_str(&self) -> &'static str {
93        match self {
94            Self::NotJson => "urn:ietf:params:jmap:error:notJSON",
95            Self::NotRequest => "urn:ietf:params:jmap:error:notRequest",
96            Self::Limit => "urn:ietf:params:jmap:error:limit",
97            Self::UnknownCapability => "urn:ietf:params:jmap:error:unknownCapability",
98            Self::UnknownMethod => "urn:ietf:params:jmap:error:unknownMethod",
99            Self::InvalidArguments => "urn:ietf:params:jmap:error:invalidArguments",
100            Self::AccountNotFound => "urn:ietf:params:jmap:error:accountNotFound",
101            Self::AccountNotSupportedByMethod => {
102                "urn:ietf:params:jmap:error:accountNotSupportedByMethod"
103            }
104            Self::AccountReadOnly => "urn:ietf:params:jmap:error:accountReadOnly",
105            Self::ServerFail => "urn:ietf:params:jmap:error:serverFail",
106            Self::ServerUnavailable => "urn:ietf:params:jmap:error:serverUnavailable",
107            Self::ServerPartialFailure => "urn:ietf:params:jmap:error:serverPartialFailure",
108            Self::Forbidden => "urn:ietf:params:jmap:error:forbidden",
109        }
110    }
111}
112
113/// Authenticated principal — attached to every authorized JMAP request by the
114/// auth middleware (`crate::auth::JmapAuthLayer`).
115///
116/// Method handlers receive `&Principal` and use it to enforce that the
117/// `accountId` named in each JMAP request belongs to the authenticated
118/// caller. See [`Principal::owns_account`].
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Principal {
121    /// Username of the authenticated user (e.g. `alice@example.com`).
122    pub username: String,
123    /// Canonical account identifier this principal owns.
124    ///
125    /// JMAP requests carrying a different `accountId` are rejected with
126    /// [`JmapErrorType::Forbidden`].
127    pub account_id: String,
128    /// Granted scopes (e.g. capability URIs the principal is allowed to use).
129    /// An empty set means "all scopes" — refine when scope-based authorization
130    /// is wired in (see follow-ups in `TODO.md`).
131    pub scopes: Vec<String>,
132}
133
134/// Scope value granted to administrative principals — bypasses the per-account
135/// ownership check in [`Principal::owns_account`]. Currently used by the
136/// in-tree test fixtures that exercise the dispatch layer; production
137/// administrative consoles will want it too.
138pub const SCOPE_ADMIN: &str = "rusmes:admin:any-account";
139
140/// Build an admin-scoped principal for use in unit tests.
141///
142/// The returned principal owns every `accountId` because it carries the
143/// [`SCOPE_ADMIN`] scope. Test fixtures that want to specifically exercise
144/// ownership enforcement should construct their own [`Principal`] manually
145/// instead of using this helper.
146#[doc(hidden)]
147pub fn admin_principal_for_tests() -> Principal {
148    Principal {
149        username: "test-admin".to_string(),
150        account_id: "test-admin-account".to_string(),
151        scopes: vec![SCOPE_ADMIN.to_string()],
152    }
153}
154
155impl Principal {
156    /// Build a principal from a username, deriving the canonical `account_id`
157    /// the same way the session endpoint does.
158    pub fn from_username(username: impl Into<String>) -> Self {
159        let username = username.into();
160        let account_id = derive_account_id(&username);
161        Self {
162            username,
163            account_id,
164            scopes: Vec::new(),
165        }
166    }
167
168    /// True iff `requested_account_id` equals this principal's owned account
169    /// OR this principal has been granted the [`SCOPE_ADMIN`] scope.
170    pub fn owns_account(&self, requested_account_id: &str) -> bool {
171        self.account_id == requested_account_id || self.scopes.iter().any(|s| s == SCOPE_ADMIN)
172    }
173}
174
175/// Canonical mapping from a username to the account ID exposed in the JMAP
176/// session. Centralized here so the session endpoint, auth middleware and
177/// tests all agree on the same scheme.
178pub fn derive_account_id(username: &str) -> String {
179    format!("account-{}", username.replace('@', "-"))
180}
181
182/// JMAP error response
183#[derive(Debug, Clone, Serialize)]
184pub struct JmapError {
185    #[serde(rename = "type")]
186    pub error_type: String,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub status: Option<u16>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub detail: Option<String>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub limit: Option<String>,
193}
194
195impl JmapError {
196    /// Create a new JMAP error
197    pub fn new(error_type: JmapErrorType) -> Self {
198        Self {
199            error_type: error_type.as_str().to_string(),
200            status: None,
201            detail: None,
202            limit: None,
203        }
204    }
205
206    /// Set the status code
207    pub fn with_status(mut self, status: u16) -> Self {
208        self.status = Some(status);
209        self
210    }
211
212    /// Set the detail message
213    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
214        self.detail = Some(detail.into());
215        self
216    }
217
218    /// Set the limit information
219    pub fn with_limit(mut self, limit: impl Into<String>) -> Self {
220        self.limit = Some(limit.into());
221        self
222    }
223}
224
225/// RFC 8620 §5.1 PushSubscription — a registered WebPush endpoint that the
226/// server notifies whenever the principal's data changes.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct PushSubscription {
229    /// Server-assigned unique identifier.
230    pub id: String,
231
232    /// Opaque client-chosen identifier used to deduplicate registrations from
233    /// the same device across sessions.
234    #[serde(rename = "deviceClientId")]
235    pub device_client_id: String,
236
237    /// HTTPS URL of the push endpoint (RFC 8030).
238    pub url: String,
239
240    /// Optional Web Crypto key material for encrypted push (RFC 8291).
241    /// When `None`, the server sends an unencrypted "tickle" (empty body).
242    pub keys: Option<PushKeys>,
243
244    /// Short-lived secret the server sends to the push endpoint for out-of-band
245    /// verification.  Held in memory only; never serialized to API responses.
246    #[serde(skip)]
247    pub verification_code: Option<String>,
248
249    /// RFC 3339 expiry.  When `Some` and in the past the subscription is
250    /// silently dropped on the next delivery attempt.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub expires: Option<chrono::DateTime<chrono::Utc>>,
253
254    /// Data-type names (e.g. `"Email"`, `"Mailbox"`) this subscription monitors.
255    /// An empty list means "all types".
256    pub types: Vec<String>,
257
258    /// Whether the subscription has been verified via the out-of-band
259    /// verification push.  Unverified subscriptions are never used for delivery.
260    /// Not serialized — internal state only.
261    #[serde(skip)]
262    pub verified: bool,
263
264    /// The `account_id` of the principal that owns this subscription.
265    /// Not serialized — internal state only.
266    #[serde(skip)]
267    pub principal_id: String,
268}
269
270/// Web Crypto key material for RFC 8291 encrypted push.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct PushKeys {
273    /// Base64url-encoded client public key (P-256 uncompressed point).
274    pub p256dh: String,
275    /// Base64url-encoded 16-byte auth secret.
276    pub auth: String,
277}
278
279/// Email address in JMAP format
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct EmailAddress {
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub name: Option<String>,
284    pub email: String,
285}
286
287impl EmailAddress {
288    /// Create a new email address
289    pub fn new(email: String) -> Self {
290        Self { name: None, email }
291    }
292
293    /// Create a new email address with name
294    pub fn with_name(email: String, name: String) -> Self {
295        Self {
296            name: Some(name),
297            email,
298        }
299    }
300}
301
302/// Email object as defined in RFC 8621
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct Email {
306    /// Unique identifier for the email
307    pub id: String,
308    /// Blob ID for the raw RFC 5322 message
309    pub blob_id: String,
310    /// Thread ID
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub thread_id: Option<String>,
313    /// Mailbox IDs (map of mailbox ID to boolean)
314    pub mailbox_ids: HashMap<String, bool>,
315    /// Keywords/flags (e.g., $seen, $flagged, $draft)
316    pub keywords: HashMap<String, bool>,
317    /// Size in bytes
318    pub size: u64,
319    /// Time email was received at the server
320    pub received_at: DateTime<Utc>,
321    /// Message-ID header
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub message_id: Option<Vec<String>>,
324    /// In-Reply-To header
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub in_reply_to: Option<Vec<String>>,
327    /// References header
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub references: Option<Vec<String>>,
330    /// Sender header
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub sender: Option<Vec<EmailAddress>>,
333    /// From header
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub from: Option<Vec<EmailAddress>>,
336    /// To header
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub to: Option<Vec<EmailAddress>>,
339    /// Cc header
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub cc: Option<Vec<EmailAddress>>,
342    /// Bcc header
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub bcc: Option<Vec<EmailAddress>>,
345    /// Reply-To header
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub reply_to: Option<Vec<EmailAddress>>,
348    /// Subject header
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub subject: Option<String>,
351    /// Sent-At date from Date header
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub sent_at: Option<DateTime<Utc>>,
354    /// Has attachment
355    pub has_attachment: bool,
356    /// Preview text
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub preview: Option<String>,
359    /// Body values (for body parts)
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub body_values: Option<HashMap<String, EmailBodyValue>>,
362    /// Text body parts
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub text_body: Option<Vec<EmailBodyPart>>,
365    /// HTML body parts
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub html_body: Option<Vec<EmailBodyPart>>,
368    /// Attachments
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub attachments: Option<Vec<EmailBodyPart>>,
371}
372
373/// Email body value
374#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct EmailBodyValue {
377    pub value: String,
378    pub is_encoding_problem: bool,
379    pub is_truncated: bool,
380}
381
382/// Email body part
383#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct EmailBodyPart {
386    pub part_id: String,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub blob_id: Option<String>,
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub size: Option<u64>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub name: Option<String>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub r#type: Option<String>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub charset: Option<String>,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub disposition: Option<String>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub cid: Option<String>,
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub language: Option<Vec<String>>,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub location: Option<String>,
405}
406
407/// Email/get request
408#[derive(Debug, Clone, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct EmailGetRequest {
411    pub account_id: String,
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub ids: Option<Vec<String>>,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub properties: Option<Vec<String>>,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub body_properties: Option<Vec<String>>,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub fetch_text_body_values: Option<bool>,
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub fetch_html_body_values: Option<bool>,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub fetch_all_body_values: Option<bool>,
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub max_body_value_bytes: Option<u64>,
426}
427
428/// Email/get response
429#[derive(Debug, Clone, Serialize)]
430#[serde(rename_all = "camelCase")]
431pub struct EmailGetResponse {
432    pub account_id: String,
433    pub state: String,
434    pub list: Vec<Email>,
435    pub not_found: Vec<String>,
436}
437
438/// Email/set request
439#[derive(Debug, Clone, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct EmailSetRequest {
442    pub account_id: String,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub if_in_state: Option<String>,
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub create: Option<HashMap<String, EmailSetObject>>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub update: Option<HashMap<String, serde_json::Value>>,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub destroy: Option<Vec<String>>,
451}
452
453/// Email object for Email/set create (RFC 8621 §5.2)
454#[derive(Debug, Clone, Deserialize)]
455#[serde(rename_all = "camelCase")]
456pub struct EmailSetObject {
457    /// Mailboxes to file this message into (mailbox ID → true)
458    pub mailbox_ids: HashMap<String, bool>,
459    /// Initial keywords/flags
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub keywords: Option<HashMap<String, bool>>,
462    /// Override for the received-at timestamp
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub received_at: Option<DateTime<Utc>>,
465    /// From header addresses
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub from: Option<Vec<EmailAddress>>,
468    /// To header addresses
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub to: Option<Vec<EmailAddress>>,
471    /// Cc header addresses
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub cc: Option<Vec<EmailAddress>>,
474    /// Bcc header addresses
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub bcc: Option<Vec<EmailAddress>>,
477    /// Reply-To header addresses
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub reply_to: Option<Vec<EmailAddress>>,
480    /// Sender header addresses
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub sender: Option<Vec<EmailAddress>>,
483    /// Subject header
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub subject: Option<String>,
486    /// Date sent (encoded into the Date header)
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub sent_at: Option<DateTime<Utc>>,
489    /// In-Reply-To message ID header values
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub in_reply_to: Option<Vec<String>>,
492    /// References header values
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub references: Option<Vec<String>>,
495    /// Message-ID header values
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub message_id: Option<Vec<String>>,
498    /// Body part values (keyed by part ID)
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub body_values: Option<HashMap<String, EmailBodyValue>>,
501    /// Ordered text-body part references
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub text_body: Option<Vec<EmailBodyPart>>,
504    /// Ordered HTML-body part references
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub html_body: Option<Vec<EmailBodyPart>>,
507    /// Attachments
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub attachments: Option<Vec<EmailBodyPart>>,
510}
511
512/// Email/set response
513#[derive(Debug, Clone, Serialize)]
514#[serde(rename_all = "camelCase")]
515pub struct EmailSetResponse {
516    pub account_id: String,
517    pub old_state: String,
518    pub new_state: String,
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub created: Option<HashMap<String, Email>>,
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub updated: Option<HashMap<String, Option<Email>>>,
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub destroyed: Option<Vec<String>>,
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub not_created: Option<HashMap<String, JmapSetError>>,
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub not_updated: Option<HashMap<String, JmapSetError>>,
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub not_destroyed: Option<HashMap<String, JmapSetError>>,
531}
532
533/// JMAP set error
534#[derive(Debug, Clone, Serialize)]
535pub struct JmapSetError {
536    #[serde(rename = "type")]
537    pub error_type: String,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub description: Option<String>,
540}
541
542/// Email/query request
543#[derive(Debug, Clone, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct EmailQueryRequest {
546    pub account_id: String,
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub filter: Option<EmailFilterCondition>,
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub sort: Option<Vec<EmailSort>>,
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub position: Option<i64>,
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub anchor: Option<String>,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub anchor_offset: Option<i64>,
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub limit: Option<u64>,
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub calculate_total: Option<bool>,
561}
562
563/// Email filter condition
564#[derive(Debug, Clone, Deserialize)]
565#[serde(rename_all = "camelCase")]
566pub struct EmailFilterCondition {
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub in_mailbox: Option<String>,
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub in_mailbox_other_than: Option<Vec<String>>,
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub before: Option<DateTime<Utc>>,
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub after: Option<DateTime<Utc>>,
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub min_size: Option<u64>,
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub max_size: Option<u64>,
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub all_in_thread_have_keyword: Option<String>,
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub some_in_thread_have_keyword: Option<String>,
583    #[serde(skip_serializing_if = "Option::is_none")]
584    pub none_in_thread_have_keyword: Option<String>,
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub has_keyword: Option<String>,
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub not_keyword: Option<String>,
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub has_attachment: Option<bool>,
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub text: Option<String>,
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub from: Option<String>,
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub to: Option<String>,
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub cc: Option<String>,
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub bcc: Option<String>,
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub subject: Option<String>,
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub body: Option<String>,
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub header: Option<Vec<String>>,
607}
608
609/// Email sort comparator
610#[derive(Debug, Clone, Deserialize)]
611#[serde(rename_all = "camelCase")]
612pub struct EmailSort {
613    pub property: String,
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub is_ascending: Option<bool>,
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub collation: Option<String>,
618}
619
620/// Email/query response
621#[derive(Debug, Clone, Serialize)]
622#[serde(rename_all = "camelCase")]
623pub struct EmailQueryResponse {
624    pub account_id: String,
625    pub query_state: String,
626    pub can_calculate_changes: bool,
627    pub position: i64,
628    pub ids: Vec<String>,
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub total: Option<u64>,
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub limit: Option<u64>,
633}