Skip to main content

io_email/message/jmap/
add.rs

1//! JMAP message-add coroutine: Blob/upload then Email/import.
2//!
3//! # Example
4//!
5//! ```rust,ignore
6//! use io_email::message::jmap::add::JmapMessageAdd;
7//!
8//! let id = client.run(JmapMessageAdd::new(&session, &auth, "mailbox-id", &flags, raw)?)?;
9//! ```
10
11use alloc::{collections::BTreeMap, string::String, vec::Vec};
12use core::mem;
13
14use io_jmap::{
15    coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
16    rfc8620::{
17        JmapSession,
18        blob_upload::{JmapBlobUpload, JmapBlobUploadError, JmapBlobUploadOutput},
19        coroutine::JmapRedirectYield,
20    },
21    rfc8621::{
22        MAIL_CAPABILITY,
23        email::{
24            JmapEmailImportArgs,
25            import::{JmapEmailImport as InnerImport, JmapEmailImportError as ImportErr},
26        },
27    },
28};
29use log::trace;
30use secrecy::SecretString;
31use thiserror::Error;
32use url::Url;
33
34use crate::{flag::types::Flag, jmap::convert::keyword_from};
35
36/// Errors produced by [`JmapMessageAdd`].
37#[derive(Debug, Error)]
38pub enum JmapMessageAddError {
39    #[error(transparent)]
40    BlobUpload(#[from] JmapBlobUploadError),
41    #[error(transparent)]
42    Import(#[from] ImportErr),
43    #[error("resolved JMAP upload URL is invalid: {0}")]
44    InvalidUploadUrl(String),
45    #[error("Email/import did not create the imported email")]
46    NotImported,
47    #[error("JMAP blob upload reached unexpected redirection")]
48    UnsupportedRedirect,
49    #[error("coroutine was resumed after completion")]
50    ResumedAfterDone,
51}
52
53/// I/O-free coroutine appending a raw RFC 5322 message into a mailbox.
54pub struct JmapMessageAdd {
55    state: State,
56    mailbox_id: String,
57    keywords: BTreeMap<String, bool>,
58    session: JmapSession,
59    http_auth: SecretString,
60}
61
62impl JmapMessageAdd {
63    pub fn new(
64        session: &JmapSession,
65        http_auth: &SecretString,
66        mailbox: &str,
67        flags: &[Flag],
68        raw: Vec<u8>,
69    ) -> Result<Self, JmapMessageAddError> {
70        trace!("prepare JMAP message add");
71        let upload_url = resolve_upload_url(session)?;
72        let upload = JmapBlobUpload::new(http_auth, &upload_url, "message/rfc822", raw);
73        let keywords = flags.iter().map(|f| (keyword_from(f), true)).collect();
74        Ok(Self {
75            state: State::Uploading(upload),
76            mailbox_id: mailbox.into(),
77            keywords,
78            session: session.clone(),
79            http_auth: http_auth.clone(),
80        })
81    }
82}
83
84enum State {
85    Uploading(JmapBlobUpload),
86    Importing {
87        import: InnerImport,
88        client_id: String,
89    },
90    Done,
91}
92
93impl JmapCoroutine for JmapMessageAdd {
94    type Yield = JmapYield;
95    type Return = Result<String, JmapMessageAddError>;
96
97    fn resume(&mut self, bytes: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
98        match mem::replace(&mut self.state, State::Done) {
99            State::Uploading(mut upload) => match upload.resume(bytes) {
100                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
101                    self.state = State::Uploading(upload);
102                    JmapCoroutineState::Yielded(JmapYield::WantsRead)
103                }
104                JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(out)) => {
105                    self.state = State::Uploading(upload);
106                    JmapCoroutineState::Yielded(JmapYield::WantsWrite(out))
107                }
108                JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { .. }) => {
109                    JmapCoroutineState::Complete(Err(JmapMessageAddError::UnsupportedRedirect))
110                }
111                JmapCoroutineState::Complete(Ok(JmapBlobUploadOutput { blob_id, .. })) => {
112                    let mut mailbox_ids = BTreeMap::new();
113                    mailbox_ids.insert(self.mailbox_id.clone(), true);
114                    let client_id = String::from("new");
115                    let mut imports = BTreeMap::new();
116                    imports.insert(
117                        client_id.clone(),
118                        JmapEmailImportArgs {
119                            blob_id,
120                            mailbox_ids,
121                            keywords: if self.keywords.is_empty() {
122                                None
123                            } else {
124                                Some(self.keywords.clone())
125                            },
126                            received_at: None,
127                        },
128                    );
129                    let import = match InnerImport::new(&self.session, &self.http_auth, imports) {
130                        Ok(i) => i,
131                        Err(err) => return JmapCoroutineState::Complete(Err(err.into())),
132                    };
133                    self.state = State::Importing { import, client_id };
134                    JmapCoroutine::resume(self, None)
135                }
136                JmapCoroutineState::Complete(Err(err)) => {
137                    JmapCoroutineState::Complete(Err(err.into()))
138                }
139            },
140            State::Importing {
141                mut import,
142                client_id,
143            } => match import.resume(bytes) {
144                JmapCoroutineState::Complete(Ok(ok)) => {
145                    let Some(email) = ok.created.get(&client_id) else {
146                        return JmapCoroutineState::Complete(Err(JmapMessageAddError::NotImported));
147                    };
148                    JmapCoroutineState::Complete(Ok(email.id.clone().unwrap_or_default()))
149                }
150                JmapCoroutineState::Yielded(y) => {
151                    self.state = State::Importing { import, client_id };
152                    JmapCoroutineState::Yielded(y)
153                }
154                JmapCoroutineState::Complete(Err(err)) => {
155                    JmapCoroutineState::Complete(Err(err.into()))
156                }
157            },
158            State::Done => JmapCoroutineState::Complete(Err(JmapMessageAddError::ResumedAfterDone)),
159        }
160    }
161}
162
163/// Resolves the RFC 8620 upload URL template by substituting
164/// `{accountId}`.
165fn resolve_upload_url(session: &JmapSession) -> Result<Url, JmapMessageAddError> {
166    let account_id = session
167        .primary_accounts
168        .get(MAIL_CAPABILITY)
169        .cloned()
170        .unwrap_or_default();
171    let url_str = session.upload_url.replace("{accountId}", &account_id);
172    Url::parse(&url_str).map_err(|_| JmapMessageAddError::InvalidUploadUrl(url_str))
173}