From efa83c2255b00e22247e9cfbbfc36a91a664b6fa Mon Sep 17 00:00:00 2001 From: mdecimus Date: Thu, 25 Sep 2025 19:42:12 +0200 Subject: [PATCH] JMAP protocol layer refactoring (part 3) --- crates/jmap-proto/src/method/get.rs | 49 +- crates/jmap-proto/src/method/query.rs | 811 ++++++------------ crates/jmap-proto/src/method/query_changes.rs | 7 +- .../jmap-proto/src/method/search_snippet.rs | 26 +- crates/jmap-proto/src/method/set.rs | 58 +- crates/jmap-proto/src/method/upload.rs | 6 +- crates/jmap-proto/src/object/blob.rs | 8 + crates/jmap-proto/src/object/email.rs | 348 +++++++- .../jmap-proto/src/object/email_submission.rs | 117 ++- crates/jmap-proto/src/object/identity.rs | 8 + crates/jmap-proto/src/object/mailbox.rs | 115 ++- crates/jmap-proto/src/object/mod.rs | 10 +- crates/jmap-proto/src/object/principal.rs | 118 ++- .../src/object/push_subscription.rs | 8 + crates/jmap-proto/src/object/quota.rs | 101 ++- crates/jmap-proto/src/object/sieve.rs | 86 +- crates/jmap-proto/src/object/thread.rs | 8 + .../src/object/vacation_response.rs | 8 + crates/jmap-proto/src/request/deserialize.rs | 2 +- crates/jmap-proto/src/request/mod.rs | 14 +- crates/jmap-proto/src/request/parser.rs | 40 + crates/jmap-proto/src/request/reference.rs | 18 + crates/jmap-proto/src/response/mod.rs | 384 ++++++--- crates/jmap/src/sieve/set.rs | 20 + 24 files changed, 1599 insertions(+), 771 deletions(-) diff --git a/crates/jmap-proto/src/method/get.rs b/crates/jmap-proto/src/method/get.rs index 29cd709e..0ebd947b 100644 --- a/crates/jmap-proto/src/method/get.rs +++ b/crates/jmap-proto/src/method/get.rs @@ -13,9 +13,9 @@ use crate::{ }, types::state::State, }; -use jmap_tools::{Property, Value}; +use jmap_tools::Value; use serde::{Deserialize, Deserializer}; -use types::{blob::BlobId, id::Id}; +use types::id::Id; #[derive(Debug, Clone)] pub struct GetRequest { @@ -90,14 +90,26 @@ impl Default for GetRequest { } } -/* impl GetRequest { pub fn unwrap_properties(&mut self, default: &[T::Property]) -> Vec { - if let Some(mut properties) = self.properties.take().map(|p| p.unwrap()) { - // Add Id Property - if !properties.contains(&Property::Id) { - properties.push(Property::Id); + if let Some(properties_) = self.properties.take().map(|p| p.unwrap()) { + let mut properties = Vec::with_capacity(properties_.len()); + let id_prop = T::ID_PROPERTY; + let mut has_id = false; + + for prop in properties_ { + if let MaybeInvalid::Value(p) = prop { + if p == id_prop { + has_id = true; + } + properties.push(p); + } } + + if !has_id { + properties.push(id_prop); + } + properties } else { default.to_vec() @@ -110,27 +122,7 @@ impl GetRequest { if ids.len() <= max_objects_in_get { Ok(Some( ids.into_iter() - .filter_map(|id| id.try_unwrap().and_then(|id| id.into_id())) - .collect::>(), - )) - } else { - Err(trc::JmapEvent::RequestTooLarge.into_err()) - } - } else { - Ok(None) - } - } - - pub fn unwrap_blob_ids( - &mut self, - max_objects_in_get: usize, - ) -> trc::Result>> { - if let Some(ids) = self.ids.take() { - let ids = ids.unwrap(); - if ids.len() <= max_objects_in_get { - Ok(Some( - ids.into_iter() - .filter_map(|id| id.try_unwrap().and_then(|id| id.into_blob_id())) + .filter_map(|id| id.try_unwrap()) .collect::>(), )) } else { @@ -141,4 +133,3 @@ impl GetRequest { } } } -*/ diff --git a/crates/jmap-proto/src/method/query.rs b/crates/jmap-proto/src/method/query.rs index 51cf8cbf..7e4bc9de 100644 --- a/crates/jmap-proto/src/method/query.rs +++ b/crates/jmap-proto/src/method/query.rs @@ -5,18 +5,20 @@ */ use crate::{ - object::{JmapObject, email, mailbox}, - request::{ - deserialize::{DeserializeArguments, deserialize_request}, - method::MethodObject, - }, - types::{date::UTCDate, state::State}, + object::JmapObject, + request::deserialize::{DeserializeArguments, deserialize_request}, + types::state::State, +}; +use serde::{ + Deserialize, Deserializer, + de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}, +}; +use std::{ + borrow::Cow, + fmt::{self, Display, Formatter}, }; -use compact_str::format_compact; -use serde::{Deserialize, Deserializer, de::DeserializeOwned}; -use std::fmt::Display; use store::fts::{FilterItem, FilterType, FtsFilter}; -use types::{id::Id, keyword::Keyword}; +use types::{id::Id}; #[derive(Debug, Clone)] pub struct QueryRequest { @@ -57,95 +59,26 @@ pub struct QueryResponse { pub limit: Option, } -#[derive(Clone, Debug, Deserialize)] -pub enum Filter { +#[derive(Clone, Debug)] +pub enum Filter +where + T: for<'de> DeserializeArguments<'de> + Default, +{ Property(T), - And, Or, Not, Close, } -/* - -Email(String), - Name(String), - DomainName(String), - Text(String), - Type(String), - Timezone(String), - Members(Id), - QuotaLt(u32), - QuotaGt(u32), - IdentityIds(Vec), - EmailIds(Vec), - ThreadIds(Vec), - UndoStatus(String), - Before(UTCDate), - After(UTCDate), - InMailbox(Id), - InMailboxOtherThan(Vec), - MinSize(u32), - MaxSize(u32), - AllInThreadHaveKeyword(Keyword), - SomeInThreadHaveKeyword(Keyword), - NoneInThreadHaveKeyword(Keyword), - HasKeyword(Keyword), - NotKeyword(Keyword), - HasAttachment(bool), - From(String), - To(String), - Cc(String), - Bcc(String), - Subject(String), - Body(String), - Header(Vec), - Id(Vec), - SentBefore(UTCDate), - SentAfter(UTCDate), - InThread(Id), - ParentId(Option), - Role(Option), - HasAnyRole(bool), - IsSubscribed(bool), - IsActive(bool), - Scope(String), - ResourceType(String), - _T(String), - -*/ - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub struct Comparator { +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Comparator +where + T: for<'de> DeserializeArguments<'de> + Default, +{ pub is_ascending: bool, pub collation: Option, pub property: T, - //pub keyword: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SortProperty { - Type, - Name, - Email, - EmailId, - ThreadId, - SentAt, - ReceivedAt, - Size, - From, - To, - Subject, - Cc, - SortOrder, - ParentId, - IsActive, - HasKeyword, - AllInThreadHaveKeyword, - SomeInThreadHaveKeyword, - Used, - _T(String), } impl<'de, T: JmapObject> DeserializeArguments<'de> for QueryRequest { @@ -158,7 +91,7 @@ impl<'de, T: JmapObject> DeserializeArguments<'de> for QueryRequest { self.account_id = map.next_value()?; }, b"filter" => { - self.filter = map.next_value()?; + self.filter = map.next_value::>()?.0; }, b"sort" => { self.sort = map.next_value()?; @@ -212,491 +145,194 @@ impl Default for QueryRequest { } } -/* -pub fn parse_filter(parser: &mut Parser) -> trc::Result> { - let mut filter = vec![Filter::Close]; - let mut pos_stack = vec![0]; +struct FilterMapCollector<'x, T: 'x>(&'x mut Vec>) +where + T: for<'de> DeserializeArguments<'de> + Default; - loop { - match parser.next_token::()? { - Token::String(property) => { - parser.next_token::()?.assert(Token::Colon)?; - filter[*pos_stack.last().unwrap()] = match &property.hash[0] { - 0x726f_7461_7265_706f => { - match parser.next_token::()?.unwrap_string("operator")? { - 0x0044_4e41 => Filter::And, - 0x524f => Filter::Or, - 0x0054_4f4e => Filter::Not, - _ => return Err(parser.error_value()), - } - } - 0x736e_6f69_7469_646e_6f63 => { - parser.next_token::()?.assert(Token::ArrayStart)?; - continue; - } - _ => match (&property.hash[0], &property.hash[1]) { - (0x006c_6961_6d65, _) => { - Filter::Email(parser.next_token::()?.unwrap_string("email")?) - } - (0x656d_616e, _) => { - Filter::Name(parser.next_token::()?.unwrap_string("name")?) - } - (0x656d_614e_6e69_616d_6f64, _) => Filter::DomainName( - parser.next_token::()?.unwrap_string("domainName")?, - ), - (0x7478_6574, _) => { - Filter::Text(parser.next_token::()?.unwrap_string("text")?) - } - (0x6570_7974, _) => { - Filter::Type(parser.next_token::()?.unwrap_string("type")?) - } - (0x656e_6f7a_656d_6974, _) => Filter::Timezone( - parser.next_token::()?.unwrap_string("timezone")?, - ), - (0x0073_7265_626d_656d, _) => { - Filter::Members(parser.next_token::()?.unwrap_string("members")?) - } - (0x6e61_6854_7265_776f_4c61_746f_7571, _) => Filter::QuotaLt( - parser - .next_token::()? - .unwrap_uint_or_null("quotaLowerThan")? - .unwrap_or_default() as u32, - ), - (0x6e61_6854_7265_7461_6572_4761_746f_7571, _) => Filter::QuotaGt( - parser - .next_token::()? - .unwrap_uint_or_null("quotaGreaterThan")? - .unwrap_or_default() as u32, - ), - (0x0073_6449_7974_6974_6e65_6469, _) => { - Filter::IdentityIds(>::parse(parser)?) - } - (0x7364_496c_6961_6d65, _) => Filter::EmailIds(>::parse(parser)?), - (0x0073_6449_6461_6572_6874, _) => { - Filter::ThreadIds(>::parse(parser)?) - } - (0x7375_7461_7453_6f64_6e75, _) => Filter::UndoStatus( - parser.next_token::()?.unwrap_string("undoStatus")?, - ), - (0x6572_6f66_6562, _) => { - Filter::Before(parser.next_token::()?.unwrap_string("before")?) - } - (0x0072_6574_6661, _) => { - Filter::After(parser.next_token::()?.unwrap_string("after")?) - } - (0x0078_6f62_6c69_614d_6e69, _) => Filter::InMailbox( - parser.next_token::()?.unwrap_string("inMailbox")?, - ), - (0x6854_7265_6874_4f78_6f62_6c69_614d_6e69, 0x6e61) => { - Filter::InMailboxOtherThan(>::parse(parser)?) - } - (0x0065_7a69_536e_696d, _) => Filter::MinSize( - parser - .next_token::()? - .unwrap_uint_or_null("minSize")? - .unwrap_or_default() as u32, - ), - (0x0065_7a69_5378_616d, _) => Filter::MaxSize( - parser - .next_token::()? - .unwrap_uint_or_null("maxSize")? - .unwrap_or_default() as u32, - ), - (0x4b65_7661_4864_6165_7268_546e_496c_6c61, 0x6472_6f77_7965) => { - Filter::AllInThreadHaveKeyword( - parser - .next_token::()? - .unwrap_string("allInThreadHaveKeyword")?, +struct FilterListCollector<'x, T: 'x>(&'x mut Vec>) +where + T: for<'de> DeserializeArguments<'de> + Default; + +pub(super) struct FilterWrapper(pub Vec>) +where + T: for<'de> DeserializeArguments<'de> + Default; + +impl<'de, T> Deserialize<'de> for FilterWrapper +where + T: for<'de2> DeserializeArguments<'de2> + Default, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut items = Vec::new(); + FilterMapCollector(&mut items) + .deserialize(deserializer) + .map(|_| FilterWrapper(items)) + } +} + +impl<'de, 'x, T> DeserializeSeed<'de> for FilterMapCollector<'x, T> +where + T: for<'de2> DeserializeArguments<'de2> + Default, +{ + type Value = (); + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FilterVisitor<'x, T: 'x>(&'x mut Vec>) + where + T: for<'de2> DeserializeArguments<'de2> + Default; + + impl<'de, 'x, T> Visitor<'de> for FilterVisitor<'x, T> + where + T: for<'de2> DeserializeArguments<'de2> + Default, + { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a filter object") + } + + fn visit_map(self, mut map: V) -> Result<(), V::Error> + where + V: MapAccess<'de>, + { + let mut filter = T::default(); + let mut has_filter = false; + let mut has_conditions = None; + let mut op = None; + + while let Some(key) = map.next_key::>()? { + match key.len() { + 8 if key == "operator" => { + let op_ = hashify::tiny_map!( + map.next_value::<&str>()?.as_bytes(), + "AND" => Filter::And, + "OR" => Filter::Or, + "NOT" => Filter::Not, ) - } - (0x6576_6148_6461_6572_6854_6e49_656d_6f73, 0x0064_726f_7779_654b) => { - Filter::SomeInThreadHaveKeyword( - parser - .next_token::()? - .unwrap_string("someInThreadHaveKeyword")?, - ) - } - (0x6576_6148_6461_6572_6854_6e49_656e_6f6e, 0x0064_726f_7779_654b) => { - Filter::NoneInThreadHaveKeyword( - parser - .next_token::()? - .unwrap_string("noneInThreadHaveKeyword")?, - ) - } - (0x6472_6f77_7965_4b73_6168, _) => Filter::HasKeyword( - parser - .next_token::()? - .unwrap_string("hasKeyword")?, - ), - (0x6472_6f77_7965_4b74_6f6e, _) => Filter::NotKeyword( - parser - .next_token::()? - .unwrap_string("notKeyword")?, - ), - (0x0074_6e65_6d68_6361_7474_4173_6168, _) => Filter::HasAttachment( - parser - .next_token::()? - .unwrap_bool("hasAttachment")?, - ), - (0x6d6f_7266, _) => { - Filter::From(parser.next_token::()?.unwrap_string("from")?) - } - (0x6f74, _) => { - Filter::To(parser.next_token::()?.unwrap_string("to")?) - } - (0x6363, _) => { - Filter::Cc(parser.next_token::()?.unwrap_string("cc")?) - } - (0x0063_6362, _) => { - Filter::Bcc(parser.next_token::()?.unwrap_string("bcc")?) - } - (0x0074_6365_6a62_7573, _) => Filter::Subject( - parser.next_token::()?.unwrap_string("subject")?, - ), - (0x7964_6f62, _) => { - Filter::Body(parser.next_token::()?.unwrap_string("body")?) - } - (0x7265_6461_6568, _) => Filter::Header(>::parse(parser)?), - (0x6469, _) => Filter::Id(>::parse(parser)?), - (0x6572_6f66_6542_746e_6573, _) => Filter::SentBefore( - parser - .next_token::()? - .unwrap_string("sentBefore")?, - ), - (0x0072_6574_6641_746e_6573, _) => Filter::SentAfter( - parser.next_token::()?.unwrap_string("sentAfter")?, - ), - (0x6461_6572_6854_6e69, _) => { - Filter::InThread(parser.next_token::()?.unwrap_string("inThread")?) - } - (0x6449_746e_6572_6170, _) => Filter::ParentId( - parser - .next_token::()? - .unwrap_string_or_null("parentId")?, - ), - (0x656c_6f72, _) => Filter::Role( - parser - .next_token::()? - .unwrap_string_or_null("role")?, - ), - (0x656c_6f52_796e_4173_6168, _) => Filter::HasAnyRole( - parser.next_token::()?.unwrap_bool("hasAnyRole")?, - ), - (0x6465_6269_7263_7362_7553_7369, _) => Filter::IsSubscribed( - parser.next_token::()?.unwrap_bool("isSubscribed")?, - ), - (0x6576_6974_6341_7369, _) => Filter::IsActive( - parser.next_token::()?.unwrap_bool("isActive")?, - ), - (0x0065_706f_6373, _) => { - Filter::Scope(parser.next_token::()?.unwrap_string("scope")?) - } - (0x6570_7954_6563_7275_6f73_6572, _) => Filter::ResourceType( - parser - .next_token::()? - .unwrap_string("resourceType")?, - ), - _ => { - if parser.is_eof || parser.skip_string() { - let filter = Filter::_T( - String::from_utf8_lossy( - parser.bytes[parser.pos_marker..parser.pos - 1].as_ref(), - ) - .into_owned(), - ); - parser.skip_token(parser.depth_array, parser.depth_dict)?; - filter + .ok_or_else(|| { + de::Error::custom(format!("Unknown filter operator: {}", key)) + })?; + + if let Some(pos) = has_conditions { + self.0[pos] = op_; } else { - return Err(parser.error_unterminated()); + op = Some(op_); } } - }, - }; - } - Token::DictStart => { - pos_stack.push(filter.len()); - filter.push(Filter::Close); - } - Token::DictEnd => { - if !matches!(filter[pos_stack.pop().unwrap()], Filter::Close) { - if pos_stack.is_empty() { - break; - } - } else { - return Err(trc::JmapEvent::InvalidArguments - .into_err() - .details("Malformed filter")); - } - } - Token::ArrayEnd => { - filter.push(Filter::Close); - } - Token::Comma => (), - token => { - return Err(token.error("filter", "object or array")); - } - } - } - - Ok(filter) -} - -pub fn parse_sort(parser: &mut Parser) -> trc::Result> { - let mut sort = vec![]; - - loop { - match parser.next_token::()? { - Token::DictStart => { - let mut comp = Comparator { - is_ascending: true, - collation: None, - property: SortProperty::Type, - keyword: None, - }; - while let Some(key) = parser.next_dict_key::()? { - match key { - 0x0067_6e69_646e_6563_7341_7369 => { - comp.is_ascending = parser - .next_token::()? - .unwrap_bool_or_null("isAscending")? - .unwrap_or_default(); - } - 0x006e_6f69_7461_6c6c_6f63 => { - comp.collation = parser - .next_token::()? - .unwrap_string_or_null("collation")?; - } - 0x7974_7265_706f_7270 => { - comp.property = parser - .next_token::()? - .unwrap_string("property")?; - } - 0x0064_726f_7779_656b => { - comp.keyword = parser - .next_token::()? - .unwrap_string_or_null("keyword")?; + 10 if key == "conditions" => { + has_conditions = Some(self.0.len()); + self.0.push(op.take().unwrap_or(Filter::And)); + map.next_value_seed(FilterListCollector(self.0))?; + self.0.push(Filter::Close); } _ => { - parser.skip_token(parser.depth_array, parser.depth_dict)?; + filter.deserialize_argument(&key, &mut map)?; + has_filter = true; } } } - sort.push(comp); - } - Token::Comma => (), - Token::ArrayEnd => { - break; - } - token => { - return Err(token.error("sort", "object")); + + if has_filter { + if has_conditions.is_some() { + return Err(de::Error::custom( + "Cannot mix conditions with property filters", + )); + } + + self.0.push(Filter::Property(filter)); + } + + Ok(()) } } - } - Ok(sort) + deserializer.deserialize_map(FilterVisitor(self.0)) + } } -impl JsonObjectParser for SortProperty { - fn parse(parser: &mut Parser<'_>) -> trc::Result +impl<'de, 'x, T> DeserializeSeed<'de> for FilterListCollector<'x, T> +where + T: for<'de2> DeserializeArguments<'de2> + Default, +{ + type Value = (); + + fn deserialize(self, deserializer: D) -> Result where - Self: Sized, + D: Deserializer<'de>, { - let mut hash = 0; - let mut shift = 0; + struct FilterVisitor<'x, T: 'x>(&'x mut Vec>) + where + T: for<'de2> DeserializeArguments<'de2> + Default; - while let Some(ch) = parser.next_unescaped()? { - if ch.is_ascii_alphabetic() { - if shift < 128 { - hash |= (ch as u128) << shift; - shift += 8; - } else { - break; - } - } else { - hash = 0; - break; + impl<'de, 'x, T> Visitor<'de> for FilterVisitor<'x, T> + where + T: for<'de2> DeserializeArguments<'de2> + Default, + { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a filter list") + } + + fn visit_seq(self, mut seq: A) -> Result<(), A::Error> + where + A: SeqAccess<'de>, + { + while let Some(()) = seq.next_element_seed(FilterMapCollector(self.0))? {} + Ok(()) } } - match hash { - 0x6570_7974 => Ok(SortProperty::Type), - 0x656d_616e => Ok(SortProperty::Name), - 0x006c_6961_6d65 => Ok(SortProperty::Email), - 0x0064_496c_6961_6d65 => Ok(SortProperty::EmailId), - 0x6449_6461_6572_6874 => Ok(SortProperty::ThreadId), - 0x7441_746e_6573 => Ok(SortProperty::SentAt), - 0x7441_6465_7669_6563_6572 => Ok(SortProperty::ReceivedAt), - 0x657a_6973 => Ok(SortProperty::Size), - 0x6d6f_7266 => Ok(SortProperty::From), - 0x6f74 => Ok(SortProperty::To), - 0x0074_6365_6a62_7573 => Ok(SortProperty::Subject), - 0x6363 => Ok(SortProperty::Cc), - 0x0072_6564_724f_7472_6f73 => Ok(SortProperty::SortOrder), - 0x6449_746e_6572_6170 => Ok(SortProperty::ParentId), - 0x6576_6974_6341_7369 => Ok(SortProperty::IsActive), - 0x6472_6f77_7965_4b73_6168 => Ok(SortProperty::HasKeyword), - 0x4b65_7661_4864_6165_7268_546e_496c_6c61 => Ok(SortProperty::AllInThreadHaveKeyword), - 0x6576_6148_6461_6572_6854_6e49_656d_6f73 => Ok(SortProperty::SomeInThreadHaveKeyword), - 0x6465_7375 => Ok(SortProperty::Used), + deserializer.deserialize_seq(FilterVisitor(self.0)) + } +} + +impl<'de, T> DeserializeArguments<'de> for Comparator +where + T: for<'de2> DeserializeArguments<'de2> + Default, +{ + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"isAscending" => { + self.is_ascending = map.next_value()?; + }, + b"collation" => { + self.collation = map.next_value()?; + }, _ => { - if parser.is_eof || parser.skip_string() { - Ok(SortProperty::_T( - String::from_utf8_lossy( - parser.bytes[parser.pos_marker..parser.pos - 1].as_ref(), - ) - .into_owned(), - )) - } else { - Err(parser.error_unterminated()) - } + self.property.deserialize_argument(key, map)?; } - } + ); + + Ok(()) } } - - -impl Display for Filter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Filter::Email(_) => "email", - Filter::Name(_) => "name", - Filter::DomainName(_) => "domainName", - Filter::Text(_) => "text", - Filter::Type(_) => "type", - Filter::Timezone(_) => "timezone", - Filter::Members(_) => "members", - Filter::QuotaLt(_) => "quotaLt", - Filter::QuotaGt(_) => "quotaGt", - Filter::IdentityIds(_) => "identityIds", - Filter::EmailIds(_) => "emailIds", - Filter::ThreadIds(_) => "threadIds", - Filter::UndoStatus(_) => "undoStatus", - Filter::Before(_) => "before", - Filter::After(_) => "after", - Filter::InMailbox(_) => "inMailbox", - Filter::InMailboxOtherThan(_) => "inMailboxOtherThan", - Filter::MinSize(_) => "minSize", - Filter::MaxSize(_) => "maxSize", - Filter::AllInThreadHaveKeyword(_) => "allInThreadHaveKeyword", - Filter::SomeInThreadHaveKeyword(_) => "someInThreadHaveKeyword", - Filter::NoneInThreadHaveKeyword(_) => "noneInThreadHaveKeyword", - Filter::HasKeyword(_) => "hasKeyword", - Filter::NotKeyword(_) => "notKeyword", - Filter::HasAttachment(_) => "hasAttachment", - Filter::From(_) => "from", - Filter::To(_) => "to", - Filter::Cc(_) => "cc", - Filter::Bcc(_) => "bcc", - Filter::Subject(_) => "subject", - Filter::Body(_) => "body", - Filter::Header(_) => "header", - Filter::Id(_) => "id", - Filter::SentBefore(_) => "sentBefore", - Filter::SentAfter(_) => "sentAfter", - Filter::InThread(_) => "inThread", - Filter::ParentId(_) => "parentId", - Filter::Role(_) => "role", - Filter::HasAnyRole(_) => "hasAnyRole", - Filter::IsSubscribed(_) => "isSubscribed", - Filter::IsActive(_) => "isActive", - Filter::ResourceType(_) => "resourceType", - Filter::Scope(_) => "scope", - Filter::_T(v) => v.as_str(), - Filter::And => "and", - Filter::Or => "or", - Filter::Not => "not", - Filter::Close => "close", - }) +impl<'de, T> Deserialize<'de> for Comparator +where + T: for<'de2> DeserializeArguments<'de2> + Default, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_request(deserializer) } } -impl Display for SortProperty { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - SortProperty::Type => "type", - SortProperty::Name => "name", - SortProperty::Email => "email", - SortProperty::EmailId => "emailId", - SortProperty::ThreadId => "threadId", - SortProperty::SentAt => "sentAt", - SortProperty::ReceivedAt => "receivedAt", - SortProperty::Size => "size", - SortProperty::From => "from", - SortProperty::To => "to", - SortProperty::Subject => "subject", - SortProperty::Cc => "cc", - SortProperty::SortOrder => "sortOrder", - SortProperty::ParentId => "parentId", - SortProperty::IsActive => "isActive", - SortProperty::HasKeyword => "hasKeyword", - SortProperty::AllInThreadHaveKeyword => "allInThreadHaveKeyword", - SortProperty::SomeInThreadHaveKeyword => "someInThreadHaveKeyword", - SortProperty::Used => "used", - SortProperty::_T(s) => s, - }) - } -} - -impl Filter { - pub fn is_immutable(&self) -> bool { - matches!( - self, - Filter::Before(_) - | Filter::After(_) - | Filter::MinSize(_) - | Filter::MaxSize(_) - | Filter::Text(_) - | Filter::HasAttachment(_) - | Filter::From(_) - | Filter::To(_) - | Filter::Cc(_) - | Filter::Bcc(_) - | Filter::Subject(_) - | Filter::Body(_) - | Filter::Header(_) - | Filter::Id(_) - | Filter::SentBefore(_) - | Filter::SentAfter(_) - ) - } -} - -impl Comparator { - pub fn descending(property: SortProperty) -> Self { - Self { - property, - is_ascending: false, - collation: None, - keyword: None, - } - } - - pub fn ascending(property: SortProperty) -> Self { - Self { - property, - is_ascending: true, - collation: None, - keyword: None, - } - } - - pub fn is_immutable(&self) -> bool { - matches!( - &self.property, - SortProperty::SentAt - | SortProperty::ReceivedAt - | SortProperty::Size - | SortProperty::From - | SortProperty::To - | SortProperty::Subject - | SortProperty::Cc - ) - } -} - -impl From for store::query::Filter { - fn from(value: Filter) -> Self { +impl From> for store::query::Filter +where + T: for<'de> DeserializeArguments<'de> + Default, +{ + fn from(value: Filter) -> Self { match value { Filter::And => Self::And, Filter::Or => Self::Or, @@ -707,8 +343,11 @@ impl From for store::query::Filter { } } -impl + Display + Clone + std::fmt::Debug> From for FtsFilter { - fn from(value: Filter) -> Self { +impl + Display + Clone + std::fmt::Debug, U> From> for FtsFilter +where + U: for<'de> DeserializeArguments<'de> + Default, +{ + fn from(value: Filter) -> Self { match value { Filter::And => Self::And, Filter::Or => Self::Or, @@ -719,27 +358,10 @@ impl + Display + Clone + std::fmt::Debug> From for FtsFilter } } -impl FilterItem for Filter { - fn filter_type(&self) -> FilterType { - match self { - Filter::Text(_) - | Filter::From(_) - | Filter::To(_) - | Filter::Cc(_) - | Filter::Bcc(_) - | Filter::Subject(_) - | Filter::Body(_) - | Filter::Header(_) => FilterType::Fts, - Filter::And => FilterType::And, - Filter::Or => FilterType::Or, - Filter::Not => FilterType::Not, - Filter::Close => FilterType::End, - _ => FilterType::Store, - } - } -} - -impl From for Filter { +impl From for Filter +where + T: for<'de> DeserializeArguments<'de> + Default, +{ fn from(value: FilterType) -> Self { match value { FilterType::And => Filter::And, @@ -751,4 +373,53 @@ impl From for Filter { } } -*/ +impl Comparator +where + T: for<'de> DeserializeArguments<'de> + Default, +{ + pub fn descending(property: T) -> Self { + Self { + property, + is_ascending: false, + collation: None, + } + } + + pub fn ascending(property: T) -> Self { + Self { + property, + is_ascending: true, + collation: None, + } + } +} + +impl FilterItem for Filter +where + T: for<'de> DeserializeArguments<'de> + FilterItem + Default, +{ + fn filter_type(&self) -> FilterType { + match self { + Filter::And => FilterType::And, + Filter::Or => FilterType::Or, + Filter::Not => FilterType::Not, + Filter::Close => FilterType::End, + Filter::Property(p) => p.filter_type(), + } + } +} + +impl Display for Filter +where + T: for<'de> DeserializeArguments<'de> + Display + Default, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Filter::And => write!(f, "and"), + Filter::Or => write!(f, "or"), + Filter::Not => write!(f, "not"), + Filter::Close => write!(f, "close"), + Filter::Property(p) => write!(f, "{}", p), + } + } +} diff --git a/crates/jmap-proto/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs index 4ead4fec..091b409f 100644 --- a/crates/jmap-proto/src/method/query_changes.rs +++ b/crates/jmap-proto/src/method/query_changes.rs @@ -5,6 +5,7 @@ */ use crate::{ + method::query::{Comparator, Filter, FilterWrapper}, object::JmapObject, request::deserialize::{DeserializeArguments, deserialize_request}, types::state::State, @@ -15,8 +16,8 @@ use types::id::Id; #[derive(Debug, Clone)] pub struct QueryChangesRequest { pub account_id: Id, - pub filter: Vec, - pub sort: Option>, + pub filter: Vec>, + pub sort: Option>>, pub since_query_state: State, pub max_changes: Option, pub up_to_id: Option, @@ -68,7 +69,7 @@ impl<'de, T: JmapObject> DeserializeArguments<'de> for QueryChangesRequest { self.account_id = map.next_value()?; }, b"filter" => { - self.filter = map.next_value()?; + self.filter = map.next_value::>()?.0; }, b"sort" => { self.sort = map.next_value()?; diff --git a/crates/jmap-proto/src/method/search_snippet.rs b/crates/jmap-proto/src/method/search_snippet.rs index 0e8d94c2..c90ca910 100644 --- a/crates/jmap-proto/src/method/search_snippet.rs +++ b/crates/jmap-proto/src/method/search_snippet.rs @@ -5,18 +5,22 @@ */ use super::query::Filter; -use crate::request::{ - MaybeInvalid, - deserialize::{DeserializeArguments, deserialize_request}, - reference::{MaybeResultReference, ResultReference}, +use crate::{ + method::query::FilterWrapper, + object::email::EmailFilter, + request::{ + MaybeInvalid, + deserialize::{DeserializeArguments, deserialize_request}, + reference::{MaybeResultReference, ResultReference}, + }, }; -use serde::{Deserialize, Deserializer, de::DeserializeOwned}; +use serde::{Deserialize, Deserializer}; use types::id::Id; #[derive(Debug, Clone)] -pub struct GetSearchSnippetRequest { +pub struct GetSearchSnippetRequest { pub account_id: Id, - pub filter: Vec>, + pub filter: Vec>, pub email_ids: MaybeResultReference>>, } @@ -45,7 +49,7 @@ pub struct SearchSnippet { pub preview: Option, } -impl<'de, T: DeserializeOwned> DeserializeArguments<'de> for GetSearchSnippetRequest { +impl<'de> DeserializeArguments<'de> for GetSearchSnippetRequest { fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> where A: serde::de::MapAccess<'de>, @@ -55,7 +59,7 @@ impl<'de, T: DeserializeOwned> DeserializeArguments<'de> for GetSearchSnippetReq self.account_id = map.next_value()?; }, b"filter" => { - self.filter = map.next_value()?; + self.filter = map.next_value::>()?.0; }, b"emailIds" => { self.email_ids = MaybeResultReference::Value(map.next_value::>>()?); @@ -72,7 +76,7 @@ impl<'de, T: DeserializeOwned> DeserializeArguments<'de> for GetSearchSnippetReq } } -impl<'de, T: DeserializeOwned> Deserialize<'de> for GetSearchSnippetRequest { +impl<'de> Deserialize<'de> for GetSearchSnippetRequest { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -81,7 +85,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for GetSearchSnippetRequest { } } -impl Default for GetSearchSnippetRequest { +impl Default for GetSearchSnippetRequest { fn default() -> Self { Self { account_id: Id::default(), diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs index 48614ebc..3a8e5018 100644 --- a/crates/jmap-proto/src/method/set.rs +++ b/crates/jmap-proto/src/method/set.rs @@ -11,18 +11,16 @@ use crate::{ request::{ MaybeInvalid, deserialize::{DeserializeArguments, deserialize_request}, - method::MethodObject, reference::{MaybeResultReference, ResultReference}, }, response::Response, - types::{date::UTCDate, state::State}, + types::state::State, }; use ahash::AHashMap; -use compact_str::format_compact; -use jmap_tools::Value; +use jmap_tools::{Key, Map, Value}; use serde::{Deserialize, Deserializer}; -use types::{acl::Acl, blob::BlobId, id::Id, keyword::Keyword}; -use utils::map::{bitmap::Bitmap, vec_map::VecMap}; +use types::id::Id; +use utils::map::vec_map::VecMap; #[derive(Debug, Clone)] #[allow(clippy::type_complexity)] @@ -36,6 +34,7 @@ pub struct SetRequest<'x, T: JmapObject> { } #[derive(Debug, Clone, Default, serde::Serialize)] +#[allow(clippy::type_complexity)] pub struct SetResponse { #[serde(rename = "accountId")] #[serde(skip_serializing_if = "Option::is_none")] @@ -166,15 +165,14 @@ impl<'x, T: JmapObject> SetRequest<'x, T> { self.update.take().unwrap_or_default() } - /*pub fn unwrap_destroy(&mut self) -> Vec { + pub fn unwrap_destroy(&mut self) -> Vec> { self.destroy .take() .map(|ids| ids.unwrap()) .unwrap_or_default() - }*/ + } } -/* impl SetResponse { pub fn from_request(request: &SetRequest, max_objects: usize) -> trc::Result { let n_create = request.create.as_ref().map_or(0, |objs| objs.len()); @@ -213,14 +211,21 @@ impl SetResponse { self } - pub fn created(&mut self, id: String, document_id: u32) { + pub fn created(&mut self, id: String, document_id: impl Into) { self.created.insert( id, - Object::with_capacity(1).with_property(Property::Id, Value::Id(document_id.into())), + Value::Object(Map::from(vec![( + Key::Property(T::ID_PROPERTY), + Value::Element(T::Element::from(document_id.into())), + )])), ); } - pub fn invalid_property_create(&mut self, id: String, property: impl Into) { + pub fn invalid_property_create( + &mut self, + id: String, + property: impl Into>, + ) { self.not_created.append( id, SetError::invalid_properties() @@ -229,7 +234,11 @@ impl SetResponse { ); } - pub fn invalid_property_update(&mut self, id: Id, property: impl Into) { + pub fn invalid_property_update( + &mut self, + id: MaybeInvalid, + property: impl Into>, + ) { self.not_updated.append( id, SetError::invalid_properties() @@ -240,30 +249,15 @@ impl SetResponse { pub fn update_created_ids(&self, response: &mut Response) { for (user_id, obj) in &self.created { - if let Some(id) = obj.get(&Property::Id).as_id() { - response.created_ids.insert(user_id.clone(), (*id).into()); + if let Value::Object(obj) = obj + && let Some(id) = obj.get(&Key::Property(T::ID_PROPERTY)) + { + response.created_ids.insert(user_id.clone(), id.to_string()); } } } - pub fn get_object_by_id(&mut self, id: Id) -> Option<&mut Value<'x, P, E>> { - if let Some(obj) = self.updated.get_mut(&id) { - if let Some(obj) = obj { - return Some(obj); - } else { - *obj = Some(Object::with_capacity(1)); - return obj.as_mut().unwrap().into(); - } - } - - (&mut self.created) - .into_iter() - .map(|(_, obj)| obj) - .find(|obj| obj.0.get(&Property::Id) == Some(&Value::Id(id))) - } - pub fn has_changes(&self) -> bool { !self.created.is_empty() || !self.updated.is_empty() || !self.destroyed.is_empty() } } -*/ diff --git a/crates/jmap-proto/src/method/upload.rs b/crates/jmap-proto/src/method/upload.rs index 08ecc0d8..f9464a36 100644 --- a/crates/jmap-proto/src/method/upload.rs +++ b/crates/jmap-proto/src/method/upload.rs @@ -174,15 +174,15 @@ impl<'de> DeserializeArguments<'de> for DataSourceObject { } } -/*impl BlobUploadResponse { +impl BlobUploadResponse { pub fn update_created_ids(&self, response: &mut Response) { for (user_id, obj) in &self.created { response .created_ids - .insert(user_id.clone(), obj.id.clone().into()); + .insert(user_id.clone(), obj.id.to_string()); } } -}*/ +} impl<'de> Deserialize<'de> for DataSourceObject { fn deserialize(deserializer: D) -> Result diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index 4bb63510..34400a78 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -182,4 +182,12 @@ impl JmapObject for Blob { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = BlobProperty::Id; +} + +impl From for BlobValue { + fn from(id: BlobId) -> Self { + BlobValue::BlobId(id) + } } diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index 710ddb62..636e65aa 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -12,6 +12,7 @@ use crate::{ use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; use mail_parser::HeaderName; use std::{borrow::Cow, fmt::Display, str::FromStr}; +use store::fts::{FilterItem, FilterType}; use types::{blob::BlobId, id::Id, keyword::Keyword}; #[derive(Debug, Clone, Default)] @@ -318,7 +319,7 @@ impl HeaderProperty { } ); } - 2 if value == "all" && result.all == false => { + 2 if value == "all" && !result.all => { result.all = true; } _ => return None, @@ -438,9 +439,9 @@ impl JmapObject for Email { type Id = Id; - type Filter = (); + type Filter = EmailFilter; - type Comparator = (); + type Comparator = EmailComparator; type GetArguments = EmailGetArguments; @@ -449,4 +450,345 @@ impl JmapObject for Email { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = EmailProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmailFilter { + InMailbox(Id), + InMailboxOtherThan(Vec), + Before(UTCDate), + After(UTCDate), + MinSize(u32), + MaxSize(u32), + AllInThreadHaveKeyword(Keyword), + SomeInThreadHaveKeyword(Keyword), + NoneInThreadHaveKeyword(Keyword), + HasKeyword(Keyword), + NotKeyword(Keyword), + HasAttachment(bool), + From(String), + To(String), + Cc(String), + Bcc(String), + Subject(String), + Body(String), + Header(Vec), + Text(String), + SentBefore(UTCDate), + SentAfter(UTCDate), + InThread(Id), + Id(Vec), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmailComparator { + ReceivedAt, + Size, + From, + To, + Subject, + Cc, + SentAt, + ThreadId, + HasKeyword(Keyword), + AllInThreadHaveKeyword(Keyword), + SomeInThreadHaveKeyword(Keyword), + _T(String), +} + +impl<'de> DeserializeArguments<'de> for EmailFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"inMailbox" => { + *self = EmailFilter::InMailbox(map.next_value()?); + }, + b"inMailboxOtherThan" => { + *self = EmailFilter::InMailboxOtherThan(map.next_value()?); + }, + b"before" => { + *self = EmailFilter::Before(map.next_value()?); + }, + b"after" => { + *self = EmailFilter::After(map.next_value()?); + }, + b"minSize" => { + *self = EmailFilter::MinSize(map.next_value()?); + }, + b"maxSize" => { + *self = EmailFilter::MaxSize(map.next_value()?); + }, + b"allInThreadHaveKeyword" => { + *self = EmailFilter::AllInThreadHaveKeyword(map.next_value()?); + }, + b"someInThreadHaveKeyword" => { + *self = EmailFilter::SomeInThreadHaveKeyword(map.next_value()?); + }, + b"noneInThreadHaveKeyword" => { + *self = EmailFilter::NoneInThreadHaveKeyword(map.next_value()?); + }, + b"hasKeyword" => { + *self = EmailFilter::HasKeyword(map.next_value()?); + }, + b"notKeyword" => { + *self = EmailFilter::NotKeyword(map.next_value()?); + }, + b"hasAttachment" => { + *self = EmailFilter::HasAttachment(map.next_value()?); + }, + b"from" => { + *self = EmailFilter::From(map.next_value()?); + }, + b"to" => { + *self = EmailFilter::To(map.next_value()?); + }, + b"cc" => { + *self = EmailFilter::Cc(map.next_value()?); + }, + b"bcc" => { + *self = EmailFilter::Bcc(map.next_value()?); + }, + b"subject" => { + *self = EmailFilter::Subject(map.next_value()?); + }, + b"body" => { + *self = EmailFilter::Body(map.next_value()?); + }, + b"header" => { + *self = EmailFilter::Header(map.next_value()?); + }, + b"text" => { + *self = EmailFilter::Text(map.next_value()?); + }, + b"sentBefore" => { + *self = EmailFilter::SentBefore(map.next_value()?); + }, + b"sentAfter" => { + *self = EmailFilter::SentAfter(map.next_value()?); + }, + b"inThread" => { + *self = EmailFilter::InThread(map.next_value()?); + }, + b"id" => { + *self = EmailFilter::Id(map.next_value()?); + }, + _ => { + *self = EmailFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for EmailComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"receivedAt" => { + *self = EmailComparator::ReceivedAt; + }, + b"size" => { + *self = EmailComparator::Size; + }, + b"from" => { + *self = EmailComparator::From; + }, + b"to" => { + *self = EmailComparator::To; + }, + b"cc" => { + *self = EmailComparator::Cc; + }, + b"subject" => { + *self = EmailComparator::Subject; + }, + b"sentAt" => { + *self = EmailComparator::SentAt; + }, + b"threadId" => { + *self = EmailComparator::ThreadId; + }, + b"hasKeyword" => { + *self = EmailComparator::HasKeyword(self.take_keyword()); + }, + b"allInThreadHaveKeyword" => { + *self = EmailComparator::AllInThreadHaveKeyword(self.take_keyword()); + }, + b"someInThreadHaveKeyword" => { + *self = EmailComparator::SomeInThreadHaveKeyword(self.take_keyword()); + }, + _ => { + *self = EmailComparator::_T(key.to_string()); + } + ); + } else if key == "keyword" { + let keyword: Keyword = map.next_value()?; + match self { + EmailComparator::HasKeyword(_) => *self = EmailComparator::HasKeyword(keyword), + EmailComparator::AllInThreadHaveKeyword(_) => { + *self = EmailComparator::AllInThreadHaveKeyword(keyword) + } + EmailComparator::SomeInThreadHaveKeyword(_) => { + *self = EmailComparator::SomeInThreadHaveKeyword(keyword) + } + _ => { + *self = EmailComparator::HasKeyword(keyword); + } + } + } else { + let _ = map.next_value::()?; + } + + Ok(()) + } +} + +impl Default for EmailFilter { + fn default() -> Self { + EmailFilter::_T("".to_string()) + } +} + +impl Default for EmailComparator { + fn default() -> Self { + EmailComparator::_T("".to_string()) + } +} + +impl EmailComparator { + fn take_keyword(&mut self) -> Keyword { + match self { + EmailComparator::HasKeyword(k) => std::mem::replace(k, Keyword::Other(String::new())), + EmailComparator::AllInThreadHaveKeyword(k) => { + std::mem::replace(k, Keyword::Other(String::new())) + } + EmailComparator::SomeInThreadHaveKeyword(k) => { + std::mem::replace(k, Keyword::Other(String::new())) + } + _ => Keyword::Other(String::new()), + } + } +} + +impl Display for EmailFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + EmailFilter::InMailbox(_) => "inMailbox", + EmailFilter::InMailboxOtherThan(_) => "inMailboxOtherThan", + EmailFilter::Before(_) => "before", + EmailFilter::After(_) => "after", + EmailFilter::MinSize(_) => "minSize", + EmailFilter::MaxSize(_) => "maxSize", + EmailFilter::AllInThreadHaveKeyword(_) => "allInThreadHaveKeyword", + EmailFilter::SomeInThreadHaveKeyword(_) => "someInThreadHaveKeyword", + EmailFilter::NoneInThreadHaveKeyword(_) => "noneInThreadHaveKeyword", + EmailFilter::HasKeyword(_) => "hasKeyword", + EmailFilter::NotKeyword(_) => "notKeyword", + EmailFilter::HasAttachment(_) => "hasAttachment", + EmailFilter::From(_) => "from", + EmailFilter::To(_) => "to", + EmailFilter::Cc(_) => "cc", + EmailFilter::Bcc(_) => "bcc", + EmailFilter::Subject(_) => "subject", + EmailFilter::Body(_) => "body", + EmailFilter::Header(_) => "header", + EmailFilter::Text(_) => "text", + EmailFilter::SentBefore(_) => "sentBefore", + EmailFilter::SentAfter(_) => "sentAfter", + EmailFilter::InThread(_) => "inThread", + EmailFilter::Id(_) => "id", + EmailFilter::_T(v) => v.as_str(), + }) + } +} + +impl Display for EmailComparator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + EmailComparator::ReceivedAt => "receivedAt", + EmailComparator::Size => "size", + EmailComparator::From => "from", + EmailComparator::To => "to", + EmailComparator::Subject => "subject", + EmailComparator::Cc => "cc", + EmailComparator::SentAt => "sentAt", + EmailComparator::ThreadId => "threadId", + EmailComparator::HasKeyword(_) => "hasKeyword", + EmailComparator::AllInThreadHaveKeyword(_) => "allInThreadHaveKeyword", + EmailComparator::SomeInThreadHaveKeyword(_) => "someInThreadHaveKeyword", + EmailComparator::_T(v) => v.as_str(), + }) + } +} + +impl EmailFilter { + pub fn is_immutable(&self) -> bool { + matches!( + self, + EmailFilter::Before(_) + | EmailFilter::After(_) + | EmailFilter::MinSize(_) + | EmailFilter::MaxSize(_) + | EmailFilter::HasAttachment(_) + | EmailFilter::From(_) + | EmailFilter::To(_) + | EmailFilter::Cc(_) + | EmailFilter::Bcc(_) + | EmailFilter::Subject(_) + | EmailFilter::Body(_) + | EmailFilter::Header(_) + | EmailFilter::Text(_) + | EmailFilter::Id(_) + | EmailFilter::SentBefore(_) + | EmailFilter::SentAfter(_) + ) + } +} + +impl EmailComparator { + pub fn is_immutable(&self) -> bool { + matches!( + self, + EmailComparator::ReceivedAt + | EmailComparator::Size + | EmailComparator::From + | EmailComparator::To + | EmailComparator::Subject + | EmailComparator::Cc + | EmailComparator::SentAt + ) + } +} + +impl FilterItem for EmailFilter { + fn filter_type(&self) -> FilterType { + match self { + EmailFilter::From(_) + | EmailFilter::To(_) + | EmailFilter::Cc(_) + | EmailFilter::Bcc(_) + | EmailFilter::Subject(_) + | EmailFilter::Body(_) + | EmailFilter::Header(_) + | EmailFilter::Text(_) => FilterType::Fts, + _ => FilterType::Store, + } + } +} + +impl From for EmailValue { + fn from(id: Id) -> Self { + EmailValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index 72495bc9..5f2b64d0 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -297,6 +297,16 @@ impl FromStr for EmailSubmissionProperty { } } +impl<'de> serde::Deserialize<'de> for UndoStatus { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + UndoStatus::parse(<&str>::deserialize(deserializer)?) + .ok_or_else(|| serde::de::Error::custom("invalid JMAP UndoStatus")) + } +} + impl JmapObject for EmailSubmission { type Property = EmailSubmissionProperty; @@ -304,9 +314,9 @@ impl JmapObject for EmailSubmission { type Id = Id; - type Filter = (); + type Filter = EmailSubmissionFilter; - type Comparator = (); + type Comparator = EmailSubmissionComparator; type GetArguments = (); @@ -315,4 +325,107 @@ impl JmapObject for EmailSubmission { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = EmailSubmissionProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmailSubmissionFilter { + IdentityIds(Vec), + EmailIds(Vec), + ThreadIds(Vec), + Before(UTCDate), + After(UTCDate), + UndoStatus(UndoStatus), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmailSubmissionComparator { + EmailId, + ThreadId, + SentAt, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for EmailSubmissionFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"identityIds" => { + *self = EmailSubmissionFilter::IdentityIds(map.next_value()?); + }, + b"emailIds" => { + *self = EmailSubmissionFilter::EmailIds(map.next_value()?); + }, + b"threadIds" => { + *self = EmailSubmissionFilter::ThreadIds(map.next_value()?); + }, + b"before" => { + *self = EmailSubmissionFilter::Before(map.next_value()?); + }, + b"after" => { + *self = EmailSubmissionFilter::After(map.next_value()?); + }, + b"undoStatus" => { + *self = EmailSubmissionFilter::UndoStatus(map.next_value()?); + }, + _ => { + *self = EmailSubmissionFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for EmailSubmissionComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + + b"emailId" => { + *self = EmailSubmissionComparator::EmailId; + }, + b"threadId" => { + *self = EmailSubmissionComparator::ThreadId; + }, + b"sentAt" => { + *self = EmailSubmissionComparator::SentAt; + }, + _ => { + *self = EmailSubmissionComparator::_T(key.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + + Ok(()) + } +} + +impl Default for EmailSubmissionFilter { + fn default() -> Self { + EmailSubmissionFilter::_T("".to_string()) + } +} + +impl Default for EmailSubmissionComparator { + fn default() -> Self { + EmailSubmissionComparator::_T("".to_string()) + } +} + +impl From for EmailSubmissionValue { + fn from(id: Id) -> Self { + EmailSubmissionValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/identity.rs b/crates/jmap-proto/src/object/identity.rs index 7a144968..2ef28589 100644 --- a/crates/jmap-proto/src/object/identity.rs +++ b/crates/jmap-proto/src/object/identity.rs @@ -142,4 +142,12 @@ impl JmapObject for Identity { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = IdentityProperty::Id; +} + +impl From for IdentityValue { + fn from(id: Id) -> Self { + IdentityValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index 3bd565b8..294900ad 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -227,9 +227,9 @@ impl JmapObject for Mailbox { type Id = Id; - type Filter = (); + type Filter = MailboxFilter; - type Comparator = (); + type Comparator = MailboxComparator; type GetArguments = (); @@ -238,4 +238,115 @@ impl JmapObject for Mailbox { type QueryArguments = MailboxQueryArguments; type CopyArguments = (); + + const ID_PROPERTY: Self::Property = MailboxProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MailboxFilter { + Name(String), + ParentId(Option), + Role(Option), + HasAnyRole(bool), + IsSubscribed(bool), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MailboxComparator { + SortOrder, + Name, + ParentId, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for MailboxFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"name" => { + *self = MailboxFilter::Name(map.next_value()?); + }, + b"parentId" => { + *self = MailboxFilter::ParentId(map.next_value()?); + }, + b"role" => { + *self = MailboxFilter::Role(map.next_value::>()?.map(|r| r.0)); + }, + b"hasAnyRole" => { + *self = MailboxFilter::HasAnyRole(map.next_value()?); + }, + b"isSubscribed" => { + *self = MailboxFilter::IsSubscribed(map.next_value()?); + }, + _ => { + *self = MailboxFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for MailboxComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"sortOrder" => { + *self = MailboxComparator::SortOrder; + }, + b"name" => { + *self = MailboxComparator::Name; + }, + b"parentId" => { + *self = MailboxComparator::ParentId; + }, + _ => { + *self = MailboxComparator::_T(key.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + + Ok(()) + } +} + +impl Default for MailboxFilter { + fn default() -> Self { + MailboxFilter::_T("".to_string()) + } +} + +impl Default for MailboxComparator { + fn default() -> Self { + MailboxComparator::_T("".to_string()) + } +} + +struct RoleWrapper(SpecialUse); + +impl<'de> serde::Deserialize<'de> for RoleWrapper { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + SpecialUse::parse(<&str>::deserialize(deserializer)?) + .map(RoleWrapper) + .ok_or_else(|| serde::de::Error::custom("invalid JMAP role")) + } +} + +impl From for MailboxValue { + fn from(id: Id) -> Self { + MailboxValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index 4a3e50ca..44939ed2 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -6,7 +6,7 @@ use crate::request::deserialize::DeserializeArguments; use jmap_tools::{Element, Property}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::str::FromStr; pub mod blob; @@ -24,16 +24,18 @@ pub mod vacation_response; pub trait JmapObject { type Property: Property + FromStr + Serialize; - type Element: Element; + type Element: Element + From; type Id: FromStr + Serialize; - type Filter: for<'de> Deserialize<'de>; - type Comparator: for<'de> Deserialize<'de>; + type Filter: Default + for<'de> DeserializeArguments<'de>; + type Comparator: Default + for<'de> DeserializeArguments<'de>; type GetArguments: Default + for<'de> DeserializeArguments<'de>; type SetArguments: Default + for<'de> DeserializeArguments<'de>; type QueryArguments: Default + for<'de> DeserializeArguments<'de>; type CopyArguments: Default + for<'de> DeserializeArguments<'de>; + + const ID_PROPERTY: Self::Property; } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs index af122e0a..2e630aff 100644 --- a/crates/jmap-proto/src/object/principal.rs +++ b/crates/jmap-proto/src/object/principal.rs @@ -8,7 +8,7 @@ use jmap_tools::{Element, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; -use crate::object::JmapObject; +use crate::{object::JmapObject, request::deserialize::DeserializeArguments}; #[derive(Debug, Clone, Default)] pub struct Principal; @@ -141,9 +141,9 @@ impl JmapObject for Principal { type Id = Id; - type Filter = (); + type Filter = PrincipalFilter; - type Comparator = (); + type Comparator = PrincipalComparator; type GetArguments = (); @@ -152,4 +152,116 @@ impl JmapObject for Principal { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = PrincipalProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PrincipalFilter { + AccountIds(Vec), + Email(String), + Name(String), + Text(String), + Type(PrincipalType), + Timezone(String), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PrincipalComparator { + Name, + Email, + Type, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for PrincipalFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"accountIds" => { + *self = PrincipalFilter::AccountIds(map.next_value()?); + }, + b"email" => { + *self = PrincipalFilter::Email(map.next_value()?); + }, + b"name" => { + *self = PrincipalFilter::Name(map.next_value()?); + }, + b"text" => { + *self = PrincipalFilter::Text(map.next_value()?); + }, + b"type" => { + *self = PrincipalFilter::Type(map.next_value()?); + }, + b"timezone" => { + *self = PrincipalFilter::Timezone(map.next_value()?); + }, + _ => { + *self = PrincipalFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for PrincipalComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"name" => { + *self = PrincipalComparator::Name; + }, + b"email" => { + *self = PrincipalComparator::Email; + }, + b"type" => { + *self = PrincipalComparator::Type; + }, + _ => { + *self = PrincipalComparator::_T(key.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + + Ok(()) + } +} + +impl Default for PrincipalFilter { + fn default() -> Self { + PrincipalFilter::_T("".to_string()) + } +} + +impl Default for PrincipalComparator { + fn default() -> Self { + PrincipalComparator::_T("".to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for PrincipalType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + PrincipalType::parse(<&str>::deserialize(deserializer)?) + .ok_or_else(|| serde::de::Error::custom("invalid JMAP PrincipalType")) + } +} + +impl From for PrincipalValue { + fn from(id: Id) -> Self { + PrincipalValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/push_subscription.rs b/crates/jmap-proto/src/object/push_subscription.rs index ef6064bf..2515baaf 100644 --- a/crates/jmap-proto/src/object/push_subscription.rs +++ b/crates/jmap-proto/src/object/push_subscription.rs @@ -161,4 +161,12 @@ impl JmapObject for PushSubscription { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = PushSubscriptionProperty::Id; +} + +impl From for PushSubscriptionValue { + fn from(id: Id) -> Self { + PushSubscriptionValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/quota.rs b/crates/jmap-proto/src/object/quota.rs index 207b81a9..f523487b 100644 --- a/crates/jmap-proto/src/object/quota.rs +++ b/crates/jmap-proto/src/object/quota.rs @@ -4,12 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{object::JmapObject, request::deserialize::DeserializeArguments}; use jmap_tools::{Element, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::{id::Id, type_state::DataType}; -use crate::object::JmapObject; - #[derive(Debug, Clone, Default)] pub struct Quota; @@ -119,9 +118,9 @@ impl JmapObject for Quota { type Id = Id; - type Filter = (); + type Filter = QuotaFilter; - type Comparator = (); + type Comparator = QuotaComparator; type GetArguments = (); @@ -130,4 +129,98 @@ impl JmapObject for Quota { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = QuotaProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QuotaFilter { + Name(String), + Type(String), + Scope(String), + ResourceType(String), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QuotaComparator { + Name, + Type, + Used, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for QuotaFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"name" => { + *self = QuotaFilter::Name(map.next_value()?); + }, + b"type" => { + *self = QuotaFilter::Type(map.next_value()?); + }, + b"scope" => { + *self = QuotaFilter::Scope(map.next_value()?); + }, + b"resourceType" => { + *self = QuotaFilter::ResourceType(map.next_value()?); + }, + _ => { + *self = QuotaFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for QuotaComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"name" => { + *self = QuotaComparator::Name; + }, + b"type" => { + *self = QuotaComparator::Type; + }, + b"used" => { + *self = QuotaComparator::Used; + }, + _ => { + *self = QuotaComparator::_T(key.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + + Ok(()) + } +} + +impl Default for QuotaFilter { + fn default() -> Self { + QuotaFilter::_T("".to_string()) + } +} + +impl Default for QuotaComparator { + fn default() -> Self { + QuotaComparator::_T("".to_string()) + } +} + +impl From for QuotaValue { + fn from(id: Id) -> Self { + QuotaValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index 5ae1d35e..df8f9e64 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -140,9 +140,9 @@ impl JmapObject for Sieve { type Id = Id; - type Filter = (); + type Filter = SieveFilter; - type Comparator = (); + type Comparator = SieveComparator; type GetArguments = (); @@ -151,4 +151,86 @@ impl JmapObject for Sieve { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = SieveProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SieveFilter { + Name(String), + IsActive(bool), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SieveComparator { + Name, + IsActive, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for SieveFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"name" => { + *self = SieveFilter::Name(map.next_value()?); + }, + b"isActive" => { + *self = SieveFilter::IsActive(map.next_value()?); + }, + _ => { + *self = SieveFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for SieveComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"name" => { + *self = SieveComparator::Name; + }, + b"isActive" => { + *self = SieveComparator::IsActive; + }, + _ => { + *self = SieveComparator::_T(key.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + + Ok(()) + } +} + +impl Default for SieveFilter { + fn default() -> Self { + SieveFilter::_T("".to_string()) + } +} + +impl Default for SieveComparator { + fn default() -> Self { + SieveComparator::_T("".to_string()) + } +} + +impl From for SieveValue { + fn from(id: Id) -> Self { + SieveValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/thread.rs b/crates/jmap-proto/src/object/thread.rs index 2d85edc9..e1739d6c 100644 --- a/crates/jmap-proto/src/object/thread.rs +++ b/crates/jmap-proto/src/object/thread.rs @@ -100,4 +100,12 @@ impl JmapObject for Thread { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = ThreadProperty::Id; +} + +impl From for ThreadValue { + fn from(id: Id) -> Self { + ThreadValue::Id(id) + } } diff --git a/crates/jmap-proto/src/object/vacation_response.rs b/crates/jmap-proto/src/object/vacation_response.rs index 39aff7c4..51fbbdc7 100644 --- a/crates/jmap-proto/src/object/vacation_response.rs +++ b/crates/jmap-proto/src/object/vacation_response.rs @@ -123,4 +123,12 @@ impl JmapObject for VacationResponse { type QueryArguments = (); type CopyArguments = (); + + const ID_PROPERTY: Self::Property = VacationResponseProperty::Id; +} + +impl From for VacationResponseValue { + fn from(id: Id) -> Self { + VacationResponseValue::Id(id) + } } diff --git a/crates/jmap-proto/src/request/deserialize.rs b/crates/jmap-proto/src/request/deserialize.rs index bb119089..f4407bee 100644 --- a/crates/jmap-proto/src/request/deserialize.rs +++ b/crates/jmap-proto/src/request/deserialize.rs @@ -11,7 +11,7 @@ use serde::{ de::{self, MapAccess, Visitor}, }; -pub(crate) trait DeserializeArguments<'de> { +pub trait DeserializeArguments<'de> { fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> where A: MapAccess<'de>; diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index 2fb14fe7..9e09abe5 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -61,7 +61,7 @@ pub enum RequestMethod<'x> { ParseEmail(ParseEmailRequest), Query(QueryRequestMethod), QueryChanges(QueryChangesRequestMethod), - SearchSnippet(GetSearchSnippetRequest<()>), + SearchSnippet(GetSearchSnippetRequest), ValidateScript(ValidateSieveScriptRequest), LookupBlob(BlobLookupRequest), UploadBlob(BlobUploadRequest), @@ -170,3 +170,15 @@ impl Default for Request<'_> { } } } + +impl MaybeInvalid +where + T: FromStr, +{ + pub fn try_unwrap(self) -> Option { + match self { + MaybeInvalid::Value(id) => Some(id), + MaybeInvalid::Invalid(_) => None, + } + } +} diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index 927ddb0f..0787cfa4 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -462,6 +462,45 @@ mod tests { } "#; + const TEST1: &str = r#" + { + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Email/query", + { + "accountId": "0", + "filter": { "conditions": [ { "hasKeyword": "music" }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" }, + "sort": [ + { + "property": "subject", + "isAscending": true + }, + { + "property": "allInThreadHaveKeyword", + "isAscending": false, + "keyword": "$seen" + }, + { + "keyword": "$junk", + "property": "someInThreadHaveKeyword", + "collation": "i;octet", + "isAscending": false + } + ], + "position": 0, + "limit": 10 + }, + "c1" + ] + ], + "createdIds": {} + } + "#; + const TEST2: &str = r##" { "using": [ @@ -557,6 +596,7 @@ mod tests { #[test] fn parse_request() { println!("{:#?}", Request::parse(TEST.as_bytes(), 10, 10240)); + println!("{:#?}", Request::parse(TEST1.as_bytes(), 10, 10240)); println!("{:#?}", Request::parse(TEST2.as_bytes(), 10, 10240)); } } diff --git a/crates/jmap-proto/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs index 245f8f07..07015e85 100644 --- a/crates/jmap-proto/src/request/reference.rs +++ b/crates/jmap-proto/src/request/reference.rs @@ -87,3 +87,21 @@ impl Default for MaybeResultReference { MaybeResultReference::Value(V::default()) } } + +impl MaybeResultReference { + pub fn unwrap(self) -> T { + match self { + MaybeResultReference::Value(v) => v, + MaybeResultReference::Reference(_) => T::default(), + } + } +} + +impl MaybeIdReference { + pub fn try_unwrap(self) -> Option { + match self { + MaybeIdReference::Id(id) => Some(id), + _ => None, + } + } +} diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs index 2e45dc52..651579e0 100644 --- a/crates/jmap-proto/src/response/mod.rs +++ b/crates/jmap-proto/src/response/mod.rs @@ -8,10 +8,7 @@ pub mod references; pub mod serialize; pub mod status; -use std::collections::HashMap; - -use jmap_tools::{Null, Value}; - +use self::serialize::serialize_hex; use crate::{ error::method::MethodErrorWrapper, method::{ @@ -28,19 +25,23 @@ use crate::{ upload::BlobUploadResponse, validate::ValidateSieveScriptResponse, }, + object::{ + blob::Blob, email::Email, email_submission::EmailSubmission, identity::Identity, + mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota, + sieve::Sieve, thread::Thread, vacation_response::VacationResponse, + }, request::{Call, method::MethodName}, }; - -use self::serialize::serialize_hex; +use jmap_tools::{Null, Value}; +use std::collections::HashMap; #[derive(Debug, serde::Serialize)] #[serde(untagged)] pub enum ResponseMethod<'x> { - /*Get(GetResponse), - Set(SetResponse), - Changes(ChangesResponse), - Copy(CopyResponse),*/ - CopyBlob(CopyBlobResponse), + Get(GetResponseMethod), + Set(SetResponseMethod), + Changes(ChangesResponseMethod), + Copy(CopyResponseMethod), ImportEmail(ImportEmailResponse), ParseEmail(ParseEmailResponse), QueryChanges(QueryChangesResponse), @@ -53,6 +54,52 @@ pub enum ResponseMethod<'x> { Error(MethodErrorWrapper), } +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum GetResponseMethod { + Email(GetResponse), + Mailbox(GetResponse), + Thread(GetResponse), + Identity(GetResponse), + EmailSubmission(GetResponse), + PushSubscription(GetResponse), + Sieve(GetResponse), + VacationResponse(GetResponse), + Principal(GetResponse), + Quota(GetResponse), + Blob(GetResponse), +} + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum SetResponseMethod { + Email(SetResponse), + Mailbox(SetResponse), + Identity(SetResponse), + EmailSubmission(SetResponse), + PushSubscription(SetResponse), + Sieve(SetResponse), + VacationResponse(SetResponse), +} + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum ChangesResponseMethod { + Email(ChangesResponse), + Mailbox(ChangesResponse), + Thread(ChangesResponse), + Identity(ChangesResponse), + EmailSubmission(ChangesResponse), + Quota(ChangesResponse), +} + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum CopyResponseMethod { + Email(CopyResponse), + Blob(CopyBlobResponse), +} + #[derive(Debug, serde::Serialize)] pub struct Response<'x> { #[serde(rename = "methodResponses")] @@ -66,8 +113,8 @@ pub struct Response<'x> { #[serde(skip_serializing_if = "HashMap::is_empty")] pub created_ids: HashMap, } -/* -impl Response<'_> { + +impl<'x> Response<'x> { pub fn new(session_state: u32, created_ids: HashMap, capacity: usize) -> Self { Response { session_state, @@ -80,7 +127,7 @@ impl Response<'_> { &mut self, id: String, name: MethodName, - method: impl Into, + method: impl Into>, ) { self.method_responses.push(Call { id, @@ -102,97 +149,13 @@ impl Response<'_> { } } -impl From for ResponseMethod { +impl From for ResponseMethod<'_> { fn from(error: trc::Error) -> Self { ResponseMethod::Error(error.into()) } } -impl From for ResponseMethod { - fn from(echo: Echo) -> Self { - ResponseMethod::Echo(echo) - } -} - -impl From for ResponseMethod { - fn from(get: GetResponse) -> Self { - ResponseMethod::Get(get) - } -} - -impl From for ResponseMethod { - fn from(set: SetResponse) -> Self { - ResponseMethod::Set(set) - } -} - -impl From for ResponseMethod { - fn from(changes: ChangesResponse) -> Self { - ResponseMethod::Changes(changes) - } -} - -impl From for ResponseMethod { - fn from(copy: CopyResponse) -> Self { - ResponseMethod::Copy(copy) - } -} - -impl From for ResponseMethod { - fn from(copy_blob: CopyBlobResponse) -> Self { - ResponseMethod::CopyBlob(copy_blob) - } -} - -impl From for ResponseMethod { - fn from(import_email: ImportEmailResponse) -> Self { - ResponseMethod::ImportEmail(import_email) - } -} - -impl From for ResponseMethod { - fn from(parse_email: ParseEmailResponse) -> Self { - ResponseMethod::ParseEmail(parse_email) - } -} - -impl From for ResponseMethod { - fn from(query_changes: QueryChangesResponse) -> Self { - ResponseMethod::QueryChanges(query_changes) - } -} - -impl From for ResponseMethod { - fn from(query: QueryResponse) -> Self { - ResponseMethod::Query(query) - } -} - -impl From for ResponseMethod { - fn from(search_snippet: GetSearchSnippetResponse) -> Self { - ResponseMethod::SearchSnippet(search_snippet) - } -} - -impl From for ResponseMethod { - fn from(validate_script: ValidateSieveScriptResponse) -> Self { - ResponseMethod::ValidateScript(validate_script) - } -} - -impl From for ResponseMethod { - fn from(upload_blob: BlobUploadResponse) -> Self { - ResponseMethod::UploadBlob(upload_blob) - } -} - -impl From for ResponseMethod { - fn from(lookup_blob: BlobLookupResponse) -> Self { - ResponseMethod::LookupBlob(lookup_blob) - } -} - -impl> From> for ResponseMethod { +impl<'x, T: Into>> From> for ResponseMethod<'x> { fn from(result: trc::Result) -> Self { match result { Ok(value) => value.into(), @@ -200,4 +163,223 @@ impl> From> for ResponseMethod { } } } -*/ + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Email(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Mailbox(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Thread(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Identity(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::EmailSubmission(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::PushSubscription(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Sieve(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::VacationResponse(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Principal(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Quota(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Blob(value)) + } +} + +// Direct SetResponse conversions to ResponseMethod +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::Email(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::Mailbox(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::Identity(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::EmailSubmission(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::PushSubscription(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::Sieve(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::VacationResponse(value)) + } +} + +// Direct ChangesResponse conversions to ResponseMethod +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::Email(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::Mailbox(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::Thread(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::Identity(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::EmailSubmission(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::Quota(value)) + } +} + +// Direct CopyResponse conversions to ResponseMethod +impl<'x> From> for ResponseMethod<'x> { + fn from(value: CopyResponse) -> Self { + ResponseMethod::Copy(CopyResponseMethod::Email(value)) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: CopyBlobResponse) -> Self { + ResponseMethod::Copy(CopyResponseMethod::Blob(value)) + } +} + +// Other direct conversions +impl<'x> From for ResponseMethod<'x> { + fn from(value: ImportEmailResponse) -> Self { + ResponseMethod::ImportEmail(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: ParseEmailResponse) -> Self { + ResponseMethod::ParseEmail(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: QueryChangesResponse) -> Self { + ResponseMethod::QueryChanges(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: QueryResponse) -> Self { + ResponseMethod::Query(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: GetSearchSnippetResponse) -> Self { + ResponseMethod::SearchSnippet(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: ValidateSieveScriptResponse) -> Self { + ResponseMethod::ValidateScript(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: BlobLookupResponse) -> Self { + ResponseMethod::LookupBlob(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: BlobUploadResponse) -> Self { + ResponseMethod::UploadBlob(value) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: Value<'x, Null, Null>) -> Self { + ResponseMethod::Echo(value) + } +} + +impl<'x> From for ResponseMethod<'x> { + fn from(value: MethodErrorWrapper) -> Self { + ResponseMethod::Error(value) + } +} diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 67c28319..93fe11f7 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -323,6 +323,26 @@ impl SieveScriptSet for Server { self.sieve_activate_script(account_id, None).await? }; + /* + + pub fn get_object_by_id(&mut self, id: Id) -> Option<&mut Value<'x, P, E>> { + if let Some(obj) = self.updated.get_mut(&id) { + if let Some(obj) = obj { + return Some(obj); + } else { + *obj = Some(Object::with_capacity(1)); + return obj.as_mut().unwrap().into(); + } + } + + (&mut self.created) + .into_iter() + .map(|(_, obj)| obj) + .find(|obj| obj.0.get(&Property::Id) == Some(&Value::Id(id))) + } + + */ + if !changed_ids.is_empty() { for (document_id, is_active) in changed_ids { if let Some(obj) = ctx.response.get_object_by_id(Id::from(document_id)) {