use alloc::{string::String, vec, vec::Vec};
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
coroutine::*,
jmap_try,
rfc8620::{
JMAP_CORE_CAPABILITY, error::JmapMethodError, filter::JmapFilter, request::JmapBatch,
request::JmapResultReference, send::*, session::JmapSession,
},
rfc8621::{
JMAP_MAIL_CAPABILITY,
email::{JmapEmail, JmapEmailProperty},
},
};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum JmapEmailSortProperty {
ReceivedAt,
SentAt,
Size,
From,
To,
Subject,
HasAttachment,
Keyword,
AllInThreadHaveKeyword,
SomeInThreadHaveKeyword,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapEmailFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub in_mailbox: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub in_mailbox_other_than: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_in_thread_have_keyword: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub some_in_thread_have_keyword: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub none_in_thread_have_keyword: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_keyword: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub not_keyword: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_attachment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bcc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapEmailComparator {
pub property: JmapEmailSortProperty,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_ascending: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyword: Option<String>,
}
impl JmapEmailComparator {
pub fn received_at_desc() -> Self {
Self {
property: JmapEmailSortProperty::ReceivedAt,
is_ascending: Some(false),
collation: None,
keyword: None,
}
}
}
#[derive(Debug, Error)]
pub enum JmapEmailQueryError {
#[error("JMAP Email/query failed: missing Email/query response in method_responses")]
MissingQueryResponse,
#[error("JMAP Email/query failed: missing Email/get response in method_responses")]
MissingGetResponse,
#[error("JMAP Email/query failed: {0}")]
Send(#[from] JmapSendError),
#[error("JMAP Email/query failed: parse Email/query response: {0}")]
ParseQueryResponse(#[source] serde_json::Error),
#[error("JMAP Email/query failed: parse Email/get response: {0}")]
ParseGetResponse(#[source] serde_json::Error),
#[error("JMAP Email/query failed: Email/query: {0}")]
QueryMethod(JmapMethodError),
#[error("JMAP Email/query failed: Email/get: {0}")]
GetMethod(JmapMethodError),
}
#[derive(Clone, Debug, Default)]
pub struct JmapEmailQueryOptions {
pub filter: Option<JmapFilter<JmapEmailFilter>>,
pub sort: Option<Vec<JmapEmailComparator>>,
pub position: Option<u64>,
pub limit: Option<u64>,
pub properties: Option<Vec<JmapEmailProperty>>,
}
#[derive(Clone, Debug)]
pub struct JmapEmailQueryOutput {
pub emails: Vec<JmapEmail>,
pub total: Option<u64>,
pub position: u64,
pub query_state: String,
pub keep_alive: bool,
}
pub struct JmapEmailQuery {
state: State,
}
impl JmapEmailQuery {
pub fn new(
session: &JmapSession,
http_auth: &SecretString,
opts: JmapEmailQueryOptions,
) -> Result<Self, JmapEmailQueryError> {
let account_id = session
.primary_accounts
.get(JMAP_MAIL_CAPABILITY)
.cloned()
.unwrap_or_default();
let api_url = &session.api_url;
let query_args = EmailQueryArgs {
account_id: &account_id,
filter: opts.filter.as_ref(),
sort: opts.sort.as_deref(),
position: opts.position,
limit: opts.limit,
calculate_total: true,
};
let mut batch = JmapBatch::new();
let query_id = batch.add(
"Email/query",
serde_json::to_value(&query_args).map_err(JmapEmailQueryError::ParseQueryResponse)?,
);
let get_args = EmailGetByRefArgs {
account_id: &account_id,
ids_ref: JmapResultReference {
result_of: &query_id,
name: "Email/query",
path: "/ids",
},
properties: opts.properties.as_deref(),
};
batch.add(
"Email/get",
serde_json::to_value(&get_args).map_err(JmapEmailQueryError::ParseQueryResponse)?,
);
let request = batch.into_request(vec![
JMAP_CORE_CAPABILITY.into(),
JMAP_MAIL_CAPABILITY.into(),
]);
Ok(Self {
state: State::Send(JmapSend::new(http_auth, api_url, request)?),
})
}
}
impl JmapCoroutine for JmapEmailQuery {
type Yield = JmapYield;
type Return = Result<JmapEmailQueryOutput, JmapEmailQueryError>;
fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
match &mut self.state {
State::Send(send) => {
let JmapSendOutput {
response,
keep_alive,
} = jmap_try!(send, arg);
let mut responses = response.method_responses.into_iter();
let Some((query_name, query_args, _)) = responses.next() else {
return JmapCoroutineState::Complete(Err(
JmapEmailQueryError::MissingQueryResponse,
));
};
if query_name == "error" {
let err = serde_json::from_value::<JmapMethodError>(query_args)
.unwrap_or(JmapMethodError::Unknown);
return JmapCoroutineState::Complete(Err(JmapEmailQueryError::QueryMethod(
err,
)));
}
let query_response = match serde_json::from_value::<EmailQueryResponse>(query_args)
{
Ok(r) => r,
Err(err) => {
return JmapCoroutineState::Complete(Err(
JmapEmailQueryError::ParseQueryResponse(err),
));
}
};
let Some((get_name, get_args, _)) = responses.next() else {
return JmapCoroutineState::Complete(Err(
JmapEmailQueryError::MissingGetResponse,
));
};
if get_name == "error" {
let err = serde_json::from_value::<JmapMethodError>(get_args)
.unwrap_or(JmapMethodError::Unknown);
return JmapCoroutineState::Complete(Err(JmapEmailQueryError::GetMethod(err)));
}
match serde_json::from_value::<EmailGetResponse>(get_args) {
Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailQueryOutput {
emails: r.list,
total: query_response.total,
position: query_response.position,
query_state: query_response.query_state,
keep_alive,
})),
Err(err) => JmapCoroutineState::Complete(Err(
JmapEmailQueryError::ParseGetResponse(err),
)),
}
}
}
}
}
enum State {
Send(JmapSend),
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct EmailQueryArgs<'a> {
account_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
filter: Option<&'a JmapFilter<JmapEmailFilter>>,
#[serde(skip_serializing_if = "Option::is_none")]
sort: Option<&'a [JmapEmailComparator]>,
#[serde(skip_serializing_if = "Option::is_none")]
position: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u64>,
calculate_total: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct EmailGetByRefArgs<'a> {
account_id: &'a str,
#[serde(rename = "#ids")]
ids_ref: JmapResultReference<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
properties: Option<&'a [JmapEmailProperty]>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmailQueryResponse {
query_state: String,
#[serde(default)]
total: Option<u64>,
#[serde(default)]
position: u64,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmailGetResponse {
list: Vec<JmapEmail>,
}