Skip to main content

io_jmap/rfc9610/contact_card/
query.rs

1//! Batched JMAP `ContactCard/query` + `ContactCard/get` coroutine (RFC 9610
2//! §3.1 and §3.3): single HTTP request, server-side `#ids` back-reference
3//! resolves the get 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::session::JmapSession,
16//!     rfc9610::contact_card::query::{JmapContactCardQuery, JmapContactCardQueryOptions},
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:contacts": "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//!     JmapContactCardQuery::new(&session, &auth, JmapContactCardQueryOptions::default())
38//!         .unwrap();
39//! let mut arg = None;
40//!
41//! let out = loop {
42//!     match coroutine.resume(arg.take()) {
43//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
44//!             stream.write_all(&bytes).unwrap();
45//!         }
46//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
47//!             let n = stream.read(&mut buf).unwrap();
48//!             arg = Some(&buf[..n]);
49//!         }
50//!         JmapCoroutineState::Complete(Ok(out)) => break out,
51//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
52//!     }
53//! };
54//!
55//! println!("{} cards", out.cards.len());
56//! ```
57
58use core::fmt;
59
60use alloc::{string::String, vec, vec::Vec};
61
62use secrecy::SecretString;
63use serde::{Deserialize, Serialize, Serializer};
64use thiserror::Error;
65
66use crate::{
67    coroutine::*,
68    jmap_try,
69    rfc8620::{
70        JMAP_CORE_CAPABILITY, error::JmapMethodError, request::JmapBatch,
71        request::JmapResultReference, send::*, session::JmapSession,
72    },
73    rfc9610::{JMAP_CONTACTS_CAPABILITY, contact_card::JmapContactCard},
74};
75
76/// Filter for `ContactCard/query` (RFC 9610 §3.3.1); all specified
77/// conditions must apply.
78#[derive(Clone, Debug, Default, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct JmapContactCardFilter {
81    /// AddressBook id the card must be in.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub in_address_book: Option<String>,
84    /// Exact JSContact `uid` of the card.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub uid: Option<String>,
87    /// Uid the card's `members` property must contain.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub has_member: Option<String>,
90    /// Exact JSContact `kind` of the card, e.g. `group`.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub kind: Option<String>,
93    /// The card's `created` date-time must be before this UTC date.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub created_before: Option<String>,
96    /// The card's `created` date-time must be the same or after this UTC
97    /// date.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub created_after: Option<String>,
100    /// The card's `updated` date-time must be before this UTC date.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub updated_before: Option<String>,
103    /// The card's `updated` date-time must be the same or after this UTC
104    /// date.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub updated_after: Option<String>,
107    /// Free-text match against any text in the card.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub text: Option<String>,
110    /// Match against any NameComponent or the full name.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub name: Option<String>,
113    /// Match against NameComponents of kind `given`.
114    #[serde(rename = "name/given", skip_serializing_if = "Option::is_none")]
115    pub name_given: Option<String>,
116    /// Match against NameComponents of kind `surname`.
117    #[serde(rename = "name/surname", skip_serializing_if = "Option::is_none")]
118    pub name_surname: Option<String>,
119    /// Match against NameComponents of kind `surname2`.
120    #[serde(rename = "name/surname2", skip_serializing_if = "Option::is_none")]
121    pub name_surname2: Option<String>,
122    /// Match against any Nickname name.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub nickname: Option<String>,
125    /// Match against any Organization name.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub organization: Option<String>,
128    /// Match against any EmailAddress address or label.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub email: Option<String>,
131    /// Match against any Phone number or label.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub phone: Option<String>,
134    /// Match against any OnlineService service, uri, user or label.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub online_service: Option<String>,
137    /// Match against any AddressComponent or the full address.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub address: Option<String>,
140    /// Match against any Note note.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub note: Option<String>,
143}
144
145/// Sort property for `ContactCard/query` (RFC 9610 §3.3.2).
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub enum JmapContactCardSortProperty {
148    /// The `created` date on the ContactCard.
149    Created,
150    /// The `updated` date on the ContactCard.
151    Updated,
152    /// The first NameComponent of kind `given`.
153    NameGiven,
154    /// The first NameComponent of kind `surname`.
155    NameSurname,
156    /// The first NameComponent of kind `surname2`.
157    NameSurname2,
158}
159
160impl fmt::Display for JmapContactCardSortProperty {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        f.write_str(match self {
163            Self::Created => "created",
164            Self::Updated => "updated",
165            Self::NameGiven => "name/given",
166            Self::NameSurname => "name/surname",
167            Self::NameSurname2 => "name/surname2",
168        })
169    }
170}
171
172impl Serialize for JmapContactCardSortProperty {
173    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
174        s.collect_str(self)
175    }
176}
177
178/// Sort comparator for `ContactCard/query` (RFC 8620 §5.5).
179#[derive(Clone, Debug, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct JmapContactCardSortComparator {
182    /// The property to sort by.
183    pub property: JmapContactCardSortProperty,
184    /// Ascending if `None` or `Some(true)`.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub is_ascending: Option<bool>,
187}
188
189/// Failure causes during a batched JMAP `ContactCard/query` +
190/// `ContactCard/get` flow.
191#[derive(Debug, Error)]
192pub enum JmapContactCardQueryError {
193    /// The response carried no query response.
194    #[error(
195        "JMAP ContactCard/query failed: missing ContactCard/query response in method_responses"
196    )]
197    MissingQueryResponse,
198    /// The response carried no get response.
199    #[error("JMAP ContactCard/query failed: missing ContactCard/get response in method_responses")]
200    MissingGetResponse,
201    /// The inner send coroutine failed.
202    #[error("JMAP ContactCard/query failed: {0}")]
203    Send(#[from] JmapSendError),
204    /// The method arguments could not be serialized.
205    #[error("JMAP ContactCard/query failed: serialize args: {0}")]
206    SerializeArgs(#[source] serde_json::Error),
207    /// The query response could not be parsed.
208    #[error("JMAP ContactCard/query failed: parse ContactCard/query response: {0}")]
209    ParseQueryResponse(#[source] serde_json::Error),
210    /// The get response could not be parsed.
211    #[error("JMAP ContactCard/query failed: parse ContactCard/get response: {0}")]
212    ParseGetResponse(#[source] serde_json::Error),
213    /// The server returned a method-level error for the query call.
214    #[error("JMAP ContactCard/query failed: ContactCard/query: {0}")]
215    QueryMethod(JmapMethodError),
216    /// The server returned a method-level error for the get call.
217    #[error("JMAP ContactCard/query failed: ContactCard/get: {0}")]
218    GetMethod(JmapMethodError),
219}
220
221/// Options for [`JmapContactCardQuery::new`].
222#[derive(Clone, Debug, Default)]
223pub struct JmapContactCardQueryOptions {
224    /// Filter criteria; `None` matches all cards.
225    pub filter: Option<JmapContactCardFilter>,
226    /// Sort order; `None` uses the server default.
227    pub sort: Option<Vec<JmapContactCardSortComparator>>,
228    /// Zero-based offset into the result list.
229    pub position: Option<u64>,
230    /// Max number of cards to return.
231    pub limit: Option<u64>,
232    /// Card properties to fetch (JSContact property names plus `id` and
233    /// `addressBookIds`); `None` returns all.
234    pub properties: Option<Vec<String>>,
235}
236
237/// Successful terminal output of [`JmapContactCardQuery`].
238#[derive(Clone, Debug)]
239pub struct JmapContactCardQueryOutput {
240    /// The fetched contact cards.
241    pub cards: Vec<JmapContactCard>,
242    /// The total number of matching objects, when the server computed it.
243    pub total: Option<u64>,
244    /// Zero-based index of the first returned id.
245    pub position: u64,
246    /// The state the query results were computed at.
247    pub query_state: String,
248    /// Whether the server indicated the connection can be reused.
249    pub keep_alive: bool,
250}
251
252/// I/O-free coroutine for the combined `ContactCard/query` +
253/// `ContactCard/get` operation.
254pub struct JmapContactCardQuery {
255    state: State,
256}
257
258impl JmapContactCardQuery {
259    /// Prepares the method call request and builds the coroutine.
260    pub fn new(
261        session: &JmapSession,
262        http_auth: &SecretString,
263        opts: JmapContactCardQueryOptions,
264    ) -> Result<Self, JmapContactCardQueryError> {
265        let account_id = session
266            .primary_accounts
267            .get(JMAP_CONTACTS_CAPABILITY)
268            .cloned()
269            .unwrap_or_default();
270        let api_url = &session.api_url;
271
272        let query_args = ContactCardQueryArgs {
273            account_id: &account_id,
274            filter: opts.filter.as_ref(),
275            sort: opts.sort.as_deref(),
276            position: opts.position,
277            limit: opts.limit,
278            calculate_total: true,
279        };
280
281        let mut batch = JmapBatch::new();
282        let query_id = batch.add(
283            "ContactCard/query",
284            serde_json::to_value(&query_args).map_err(JmapContactCardQueryError::SerializeArgs)?,
285        );
286
287        let get_args = ContactCardGetByRefArgs {
288            account_id: &account_id,
289            ids_ref: JmapResultReference {
290                result_of: &query_id,
291                name: "ContactCard/query",
292                path: "/ids",
293            },
294            properties: opts.properties.as_deref(),
295        };
296
297        batch.add(
298            "ContactCard/get",
299            serde_json::to_value(&get_args).map_err(JmapContactCardQueryError::SerializeArgs)?,
300        );
301
302        let request = batch.into_request(vec![
303            JMAP_CORE_CAPABILITY.into(),
304            JMAP_CONTACTS_CAPABILITY.into(),
305        ]);
306
307        Ok(Self {
308            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
309        })
310    }
311}
312
313impl JmapCoroutine for JmapContactCardQuery {
314    type Yield = JmapYield;
315    type Return = Result<JmapContactCardQueryOutput, JmapContactCardQueryError>;
316
317    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
318        match &mut self.state {
319            State::Send(send) => {
320                let JmapSendOutput {
321                    response,
322                    keep_alive,
323                } = jmap_try!(send, arg);
324
325                let mut responses = response.method_responses.into_iter();
326
327                let Some((query_name, query_args, _)) = responses.next() else {
328                    return JmapCoroutineState::Complete(Err(
329                        JmapContactCardQueryError::MissingQueryResponse,
330                    ));
331                };
332
333                if query_name == "error" {
334                    let err = serde_json::from_value::<JmapMethodError>(query_args)
335                        .unwrap_or(JmapMethodError::Unknown);
336                    return JmapCoroutineState::Complete(Err(
337                        JmapContactCardQueryError::QueryMethod(err),
338                    ));
339                }
340
341                let query_response =
342                    match serde_json::from_value::<ContactCardQueryResponse>(query_args) {
343                        Ok(r) => r,
344                        Err(err) => {
345                            return JmapCoroutineState::Complete(Err(
346                                JmapContactCardQueryError::ParseQueryResponse(err),
347                            ));
348                        }
349                    };
350
351                let Some((get_name, get_args, _)) = responses.next() else {
352                    return JmapCoroutineState::Complete(Err(
353                        JmapContactCardQueryError::MissingGetResponse,
354                    ));
355                };
356
357                if get_name == "error" {
358                    let err = serde_json::from_value::<JmapMethodError>(get_args)
359                        .unwrap_or(JmapMethodError::Unknown);
360                    return JmapCoroutineState::Complete(Err(
361                        JmapContactCardQueryError::GetMethod(err),
362                    ));
363                }
364
365                match serde_json::from_value::<ContactCardGetResponse>(get_args) {
366                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapContactCardQueryOutput {
367                        cards: r.list,
368                        total: query_response.total,
369                        position: query_response.position,
370                        query_state: query_response.query_state,
371                        keep_alive,
372                    })),
373                    Err(err) => JmapCoroutineState::Complete(Err(
374                        JmapContactCardQueryError::ParseGetResponse(err),
375                    )),
376                }
377            }
378        }
379    }
380}
381
382enum State {
383    Send(JmapSend),
384}
385
386#[derive(Serialize)]
387#[serde(rename_all = "camelCase")]
388struct ContactCardQueryArgs<'a> {
389    account_id: &'a str,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    filter: Option<&'a JmapContactCardFilter>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    sort: Option<&'a [JmapContactCardSortComparator]>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    position: Option<u64>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    limit: Option<u64>,
398    calculate_total: bool,
399}
400
401#[derive(Serialize)]
402#[serde(rename_all = "camelCase")]
403struct ContactCardGetByRefArgs<'a> {
404    account_id: &'a str,
405    #[serde(rename = "#ids")]
406    ids_ref: JmapResultReference<'a>,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    properties: Option<&'a [String]>,
409}
410
411#[derive(Deserialize)]
412#[serde(rename_all = "camelCase")]
413struct ContactCardQueryResponse {
414    query_state: String,
415    #[serde(default)]
416    total: Option<u64>,
417    #[serde(default)]
418    position: u64,
419}
420
421#[derive(Deserialize)]
422#[serde(rename_all = "camelCase")]
423struct ContactCardGetResponse {
424    list: Vec<JmapContactCard>,
425}