1use alloc::{collections::BTreeMap, string::String, vec::Vec};
17use core::mem;
18
19use io_jmap::{
20 coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
21 rfc8620::{
22 JmapSession,
23 blob_upload::{JmapBlobUpload, JmapBlobUploadError, JmapBlobUploadOutput},
24 coroutine::JmapRedirectYield,
25 },
26 rfc8621::{
27 email::{
28 JmapEmailImportArgs,
29 import::{JmapEmailImport as InnerImport, JmapEmailImportError as ImportErr},
30 },
31 email_submission::{
32 JmapEmailSubmissionCreate,
33 set::{
34 JmapEmailSubmissionSet as InnerSubmit, JmapEmailSubmissionSetError as SubmitErr,
35 },
36 },
37 },
38};
39use log::trace;
40use secrecy::SecretString;
41use thiserror::Error;
42use url::Url;
43
44use crate::jmap::convert::account_id_of;
45
46#[derive(Debug, Error)]
48pub enum JmapMessageSendError {
49 #[error(transparent)]
50 BlobUpload(#[from] JmapBlobUploadError),
51 #[error(transparent)]
52 Import(#[from] ImportErr),
53 #[error(transparent)]
54 Submit(#[from] SubmitErr),
55 #[error("Email/import did not create the staged email")]
56 NotImported,
57 #[error("Email/import response did not include an email id")]
58 MissingImportedEmailId,
59 #[error("JMAP blob upload reached unexpected redirection")]
60 UnsupportedRedirect,
61 #[error("EmailSubmission/set did not submit the email")]
62 NotSubmitted,
63 #[error("resolved JMAP upload URL is invalid: {0}")]
64 InvalidUploadUrl(String),
65 #[error("coroutine was resumed after completion")]
66 ResumedAfterDone,
67}
68
69pub struct JmapMessageSend {
71 state: State,
72 identity_id: String,
73 drafts_mailbox_id: String,
74 session: JmapSession,
75 http_auth: SecretString,
76}
77
78impl JmapMessageSend {
79 pub fn new(
80 session: &JmapSession,
81 http_auth: &SecretString,
82 identity_id: &str,
83 drafts_mailbox_id: &str,
84 raw: Vec<u8>,
85 ) -> Result<Self, JmapMessageSendError> {
86 trace!("prepare JMAP message send");
87 let upload_url = resolve_upload_url(session)?;
88 let upload = JmapBlobUpload::new(http_auth, &upload_url, "message/rfc822", raw);
89 Ok(Self {
90 state: State::Uploading(upload),
91 identity_id: identity_id.into(),
92 drafts_mailbox_id: drafts_mailbox_id.into(),
93 session: session.clone(),
94 http_auth: http_auth.clone(),
95 })
96 }
97}
98
99enum State {
100 Uploading(JmapBlobUpload),
101 Importing(InnerImport),
102 Submitting(InnerSubmit),
103 Done,
104}
105
106const OUTGOING: &str = "outgoing";
107
108impl JmapCoroutine for JmapMessageSend {
109 type Yield = JmapYield;
110 type Return = Result<(), JmapMessageSendError>;
111
112 fn resume(&mut self, bytes: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
113 match mem::replace(&mut self.state, State::Done) {
114 State::Uploading(mut upload) => match upload.resume(bytes) {
115 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
116 self.state = State::Uploading(upload);
117 JmapCoroutineState::Yielded(JmapYield::WantsRead)
118 }
119 JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(out)) => {
120 self.state = State::Uploading(upload);
121 JmapCoroutineState::Yielded(JmapYield::WantsWrite(out))
122 }
123 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { .. }) => {
124 JmapCoroutineState::Complete(Err(JmapMessageSendError::UnsupportedRedirect))
125 }
126 JmapCoroutineState::Complete(Ok(JmapBlobUploadOutput { blob_id, .. })) => {
127 let mut mailbox_ids = BTreeMap::new();
128 mailbox_ids.insert(self.drafts_mailbox_id.clone(), true);
129
130 let mut keywords = BTreeMap::new();
131 keywords.insert("$draft".into(), true);
132
133 let mut emails = BTreeMap::new();
134 emails.insert(
135 OUTGOING.into(),
136 JmapEmailImportArgs {
137 blob_id,
138 mailbox_ids,
139 keywords: Some(keywords),
140 received_at: None,
141 },
142 );
143
144 let import = match InnerImport::new(&self.session, &self.http_auth, emails) {
145 Ok(i) => i,
146 Err(err) => return JmapCoroutineState::Complete(Err(err.into())),
147 };
148 self.state = State::Importing(import);
149 JmapCoroutine::resume(self, None)
150 }
151 JmapCoroutineState::Complete(Err(err)) => {
152 JmapCoroutineState::Complete(Err(err.into()))
153 }
154 },
155 State::Importing(mut import) => match import.resume(bytes) {
156 JmapCoroutineState::Yielded(y) => {
157 self.state = State::Importing(import);
158 JmapCoroutineState::Yielded(y)
159 }
160 JmapCoroutineState::Complete(Ok(mut ok)) => {
161 if !ok.not_created.is_empty() {
162 return JmapCoroutineState::Complete(Err(
163 JmapMessageSendError::NotImported,
164 ));
165 }
166 let Some(email) = ok.created.remove(OUTGOING) else {
167 return JmapCoroutineState::Complete(Err(
168 JmapMessageSendError::NotImported,
169 ));
170 };
171 let Some(email_id) = email.id else {
172 return JmapCoroutineState::Complete(Err(
173 JmapMessageSendError::MissingImportedEmailId,
174 ));
175 };
176
177 let mut submissions = BTreeMap::new();
178 submissions.insert(
179 OUTGOING.into(),
180 JmapEmailSubmissionCreate {
181 identity_id: self.identity_id.clone(),
182 email_id,
183 envelope: None,
184 },
185 );
186 let submit = match InnerSubmit::new(&self.session, &self.http_auth, submissions)
187 {
188 Ok(s) => s,
189 Err(err) => return JmapCoroutineState::Complete(Err(err.into())),
190 };
191 self.state = State::Submitting(submit);
192 JmapCoroutine::resume(self, None)
193 }
194 JmapCoroutineState::Complete(Err(err)) => {
195 JmapCoroutineState::Complete(Err(err.into()))
196 }
197 },
198 State::Submitting(mut submit) => match submit.resume(bytes) {
199 JmapCoroutineState::Yielded(y) => {
200 self.state = State::Submitting(submit);
201 JmapCoroutineState::Yielded(y)
202 }
203 JmapCoroutineState::Complete(Ok(ok)) => {
204 if !ok.not_created.is_empty() {
205 JmapCoroutineState::Complete(Err(JmapMessageSendError::NotSubmitted))
206 } else {
207 JmapCoroutineState::Complete(Ok(()))
208 }
209 }
210 JmapCoroutineState::Complete(Err(err)) => {
211 JmapCoroutineState::Complete(Err(err.into()))
212 }
213 },
214 State::Done => {
215 JmapCoroutineState::Complete(Err(JmapMessageSendError::ResumedAfterDone))
216 }
217 }
218 }
219}
220
221fn resolve_upload_url(session: &JmapSession) -> Result<Url, JmapMessageSendError> {
223 let account_id = account_id_of(session);
224 let url_str = session.upload_url.replace("{accountId}", &account_id);
225 Url::parse(&url_str).map_err(|_| JmapMessageSendError::InvalidUploadUrl(url_str))
226}