Skip to main content

io_jmap/rfc8621/mailbox/
query.rs

1//! Batched JMAP `Mailbox/query` + `Mailbox/get` coroutine (RFC 8621 §2.4–2.5):
2//! single HTTP request, server-side `#ids` back-reference resolves the get
3//! against the query results.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::{
9//!     io::{Read, Write},
10//!     net::TcpStream,
11//! };
12//!
13//! use io_jmap::{
14//!     coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
15//!     rfc8620::JmapSession,
16//!     rfc8621::mailbox::query::{JmapMailboxQuery, JmapMailboxQueryOptions},
17//! };
18//! use secrecy::SecretString;
19//!
20//! // Ready stream needed (TCP-connected, TLS-negociated)
21//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
22//! let mut buf = [0u8; 4096];
23//!
24//! let session: JmapSession = serde_json::from_str(r#"{
25//!     "username": "",
26//!     "accounts": {},
27//!     "primaryAccounts": {"urn:ietf:params:jmap:mail": "a1"},
28//!     "capabilities": {},
29//!     "apiUrl": "https://api.example.com/jmap/",
30//!     "downloadUrl": "",
31//!     "uploadUrl": "",
32//!     "eventSourceUrl": "",
33//!     "state": ""
34//! }"#).unwrap();
35//! let auth = SecretString::from("Bearer xyz");
36//! let mut coroutine =
37//!     JmapMailboxQuery::new(&session, &auth, JmapMailboxQueryOptions::default()).unwrap();
38//! let mut arg = None;
39//!
40//! let out = loop {
41//!     match coroutine.resume(arg.take()) {
42//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
43//!             stream.write_all(&bytes).unwrap();
44//!         }
45//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
46//!             let n = stream.read(&mut buf).unwrap();
47//!             arg = Some(&buf[..n]);
48//!         }
49//!         JmapCoroutineState::Complete(Ok(out)) => break out,
50//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
51//!     }
52//! };
53//!
54//! println!("{} mailboxes", out.mailboxes.len());
55//! ```
56
57use core::fmt;
58
59use alloc::{string::String, vec, vec::Vec};
60
61use log::trace;
62use secrecy::SecretString;
63use serde::{Deserialize, Serialize};
64use thiserror::Error;
65
66use crate::{
67    coroutine::*,
68    jmap_try,
69    rfc8620::{
70        CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapResultReference, JmapSession, send::*,
71    },
72    rfc8621::{
73        MAIL_CAPABILITY,
74        mailbox::{JmapMailbox, JmapMailboxFilter, JmapMailboxProperty, JmapMailboxSortComparator},
75    },
76};
77
78/// Failure causes during a batched JMAP `Mailbox/query` + `Mailbox/get` flow.
79#[derive(Debug, Error)]
80pub enum JmapMailboxQueryError {
81    #[error("JMAP Mailbox/query failed: missing Mailbox/query response in method_responses")]
82    MissingQueryResponse,
83    #[error("JMAP Mailbox/query failed: missing Mailbox/get response in method_responses")]
84    MissingGetResponse,
85    #[error("JMAP Mailbox/query failed: {0}")]
86    Send(#[from] JmapSendError),
87    #[error("JMAP Mailbox/query failed: serialize args: {0}")]
88    SerializeArgs(#[source] serde_json::Error),
89    #[error("JMAP Mailbox/query failed: parse Mailbox/query response: {0}")]
90    ParseQueryResponse(#[source] serde_json::Error),
91    #[error("JMAP Mailbox/query failed: parse Mailbox/get response: {0}")]
92    ParseGetResponse(#[source] serde_json::Error),
93    #[error("JMAP Mailbox/query failed: Mailbox/query: {0}")]
94    QueryMethod(JmapMethodError),
95    #[error("JMAP Mailbox/query failed: Mailbox/get: {0}")]
96    GetMethod(JmapMethodError),
97}
98
99/// Options for [`JmapMailboxQuery::new`].
100#[derive(Clone, Debug, Default)]
101pub struct JmapMailboxQueryOptions {
102    /// Filter criteria; `None` matches all mailboxes.
103    pub filter: Option<JmapMailboxFilter>,
104    /// Sort order; `None` uses the server default.
105    pub sort: Option<Vec<JmapMailboxSortComparator>>,
106    /// Zero-based offset into the result list.
107    pub position: Option<u64>,
108    /// Max number of mailboxes to return.
109    pub limit: Option<u64>,
110    /// Mailbox properties to fetch; `None` returns all.
111    pub properties: Option<Vec<JmapMailboxProperty>>,
112}
113
114/// Successful terminal output of [`JmapMailboxQuery`].
115#[derive(Clone, Debug)]
116pub struct JmapMailboxQueryOutput {
117    pub mailboxes: Vec<JmapMailbox>,
118    pub total: Option<u64>,
119    pub position: u64,
120    pub query_state: String,
121    pub keep_alive: bool,
122}
123
124/// I/O-free coroutine for the combined `Mailbox/query` + `Mailbox/get`
125/// operation.
126pub struct JmapMailboxQuery {
127    state: State,
128}
129
130impl JmapMailboxQuery {
131    pub fn new(
132        session: &JmapSession,
133        http_auth: &SecretString,
134        opts: JmapMailboxQueryOptions,
135    ) -> Result<Self, JmapMailboxQueryError> {
136        let account_id = session
137            .primary_accounts
138            .get(MAIL_CAPABILITY)
139            .cloned()
140            .unwrap_or_default();
141        let api_url = &session.api_url;
142
143        let query_args = MailboxQueryArgs {
144            account_id: &account_id,
145            filter: opts.filter.as_ref(),
146            sort: opts.sort.as_deref(),
147            position: opts.position,
148            limit: opts.limit,
149            calculate_total: true,
150        };
151
152        let mut batch = JmapBatch::new();
153        let query_id = batch.add(
154            "Mailbox/query",
155            serde_json::to_value(&query_args).map_err(JmapMailboxQueryError::SerializeArgs)?,
156        );
157
158        let get_args = MailboxGetByRefArgs {
159            account_id: &account_id,
160            ids_ref: JmapResultReference {
161                result_of: &query_id,
162                name: "Mailbox/query",
163                path: "/ids",
164            },
165            properties: opts.properties.as_deref(),
166        };
167
168        batch.add(
169            "Mailbox/get",
170            serde_json::to_value(&get_args).map_err(JmapMailboxQueryError::SerializeArgs)?,
171        );
172
173        let request = batch.into_request(vec![CORE_CAPABILITY.into(), MAIL_CAPABILITY.into()]);
174
175        Ok(Self {
176            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
177        })
178    }
179}
180
181impl JmapCoroutine for JmapMailboxQuery {
182    type Yield = JmapYield;
183    type Return = Result<JmapMailboxQueryOutput, JmapMailboxQueryError>;
184
185    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
186        trace!("Mailbox/query: {}", self.state);
187        match &mut self.state {
188            State::Send(send) => {
189                let JmapSendOutput {
190                    response,
191                    keep_alive,
192                } = jmap_try!(send, arg);
193
194                let mut responses = response.method_responses.into_iter();
195
196                let Some((query_name, query_args, _)) = responses.next() else {
197                    return JmapCoroutineState::Complete(Err(
198                        JmapMailboxQueryError::MissingQueryResponse,
199                    ));
200                };
201
202                if query_name == "error" {
203                    let err = serde_json::from_value::<JmapMethodError>(query_args)
204                        .unwrap_or(JmapMethodError::Unknown);
205                    return JmapCoroutineState::Complete(Err(JmapMailboxQueryError::QueryMethod(
206                        err,
207                    )));
208                }
209
210                let query_response =
211                    match serde_json::from_value::<MailboxQueryResponse>(query_args) {
212                        Ok(r) => r,
213                        Err(err) => {
214                            return JmapCoroutineState::Complete(Err(
215                                JmapMailboxQueryError::ParseQueryResponse(err),
216                            ));
217                        }
218                    };
219
220                let Some((get_name, get_args, _)) = responses.next() else {
221                    return JmapCoroutineState::Complete(Err(
222                        JmapMailboxQueryError::MissingGetResponse,
223                    ));
224                };
225
226                if get_name == "error" {
227                    let err = serde_json::from_value::<JmapMethodError>(get_args)
228                        .unwrap_or(JmapMethodError::Unknown);
229                    return JmapCoroutineState::Complete(Err(JmapMailboxQueryError::GetMethod(
230                        err,
231                    )));
232                }
233
234                match serde_json::from_value::<MailboxGetResponse>(get_args) {
235                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapMailboxQueryOutput {
236                        mailboxes: r.list,
237                        total: query_response.total,
238                        position: query_response.position,
239                        query_state: query_response.query_state,
240                        keep_alive,
241                    })),
242                    Err(err) => JmapCoroutineState::Complete(Err(
243                        JmapMailboxQueryError::ParseGetResponse(err),
244                    )),
245                }
246            }
247        }
248    }
249}
250
251enum State {
252    Send(JmapSend),
253}
254
255impl fmt::Display for State {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::Send(_) => f.write_str("send"),
259        }
260    }
261}
262
263#[derive(Serialize)]
264#[serde(rename_all = "camelCase")]
265struct MailboxQueryArgs<'a> {
266    account_id: &'a str,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    filter: Option<&'a JmapMailboxFilter>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    sort: Option<&'a [JmapMailboxSortComparator]>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    position: Option<u64>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    limit: Option<u64>,
275    calculate_total: bool,
276}
277
278#[derive(Serialize)]
279#[serde(rename_all = "camelCase")]
280struct MailboxGetByRefArgs<'a> {
281    account_id: &'a str,
282    #[serde(rename = "#ids")]
283    ids_ref: JmapResultReference<'a>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    properties: Option<&'a [JmapMailboxProperty]>,
286}
287
288#[derive(Deserialize)]
289#[serde(rename_all = "camelCase")]
290struct MailboxQueryResponse {
291    query_state: String,
292    #[serde(default)]
293    total: Option<u64>,
294    #[serde(default)]
295    position: u64,
296}
297
298#[derive(Deserialize)]
299#[serde(rename_all = "camelCase")]
300struct MailboxGetResponse {
301    list: Vec<JmapMailbox>,
302}