Skip to main content

io_email/message/jmap/
get.rs

1//! JMAP message-get coroutine: Email/get(blobId) then Blob/download
2//! to return raw RFC 5322 bytes.
3//!
4//! Redirects (RFC 8620 ยง6.2) surface as an error rather than being
5//! followed.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use io_email::message::jmap::get::JmapMessageGet;
11//!
12//! let raw = client.run(JmapMessageGet::new(&session, &auth, "_", "email-id")?)?;
13//! ```
14
15use 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/// Errors produced by [`JmapMessageGet`].
42#[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
60/// I/O-free coroutine fetching the raw RFC 5322 bytes of a JMAP email.
61pub 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
159/// Substitutes `{accountId, blobId, type, name}` in the download URL
160/// template.
161fn 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}