Skip to main content

io_jmap/rfc8621/email/
import.rs

1//! JMAP `Email/import` coroutine (RFC 8621 ยง4.9): imports RFC 5322 messages
2//! (previously uploaded as blobs) into mailboxes. JMAP equivalent of IMAP
3//! `APPEND`.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::{
9//!     collections::BTreeMap,
10//!     io::{Read, Write},
11//!     net::TcpStream,
12//! };
13//!
14//! use io_jmap::{
15//!     coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
16//!     rfc8620::JmapSession,
17//!     rfc8621::email::{JmapEmailImportArgs, import::JmapEmailImport},
18//! };
19//! use secrecy::SecretString;
20//!
21//! // Ready stream needed (TCP-connected, TLS-negociated)
22//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
23//! let mut buf = [0u8; 4096];
24//!
25//! let session: JmapSession = serde_json::from_str(r#"{
26//!     "username": "",
27//!     "accounts": {},
28//!     "primaryAccounts": {"urn:ietf:params:jmap:mail": "a1"},
29//!     "capabilities": {},
30//!     "apiUrl": "https://api.example.com/jmap/",
31//!     "downloadUrl": "",
32//!     "uploadUrl": "",
33//!     "eventSourceUrl": "",
34//!     "state": ""
35//! }"#).unwrap();
36//! let auth = SecretString::from("Bearer xyz");
37//! let mut emails = BTreeMap::new();
38//! emails.insert(
39//!     "c1".to_string(),
40//!     JmapEmailImportArgs {
41//!         blob_id: "b1".into(),
42//!         mailbox_ids: Default::default(),
43//!         keywords: None,
44//!         received_at: None,
45//!     },
46//! );
47//! let mut coroutine = JmapEmailImport::new(&session, &auth, emails).unwrap();
48//! let mut arg = None;
49//!
50//! let out = loop {
51//!     match coroutine.resume(arg.take()) {
52//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
53//!             stream.write_all(&bytes).unwrap();
54//!         }
55//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
56//!             let n = stream.read(&mut buf).unwrap();
57//!             arg = Some(&buf[..n]);
58//!         }
59//!         JmapCoroutineState::Complete(Ok(out)) => break out,
60//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
61//!     }
62//! };
63//!
64//! println!("{} created", out.created.len());
65//! ```
66
67use core::fmt;
68
69use alloc::{collections::BTreeMap, string::String, vec};
70
71use log::trace;
72use secrecy::SecretString;
73use serde::{Deserialize, Serialize};
74use thiserror::Error;
75
76use crate::{
77    coroutine::*,
78    jmap_try,
79    rfc8620::{CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapSession, send::*},
80    rfc8621::{
81        MAIL_CAPABILITY,
82        email::{JmapEmail, JmapEmailImportArgs, JmapEmailImportItemError},
83    },
84};
85
86/// Failure causes during a JMAP `Email/import` flow.
87#[derive(Debug, Error)]
88pub enum JmapEmailImportError {
89    #[error("JMAP Email/import failed: missing response in method_responses")]
90    MissingResponse,
91    #[error("JMAP Email/import failed: {0}")]
92    Send(#[from] JmapSendError),
93    #[error("JMAP Email/import failed: serialize args: {0}")]
94    SerializeArgs(#[source] serde_json::Error),
95    #[error("JMAP Email/import failed: parse response: {0}")]
96    ParseResponse(#[source] serde_json::Error),
97    #[error("JMAP Email/import failed: {0}")]
98    Method(#[from] JmapMethodError),
99}
100
101/// Successful terminal output of [`JmapEmailImport`].
102#[derive(Clone, Debug)]
103pub struct JmapEmailImportOutput {
104    pub new_state: String,
105    pub created: BTreeMap<String, JmapEmail>,
106    pub not_created: BTreeMap<String, JmapEmailImportItemError>,
107    pub keep_alive: bool,
108}
109
110/// I/O-free coroutine for the JMAP `Email/import` method.
111pub struct JmapEmailImport {
112    state: State,
113}
114
115impl JmapEmailImport {
116    /// `emails` maps client-assigned IDs to [`JmapEmailImportArgs`] descriptors.
117    pub fn new(
118        session: &JmapSession,
119        http_auth: &SecretString,
120        emails: BTreeMap<String, JmapEmailImportArgs>,
121    ) -> Result<Self, JmapEmailImportError> {
122        let account_id = session
123            .primary_accounts
124            .get(MAIL_CAPABILITY)
125            .cloned()
126            .unwrap_or_default();
127        let api_url = &session.api_url;
128
129        let args = serde_json::to_value(EmailImportArgs { account_id, emails })
130            .map_err(JmapEmailImportError::SerializeArgs)?;
131
132        let mut batch = JmapBatch::new();
133        batch.add("Email/import", args);
134        let request = batch.into_request(vec![CORE_CAPABILITY.into(), MAIL_CAPABILITY.into()]);
135
136        Ok(Self {
137            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
138        })
139    }
140}
141
142impl JmapCoroutine for JmapEmailImport {
143    type Yield = JmapYield;
144    type Return = Result<JmapEmailImportOutput, JmapEmailImportError>;
145
146    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
147        trace!("Email/import: {}", self.state);
148        match &mut self.state {
149            State::Send(send) => {
150                let JmapSendOutput {
151                    response,
152                    keep_alive,
153                } = jmap_try!(send, arg);
154
155                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
156                    return JmapCoroutineState::Complete(Err(
157                        JmapEmailImportError::MissingResponse,
158                    ));
159                };
160
161                if name == "error" {
162                    let err = serde_json::from_value::<JmapMethodError>(args)
163                        .unwrap_or(JmapMethodError::Unknown);
164                    return JmapCoroutineState::Complete(Err(err.into()));
165                }
166
167                match serde_json::from_value::<EmailImportResponse>(args) {
168                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailImportOutput {
169                        new_state: r.new_state,
170                        created: r.created,
171                        not_created: r.not_created,
172                        keep_alive,
173                    })),
174                    Err(err) => {
175                        JmapCoroutineState::Complete(Err(JmapEmailImportError::ParseResponse(err)))
176                    }
177                }
178            }
179        }
180    }
181}
182
183enum State {
184    Send(JmapSend),
185}
186
187impl fmt::Display for State {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            Self::Send(_) => f.write_str("send"),
191        }
192    }
193}
194
195#[derive(Serialize)]
196#[serde(rename_all = "camelCase")]
197struct EmailImportArgs {
198    account_id: String,
199    emails: BTreeMap<String, JmapEmailImportArgs>,
200}
201
202#[derive(Deserialize)]
203#[serde(rename_all = "camelCase")]
204struct EmailImportResponse {
205    new_state: String,
206    #[serde(default)]
207    created: BTreeMap<String, JmapEmail>,
208    #[serde(default)]
209    not_created: BTreeMap<String, JmapEmailImportItemError>,
210}