diff --git a/crates/jmap-proto/src/lib.rs b/crates/jmap-proto/src/lib.rs index d6f926bb..7c051e65 100644 --- a/crates/jmap-proto/src/lib.rs +++ b/crates/jmap-proto/src/lib.rs @@ -7,6 +7,7 @@ pub mod error; pub mod method; pub mod object; +pub mod references; pub mod request; pub mod response; pub mod types; diff --git a/crates/jmap-proto/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs index 5e446c67..d1fe2bec 100644 --- a/crates/jmap-proto/src/method/changes.rs +++ b/crates/jmap-proto/src/method/changes.rs @@ -5,6 +5,7 @@ */ use crate::{ + method::PropertyWrapper, object::JmapObject, request::deserialize::{DeserializeArguments, deserialize_request}, types::state::State, @@ -41,7 +42,7 @@ pub struct ChangesResponse { #[serde(rename = "updatedProperties")] #[serde(skip_serializing_if = "Option::is_none")] - pub updated_properties: Option>, + pub updated_properties: Option>>, } impl<'de> DeserializeArguments<'de> for ChangesRequest { diff --git a/crates/jmap-proto/src/method/import.rs b/crates/jmap-proto/src/method/import.rs index 764c1386..0879d6ce 100644 --- a/crates/jmap-proto/src/method/import.rs +++ b/crates/jmap-proto/src/method/import.rs @@ -6,7 +6,10 @@ use crate::{ error::set::SetError, - object::email::{EmailProperty, EmailValue}, + object::{ + AnyId, + email::{EmailProperty, EmailValue}, + }, request::{ MaybeInvalid, deserialize::{DeserializeArguments, deserialize_request}, @@ -135,7 +138,7 @@ impl ImportEmailResponse { && let Some(Value::Element(EmailValue::Id(id))) = obj.get(&Key::Property(EmailProperty::Id)) { - response.created_ids.insert(user_id.clone(), (*id).into()); + response.created_ids.insert(user_id.clone(), AnyId::Id(*id)); } } } diff --git a/crates/jmap-proto/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs index 71def917..a1959950 100644 --- a/crates/jmap-proto/src/method/mod.rs +++ b/crates/jmap-proto/src/method/mod.rs @@ -5,6 +5,7 @@ */ use ahash::AHashMap; +use jmap_tools::Property; pub mod changes; pub mod copy; @@ -23,3 +24,16 @@ pub mod validate; fn ahash_is_empty(map: &AHashMap) -> bool { map.is_empty() } + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(transparent)] +#[repr(transparent)] +pub struct PropertyWrapper(pub T) +where + T: serde::Serialize + Property; + +impl From for PropertyWrapper { + fn from(value: T) -> Self { + Self(value) + } +} diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs index 3a8e5018..123e8be5 100644 --- a/crates/jmap-proto/src/method/set.rs +++ b/crates/jmap-proto/src/method/set.rs @@ -7,7 +7,7 @@ use super::ahash_is_empty; use crate::{ error::set::{InvalidProperty, SetError}, - object::JmapObject, + object::{JmapObject, JmapObjectId}, request::{ MaybeInvalid, deserialize::{DeserializeArguments, deserialize_request}, @@ -250,9 +250,10 @@ impl SetResponse { pub fn update_created_ids(&self, response: &mut Response) { for (user_id, obj) in &self.created { if let Value::Object(obj) = obj - && let Some(id) = obj.get(&Key::Property(T::ID_PROPERTY)) + && let Some(Value::Element(id)) = obj.get(&Key::Property(T::ID_PROPERTY)) + && let Some(id) = id.as_any_id() { - response.created_ids.insert(user_id.clone(), id.to_string()); + response.created_ids.insert(user_id.clone(), id); } } } diff --git a/crates/jmap-proto/src/method/upload.rs b/crates/jmap-proto/src/method/upload.rs index f9464a36..3f4e1b09 100644 --- a/crates/jmap-proto/src/method/upload.rs +++ b/crates/jmap-proto/src/method/upload.rs @@ -9,7 +9,7 @@ use std::borrow::Cow; use super::ahash_is_empty; use crate::{ error::set::SetError, - object::blob::BlobProperty, + object::{AnyId, blob::BlobProperty}, request::{ deserialize::{DeserializeArguments, deserialize_request}, reference::MaybeIdReference, @@ -179,7 +179,7 @@ impl BlobUploadResponse { for (user_id, obj) in &self.created { response .created_ids - .insert(user_id.clone(), obj.id.to_string()); + .insert(user_id.clone(), AnyId::BlobId(obj.id.clone())); } } } diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index 34400a78..ccfbeb40 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -5,12 +5,12 @@ */ use crate::{ - object::{JmapObject, MaybeReference, parse_ref}, + object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::deserialize::DeserializeArguments, }; use jmap_tools::{Element, Key, Property}; use std::{borrow::Cow, str::FromStr}; -use types::blob::BlobId; +use types::{blob::BlobId, id::Id}; #[derive(Debug, Clone, Default)] pub struct Blob; @@ -191,3 +191,35 @@ impl From for BlobValue { BlobValue::BlobId(id) } } + +impl JmapObjectId for BlobValue { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + match self { + BlobValue::BlobId(id) => Some(AnyId::BlobId(id.clone())), + _ => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let BlobValue::IdReference(r) = self { + Some(r) + } else { + None + } + } +} + +impl TryFrom for BlobValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::BlobId(id) => Ok(BlobValue::BlobId(id)), + _ => Err(()), + } + } +} diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index 636e65aa..d68fe1fb 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -5,7 +5,7 @@ */ use crate::{ - object::{JmapObject, MaybeReference, parse_ref}, + object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::{MaybeInvalid, deserialize::DeserializeArguments}, types::date::UTCDate, }; @@ -792,3 +792,40 @@ impl From for EmailValue { EmailValue::Id(id) } } + +impl JmapObjectId for EmailValue { + fn as_id(&self) -> Option { + if let EmailValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + match self { + EmailValue::Id(id) => Some(AnyId::Id(*id)), + EmailValue::BlobId(id) => Some(AnyId::BlobId(id.clone())), + _ => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let EmailValue::IdReference(r) = self { + Some(r) + } else { + None + } + } +} + +impl TryFrom for EmailValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(EmailValue::Id(id)), + AnyId::BlobId(id) => Ok(EmailValue::BlobId(id)), + } + } +} diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index 5f2b64d0..54b847a5 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -6,7 +6,7 @@ use crate::{ object::{ - JmapObject, MaybeReference, + AnyId, JmapObject, JmapObjectId, MaybeReference, email::{EmailProperty, EmailValue}, parse_ref, }, @@ -429,3 +429,39 @@ impl From for EmailSubmissionValue { EmailSubmissionValue::Id(id) } } + +impl JmapObjectId for EmailSubmissionValue { + fn as_id(&self) -> Option { + match self { + EmailSubmissionValue::Id(id) => Some(*id), + _ => None, + } + } + + fn as_any_id(&self) -> Option { + match self { + EmailSubmissionValue::Id(id) => Some(AnyId::Id(*id)), + EmailSubmissionValue::BlobId(blob_id) => Some(AnyId::BlobId(blob_id.clone())), + _ => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let EmailSubmissionValue::IdReference(r) = self { + Some(r) + } else { + None + } + } +} + +impl TryFrom for EmailSubmissionValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(EmailSubmissionValue::Id(id)), + AnyId::BlobId(blob_id) => Ok(EmailSubmissionValue::BlobId(blob_id)), + } + } +} diff --git a/crates/jmap-proto/src/object/identity.rs b/crates/jmap-proto/src/object/identity.rs index 2ef28589..9597866c 100644 --- a/crates/jmap-proto/src/object/identity.rs +++ b/crates/jmap-proto/src/object/identity.rs @@ -8,7 +8,7 @@ use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; -use crate::object::JmapObject; +use crate::object::{AnyId, JmapObject, JmapObjectId}; #[derive(Debug, Clone, Default)] pub struct Identity; @@ -151,3 +151,32 @@ impl From for IdentityValue { IdentityValue::Id(id) } } + +impl JmapObjectId for IdentityValue { + fn as_id(&self) -> Option { + match self { + IdentityValue::Id(id) => Some(*id), + } + } + + fn as_any_id(&self) -> Option { + match self { + IdentityValue::Id(id) => Some(AnyId::Id(*id)), + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for IdentityValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(IdentityValue::Id(id)), + _ => Err(()), + } + } +} diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index 294900ad..00f9f5ce 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -10,7 +10,7 @@ use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; use types::{id::Id, special_use::SpecialUse}; use crate::{ - object::{JmapObject, MaybeReference, parse_ref}, + object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::deserialize::DeserializeArguments, }; @@ -350,3 +350,41 @@ impl From for MailboxValue { MailboxValue::Id(id) } } + +impl JmapObjectId for MailboxValue { + fn as_id(&self) -> Option { + if let MailboxValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let MailboxValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let MailboxValue::IdReference(r) = self { + Some(r) + } else { + None + } + } +} + +impl TryFrom for MailboxValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + if let AnyId::Id(id) = value { + Ok(MailboxValue::Id(id)) + } else { + Err(()) + } + } +} diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index 44939ed2..c852ce87 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -7,7 +7,8 @@ use crate::request::deserialize::DeserializeArguments; use jmap_tools::{Element, Property}; use serde::Serialize; -use std::str::FromStr; +use std::{fmt::Debug, str::FromStr}; +use types::{blob::BlobId, id::Id}; pub mod blob; pub mod email; @@ -22,22 +23,35 @@ pub mod sieve; pub mod thread; pub mod vacation_response; -pub trait JmapObject { - type Property: Property + FromStr + Serialize; - type Element: Element + From; - type Id: FromStr + Serialize; +pub trait JmapObject: std::fmt::Debug { + type Property: Property + FromStr + Serialize + Debug; + type Element: Element + From + JmapObjectId + Debug; + type Id: FromStr + TryFrom + Serialize + Debug; - type Filter: Default + for<'de> DeserializeArguments<'de>; - type Comparator: Default + for<'de> DeserializeArguments<'de>; + type Filter: Default + for<'de> DeserializeArguments<'de> + Debug; + type Comparator: Default + for<'de> DeserializeArguments<'de> + Debug; - 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>; + type GetArguments: Default + for<'de> DeserializeArguments<'de> + Debug; + type SetArguments: Default + for<'de> DeserializeArguments<'de> + Debug; + type QueryArguments: Default + for<'de> DeserializeArguments<'de> + Debug; + type CopyArguments: Default + for<'de> DeserializeArguments<'de> + Debug; const ID_PROPERTY: Self::Property; } +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(untagged)] +pub enum AnyId { + Id(Id), + BlobId(BlobId), +} + +pub trait JmapObjectId: TryFrom { + fn as_id(&self) -> Option; + fn as_any_id(&self) -> Option; + fn as_id_ref(&self) -> Option<&str>; +} + #[derive(Debug, Clone, PartialEq, Eq)] enum MaybeReference { Value(T), @@ -54,3 +68,58 @@ fn parse_ref(value: &str) -> MaybeReference { .unwrap_or(MaybeReference::ParseError) } } + +impl From for AnyId { + fn from(value: Id) -> Self { + AnyId::Id(value) + } +} + +impl From for AnyId { + fn from(value: BlobId) -> Self { + AnyId::BlobId(value) + } +} + +impl TryFrom for Id { + type Error = (); + + fn try_from(value: AnyId) -> Result { + if let AnyId::Id(id) = value { + Ok(id) + } else { + Err(()) + } + } +} + +impl TryFrom for BlobId { + type Error = (); + + fn try_from(value: AnyId) -> Result { + if let AnyId::BlobId(id) = value { + Ok(id) + } else { + Err(()) + } + } +} + +impl<'de> serde::Deserialize<'de> for AnyId { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = <&str>::deserialize(deserializer)?; + if let Some(blob_id) = BlobId::from_base32(value) { + Ok(AnyId::BlobId(blob_id)) + } else if let Ok(id) = Id::from_str(value) { + Ok(AnyId::Id(id)) + } else { + Err(serde::de::Error::custom(format!( + "Invalid AnyId: {}", + value + ))) + } + } +} diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs index 2e630aff..d2a96c3d 100644 --- a/crates/jmap-proto/src/object/principal.rs +++ b/crates/jmap-proto/src/object/principal.rs @@ -8,7 +8,10 @@ use jmap_tools::{Element, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; -use crate::{object::JmapObject, request::deserialize::DeserializeArguments}; +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::deserialize::DeserializeArguments, +}; #[derive(Debug, Clone, Default)] pub struct Principal; @@ -265,3 +268,37 @@ impl From for PrincipalValue { PrincipalValue::Id(id) } } + +impl JmapObjectId for PrincipalValue { + fn as_id(&self) -> Option { + if let PrincipalValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let PrincipalValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for PrincipalValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + if let AnyId::Id(id) = value { + Ok(PrincipalValue::Id(id)) + } else { + Err(()) + } + } +} diff --git a/crates/jmap-proto/src/object/push_subscription.rs b/crates/jmap-proto/src/object/push_subscription.rs index 2515baaf..4f05ffb9 100644 --- a/crates/jmap-proto/src/object/push_subscription.rs +++ b/crates/jmap-proto/src/object/push_subscription.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::object::JmapObject; +use crate::object::{AnyId, JmapObject, JmapObjectId}; use crate::types::date::UTCDate; use jmap_tools::{Element, JsonPointer, JsonPointerItem}; use jmap_tools::{Key, Property}; @@ -170,3 +170,34 @@ impl From for PushSubscriptionValue { PushSubscriptionValue::Id(id) } } + +impl JmapObjectId for PushSubscriptionValue { + fn as_id(&self) -> Option { + match self { + PushSubscriptionValue::Id(id) => Some(*id), + _ => None, + } + } + + fn as_any_id(&self) -> Option { + match self { + PushSubscriptionValue::Id(id) => Some(AnyId::Id(*id)), + _ => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for PushSubscriptionValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(PushSubscriptionValue::Id(id)), + _ => Err(()), + } + } +} diff --git a/crates/jmap-proto/src/object/quota.rs b/crates/jmap-proto/src/object/quota.rs index f523487b..ee552159 100644 --- a/crates/jmap-proto/src/object/quota.rs +++ b/crates/jmap-proto/src/object/quota.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{object::JmapObject, request::deserialize::DeserializeArguments}; +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::deserialize::DeserializeArguments, +}; use jmap_tools::{Element, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::{id::Id, type_state::DataType}; @@ -224,3 +227,33 @@ impl From for QuotaValue { QuotaValue::Id(id) } } + +impl JmapObjectId for QuotaValue { + fn as_id(&self) -> Option { + if let QuotaValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + self.as_id().map(AnyId::Id) + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for QuotaValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + if let AnyId::Id(id) = value { + Ok(QuotaValue::Id(id)) + } else { + Err(()) + } + } +} diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index df8f9e64..04305106 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -5,7 +5,7 @@ */ use crate::{ - object::{DeserializeArguments, JmapObject, MaybeReference, parse_ref}, + object::{AnyId, DeserializeArguments, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::reference::MaybeIdReference, }; use jmap_tools::{Element, Key, Property}; @@ -234,3 +234,39 @@ impl From for SieveValue { SieveValue::Id(id) } } + +impl JmapObjectId for SieveValue { + fn as_id(&self) -> Option { + match self { + SieveValue::Id(id) => Some(*id), + _ => None, + } + } + + fn as_any_id(&self) -> Option { + match self { + SieveValue::Id(id) => Some(AnyId::Id(*id)), + SieveValue::BlobId(id) => Some(AnyId::BlobId(id.clone())), + SieveValue::IdReference(_) => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let SieveValue::IdReference(r) = self { + Some(r) + } else { + None + } + } +} + +impl TryFrom for SieveValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(SieveValue::Id(id)), + AnyId::BlobId(id) => Ok(SieveValue::BlobId(id)), + } + } +} diff --git a/crates/jmap-proto/src/object/thread.rs b/crates/jmap-proto/src/object/thread.rs index e1739d6c..345cb4a4 100644 --- a/crates/jmap-proto/src/object/thread.rs +++ b/crates/jmap-proto/src/object/thread.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::{AnyId, JmapObject, JmapObjectId}; #[derive(Debug, Clone, Default)] pub struct Thread; @@ -109,3 +109,30 @@ impl From for ThreadValue { ThreadValue::Id(id) } } + +impl JmapObjectId for ThreadValue { + fn as_id(&self) -> Option { + match self { + ThreadValue::Id(id) => Some(*id), + } + } + + fn as_any_id(&self) -> Option { + self.as_id().map(AnyId::Id) + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for ThreadValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(ThreadValue::Id(id)), + _ => Err(()), + } + } +} diff --git a/crates/jmap-proto/src/object/vacation_response.rs b/crates/jmap-proto/src/object/vacation_response.rs index 51fbbdc7..19c5fb4f 100644 --- a/crates/jmap-proto/src/object/vacation_response.rs +++ b/crates/jmap-proto/src/object/vacation_response.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{object::JmapObject, types::date::UTCDate}; +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + types::date::UTCDate, +}; use jmap_tools::{Element, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; @@ -132,3 +135,34 @@ impl From for VacationResponseValue { VacationResponseValue::Id(id) } } + +impl JmapObjectId for VacationResponseValue { + fn as_id(&self) -> Option { + match self { + VacationResponseValue::Id(id) => Some(*id), + _ => None, + } + } + + fn as_any_id(&self) -> Option { + match self { + VacationResponseValue::Id(id) => Some(AnyId::Id(*id)), + _ => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for VacationResponseValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(VacationResponseValue::Id(id)), + _ => Err(()), + } + } +} diff --git a/crates/jmap-proto/src/references/eval.rs b/crates/jmap-proto/src/references/eval.rs new file mode 100644 index 00000000..f5c41d9a --- /dev/null +++ b/crates/jmap-proto/src/references/eval.rs @@ -0,0 +1,176 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{AnyId, JmapObjectId}, + references::{ + Graph, + jsptr::{EvalResults, ResponsePtr}, + }, + request::reference::ResultReference, + response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod}, +}; +use compact_str::format_compact; +use jmap_tools::{Element, Property, Value}; +use types::id::Id; + +impl Response<'_> { + pub(crate) fn eval_result_references(&self, rr: &ResultReference) -> trc::Result { + let mut results = EvalResults::default(); + + for response in &self.method_responses { + if response.id == rr.result_of && response.name == rr.name { + let path = rr.path.iter(); + let success = match &response.method { + ResponseMethod::Get(response) => match response { + GetResponseMethod::Email(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Mailbox(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Thread(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Identity(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::EmailSubmission(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::PushSubscription(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Sieve(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::VacationResponse(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Principal(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Quota(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::Blob(response) => response.eval_jptr(path, &mut results), + }, + ResponseMethod::Changes(response) => match response { + ChangesResponseMethod::Email(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::Mailbox(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::Thread(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::Identity(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::EmailSubmission(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::Quota(response) => { + response.eval_jptr(path, &mut results) + } + }, + ResponseMethod::Query(response) => response.eval_jptr(path, &mut results), + ResponseMethod::QueryChanges(response) => { + response.eval_jptr(path, &mut results) + } + _ => false, + }; + + if success { + return Ok(results); + } + } + } + + Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Result reference to {}#{} not found.", + rr.result_of, + rr.name + ))) + } + + pub(crate) fn eval_id_reference(&self, ir: &str) -> trc::Result { + if let Some(AnyId::Id(id)) = self.created_ids.get(ir) { + Ok(*id) + } else { + Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!("Id reference {ir:?} not found."))) + } + } +} + +pub(crate) trait EvalObjectReferences { + fn eval_object_references( + &mut self, + response: &Response<'_>, + graph: &mut Graph<'_>, + ) -> trc::Result<()>; +} + +impl<'x, P, E> EvalObjectReferences for Value<'x, P, E> +where + P: Property, + E: Element + JmapObjectId + TryFrom, +{ + fn eval_object_references( + &mut self, + response: &Response<'_>, + graph: &mut Graph<'_>, + ) -> trc::Result<()> { + let Value::Object(obj) = self else { + return Ok(()); + }; + + for (_, value) in obj.as_mut_vec() { + match value { + Value::Element(element) => { + if let Some(id_ref) = element.as_id_ref() { + if let Some(id) = response.created_ids.get(id_ref) { + match E::try_from(id.clone()) { + Ok(eid) => { + *element = eid; + } + Err(_) => { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Id reference {id_ref:?} points to invalid type." + ))); + } + } + } else if let Graph::Some { child_id, graph } = graph { + graph + .entry(child_id.to_string()) + .or_insert_with(Vec::new) + .push(id_ref.to_string()); + } else { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!("Id reference {id_ref:?} not found."))); + } + } + } + Value::Array(items) => { + for item in items { + item.eval_object_references(response, graph)?; + } + } + _ => {} + } + } + + Ok(()) + } +} diff --git a/crates/jmap-proto/src/references/jsptr.rs b/crates/jmap-proto/src/references/jsptr.rs new file mode 100644 index 00000000..b376e24b --- /dev/null +++ b/crates/jmap-proto/src/references/jsptr.rs @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + method::{ + PropertyWrapper, + changes::ChangesResponse, + get::GetResponse, + query::QueryResponse, + query_changes::{AddedItem, QueryChangesResponse}, + }, + object::{AnyId, JmapObject, JmapObjectId}, + request::reference::ResultReference, +}; +use compact_str::format_compact; +use jmap_tools::{Element, JsonPointerItem, JsonPointerIter, Key, Null, Property, Value}; +use std::{borrow::Cow, str::FromStr}; +use types::{blob::BlobId, id::Id}; + +pub(crate) trait ResponsePtr { + fn eval_jptr(&self, pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool; +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub(crate) struct EvalResults(Vec); + +#[derive(Debug)] +pub(crate) enum EvalResult { + Id(AnyId), + Property(Cow<'static, str>), +} + +impl ResponsePtr for Vec +where + T: ResponsePtr, +{ + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next() { + Some(JsonPointerItem::Number(n)) => { + if let Some(v) = self.get(*n as usize) { + v.eval_jptr(pointer, results); + } + } + Some(JsonPointerItem::Wildcard | JsonPointerItem::Root) | None => { + for v in self { + v.eval_jptr(pointer.clone(), results); + } + } + _ => (), + } + + true + } +} + +impl<'ctx, P, E> ResponsePtr for Value<'ctx, P, E> +where + P: Property, + E: Element + JmapObjectId, +{ + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next() { + Some(JsonPointerItem::Key(key)) => { + if let Some(key) = key.as_string_key() + && let Value::Object(map) = self + && let Some(v) = map.get(&Key::Borrowed(key)) + { + v.eval_jptr(pointer, results); + } + } + Some(JsonPointerItem::Number(n)) => match self { + Value::Array(values) => { + if let Some(v) = values.get(*n as usize) { + v.eval_jptr(pointer, results); + } + } + Value::Object(map) => { + let n = Key::Owned(n.to_string()); + if let Some(v) = map.get(&n) { + v.eval_jptr(pointer, results); + } + } + _ => {} + }, + Some(JsonPointerItem::Wildcard) => match self { + Value::Array(values) => { + for v in values { + v.eval_jptr(pointer.clone(), results); + } + } + Value::Object(map) => { + for v in map.values() { + v.eval_jptr(pointer.clone(), results); + } + } + _ => {} + }, + Some(JsonPointerItem::Root) | None => match self { + Value::Element(e) => { + if let Some(id) = e.as_any_id() { + results.0.push(EvalResult::Id(id)); + } + } + Value::Array(list) => { + for item in list { + if let Value::Element(e) = item + && let Some(id) = e.as_any_id() + { + results.0.push(EvalResult::Id(id)); + } + } + } + _ => (), + }, + } + + true + } +} + +impl ResponsePtr for Id { + fn eval_jptr(&self, _pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + results.0.push(EvalResult::Id(AnyId::Id(*self))); + true + } +} + +impl ResponsePtr for BlobId { + fn eval_jptr(&self, _pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + results.0.push(EvalResult::Id(AnyId::BlobId(self.clone()))); + true + } +} + +impl ResponsePtr for PropertyWrapper { + fn eval_jptr(&self, _: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + results.0.push(EvalResult::Property(self.0.to_cow())); + true + } +} + +impl ResponsePtr for GetResponse { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("list") => { + self.list.eval_jptr(pointer, results); + true + } + _ => false, + } + } +} + +impl ResponsePtr for ChangesResponse { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + if let Some(property) = pointer.next().and_then(|item| item.as_string_key()) { + hashify::fnc_map!(property.as_bytes(), + "created" => { + self.created.eval_jptr(pointer, results); + }, + "updated" => { + self.updated.eval_jptr(pointer, results); + }, + "updatedProperties" => { + if let Some(props) = &self.updated_properties { + props.eval_jptr(pointer, results); + } + }, + _ => { + return false; + } + ); + + true + } else { + false + } + } +} + +impl ResponsePtr for QueryResponse { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("ids") => { + self.ids.eval_jptr(pointer, results); + true + } + _ => false, + } + } +} + +impl ResponsePtr for QueryChangesResponse { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("added") => { + self.added.eval_jptr(pointer, results); + true + } + _ => false, + } + } +} + +impl ResponsePtr for AddedItem { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("id") => { + results.0.push(EvalResult::Id(AnyId::Id(self.id))); + true + } + _ => false, + } + } +} + +impl EvalResults { + pub fn into_ids>( + self, + rr: &ResultReference, + ) -> impl Iterator> { + self.0.into_iter().map(move |id| { + if let EvalResult::Id(any_id) = id { + T::try_from(any_id).map_err(|_| { + trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Failed to evaluate {rr} result reference: Invalid Id type." + )) + }) + } else { + Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Failed to evaluate {rr} result reference: Invalid Id type." + ))) + } + }) + } + + pub fn into_properties( + self, + rr: &ResultReference, + ) -> impl Iterator> { + self.0.into_iter().map(move |prop| { + if let EvalResult::Property(prop) = prop { + T::from_str(&prop).map_err(|_| { + trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Failed to evaluate {rr} result reference: Invalid property." + )) + }) + } else { + Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Failed to evaluate {rr} result reference: Invalid property." + ))) + } + }) + } +} diff --git a/crates/jmap-proto/src/references/mod.rs b/crates/jmap-proto/src/references/mod.rs new file mode 100644 index 00000000..39f8a98e --- /dev/null +++ b/crates/jmap-proto/src/references/mod.rs @@ -0,0 +1,724 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use compact_str::format_compact; +use std::collections::HashMap; +use utils::map::vec_map::VecMap; + +pub mod eval; +pub mod jsptr; +pub mod resolve; + +pub(crate) enum Graph<'x> { + Some { + child_id: &'x str, + graph: &'x mut HashMap>, + }, + None, +} + +fn topological_sort( + create: &mut VecMap, + graph: HashMap>, +) -> trc::Result> { + // Make sure all references exist + for (from_id, to_ids) in graph.iter() { + for to_id in to_ids { + if !create.contains_key(to_id) { + return Err(trc::JmapEvent::InvalidResultReference.into_err().details( + format_compact!( + "Invalid reference to non-existing object {to_id:?} from {from_id:?}" + ), + )); + } + } + } + + let mut sorted_create = VecMap::with_capacity(create.len()); + let mut it_stack = Vec::new(); + let keys = graph.keys().cloned().collect::>(); + let mut it = keys.iter(); + + 'main: loop { + while let Some(from_id) = it.next() { + if let Some(to_ids) = graph.get(from_id) { + it_stack.push((it, from_id)); + if it_stack.len() > 1000 { + return Err(trc::JmapEvent::InvalidArguments + .into_err() + .details("Cyclical references are not allowed.")); + } + it = to_ids.iter(); + continue; + } else if let Some((id, value)) = create.remove_entry(from_id) { + sorted_create.append(id, value); + if create.is_empty() { + break 'main; + } + } + } + + if let Some((prev_it, from_id)) = it_stack.pop() { + it = prev_it; + if let Some((id, value)) = create.remove_entry(from_id) { + sorted_create.append(id, value); + if create.is_empty() { + break 'main; + } + } + } else { + break; + } + } + + // Add remaining items + if !create.is_empty() { + for (id, value) in std::mem::take(create) { + sorted_create.append(id, value); + } + } + Ok(sorted_create) +} + +#[cfg(test)] +mod tests { + use crate::{ + method::{changes::ChangesResponse, get::GetResponse, query::QueryResponse}, + object::{ + email::{EmailProperty, EmailValue}, + mailbox::{MailboxProperty, MailboxValue}, + thread::{ThreadProperty, ThreadValue}, + }, + request::{ + Call, GetRequestMethod, Request, RequestMethod, SetRequestMethod, + reference::{MaybeIdReference, MaybeResultReference}, + }, + response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod}, + }; + use jmap_tools::{Key, Map, Value}; + use std::collections::HashMap; + use types::id::Id; + + #[test] + fn eval_value_references() { + let request = Request::parse( + br##"{ + "using":["urn:ietf:params:jmap:mail"], + "methodCalls": [[ "Email/query", { + "accountId": "a", + "filter": { "inMailbox": "a" }, + "sort": [{ "property": "receivedAt", "isAscending": false }], + "collapseThreads": true, + "position": 0, + "limit": 10, + "calculateTotal": true + }, "t0" ], + [ "Email/get", { + "accountId": "a", + "#ids": { + "resultOf": "t0", + "name": "Email/query", + "path": "/ids" + }, + "properties": [ "threadId" ] + }, "t1" ], + [ "Thread/get", { + "accountId": "a", + "#ids": { + "resultOf": "t1", + "name": "Email/get", + "path": "/list/*/threadId" + } + }, "t2" ], + [ "Email/get", { + "accountId": "a", + "#ids": { + "resultOf": "t2", + "name": "Thread/get", + "path": "/list/*/emailIds" + }, + "properties": [ "from", "receivedAt", "subject" ] + }, "t3" ]] + }"##, + 100, + 1024 * 1024, + ) + .unwrap(); + + let mut response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + assert_eq!(request.method_calls.len(), 4); + + for (test_num, mut call) in request.method_calls.into_iter().enumerate() { + match test_num { + 0 => { + response.method_responses.push(Call { + id: call.id, + name: call.name, + method: ResponseMethod::Query(QueryResponse { + account_id: Id::new(1), + query_state: Default::default(), + can_calculate_changes: Default::default(), + position: Default::default(), + ids: vec![Id::new(4), Id::new(5)], + total: Default::default(), + limit: Default::default(), + }), + }); + } + 1 => { + response.resolve_references(&mut call.method).unwrap(); + match call.method { + RequestMethod::Get(GetRequestMethod::Email(req)) => { + assert_eq!( + req.ids, + Some(MaybeResultReference::Value(vec![ + MaybeIdReference::Id(Id::new(4)), + MaybeIdReference::Id(Id::new(5)) + ])) + ); + } + _ => panic!("Expected Email Get Request"), + } + response.method_responses.push(Call { + id: call.id, + name: call.name, + method: ResponseMethod::Get(GetResponseMethod::Email(GetResponse { + account_id: Id::new(1).into(), + state: Default::default(), + list: vec![ + Value::Object(Map::from(vec![( + Key::Property(EmailProperty::ThreadId), + Value::Element(EmailValue::Id(Id::new(9))), + )])), + Value::Object(Map::from(vec![( + Key::Property(EmailProperty::ThreadId), + Value::Element(EmailValue::Id(Id::new(10))), + )])), + ], + not_found: Default::default(), + })), + }); + } + 2 => { + response.resolve_references(&mut call.method).unwrap(); + match call.method { + RequestMethod::Get(GetRequestMethod::Thread(req)) => { + assert_eq!( + req.ids, + Some(MaybeResultReference::Value(vec![ + MaybeIdReference::Id(Id::new(9)), + MaybeIdReference::Id(Id::new(10)) + ])) + ); + } + _ => panic!("Expected Thread Get Request"), + } + response.method_responses.push(Call { + id: call.id, + name: call.name, + method: ResponseMethod::Get(GetResponseMethod::Thread(GetResponse { + account_id: Id::new(1).into(), + state: Default::default(), + list: vec![ + Value::Object(Map::from(vec![( + Key::Property(ThreadProperty::EmailIds), + Value::Array(vec![ + Value::Element(ThreadValue::Id(Id::new(100))), + Value::Element(ThreadValue::Id(Id::new(101))), + ]), + )])), + Value::Object(Map::from(vec![( + Key::Property(ThreadProperty::EmailIds), + Value::Array(vec![ + Value::Element(ThreadValue::Id(Id::new(102))), + Value::Element(ThreadValue::Id(Id::new(103))), + ]), + )])), + ], + not_found: Default::default(), + })), + }); + } + 3 => { + response.resolve_references(&mut call.method).unwrap(); + match call.method { + RequestMethod::Get(GetRequestMethod::Email(req)) => { + assert_eq!( + req.ids, + Some(MaybeResultReference::Value(vec![ + MaybeIdReference::Id(Id::new(100)), + MaybeIdReference::Id(Id::new(101)), + MaybeIdReference::Id(Id::new(102)), + MaybeIdReference::Id(Id::new(103)), + ])) + ); + } + _ => panic!("Expected Mailbox Get Request"), + } + } + _ => panic!("Unexpected invocation {}", test_num), + } + } + } + + #[test] + fn eval_property_references() { + let request = Request::parse( + br##"{ + "using":["urn:ietf:params:jmap:mail"], + "methodCalls": [ + ["Mailbox/changes",{ + "accountId":"s", + "sinceState":"srxqk071myhgkyay" + },"0"], + ["Mailbox/get",{ + "accountId":"s", + "#ids":{"name":"Mailbox/changes","path":"/created","resultOf":"0"} + },"1"], + ["Mailbox/get",{ + "accountId":"s", + "#ids":{"name":"Mailbox/changes","path":"/updated","resultOf":"0"}, + "#properties":{"name":"Mailbox/changes","path":"/updatedProperties","resultOf":"0"} + },"2"] + ] + }"##, + 100, + 1024 * 1024, + ) + .unwrap(); + + let mut response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + assert_eq!(request.method_calls.len(), 3); + + for (test_num, mut call) in request.method_calls.into_iter().enumerate() { + match test_num { + 0 => { + response.method_responses.push(Call { + id: call.id, + name: call.name, + method: ResponseMethod::Changes(ChangesResponseMethod::Mailbox( + ChangesResponse { + account_id: Id::new(1), + old_state: Default::default(), + new_state: Default::default(), + has_more_changes: Default::default(), + created: Default::default(), + updated: vec![Id::new(2), Id::new(3)], + destroyed: Default::default(), + updated_properties: Some(vec![ + MailboxProperty::Name.into(), + MailboxProperty::ParentId.into(), + ]), + }, + )), + }); + } + 1 => { + response.resolve_references(&mut call.method).unwrap(); + match call.method { + RequestMethod::Get(GetRequestMethod::Mailbox(req)) => { + assert_eq!(req.ids, Some(MaybeResultReference::Value(vec![]))); + } + _ => panic!("Expected Mailbox Get Request"), + } + } + 2 => { + response.resolve_references(&mut call.method).unwrap(); + match call.method { + RequestMethod::Get(GetRequestMethod::Mailbox(req)) => { + assert_eq!( + req.ids, + Some(MaybeResultReference::Value(vec![ + MaybeIdReference::Id(Id::new(2)), + MaybeIdReference::Id(Id::new(3)) + ])) + ); + } + _ => panic!("Expected Mailbox Get Request"), + } + } + _ => panic!("Unexpected invocation {}", test_num), + } + } + } + + #[test] + fn eval_create_references() { + let request = Request::parse( + br##"{ + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "Folder a", + "parentId": "#b" + }, + "b": { + "name": "Folder b", + "parentId": "#c" + }, + "c": { + "name": "Folder c", + "parentId": "#d" + }, + "d": { + "name": "Folder d", + "parentId": "#e" + }, + "e": { + "name": "Folder e", + "parentId": "#f" + }, + "f": { + "name": "Folder f", + "parentId": "#g" + }, + "g": { + "name": "Folder g", + "parentId": null + } + } + }, + "fulltree" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a1": { + "name": "Folder a1", + "parentId": null + }, + "b2": { + "name": "Folder b2", + "parentId": "#a1" + }, + "c3": { + "name": "Folder c3", + "parentId": "#a1" + }, + "d4": { + "name": "Folder d4", + "parentId": "#b2" + }, + "e5": { + "name": "Folder e5", + "parentId": "#b2" + }, + "f6": { + "name": "Folder f6", + "parentId": "#d4" + }, + "g7": { + "name": "Folder g7", + "parentId": "#e5" + } + } + }, + "fulltree2" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "z": { + "name": "Folder Z", + "parentId": "#x" + }, + "y": { + "name": null + }, + "x": { + "name": "Folder X" + } + } + }, + "xyz" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "Folder a", + "parentId": "#b" + }, + "b": { + "name": "Folder b", + "parentId": "#c" + }, + "c": { + "name": "Folder c", + "parentId": "#d" + }, + "d": { + "name": "Folder d", + "parentId": "#a" + } + } + }, + "circular" + ] + ] + }"##, + 100, + 1024 * 1024, + ) + .unwrap(); + + let response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + for (test_num, mut call) in request.method_calls.into_iter().enumerate() { + match response.resolve_references(&mut call.method) { + Ok(_) => assert!( + (0..3).contains(&test_num), + "Unexpected invocation {}", + test_num + ), + Err(err) => { + assert_eq!(test_num, 3); + assert!( + err.matches(trc::EventType::Jmap(trc::JmapEvent::InvalidArguments)), + "{:?}", + err + ); + continue; + } + } + + if let RequestMethod::Set(SetRequestMethod::Mailbox(request)) = call.method { + if test_num == 0 { + assert_eq!( + request + .create + .unwrap() + .into_iter() + .map(|b| b.0) + .collect::>(), + ["g", "f", "e", "d", "c", "b", "a"] + .iter() + .map(|i| i.to_string()) + .collect::>() + ); + } else if test_num == 1 { + let mut pending_ids = vec!["a1", "b2", "d4", "e5", "f6", "c3", "g7"]; + + for (id, _) in request.create.as_ref().unwrap() { + match id.as_str() { + "a1" => (), + "b2" | "c3" => assert!(!pending_ids.contains(&"a1")), + "d4" | "e5" => assert!(!pending_ids.contains(&"b2")), + "f6" => assert!(!pending_ids.contains(&"d4")), + "g7" => assert!(!pending_ids.contains(&"e5")), + _ => panic!("Unexpected ID"), + } + pending_ids.retain(|i| i != id); + } + + if !pending_ids.is_empty() { + panic!( + "Unexpected order: {:?}", + request + .create + .as_ref() + .unwrap() + .iter() + .map(|b| b.0.to_string()) + .collect::>() + ); + } + } else if test_num == 2 { + assert_eq!( + request + .create + .unwrap() + .into_iter() + .map(|b| b.0) + .collect::>(), + ["x", "z", "y"] + .iter() + .map(|i| i.to_string()) + .collect::>() + ); + } + } else { + panic!("Expected Set Mailbox Request"); + } + } + + let request = Request::parse( + br##"{ + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "a", + "parentId": "#x" + }, + "b": { + "name": "b", + "parentId": "#y" + }, + "c": { + "name": "c", + "parentId": "#z" + } + } + }, + "ref1" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a1": { + "name": "a1", + "parentId": "#a" + }, + "b2": { + "name": "b2", + "parentId": "#b" + }, + "c3": { + "name": "c3", + "parentId": "#c" + } + } + }, + "red2" + ] + ], + "createdIds": { + "x": "b", + "y": "c", + "z": "d" + } + }"##, + 1024, + 1024 * 1024, + ) + .unwrap(); + + let mut response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + let mut invocations = request.method_calls.into_iter(); + let mut call = invocations.next().unwrap(); + response.resolve_references(&mut call.method).unwrap(); + + if let RequestMethod::Set(SetRequestMethod::Mailbox(request)) = call.method { + let create = request + .create + .as_ref() + .unwrap() + .iter() + .map(|(p, v)| { + ( + p.as_str(), + v.as_object() + .unwrap() + .get(&Key::Property(MailboxProperty::ParentId)) + .unwrap(), + ) + }) + .collect::>(); + assert_eq!( + *create.get("a").unwrap(), + &Value::Element(MailboxValue::Id(Id::new(1))) + ); + assert_eq!( + *create.get("b").unwrap(), + &Value::Element(MailboxValue::Id(Id::new(2))) + ); + assert_eq!( + *create.get("c").unwrap(), + &Value::Element(MailboxValue::Id(Id::new(3))) + ); + } else { + panic!("Expected Mailbox Set Request"); + } + + response + .created_ids + .insert("a".to_string(), Id::new(5).into()); + response + .created_ids + .insert("b".to_string(), Id::new(6).into()); + response + .created_ids + .insert("c".to_string(), Id::new(7).into()); + + let mut call = invocations.next().unwrap(); + response.resolve_references(&mut call.method).unwrap(); + + if let RequestMethod::Set(SetRequestMethod::Mailbox(request)) = call.method { + let create = request + .create + .as_ref() + .unwrap() + .iter() + .map(|(p, v)| { + ( + p.as_str(), + v.as_object() + .unwrap() + .get(&Key::Property(MailboxProperty::ParentId)) + .unwrap(), + ) + }) + .collect::>(); + assert_eq!( + *create.get("a1").unwrap(), + &Value::Element(MailboxValue::Id(Id::new(5))) + ); + assert_eq!( + *create.get("b2").unwrap(), + &Value::Element(MailboxValue::Id(Id::new(6))) + ); + assert_eq!( + *create.get("c3").unwrap(), + &Value::Element(MailboxValue::Id(Id::new(7))) + ); + } else { + panic!("Expected Mailbox Set Request"); + } + } +} diff --git a/crates/jmap-proto/src/references/resolve.rs b/crates/jmap-proto/src/references/resolve.rs new file mode 100644 index 00000000..0151f7c3 --- /dev/null +++ b/crates/jmap-proto/src/references/resolve.rs @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + method::{ + copy::CopyRequest, + get::GetRequest, + import::ImportEmailRequest, + search_snippet::GetSearchSnippetRequest, + set::{SetRequest, SetResponse}, + upload::{BlobUploadRequest, DataSourceObject}, + }, + object::{AnyId, JmapObject, JmapObjectId}, + references::{Graph, eval::EvalObjectReferences, topological_sort}, + request::{ + CopyRequestMethod, GetRequestMethod, MaybeInvalid, RequestMethod, SetRequestMethod, + reference::{MaybeIdReference, MaybeResultReference}, + }, + response::Response, +}; +use compact_str::format_compact; +use jmap_tools::{Element, Property, Value}; +use std::collections::HashMap; +use types::id::Id; + +impl Response<'_> { + pub fn resolve_references(&self, request: &mut RequestMethod) -> trc::Result<()> { + match request { + RequestMethod::Get(request) => match request { + GetRequestMethod::Email(request) => request.resolve_references(self)?, + GetRequestMethod::Mailbox(request) => request.resolve_references(self)?, + GetRequestMethod::Thread(request) => request.resolve_references(self)?, + GetRequestMethod::Identity(request) => request.resolve_references(self)?, + GetRequestMethod::EmailSubmission(request) => request.resolve_references(self)?, + GetRequestMethod::PushSubscription(request) => request.resolve_references(self)?, + GetRequestMethod::Sieve(request) => request.resolve_references(self)?, + GetRequestMethod::VacationResponse(request) => request.resolve_references(self)?, + GetRequestMethod::Principal(request) => request.resolve_references(self)?, + GetRequestMethod::Quota(request) => request.resolve_references(self)?, + GetRequestMethod::Blob(request) => request.resolve_references(self)?, + }, + RequestMethod::Set(request) => match request { + SetRequestMethod::Email(request) => request.resolve_references(self)?, + SetRequestMethod::Mailbox(request) => request.resolve_references(self)?, + SetRequestMethod::Identity(request) => request.resolve_references(self)?, + SetRequestMethod::EmailSubmission(request) => request.resolve_references(self)?, + SetRequestMethod::PushSubscription(request) => request.resolve_references(self)?, + SetRequestMethod::Sieve(request) => request.resolve_references(self)?, + SetRequestMethod::VacationResponse(request) => request.resolve_references(self)?, + }, + RequestMethod::Copy(request) => match request { + CopyRequestMethod::Email(request) => request.resolve_references(self)?, + CopyRequestMethod::Blob(_) => (), + }, + RequestMethod::ImportEmail(request) => request.resolve_references(self)?, + RequestMethod::SearchSnippet(request) => request.resolve_references(self)?, + RequestMethod::UploadBlob(request) => request.resolve_references(self)?, + _ => {} + } + + Ok(()) + } +} + +pub trait ResolveCreatedReference +where + P: Property, + E: Element + JmapObjectId, +{ + fn get_created_id(&self, id_ref: &str) -> Option; + + fn resolve_self_references(&mut self, value: &mut Value<'_, P, E>) -> trc::Result<()> { + match value { + Value::Element(element) => { + if let Some(id_ref) = element.as_id_ref() { + if let Some(id) = self.get_created_id(id_ref) { + match E::try_from(id) { + Ok(eid) => { + *element = eid; + } + Err(_) => { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Id reference {id_ref:?} points to invalid type." + ))); + } + } + } else { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!("Id reference {id_ref:?} not found."))); + } + } + } + Value::Array(items) => { + for item in items { + self.resolve_self_references(item)?; + } + } + _ => {} + } + + Ok(()) + } +} + +pub(crate) trait ResolveReference { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()>; +} + +impl ResolveReference for GetRequest { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { + // Resolve id references + match &mut self.ids { + Some(MaybeResultReference::Reference(reference)) => { + self.ids = Some(MaybeResultReference::Value( + response + .eval_result_references(reference)? + .into_ids::(reference) + .map(|f| f.map(MaybeIdReference::Id)) + .collect::>()?, + )); + } + Some(MaybeResultReference::Value(ids)) => { + for id in ids { + if let MaybeIdReference::Reference(reference) = id { + if let Some(resolved_id) = response + .created_ids + .get(reference) + .cloned() + .and_then(|v| T::Id::try_from(v).ok()) + { + *id = MaybeIdReference::Id(resolved_id); + } else { + return Err(trc::JmapEvent::InvalidResultReference.into_err().details( + format_compact!( + "Id reference {reference:?} does not exist or is invalid." + ), + )); + } + } + } + } + _ => (), + } + + // Resolve properties references + if let Some(MaybeResultReference::Reference(reference)) = &self.properties { + self.properties = Some(MaybeResultReference::Value( + response + .eval_result_references(reference)? + .into_properties::(reference) + .map(|f| f.map(MaybeInvalid::Value)) + .collect::>()?, + )); + } + + Ok(()) + } +} + +impl<'x, T: JmapObject> ResolveReference for SetRequest<'x, T> { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { + // Resolve create references + if let Some(create) = &mut self.create { + let mut graph = HashMap::with_capacity(create.len()); + for (id, obj) in create.iter_mut() { + obj.eval_object_references( + response, + &mut Graph::Some { + child_id: &*id, + graph: &mut graph, + }, + )?; + } + + // Perform topological sort + if !graph.is_empty() { + self.create = topological_sort(create, graph)?.into(); + } + } + + // Resolve update references + if let Some(update) = &mut self.update { + for obj in update.values_mut() { + obj.eval_object_references(response, &mut Graph::None)?; + } + } + + // Resolve destroy references + if let Some(MaybeResultReference::Reference(reference)) = &self.destroy { + self.destroy = Some(MaybeResultReference::Value( + response + .eval_result_references(reference)? + .into_ids::(reference) + .map(|f| f.map(MaybeInvalid::Value)) + .collect::>()?, + )); + } + + Ok(()) + } +} + +impl<'x, T: JmapObject> ResolveReference for CopyRequest<'x, T> { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { + // Resolve create references + for (id, obj) in self.create.iter_mut() { + obj.eval_object_references(response, &mut Graph::None)?; + + if let MaybeIdReference::Reference(ir) = id { + *id = MaybeIdReference::Id(response.eval_id_reference(ir)?); + } + } + + Ok(()) + } +} + +impl ResolveReference for ImportEmailRequest { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { + // Resolve email mailbox references + for email in self.emails.values_mut() { + match &mut email.mailbox_ids { + MaybeResultReference::Reference(reference) => { + email.mailbox_ids = MaybeResultReference::Value( + response + .eval_result_references(reference)? + .into_ids::(reference) + .map(|f| f.map(MaybeIdReference::Id)) + .collect::>()?, + ); + } + MaybeResultReference::Value(values) => { + for value in values { + if let MaybeIdReference::Reference(ir) = value { + *value = MaybeIdReference::Id(response.eval_id_reference(ir)?); + } + } + } + } + } + + Ok(()) + } +} + +impl ResolveReference for GetSearchSnippetRequest { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { + // Resolve emailIds references + if let MaybeResultReference::Reference(reference) = &self.email_ids { + self.email_ids = MaybeResultReference::Value( + response + .eval_result_references(reference)? + .into_ids::(reference) + .map(|f| f.map(MaybeInvalid::Value)) + .collect::>()?, + ); + } + + Ok(()) + } +} + +impl ResolveReference for BlobUploadRequest { + fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { + let mut graph = HashMap::with_capacity(self.create.len()); + for (create_id, object) in self.create.iter_mut() { + for data in &mut object.data { + if let DataSourceObject::Id { id, .. } = data + && let MaybeIdReference::Reference(parent_id) = id + { + match response.created_ids.get(parent_id) { + Some(AnyId::BlobId(blob_id)) => { + *id = MaybeIdReference::Id(blob_id.clone()); + } + Some(_) => { + return Err(trc::JmapEvent::InvalidResultReference.into_err().details( + format_compact!( + "Id reference {parent_id:?} points to invalid type." + ), + )); + } + None => { + graph + .entry(create_id.to_string()) + .or_insert_with(Vec::new) + .push(parent_id.to_string()); + } + } + } + } + } + + // Perform topological sort + if !graph.is_empty() { + self.create = topological_sort(&mut self.create, graph)?; + } + + Ok(()) + } +} + +impl ResolveCreatedReference for SetResponse +where + T: JmapObject, +{ + fn get_created_id(&self, id_ref: &str) -> Option { + self.created + .get(id_ref) + .and_then(|v| v.as_element()) + .and_then(|v| v.as_any_id()) + } +} diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index 9e09abe5..dc684b29 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -28,7 +28,7 @@ use crate::{ validate::ValidateSieveScriptRequest, }, object::{ - blob::Blob, email::Email, email_submission::EmailSubmission, identity::Identity, + AnyId, 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, }, @@ -41,7 +41,7 @@ use std::{collections::HashMap, fmt::Debug, str::FromStr}; pub struct Request<'x> { pub using: CapabilityIds, pub method_calls: Vec>>, - pub created_ids: Option>, + pub created_ids: Option>, } #[derive(Debug)] diff --git a/crates/jmap-proto/src/request/websocket.rs b/crates/jmap-proto/src/request/websocket.rs index 11056d29..b807214e 100644 --- a/crates/jmap-proto/src/request/websocket.rs +++ b/crates/jmap-proto/src/request/websocket.rs @@ -7,6 +7,7 @@ use super::Request; use crate::{ error::request::{RequestError, RequestErrorType, RequestLimitError}, + object::AnyId, request::{Call, deserialize::DeserializeArguments}, response::{Response, ResponseMethod, serialize::serialize_hex}, types::state::State, @@ -39,7 +40,7 @@ pub struct WebSocketResponse<'x> { #[serde(rename(deserialize = "createdIds"))] #[serde(skip_serializing_if = "HashMap::is_empty")] - created_ids: HashMap, + created_ids: HashMap, #[serde(rename = "requestId")] #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs index 651579e0..a52c9bff 100644 --- a/crates/jmap-proto/src/response/mod.rs +++ b/crates/jmap-proto/src/response/mod.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -pub mod references; pub mod serialize; pub mod status; @@ -26,7 +25,7 @@ use crate::{ validate::ValidateSieveScriptResponse, }, object::{ - blob::Blob, email::Email, email_submission::EmailSubmission, identity::Identity, + AnyId, 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, }, @@ -111,11 +110,11 @@ pub struct Response<'x> { #[serde(rename = "createdIds")] #[serde(skip_serializing_if = "HashMap::is_empty")] - pub created_ids: HashMap, + pub created_ids: HashMap, } impl<'x> Response<'x> { - pub fn new(session_state: u32, created_ids: HashMap, capacity: usize) -> Self { + pub fn new(session_state: u32, created_ids: HashMap, capacity: usize) -> Self { Response { session_state, created_ids, @@ -144,7 +143,7 @@ impl<'x> Response<'x> { }); } - pub fn push_created_id(&mut self, create_id: String, id: impl Into) { + pub fn push_created_id(&mut self, create_id: String, id: impl Into) { self.created_ids.insert(create_id, id.into()); } } diff --git a/crates/jmap-proto/src/response/references.rs b/crates/jmap-proto/src/response/references.rs deleted file mode 100644 index e96fc9c6..00000000 --- a/crates/jmap-proto/src/response/references.rs +++ /dev/null @@ -1,886 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::{Response, ResponseMethod}; -use crate::{ - error::set::SetError, - method::{copy::CopyResponse, set::SetResponse, upload::DataSourceObject}, - request::{RequestMethod, reference::ResultReference}, -}; -use compact_str::format_compact; -use std::collections::HashMap; -use types::id::Id; -use utils::map::vec_map::VecMap; - -/* -enum EvalResult { - Properties(Vec), - Values(Vec), - Failed, -} - -impl Response { - pub fn resolve_references(&self, request: &mut RequestMethod) -> trc::Result<()> { - match request { - RequestMethod::Get(request) => { - // Resolve id references - match &mut request.ids { - Some(MaybeReference::Reference(reference)) => { - request.ids = Some(MaybeReference::Value( - self.eval_result_references(reference) - .unwrap_any_ids(reference)?, - )); - } - Some(MaybeReference::Value(ids)) => { - for id in ids { - if let MaybeReference::Reference(reference) = id { - if let Some(resolved_id) = self.created_ids.get(reference) { - *id = MaybeReference::Value(resolved_id.clone()); - } else { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!( - "Id reference {reference:?} does not exist." - ))); - } - } - } - } - _ => (), - } - - // Resolve properties references - if let Some(MaybeReference::Reference(reference)) = &request.properties { - request.properties = Some(MaybeReference::Value( - self.eval_result_references(reference) - .unwrap_properties(reference)?, - )); - } - } - RequestMethod::Set(request) => { - // Resolve create references - if let Some(create) = &mut request.create { - let mut graph = HashMap::with_capacity(create.len()); - for (id, obj) in create.iter_mut() { - self.eval_object_references(obj, Some((&*id, &mut graph)))?; - } - - // Perform topological sort - if !graph.is_empty() { - request.create = topological_sort(create, graph)?.into(); - } - } - - // Resolve update references - if let Some(update) = &mut request.update { - for obj in update.values_mut() { - self.eval_object_references(obj, None)?; - } - } - - // Resolve destroy references - if let Some(MaybeReference::Reference(reference)) = &request.destroy { - request.destroy = Some(MaybeReference::Value( - self.eval_result_references(reference) - .unwrap_ids(reference)?, - )); - } - } - RequestMethod::Copy(request) => { - // Resolve create references - for (id, obj) in request.create.iter_mut() { - self.eval_object_references(obj, None)?; - if let MaybeReference::Reference(ir) = id { - *id = MaybeReference::Value(self.eval_id_reference(ir)?); - } - } - } - RequestMethod::ImportEmail(request) => { - // Resolve email mailbox references - for email in request.emails.values_mut() { - match &mut email.mailbox_ids { - MaybeReference::Reference(rr) => { - email.mailbox_ids = MaybeReference::Value( - self.eval_result_references(rr) - .unwrap_ids(rr)? - .into_iter() - .map(MaybeReference::Value) - .collect(), - ); - } - MaybeReference::Value(values) => { - for value in values { - if let MaybeReference::Reference(ir) = value { - *value = MaybeReference::Value(self.eval_id_reference(ir)?); - } - } - } - } - } - } - RequestMethod::SearchSnippet(request) => { - // Resolve emailIds references - if let MaybeReference::Reference(reference) = &request.email_ids { - request.email_ids = MaybeReference::Value( - self.eval_result_references(reference) - .unwrap_ids(reference)?, - ); - } - } - RequestMethod::UploadBlob(request) => { - let mut graph = HashMap::with_capacity(request.create.len()); - for (create_id, object) in request.create.iter_mut() { - for data in &mut object.data { - if let DataSourceObject::Id { id, .. } = data - && let MaybeReference::Reference(parent_id) = id - { - match self.created_ids.get(parent_id) { - Some(AnyId::Blob(blob_id)) => { - *id = MaybeReference::Value(blob_id.clone()); - } - Some(_) => { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!( - "Id reference {parent_id:?} points to invalid type." - ))); - } - None => { - graph - .entry(create_id.to_string()) - .or_insert_with(Vec::new) - .push(parent_id.to_string()); - } - } - } - } - } - - // Perform topological sort - if !graph.is_empty() { - request.create = topological_sort(&mut request.create, graph)?; - } - } - _ => {} - } - - Ok(()) - } - - fn eval_result_references(&self, rr: &ResultReference) -> EvalResult { - for response in &self.method_responses { - if response.id == rr.result_of && response.name == rr.name { - match &response.method { - ResponseMethod::Get(response) => { - return match rr.path.item_subquery() { - Some(("list", property)) => { - let property = Property::parse(property); - - EvalResult::Values( - response - .list - .iter() - .filter_map(|obj| obj.0.get(&property).cloned()) - .collect(), - ) - } - _ => EvalResult::Failed, - }; - } - ResponseMethod::Changes(response) => { - return match rr.path.item_query() { - Some("created") => EvalResult::Values( - response - .created - .clone() - .into_iter() - .map(Into::into) - .collect(), - ), - Some("updated") => EvalResult::Values( - response - .updated - .clone() - .into_iter() - .map(Into::into) - .collect(), - ), - Some("updatedProperties") => EvalResult::Properties( - response.updated_properties.clone().unwrap_or_default(), - ), - _ => EvalResult::Failed, - }; - } - ResponseMethod::Query(response) => { - return if rr.path.item_query() == Some("ids") { - EvalResult::Values( - response.ids.iter().copied().map(Into::into).collect(), - ) - } else { - EvalResult::Failed - }; - } - ResponseMethod::QueryChanges(response) => { - return if rr.path.item_subquery() == Some(("added", "id")) { - EvalResult::Values( - response.added.iter().map(|item| item.id.into()).collect(), - ) - } else { - EvalResult::Failed - }; - } - _ => (), - } - } - } - - EvalResult::Failed - } - - fn eval_id_reference(&self, ir: &str) -> trc::Result { - if let Some(AnyId::Id(id)) = self.created_ids.get(ir) { - Ok(*id) - } else { - Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!("Id reference {ir:?} not found."))) - } - } - - fn eval_object_references( - &self, - obj: &mut Value<'x, P, E>, - mut graph: Option<(&str, &mut HashMap>)>, - ) -> trc::Result<()> { - for set_value in obj.0.values_mut() { - match set_value { - SetValue::IdReference(MaybeReference::Reference(parent_id)) => { - if let Some(id) = self.created_ids.get(parent_id) { - *set_value = SetValue::Value(id.into()); - } else if let Some((child_id, graph)) = &mut graph { - graph - .entry(child_id.to_string()) - .or_insert_with(Vec::new) - .push(parent_id.to_string()); - } else { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!("Id reference {parent_id:?} not found."))); - } - } - SetValue::IdReferences(id_refs) => { - for id_ref in id_refs { - if let MaybeReference::Reference(parent_id) = id_ref { - if let Some(id) = self.created_ids.get(parent_id) { - *id_ref = MaybeReference::Value(id.clone()); - } else if let Some((child_id, graph)) = &mut graph { - graph - .entry(child_id.to_string()) - .or_insert_with(Vec::new) - .push(parent_id.to_string()); - } else { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!( - "Id reference {parent_id:?} not found." - ))); - } - } - } - } - SetValue::ResultReference(rr) => { - *set_value = - SetValue::Value(self.eval_result_references(rr).unwrap_ids(rr)?.into()); - } - _ => (), - } - } - - Ok(()) - } -} - -fn topological_sort( - create: &mut VecMap, - graph: HashMap>, -) -> trc::Result> { - // Make sure all references exist - for (from_id, to_ids) in graph.iter() { - for to_id in to_ids { - if !create.contains_key(to_id) { - return Err(trc::JmapEvent::InvalidResultReference.into_err().details( - format_compact!( - "Invalid reference to non-existing object {to_id:?} from {from_id:?}" - ), - )); - } - } - } - - let mut sorted_create = VecMap::with_capacity(create.len()); - let mut it_stack = Vec::new(); - let keys = graph.keys().cloned().collect::>(); - let mut it = keys.iter(); - - 'main: loop { - while let Some(from_id) = it.next() { - if let Some(to_ids) = graph.get(from_id) { - it_stack.push((it, from_id)); - if it_stack.len() > 1000 { - return Err(trc::JmapEvent::InvalidArguments - .into_err() - .details("Cyclical references are not allowed.")); - } - it = to_ids.iter(); - continue; - } else if let Some((id, value)) = create.remove_entry(from_id) { - sorted_create.append(id, value); - if create.is_empty() { - break 'main; - } - } - } - - if let Some((prev_it, from_id)) = it_stack.pop() { - it = prev_it; - if let Some((id, value)) = create.remove_entry(from_id) { - sorted_create.append(id, value); - if create.is_empty() { - break 'main; - } - } - } else { - break; - } - } - - // Add remaining items - if !create.is_empty() { - for (id, value) in std::mem::take(create) { - sorted_create.append(id, value); - } - } - Ok(sorted_create) -} - -pub trait EvalObjectReferences { - fn get_id(&self, id_ref: &str) -> Option; - - fn eval_object_references(&self, set_value: SetValue) -> Result> { - match set_value { - SetValue::Value(value) => Ok(MaybePatchValue::Value(value)), - SetValue::Patch(patch) => Ok(MaybePatchValue::Patch(patch)), - SetValue::IdReference(MaybeReference::Reference(id_ref)) => { - if let Some(id) = self.get_id(&id_ref) { - Ok(MaybePatchValue::Value(id)) - } else { - Err(SetError::not_found() - .with_description(format!("Id reference {id_ref:?} not found."))) - } - } - SetValue::IdReference(MaybeReference::Value(AnyId::Id(id))) => { - Ok(MaybePatchValue::Value(Value::Id(id))) - } - SetValue::IdReference(MaybeReference::Value(AnyId::Blob(blob_id))) => { - Ok(MaybePatchValue::Value(Value::BlobId(blob_id))) - } - SetValue::IdReferences(id_refs) => { - let mut ids = Vec::with_capacity(id_refs.len()); - for id_ref in id_refs { - match id_ref { - MaybeReference::Value(AnyId::Id(id)) => { - ids.push(Value::Id(id)); - } - MaybeReference::Value(AnyId::Blob(blob_id)) => { - ids.push(Value::BlobId(blob_id)); - } - MaybeReference::Reference(id_ref) => { - if let Some(id) = self.get_id(&id_ref) { - ids.push(id); - } else { - return Err(SetError::not_found().with_description(format!( - "Id reference {id_ref:?} not found." - ))); - } - } - } - } - Ok(MaybePatchValue::Value(Value::List(ids))) - } - _ => unreachable!(), - } - } -} - -impl EvalObjectReferences for SetResponse { - fn get_id(&self, id_ref: &str) -> Option { - self.created - .get(id_ref) - .and_then(|obj| obj.0.get(&Property::Id)) - .and_then(|value| match value { - Value::Id(id) => Value::Id(*id).into(), - Value::BlobId(blob_id) => Value::BlobId(blob_id.clone()).into(), - _ => None, - }) - } -} - -impl EvalObjectReferences for CopyResponse { - fn get_id(&self, _id_ref: &str) -> Option { - None - } -} - -impl EvalResult { - pub fn unwrap_ids(self, rr: &ResultReference) -> trc::Result> { - if let EvalResult::Values(values) = self { - let mut ids = Vec::with_capacity(values.len()); - for value in values { - match value { - Value::Id(id) => ids.push(id), - Value::List(list) => { - for value in list { - match value { - Value::Id(id) => ids.push(id), - _ => { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!( - "Failed to evaluate {rr} result reference." - ))); - } - } - } - } - _ => { - return Err(trc::JmapEvent::InvalidResultReference.into_err().details( - format_compact!("Failed to evaluate {rr} result reference."), - )); - } - } - } - Ok(ids) - } else { - Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!("Failed to evaluate {rr} result reference."))) - } - } - - pub fn unwrap_any_ids( - self, - rr: &ResultReference, - ) -> trc::Result>> { - if let EvalResult::Values(values) = self { - let mut ids = Vec::with_capacity(values.len()); - for value in values { - match value { - Value::Id(id) => ids.push(MaybeReference::Value(id.into())), - Value::BlobId(blob_id) => ids.push(MaybeReference::Value(blob_id.into())), - Value::List(list) => { - for value in list { - match value { - Value::Id(id) => ids.push(MaybeReference::Value(id.into())), - Value::BlobId(blob_id) => { - ids.push(MaybeReference::Value(blob_id.into())) - } - _ => { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!( - "Failed to evaluate {rr} result reference." - ))); - } - } - } - } - _ => { - return Err(trc::JmapEvent::InvalidResultReference.into_err().details( - format_compact!("Failed to evaluate {rr} result reference."), - )); - } - } - } - Ok(ids) - } else { - Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!("Failed to evaluate {rr} result reference."))) - } - } - - pub fn unwrap_properties(self, rr: &ResultReference) -> trc::Result> { - if let EvalResult::Properties(properties) = self { - Ok(properties) - } else { - Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!("Failed to evaluate {rr} result reference."))) - } - } -} - -#[cfg(test)] -mod tests { - use types::id::Id; - - use crate::{ - request::{Request, RequestMethod}, - response::Response, - }; - use std::collections::HashMap; - - #[test] - fn eval_references() { - let request = Request::parse( - br##"{ - "using": [ - "urn:ietf:params:jmap:core", - "urn:ietf:params:jmap:mail" - ], - "methodCalls": [ - [ - "Mailbox/set", - { - "accountId": "b", - "create": { - "a": { - "name": "Folder a", - "parentId": "#b" - }, - "b": { - "name": "Folder b", - "parentId": "#c" - }, - "c": { - "name": "Folder c", - "parentId": "#d" - }, - "d": { - "name": "Folder d", - "parentId": "#e" - }, - "e": { - "name": "Folder e", - "parentId": "#f" - }, - "f": { - "name": "Folder f", - "parentId": "#g" - }, - "g": { - "name": "Folder g", - "parentId": null - } - } - }, - "fulltree" - ], - [ - "Mailbox/set", - { - "accountId": "b", - "create": { - "a1": { - "name": "Folder a1", - "parentId": null - }, - "b2": { - "name": "Folder b2", - "parentId": "#a1" - }, - "c3": { - "name": "Folder c3", - "parentId": "#a1" - }, - "d4": { - "name": "Folder d4", - "parentId": "#b2" - }, - "e5": { - "name": "Folder e5", - "parentId": "#b2" - }, - "f6": { - "name": "Folder f6", - "parentId": "#d4" - }, - "g7": { - "name": "Folder g7", - "parentId": "#e5" - } - } - }, - "fulltree2" - ], - [ - "Mailbox/set", - { - "accountId": "b", - "create": { - "z": { - "name": "Folder Z", - "parentId": "#x" - }, - "y": { - "name": null - }, - "x": { - "name": "Folder X" - } - } - }, - "xyz" - ], - [ - "Mailbox/set", - { - "accountId": "b", - "create": { - "a": { - "name": "Folder a", - "parentId": "#b" - }, - "b": { - "name": "Folder b", - "parentId": "#c" - }, - "c": { - "name": "Folder c", - "parentId": "#d" - }, - "d": { - "name": "Folder d", - "parentId": "#a" - } - } - }, - "circular" - ] - ] - }"##, - 100, - 1024 * 1024, - ) - .unwrap(); - - let response = Response::new( - 1234, - request.created_ids.unwrap_or_default(), - request.method_calls.len(), - ); - - for (test_num, mut call) in request.method_calls.into_iter().enumerate() { - match response.resolve_references(&mut call.method) { - Ok(_) => assert!( - (0..3).contains(&test_num), - "Unexpected invocation {}", - test_num - ), - Err(err) => { - assert_eq!(test_num, 3); - assert!( - err.matches(trc::EventType::Jmap(trc::JmapEvent::InvalidArguments)), - "{:?}", - err - ); - continue; - } - } - - if let RequestMethod::Set(request) = call.method { - if test_num == 0 { - assert_eq!( - request - .create - .unwrap() - .into_iter() - .map(|b| b.0) - .collect::>(), - ["g", "f", "e", "d", "c", "b", "a"] - .iter() - .map(|i| i.to_string()) - .collect::>() - ); - } else if test_num == 1 { - let mut pending_ids = vec!["a1", "b2", "d4", "e5", "f6", "c3", "g7"]; - - for (id, _) in request.create.as_ref().unwrap() { - match id.as_str() { - "a1" => (), - "b2" | "c3" => assert!(!pending_ids.contains(&"a1")), - "d4" | "e5" => assert!(!pending_ids.contains(&"b2")), - "f6" => assert!(!pending_ids.contains(&"d4")), - "g7" => assert!(!pending_ids.contains(&"e5")), - _ => panic!("Unexpected ID"), - } - pending_ids.retain(|i| i != id); - } - - if !pending_ids.is_empty() { - panic!( - "Unexpected order: {:?}", - request - .create - .as_ref() - .unwrap() - .iter() - .map(|b| b.0.to_string()) - .collect::>() - ); - } - } else if test_num == 2 { - assert_eq!( - request - .create - .unwrap() - .into_iter() - .map(|b| b.0) - .collect::>(), - ["x", "z", "y"] - .iter() - .map(|i| i.to_string()) - .collect::>() - ); - } - } else { - panic!("Expected Set Mailbox Request"); - } - } - - let request = Request::parse( - br##"{ - "using": [ - "urn:ietf:params:jmap:core", - "urn:ietf:params:jmap:mail" - ], - "methodCalls": [ - [ - "Mailbox/set", - { - "accountId": "b", - "create": { - "a": { - "name": "a", - "parentId": "#x" - }, - "b": { - "name": "b", - "parentId": "#y" - }, - "c": { - "name": "c", - "parentId": "#z" - } - } - }, - "ref1" - ], - [ - "Mailbox/set", - { - "accountId": "b", - "create": { - "a1": { - "name": "a1", - "parentId": "#a" - }, - "b2": { - "name": "b2", - "parentId": "#b" - }, - "c3": { - "name": "c3", - "parentId": "#c" - } - } - }, - "red2" - ] - ], - "createdIds": { - "x": "b", - "y": "c", - "z": "d" - } - }"##, - 1024, - 1024 * 1024, - ) - .unwrap(); - - let mut response = Response::new( - 1234, - request.created_ids.unwrap_or_default(), - request.method_calls.len(), - ); - - let mut invocations = request.method_calls.into_iter(); - let mut call = invocations.next().unwrap(); - response.resolve_references(&mut call.method).unwrap(); - - if let RequestMethod::Set(request) = call.method { - let create = request - .create - .unwrap() - .into_iter() - .map(|(p, mut v)| (p, v.0.remove(&Property::ParentId).unwrap())) - .collect::>(); - assert_eq!( - create.get("a").unwrap(), - &SetValue::Value(Value::Id(Id::new(1))) - ); - assert_eq!( - create.get("b").unwrap(), - &SetValue::Value(Value::Id(Id::new(2))) - ); - assert_eq!( - create.get("c").unwrap(), - &SetValue::Value(Value::Id(Id::new(3))) - ); - } else { - panic!("Expected Mailbox Set Request"); - } - - response - .created_ids - .insert("a".to_string(), Id::new(5).into()); - response - .created_ids - .insert("b".to_string(), Id::new(6).into()); - response - .created_ids - .insert("c".to_string(), Id::new(7).into()); - - let mut call = invocations.next().unwrap(); - response.resolve_references(&mut call.method).unwrap(); - - if let RequestMethod::Set(request) = call.method { - let create = request - .create - .unwrap() - .into_iter() - .map(|(p, mut v)| (p, v.0.remove(&Property::ParentId).unwrap())) - .collect::>(); - assert_eq!( - create.get("a1").unwrap(), - &SetValue::Value(Value::Id(Id::new(5))) - ); - assert_eq!( - create.get("b2").unwrap(), - &SetValue::Value(Value::Id(Id::new(6))) - ); - assert_eq!( - create.get("c3").unwrap(), - &SetValue::Value(Value::Id(Id::new(7))) - ); - } else { - panic!("Expected Mailbox Set Request"); - } - } -} - -*/