io_jmap/rfc8621/email/
parse.rs1use core::fmt;
62
63use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
64
65use log::trace;
66use secrecy::SecretString;
67use serde::{Deserialize, Serialize};
68use thiserror::Error;
69
70use crate::{
71 coroutine::*,
72 jmap_try,
73 rfc8620::{CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapSession, send::*},
74 rfc8621::{
75 MAIL_CAPABILITY,
76 email::{JmapEmail, JmapEmailProperty},
77 },
78};
79
80#[derive(Debug, Error)]
82pub enum JmapEmailParseError {
83 #[error("JMAP Email/parse failed: missing response in method_responses")]
84 MissingResponse,
85 #[error("JMAP Email/parse failed: {0}")]
86 Send(#[from] JmapSendError),
87 #[error("JMAP Email/parse failed: serialize args: {0}")]
88 SerializeArgs(#[source] serde_json::Error),
89 #[error("JMAP Email/parse failed: parse response: {0}")]
90 ParseResponse(#[source] serde_json::Error),
91 #[error("JMAP Email/parse failed: {0}")]
92 Method(#[from] JmapMethodError),
93}
94
95#[derive(Clone, Debug, Default)]
97pub struct JmapEmailParseOptions {
98 pub properties: Option<Vec<JmapEmailProperty>>,
100}
101
102#[derive(Clone, Debug)]
104pub struct JmapEmailParseOutput {
105 pub parsed: BTreeMap<String, JmapEmail>,
106 pub not_parsable: Vec<String>,
107 pub not_found: Vec<String>,
108 pub keep_alive: bool,
109}
110
111pub struct JmapEmailParse {
113 state: State,
114}
115
116impl JmapEmailParse {
117 pub fn new(
118 session: &JmapSession,
119 http_auth: &SecretString,
120 blob_ids: Vec<String>,
121 opts: JmapEmailParseOptions,
122 ) -> Result<Self, JmapEmailParseError> {
123 let account_id = session
124 .primary_accounts
125 .get(MAIL_CAPABILITY)
126 .cloned()
127 .unwrap_or_default();
128 let api_url = &session.api_url;
129
130 let parse_args = EmailParseArgs {
131 account_id: &account_id,
132 blob_ids: &blob_ids,
133 properties: opts.properties.as_deref(),
134 fetch_text_body_values: true,
135 fetch_html_body_values: true,
136 max_body_value_bytes: None,
137 };
138
139 let mut batch = JmapBatch::new();
140 batch.add(
141 "Email/parse",
142 serde_json::to_value(&parse_args).map_err(JmapEmailParseError::SerializeArgs)?,
143 );
144 let request = batch.into_request(vec![CORE_CAPABILITY.into(), MAIL_CAPABILITY.into()]);
145
146 Ok(Self {
147 state: State::Send(JmapSend::new(http_auth, api_url, request)?),
148 })
149 }
150}
151
152impl JmapCoroutine for JmapEmailParse {
153 type Yield = JmapYield;
154 type Return = Result<JmapEmailParseOutput, JmapEmailParseError>;
155
156 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
157 trace!("Email/parse: {}", self.state);
158 match &mut self.state {
159 State::Send(send) => {
160 let JmapSendOutput {
161 response,
162 keep_alive,
163 } = jmap_try!(send, arg);
164
165 let Some((name, args, _)) = response.method_responses.into_iter().next() else {
166 return JmapCoroutineState::Complete(Err(JmapEmailParseError::MissingResponse));
167 };
168
169 if name == "error" {
170 let err = serde_json::from_value::<JmapMethodError>(args)
171 .unwrap_or(JmapMethodError::Unknown);
172 return JmapCoroutineState::Complete(Err(err.into()));
173 }
174
175 match serde_json::from_value::<EmailParseResponse>(args) {
176 Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailParseOutput {
177 parsed: r.parsed,
178 not_parsable: r.not_parsable.unwrap_or_default(),
179 not_found: r.not_found.unwrap_or_default(),
180 keep_alive,
181 })),
182 Err(err) => {
183 JmapCoroutineState::Complete(Err(JmapEmailParseError::ParseResponse(err)))
184 }
185 }
186 }
187 }
188 }
189}
190
191enum State {
192 Send(JmapSend),
193}
194
195impl fmt::Display for State {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::Send(_) => f.write_str("send"),
199 }
200 }
201}
202
203#[derive(Serialize)]
204#[serde(rename_all = "camelCase")]
205struct EmailParseArgs<'a> {
206 account_id: &'a str,
207 blob_ids: &'a [String],
208 #[serde(skip_serializing_if = "Option::is_none")]
209 properties: Option<&'a [JmapEmailProperty]>,
210 fetch_text_body_values: bool,
211 #[serde(rename = "fetchHTMLBodyValues")]
212 fetch_html_body_values: bool,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 max_body_value_bytes: Option<u64>,
215}
216
217#[derive(Deserialize)]
218#[serde(rename_all = "camelCase")]
219struct EmailParseResponse {
220 #[serde(default)]
221 parsed: BTreeMap<String, JmapEmail>,
222 not_parsable: Option<Vec<String>>,
223 not_found: Option<Vec<String>>,
224}