io_email/message/jmap/
get.rs1use alloc::{
16 string::{String, ToString},
17 vec,
18 vec::Vec,
19};
20use core::mem;
21
22use io_jmap::{
23 coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
24 rfc8620::{
25 JmapSession,
26 blob_download::{JmapBlobDownload, JmapBlobDownloadError, JmapBlobDownloadOutput},
27 coroutine::JmapRedirectYield,
28 },
29 rfc8621::email::{
30 JmapEmailProperty,
31 get::{JmapEmailGet as InnerGet, JmapEmailGetError as InnerErr, JmapEmailGetOptions},
32 },
33};
34use log::trace;
35use secrecy::SecretString;
36use thiserror::Error;
37use url::Url;
38
39use crate::jmap::convert::account_id_of;
40
41#[derive(Debug, Error)]
43pub enum JmapMessageGetError {
44 #[error(transparent)]
45 EmailGet(#[from] InnerErr),
46 #[error(transparent)]
47 BlobDownload(#[from] JmapBlobDownloadError),
48 #[error("Email/get returned no email for the requested id")]
49 EmailNotFound,
50 #[error("Email/get response did not include a blobId")]
51 MissingBlobId,
52 #[error("resolved JMAP download URL is invalid: {0}")]
53 InvalidDownloadUrl(String),
54 #[error("JMAP blob download was redirected; not yet supported")]
55 UnsupportedRedirect,
56 #[error("coroutine was resumed after completion")]
57 ResumedAfterDone,
58}
59
60pub struct JmapMessageGet {
62 state: State,
63 http_auth: SecretString,
64 download_url_template: String,
65 account_id: String,
66}
67
68impl JmapMessageGet {
69 pub fn new(
70 session: &JmapSession,
71 http_auth: &SecretString,
72 _mailbox: &str,
73 id: &str,
74 ) -> Result<Self, JmapMessageGetError> {
75 trace!("prepare JMAP message get");
76 let opts = JmapEmailGetOptions {
77 properties: Some(vec![JmapEmailProperty::BlobId]),
78 ..Default::default()
79 };
80 let get = InnerGet::new(session, http_auth, vec![id.to_string()], opts)?;
81 Ok(Self {
82 state: State::GettingEmail(get),
83 http_auth: http_auth.clone(),
84 download_url_template: session.download_url.clone(),
85 account_id: account_id_of(session),
86 })
87 }
88}
89
90enum State {
91 GettingEmail(InnerGet),
92 Downloading(JmapBlobDownload),
93 Done,
94}
95
96impl JmapCoroutine for JmapMessageGet {
97 type Yield = JmapYield;
98 type Return = Result<Vec<u8>, JmapMessageGetError>;
99
100 fn resume(&mut self, bytes: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
101 match mem::replace(&mut self.state, State::Done) {
102 State::GettingEmail(mut get) => match get.resume(bytes) {
103 JmapCoroutineState::Yielded(y) => {
104 self.state = State::GettingEmail(get);
105 JmapCoroutineState::Yielded(y)
106 }
107 JmapCoroutineState::Complete(Ok(ok)) => {
108 let Some(email) = ok.emails.into_iter().next() else {
109 return JmapCoroutineState::Complete(Err(
110 JmapMessageGetError::EmailNotFound,
111 ));
112 };
113 let Some(blob_id) = email.blob_id else {
114 return JmapCoroutineState::Complete(Err(
115 JmapMessageGetError::MissingBlobId,
116 ));
117 };
118 let url_str = resolve_download_url(
119 &self.download_url_template,
120 &self.account_id,
121 &blob_id,
122 );
123 let Ok(url) = Url::parse(&url_str) else {
124 return JmapCoroutineState::Complete(Err(
125 JmapMessageGetError::InvalidDownloadUrl(url_str),
126 ));
127 };
128 self.state = State::Downloading(JmapBlobDownload::new(&self.http_auth, &url));
129 JmapCoroutine::resume(self, None)
130 }
131 JmapCoroutineState::Complete(Err(err)) => {
132 JmapCoroutineState::Complete(Err(err.into()))
133 }
134 },
135 State::Downloading(mut dl) => match dl.resume(bytes) {
136 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
137 self.state = State::Downloading(dl);
138 JmapCoroutineState::Yielded(JmapYield::WantsRead)
139 }
140 JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(out)) => {
141 self.state = State::Downloading(dl);
142 JmapCoroutineState::Yielded(JmapYield::WantsWrite(out))
143 }
144 JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { .. }) => {
145 JmapCoroutineState::Complete(Err(JmapMessageGetError::UnsupportedRedirect))
146 }
147 JmapCoroutineState::Complete(Ok(JmapBlobDownloadOutput { data, .. })) => {
148 JmapCoroutineState::Complete(Ok(data))
149 }
150 JmapCoroutineState::Complete(Err(err)) => {
151 JmapCoroutineState::Complete(Err(err.into()))
152 }
153 },
154 State::Done => JmapCoroutineState::Complete(Err(JmapMessageGetError::ResumedAfterDone)),
155 }
156 }
157}
158
159fn resolve_download_url(template: &str, account_id: &str, blob_id: &str) -> String {
162 template
163 .replace("{accountId}", account_id)
164 .replace("{blobId}", blob_id)
165 .replace("{type}", "message%2Frfc822")
166 .replace("{name}", "message.eml")
167}