Skip to main content

io_jmap/rfc9610/contact_card/
copy.rs

1//! JMAP `ContactCard/copy` coroutine (RFC 9610 §3.6): copies cards between
2//! accounts per the standard `/copy` method (RFC 8620 §5.4).
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::{
8//!     collections::BTreeMap,
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::copy::JmapContactCardCopy,
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//!     JmapContactCardCopy::new(&session, &auth, "a2", BTreeMap::new()).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!("{} created", out.created.len());
55//! ```
56
57use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
58
59use secrecy::SecretString;
60use serde::{Deserialize, Serialize};
61use thiserror::Error;
62
63use crate::{
64    coroutine::*,
65    jmap_try,
66    rfc8620::{
67        JMAP_CORE_CAPABILITY, error::JmapMethodError, request::JmapBatch, send::*,
68        session::JmapSession,
69    },
70    rfc9610::{JMAP_CONTACTS_CAPABILITY, contact_card::JmapContactCard},
71};
72
73/// Arguments for copying a single card between accounts via
74/// `ContactCard/copy` (RFC 9610 §3.6).
75#[derive(Clone, Debug, Default, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct JmapContactCardCopyArgs {
78    /// Source ContactCard id.
79    pub id: String,
80    /// `{ address-book-id -> true }` in the destination account.
81    pub address_book_ids: BTreeMap<String, bool>,
82}
83
84/// Per-object error returned in `ContactCard/copy` responses (RFC 8620
85/// §5.4).
86#[derive(Clone, Debug, Deserialize)]
87#[serde(tag = "type", rename_all = "camelCase")]
88pub enum JmapContactCardCopyItemError {
89    /// The card already exists in the destination account (RFC 8620 §5.4).
90    AlreadyExists {
91        /// Optional human-readable detail.
92        description: Option<String>,
93    },
94    /// Standard set error (RFC 8620 §5.3): target id not found.
95    NotFound {
96        /// Optional human-readable detail.
97        description: Option<String>,
98    },
99    /// Standard set error (RFC 8620 §5.3): one or more properties were
100    /// invalid.
101    InvalidProperties {
102        /// Optional human-readable detail.
103        description: Option<String>,
104        /// The invalid property names.
105        #[serde(default)]
106        properties: Vec<String>,
107    },
108    /// Catch-all for set errors not modelled above.
109    #[serde(other)]
110    Unknown,
111}
112
113/// Failure causes during a JMAP `ContactCard/copy` flow.
114#[derive(Debug, Error)]
115pub enum JmapContactCardCopyError {
116    /// The response carried no method response.
117    #[error("JMAP ContactCard/copy failed: missing response in method_responses")]
118    MissingResponse,
119    /// The inner send coroutine failed.
120    #[error("JMAP ContactCard/copy failed: {0}")]
121    Send(#[from] JmapSendError),
122    /// The method arguments could not be serialized.
123    #[error("JMAP ContactCard/copy failed: serialize args: {0}")]
124    SerializeArgs(#[source] serde_json::Error),
125    /// The method response could not be parsed.
126    #[error("JMAP ContactCard/copy failed: parse response: {0}")]
127    ParseResponse(#[source] serde_json::Error),
128    /// The server returned a method-level error.
129    #[error("JMAP ContactCard/copy failed: {0}")]
130    Method(#[from] JmapMethodError),
131}
132
133/// Successful terminal output of [`JmapContactCardCopy`].
134#[derive(Clone, Debug)]
135pub struct JmapContactCardCopyOutput {
136    /// The new server state after the call.
137    pub new_state: String,
138    /// The created cards, keyed by client id.
139    pub created: BTreeMap<String, JmapContactCard>,
140    /// The failed copies, keyed by client id.
141    pub not_created: BTreeMap<String, JmapContactCardCopyItemError>,
142    /// Whether the server indicated the connection can be reused.
143    pub keep_alive: bool,
144}
145
146/// I/O-free coroutine for the JMAP `ContactCard/copy` method.
147pub struct JmapContactCardCopy {
148    state: State,
149}
150
151impl JmapContactCardCopy {
152    /// Prepares the method call request and builds the coroutine.
153    pub fn new(
154        session: &JmapSession,
155        http_auth: &SecretString,
156        from_account_id: impl Into<String>,
157        cards: BTreeMap<String, JmapContactCardCopyArgs>,
158    ) -> Result<Self, JmapContactCardCopyError> {
159        let account_id = session
160            .primary_accounts
161            .get(JMAP_CONTACTS_CAPABILITY)
162            .cloned()
163            .unwrap_or_default();
164        let api_url = &session.api_url;
165
166        let args = serde_json::to_value(ContactCardCopyArgs {
167            from_account_id: from_account_id.into(),
168            account_id,
169            create: cards,
170        })
171        .map_err(JmapContactCardCopyError::SerializeArgs)?;
172
173        let mut batch = JmapBatch::new();
174        batch.add("ContactCard/copy", args);
175        let request = batch.into_request(vec![
176            JMAP_CORE_CAPABILITY.into(),
177            JMAP_CONTACTS_CAPABILITY.into(),
178        ]);
179
180        Ok(Self {
181            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
182        })
183    }
184}
185
186impl JmapCoroutine for JmapContactCardCopy {
187    type Yield = JmapYield;
188    type Return = Result<JmapContactCardCopyOutput, JmapContactCardCopyError>;
189
190    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
191        match &mut self.state {
192            State::Send(send) => {
193                let JmapSendOutput {
194                    response,
195                    keep_alive,
196                } = jmap_try!(send, arg);
197
198                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
199                    return JmapCoroutineState::Complete(Err(
200                        JmapContactCardCopyError::MissingResponse,
201                    ));
202                };
203
204                if name == "error" {
205                    let err = serde_json::from_value::<JmapMethodError>(args)
206                        .unwrap_or(JmapMethodError::Unknown);
207                    return JmapCoroutineState::Complete(Err(err.into()));
208                }
209
210                match serde_json::from_value::<ContactCardCopyResponse>(args) {
211                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapContactCardCopyOutput {
212                        new_state: r.new_state,
213                        created: r.created,
214                        not_created: r.not_created,
215                        keep_alive,
216                    })),
217                    Err(err) => JmapCoroutineState::Complete(Err(
218                        JmapContactCardCopyError::ParseResponse(err),
219                    )),
220                }
221            }
222        }
223    }
224}
225
226enum State {
227    Send(JmapSend),
228}
229
230#[derive(Serialize)]
231#[serde(rename_all = "camelCase")]
232struct ContactCardCopyArgs {
233    from_account_id: String,
234    account_id: String,
235    create: BTreeMap<String, JmapContactCardCopyArgs>,
236}
237
238#[derive(Deserialize)]
239#[serde(rename_all = "camelCase")]
240struct ContactCardCopyResponse {
241    new_state: String,
242    #[serde(default)]
243    created: BTreeMap<String, JmapContactCard>,
244    #[serde(default)]
245    not_created: BTreeMap<String, JmapContactCardCopyItemError>,
246}