io_email/message/jmap/
add.rs1use 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#[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
53pub 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
163fn 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}