1use alloc::{string::String, vec, vec::Vec};
69
70use secrecy::SecretString;
71use serde::{Deserialize, Serialize};
72use thiserror::Error;
73
74use crate::{
75 coroutine::*,
76 jmap_try,
77 rfc8620::{
78 JMAP_CORE_CAPABILITY, error::JmapMethodError, filter::JmapFilter, request::JmapBatch,
79 request::JmapResultReference, send::*, session::JmapSession,
80 },
81 rfc8621::{
82 JMAP_MAIL_CAPABILITY,
83 email::{JmapEmail, JmapEmailProperty},
84 },
85};
86
87#[derive(Clone, Debug, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub enum JmapEmailSortProperty {
91 ReceivedAt,
93 SentAt,
95 Size,
97 From,
99 To,
101 Subject,
103 HasAttachment,
105 Keyword,
107 AllInThreadHaveKeyword,
110 SomeInThreadHaveKeyword,
113}
114
115#[derive(Clone, Debug, Default, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct JmapEmailFilter {
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub in_mailbox: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub in_mailbox_other_than: Option<Vec<String>>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub before: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub after: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub min_size: Option<u64>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub max_size: Option<u64>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub all_in_thread_have_keyword: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub some_in_thread_have_keyword: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub none_in_thread_have_keyword: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub has_keyword: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub not_keyword: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub has_attachment: Option<bool>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub text: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub from: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub to: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub cc: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub bcc: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub subject: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub body: Option<String>,
176}
177
178#[derive(Clone, Debug, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct JmapEmailComparator {
182 pub property: JmapEmailSortProperty,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub is_ascending: Option<bool>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub collation: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
193 pub keyword: Option<String>,
194}
195
196impl JmapEmailComparator {
197 pub fn received_at_desc() -> Self {
199 Self {
200 property: JmapEmailSortProperty::ReceivedAt,
201 is_ascending: Some(false),
202 collation: None,
203 keyword: None,
204 }
205 }
206}
207
208#[derive(Debug, Error)]
210pub enum JmapEmailQueryError {
211 #[error("JMAP Email/query failed: missing Email/query response in method_responses")]
213 MissingQueryResponse,
214 #[error("JMAP Email/query failed: missing Email/get response in method_responses")]
216 MissingGetResponse,
217 #[error("JMAP Email/query failed: {0}")]
219 Send(#[from] JmapSendError),
220 #[error("JMAP Email/query failed: parse Email/query response: {0}")]
222 ParseQueryResponse(#[source] serde_json::Error),
223 #[error("JMAP Email/query failed: parse Email/get response: {0}")]
225 ParseGetResponse(#[source] serde_json::Error),
226 #[error("JMAP Email/query failed: Email/query: {0}")]
228 QueryMethod(JmapMethodError),
229 #[error("JMAP Email/query failed: Email/get: {0}")]
231 GetMethod(JmapMethodError),
232}
233
234#[derive(Clone, Debug, Default)]
236pub struct JmapEmailQueryOptions {
237 pub filter: Option<JmapFilter<JmapEmailFilter>>,
239 pub sort: Option<Vec<JmapEmailComparator>>,
241 pub position: Option<u64>,
243 pub limit: Option<u64>,
245 pub properties: Option<Vec<JmapEmailProperty>>,
247}
248
249#[derive(Clone, Debug)]
251pub struct JmapEmailQueryOutput {
252 pub emails: Vec<JmapEmail>,
254 pub total: Option<u64>,
256 pub position: u64,
258 pub query_state: String,
260 pub keep_alive: bool,
262}
263
264pub struct JmapEmailQuery {
266 state: State,
267}
268
269impl JmapEmailQuery {
270 pub fn new(
272 session: &JmapSession,
273 http_auth: &SecretString,
274 opts: JmapEmailQueryOptions,
275 ) -> Result<Self, JmapEmailQueryError> {
276 let account_id = session
277 .primary_accounts
278 .get(JMAP_MAIL_CAPABILITY)
279 .cloned()
280 .unwrap_or_default();
281 let api_url = &session.api_url;
282
283 let query_args = EmailQueryArgs {
284 account_id: &account_id,
285 filter: opts.filter.as_ref(),
286 sort: opts.sort.as_deref(),
287 position: opts.position,
288 limit: opts.limit,
289 calculate_total: true,
290 };
291
292 let mut batch = JmapBatch::new();
293 let query_id = batch.add(
294 "Email/query",
295 serde_json::to_value(&query_args).map_err(JmapEmailQueryError::ParseQueryResponse)?,
296 );
297
298 let get_args = EmailGetByRefArgs {
299 account_id: &account_id,
300 ids_ref: JmapResultReference {
301 result_of: &query_id,
302 name: "Email/query",
303 path: "/ids",
304 },
305 properties: opts.properties.as_deref(),
306 };
307
308 batch.add(
309 "Email/get",
310 serde_json::to_value(&get_args).map_err(JmapEmailQueryError::ParseQueryResponse)?,
311 );
312
313 let request = batch.into_request(vec![
314 JMAP_CORE_CAPABILITY.into(),
315 JMAP_MAIL_CAPABILITY.into(),
316 ]);
317
318 Ok(Self {
319 state: State::Send(JmapSend::new(http_auth, api_url, request)?),
320 })
321 }
322}
323
324impl JmapCoroutine for JmapEmailQuery {
325 type Yield = JmapYield;
326 type Return = Result<JmapEmailQueryOutput, JmapEmailQueryError>;
327
328 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
329 match &mut self.state {
330 State::Send(send) => {
331 let JmapSendOutput {
332 response,
333 keep_alive,
334 } = jmap_try!(send, arg);
335
336 let mut responses = response.method_responses.into_iter();
337
338 let Some((query_name, query_args, _)) = responses.next() else {
339 return JmapCoroutineState::Complete(Err(
340 JmapEmailQueryError::MissingQueryResponse,
341 ));
342 };
343
344 if query_name == "error" {
345 let err = serde_json::from_value::<JmapMethodError>(query_args)
346 .unwrap_or(JmapMethodError::Unknown);
347 return JmapCoroutineState::Complete(Err(JmapEmailQueryError::QueryMethod(
348 err,
349 )));
350 }
351
352 let query_response = match serde_json::from_value::<EmailQueryResponse>(query_args)
353 {
354 Ok(r) => r,
355 Err(err) => {
356 return JmapCoroutineState::Complete(Err(
357 JmapEmailQueryError::ParseQueryResponse(err),
358 ));
359 }
360 };
361
362 let Some((get_name, get_args, _)) = responses.next() else {
363 return JmapCoroutineState::Complete(Err(
364 JmapEmailQueryError::MissingGetResponse,
365 ));
366 };
367
368 if get_name == "error" {
369 let err = serde_json::from_value::<JmapMethodError>(get_args)
370 .unwrap_or(JmapMethodError::Unknown);
371 return JmapCoroutineState::Complete(Err(JmapEmailQueryError::GetMethod(err)));
372 }
373
374 match serde_json::from_value::<EmailGetResponse>(get_args) {
375 Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailQueryOutput {
376 emails: r.list,
377 total: query_response.total,
378 position: query_response.position,
379 query_state: query_response.query_state,
380 keep_alive,
381 })),
382 Err(err) => JmapCoroutineState::Complete(Err(
383 JmapEmailQueryError::ParseGetResponse(err),
384 )),
385 }
386 }
387 }
388 }
389}
390
391enum State {
392 Send(JmapSend),
393}
394
395#[derive(Serialize)]
396#[serde(rename_all = "camelCase")]
397struct EmailQueryArgs<'a> {
398 account_id: &'a str,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 filter: Option<&'a JmapFilter<JmapEmailFilter>>,
401 #[serde(skip_serializing_if = "Option::is_none")]
402 sort: Option<&'a [JmapEmailComparator]>,
403 #[serde(skip_serializing_if = "Option::is_none")]
404 position: Option<u64>,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 limit: Option<u64>,
407 calculate_total: bool,
408}
409
410#[derive(Serialize)]
411#[serde(rename_all = "camelCase")]
412struct EmailGetByRefArgs<'a> {
413 account_id: &'a str,
414 #[serde(rename = "#ids")]
415 ids_ref: JmapResultReference<'a>,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 properties: Option<&'a [JmapEmailProperty]>,
418}
419
420#[derive(Deserialize)]
421#[serde(rename_all = "camelCase")]
422struct EmailQueryResponse {
423 query_state: String,
424 #[serde(default)]
425 total: Option<u64>,
426 #[serde(default)]
427 position: u64,
428}
429
430#[derive(Deserialize)]
431#[serde(rename_all = "camelCase")]
432struct EmailGetResponse {
433 list: Vec<JmapEmail>,
434}