Skip to main content

io_jmap/rfc8621/email/
copy.rs

1//! JMAP `Email/copy` coroutine (RFC 8621 ยง4.10): copies emails from one account
2//! into the current session account.
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::JmapSession,
16//!     rfc8621::email::{JmapEmailCopyArgs, copy::JmapEmailCopy},
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 create = BTreeMap::new();
37//! create.insert(
38//!     "c1".to_string(),
39//!     JmapEmailCopyArgs {
40//!         id: "e1".into(),
41//!         mailbox_ids: Default::default(),
42//!         keywords: None,
43//!         received_at: None,
44//!     },
45//! );
46//! let mut coroutine = JmapEmailCopy::new(&session, &auth, "from", create).unwrap();
47//! let mut arg = None;
48//!
49//! let out = loop {
50//!     match coroutine.resume(arg.take()) {
51//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
52//!             stream.write_all(&bytes).unwrap();
53//!         }
54//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
55//!             let n = stream.read(&mut buf).unwrap();
56//!             arg = Some(&buf[..n]);
57//!         }
58//!         JmapCoroutineState::Complete(Ok(out)) => break out,
59//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
60//!     }
61//! };
62//!
63//! println!("{} created", out.created.len());
64//! ```
65
66use core::fmt;
67
68use alloc::{collections::BTreeMap, string::String, vec};
69
70use log::trace;
71use secrecy::SecretString;
72use serde::{Deserialize, Serialize};
73use thiserror::Error;
74
75use crate::{
76    coroutine::*,
77    jmap_try,
78    rfc8620::{CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapSession, send::*},
79    rfc8621::{
80        MAIL_CAPABILITY,
81        email::{JmapEmail, JmapEmailCopyArgs, JmapEmailCopyItemError},
82    },
83};
84
85/// Failure causes during a JMAP `Email/copy` flow.
86#[derive(Debug, Error)]
87pub enum JmapEmailCopyError {
88    #[error("JMAP Email/copy failed: missing response in method_responses")]
89    MissingResponse,
90    #[error("JMAP Email/copy failed: {0}")]
91    Send(#[from] JmapSendError),
92    #[error("JMAP Email/copy failed: serialize args: {0}")]
93    SerializeArgs(#[source] serde_json::Error),
94    #[error("JMAP Email/copy failed: parse response: {0}")]
95    ParseResponse(#[source] serde_json::Error),
96    #[error("JMAP Email/copy failed: {0}")]
97    Method(#[from] JmapMethodError),
98}
99
100/// Successful terminal output of [`JmapEmailCopy`].
101#[derive(Clone, Debug)]
102pub struct JmapEmailCopyOutput {
103    pub new_state: String,
104    pub created: BTreeMap<String, JmapEmail>,
105    pub not_created: BTreeMap<String, JmapEmailCopyItemError>,
106    pub keep_alive: bool,
107}
108
109/// I/O-free coroutine for the JMAP `Email/copy` method.
110pub struct JmapEmailCopy {
111    state: State,
112}
113
114impl JmapEmailCopy {
115    pub fn new(
116        session: &JmapSession,
117        http_auth: &SecretString,
118        from_account_id: impl Into<String>,
119        emails: BTreeMap<String, JmapEmailCopyArgs>,
120    ) -> Result<Self, JmapEmailCopyError> {
121        let account_id = session
122            .primary_accounts
123            .get(MAIL_CAPABILITY)
124            .cloned()
125            .unwrap_or_default();
126        let api_url = &session.api_url;
127
128        let args = serde_json::to_value(EmailCopyArgs {
129            from_account_id: from_account_id.into(),
130            account_id,
131            create: emails,
132        })
133        .map_err(JmapEmailCopyError::SerializeArgs)?;
134
135        let mut batch = JmapBatch::new();
136        batch.add("Email/copy", args);
137        let request = batch.into_request(vec![CORE_CAPABILITY.into(), MAIL_CAPABILITY.into()]);
138
139        Ok(Self {
140            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
141        })
142    }
143}
144
145impl JmapCoroutine for JmapEmailCopy {
146    type Yield = JmapYield;
147    type Return = Result<JmapEmailCopyOutput, JmapEmailCopyError>;
148
149    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
150        trace!("Email/copy: {}", self.state);
151        match &mut self.state {
152            State::Send(send) => {
153                let JmapSendOutput {
154                    response,
155                    keep_alive,
156                } = jmap_try!(send, arg);
157
158                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
159                    return JmapCoroutineState::Complete(Err(JmapEmailCopyError::MissingResponse));
160                };
161
162                if name == "error" {
163                    let err = serde_json::from_value::<JmapMethodError>(args)
164                        .unwrap_or(JmapMethodError::Unknown);
165                    return JmapCoroutineState::Complete(Err(err.into()));
166                }
167
168                match serde_json::from_value::<EmailCopyResponse>(args) {
169                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailCopyOutput {
170                        new_state: r.new_state,
171                        created: r.created,
172                        not_created: r.not_created,
173                        keep_alive,
174                    })),
175                    Err(err) => {
176                        JmapCoroutineState::Complete(Err(JmapEmailCopyError::ParseResponse(err)))
177                    }
178                }
179            }
180        }
181    }
182}
183
184enum State {
185    Send(JmapSend),
186}
187
188impl fmt::Display for State {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::Send(_) => f.write_str("send"),
192        }
193    }
194}
195
196#[derive(Serialize)]
197#[serde(rename_all = "camelCase")]
198struct EmailCopyArgs {
199    from_account_id: String,
200    account_id: String,
201    create: BTreeMap<String, JmapEmailCopyArgs>,
202}
203
204#[derive(Deserialize)]
205#[serde(rename_all = "camelCase")]
206struct EmailCopyResponse {
207    new_state: String,
208    #[serde(default)]
209    created: BTreeMap<String, JmapEmail>,
210    #[serde(default)]
211    not_created: BTreeMap<String, JmapEmailCopyItemError>,
212}