io_jmap/rfc9610/address_book/get.rs
1//! JMAP `AddressBook/get` coroutine (RFC 9610 §2.1): wraps the generic
2//! [`JmapGet`] with the JMAP-Contacts capability set.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::{
8//! io::{Read, Write},
9//! net::TcpStream,
10//! };
11//!
12//! use io_jmap::{
13//! coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
14//! rfc8620::session::JmapSession,
15//! rfc9610::address_book::get::{JmapAddressBookGet, JmapAddressBookGetOptions},
16//! };
17//! use secrecy::SecretString;
18//!
19//! // Ready stream needed (TCP-connected, TLS-negociated)
20//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
21//! let mut buf = [0u8; 4096];
22//!
23//! let session: JmapSession = serde_json::from_str(r#"{
24//! "username": "",
25//! "accounts": {},
26//! "primaryAccounts": {"urn:ietf:params:jmap:contacts": "a1"},
27//! "capabilities": {},
28//! "apiUrl": "https://api.example.com/jmap/",
29//! "downloadUrl": "",
30//! "uploadUrl": "",
31//! "eventSourceUrl": "",
32//! "state": ""
33//! }"#).unwrap();
34//! let auth = SecretString::from("Bearer xyz");
35//! let mut coroutine =
36//! JmapAddressBookGet::new(&session, &auth, JmapAddressBookGetOptions::default()).unwrap();
37//! let mut arg = None;
38//!
39//! let out = loop {
40//! match coroutine.resume(arg.take()) {
41//! JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
42//! stream.write_all(&bytes).unwrap();
43//! }
44//! JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
45//! let n = stream.read(&mut buf).unwrap();
46//! arg = Some(&buf[..n]);
47//! }
48//! JmapCoroutineState::Complete(Ok(out)) => break out,
49//! JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
50//! }
51//! };
52//!
53//! println!("{} address books", out.address_books.len());
54//! ```
55
56use alloc::{borrow::ToOwned, format, string::String, vec, vec::Vec};
57
58use secrecy::SecretString;
59use serde::Serialize;
60use thiserror::Error;
61
62use crate::{
63 coroutine::*,
64 jmap_try,
65 rfc8620::{JMAP_CORE_CAPABILITY, get::*, session::JmapSession},
66 rfc9610::{JMAP_CONTACTS_CAPABILITY, address_book::JmapAddressBook},
67};
68
69/// [`JmapAddressBook`] properties requestable in `AddressBook/get`
70/// (RFC 9610 §2).
71#[derive(Clone, Debug, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub enum JmapAddressBookProperty {
74 /// The `id` property.
75 Id,
76 /// The `name` property.
77 Name,
78 /// The `description` property.
79 Description,
80 /// The `sortOrder` property.
81 SortOrder,
82 /// The `isDefault` property.
83 IsDefault,
84 /// The `isSubscribed` property.
85 IsSubscribed,
86 /// The `shareWith` property.
87 ShareWith,
88 /// The `myRights` property.
89 MyRights,
90}
91
92/// Failure causes during a JMAP `AddressBook/get` flow.
93#[derive(Debug, Error)]
94pub enum JmapAddressBookGetError {
95 /// The inner generic get coroutine failed.
96 #[error("JMAP AddressBook/get failed: {0}")]
97 Get(#[from] JmapGetError),
98}
99
100/// Options for [`JmapAddressBookGet::new`].
101#[derive(Clone, Debug, Default)]
102pub struct JmapAddressBookGetOptions {
103 /// Restrict the fetch to these AddressBook IDs; `None` fetches all.
104 pub ids: Option<Vec<String>>,
105 /// Restrict the returned properties; `None` returns all.
106 pub properties: Option<Vec<JmapAddressBookProperty>>,
107}
108
109/// Successful terminal output of [`JmapAddressBookGet`].
110#[derive(Clone, Debug)]
111pub struct JmapAddressBookGetOutput {
112 /// The fetched address books.
113 pub address_books: Vec<JmapAddressBook>,
114 /// The requested ids the server did not find.
115 pub not_found: Vec<String>,
116 /// The new server state after the call.
117 pub new_state: String,
118 /// Whether the server indicated the connection can be reused.
119 pub keep_alive: bool,
120}
121
122/// I/O-free coroutine for the JMAP `AddressBook/get` method.
123pub struct JmapAddressBookGet {
124 state: State,
125}
126
127impl JmapAddressBookGet {
128 /// Prepares the method call request and builds the coroutine.
129 pub fn new(
130 session: &JmapSession,
131 http_auth: &SecretString,
132 opts: JmapAddressBookGetOptions,
133 ) -> Result<Self, JmapAddressBookGetError> {
134 let account_id = session
135 .primary_accounts
136 .get(JMAP_CONTACTS_CAPABILITY)
137 .cloned()
138 .unwrap_or_default();
139 let api_url = &session.api_url;
140
141 let props = opts.properties.map(|ps| {
142 ps.iter()
143 .map(|p| {
144 serde_json::to_value(p)
145 .ok()
146 .and_then(|v| v.as_str().map(str::to_owned))
147 .unwrap_or_else(|| format!("{p:?}"))
148 })
149 .collect()
150 });
151
152 Ok(Self {
153 state: State::Get(JmapGet::new(
154 account_id,
155 http_auth,
156 api_url,
157 "AddressBook/get",
158 vec![JMAP_CORE_CAPABILITY.into(), JMAP_CONTACTS_CAPABILITY.into()],
159 JmapGetOptions {
160 ids: opts.ids,
161 properties: props,
162 },
163 )?),
164 })
165 }
166}
167
168impl JmapCoroutine for JmapAddressBookGet {
169 type Yield = JmapYield;
170 type Return = Result<JmapAddressBookGetOutput, JmapAddressBookGetError>;
171
172 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
173 match &mut self.state {
174 State::Get(get) => {
175 let JmapGetOutput {
176 list,
177 not_found,
178 state,
179 keep_alive,
180 } = jmap_try!(get, arg);
181 JmapCoroutineState::Complete(Ok(JmapAddressBookGetOutput {
182 address_books: list,
183 not_found,
184 new_state: state,
185 keep_alive,
186 }))
187 }
188 }
189 }
190}
191
192enum State {
193 Get(JmapGet<JmapAddressBook>),
194}