mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-10-30 00:15:29 +00:00
JMAP protocol layer refactoring (part 4) (fixes #1507)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<T: JmapObject> {
|
||||
|
||||
#[serde(rename = "updatedProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_properties: Option<Vec<T::Property>>,
|
||||
pub updated_properties: Option<Vec<PropertyWrapper<T::Property>>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ChangesRequest {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<K, V>(map: &AHashMap<K, V>) -> bool {
|
||||
map.is_empty()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
#[repr(transparent)]
|
||||
pub struct PropertyWrapper<T>(pub T)
|
||||
where
|
||||
T: serde::Serialize + Property;
|
||||
|
||||
impl<T: Property + serde::Serialize> From<T> for PropertyWrapper<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T: JmapObject> SetResponse<T> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BlobId> for BlobValue {
|
||||
BlobValue::BlobId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for BlobValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
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<AnyId> for BlobValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::BlobId(id) => Ok(BlobValue::BlobId(id)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for EmailValue {
|
||||
EmailValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for EmailValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let EmailValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
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<AnyId> for EmailValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(EmailValue::Id(id)),
|
||||
AnyId::BlobId(id) => Ok(EmailValue::BlobId(id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
JmapObject, MaybeReference,
|
||||
AnyId, JmapObject, JmapObjectId, MaybeReference,
|
||||
email::{EmailProperty, EmailValue},
|
||||
parse_ref,
|
||||
},
|
||||
@@ -429,3 +429,39 @@ impl From<Id> for EmailSubmissionValue {
|
||||
EmailSubmissionValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for EmailSubmissionValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
EmailSubmissionValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
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<AnyId> for EmailSubmissionValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(EmailSubmissionValue::Id(id)),
|
||||
AnyId::BlobId(blob_id) => Ok(EmailSubmissionValue::BlobId(blob_id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for IdentityValue {
|
||||
IdentityValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for IdentityValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
IdentityValue::Id(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
IdentityValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for IdentityValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(IdentityValue::Id(id)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for MailboxValue {
|
||||
MailboxValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for MailboxValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let MailboxValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
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<AnyId> for MailboxValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::Id(id) = value {
|
||||
Ok(MailboxValue::Id(id))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Property = Self::Property> + From<Self::Id>;
|
||||
type Id: FromStr + Serialize;
|
||||
pub trait JmapObject: std::fmt::Debug {
|
||||
type Property: Property + FromStr + Serialize + Debug;
|
||||
type Element: Element<Property = Self::Property> + From<Self::Id> + JmapObjectId + Debug;
|
||||
type Id: FromStr + TryFrom<AnyId> + 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<AnyId> {
|
||||
fn as_id(&self) -> Option<Id>;
|
||||
fn as_any_id(&self) -> Option<AnyId>;
|
||||
fn as_id_ref(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum MaybeReference<T: FromStr> {
|
||||
Value(T),
|
||||
@@ -54,3 +68,58 @@ fn parse_ref<T: FromStr>(value: &str) -> MaybeReference<T> {
|
||||
.unwrap_or(MaybeReference::ParseError)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for AnyId {
|
||||
fn from(value: Id) -> Self {
|
||||
AnyId::Id(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobId> for AnyId {
|
||||
fn from(value: BlobId) -> Self {
|
||||
AnyId::BlobId(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::Id(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for BlobId {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::BlobId(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for AnyId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for PrincipalValue {
|
||||
PrincipalValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for PrincipalValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let PrincipalValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let PrincipalValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for PrincipalValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::Id(id) = value {
|
||||
Ok(PrincipalValue::Id(id))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for PushSubscriptionValue {
|
||||
PushSubscriptionValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for PushSubscriptionValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
PushSubscriptionValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
PushSubscriptionValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for PushSubscriptionValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(PushSubscriptionValue::Id(id)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for QuotaValue {
|
||||
QuotaValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for QuotaValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let QuotaValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
self.as_id().map(AnyId::Id)
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for QuotaValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::Id(id) = value {
|
||||
Ok(QuotaValue::Id(id))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for SieveValue {
|
||||
SieveValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for SieveValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
SieveValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
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<AnyId> for SieveValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(SieveValue::Id(id)),
|
||||
AnyId::BlobId(id) => Ok(SieveValue::BlobId(id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for ThreadValue {
|
||||
ThreadValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ThreadValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
ThreadValue::Id(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
self.as_id().map(AnyId::Id)
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for ThreadValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(ThreadValue::Id(id)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Id> for VacationResponseValue {
|
||||
VacationResponseValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for VacationResponseValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
VacationResponseValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
VacationResponseValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for VacationResponseValue {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AnyId::Id(id) => Ok(VacationResponseValue::Id(id)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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<EvalResults> {
|
||||
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<Id> {
|
||||
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<Property = P> + JmapObjectId + TryFrom<AnyId>,
|
||||
{
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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<EvalResult>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EvalResult {
|
||||
Id(AnyId),
|
||||
Property(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl<T> ResponsePtr for Vec<T>
|
||||
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<Property = P> + 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<T: Property + serde::Serialize> ResponsePtr for PropertyWrapper<T> {
|
||||
fn eval_jptr(&self, _: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
results.0.push(EvalResult::Property(self.0.to_cow()));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> ResponsePtr for GetResponse<T> {
|
||||
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<T: JmapObject> ResponsePtr for ChangesResponse<T> {
|
||||
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<T: TryFrom<AnyId>>(
|
||||
self,
|
||||
rr: &ResultReference,
|
||||
) -> impl Iterator<Item = trc::Result<T>> {
|
||||
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<T: Property + FromStr>(
|
||||
self,
|
||||
rr: &ResultReference,
|
||||
) -> impl Iterator<Item = trc::Result<T>> {
|
||||
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."
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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<String, Vec<String>>,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
fn topological_sort<T>(
|
||||
create: &mut VecMap<String, T>,
|
||||
graph: HashMap<String, Vec<String>>,
|
||||
) -> trc::Result<VecMap<String, T>> {
|
||||
// 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::<Vec<_>>();
|
||||
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::<Vec<_>>(),
|
||||
["g", "f", "e", "d", "c", "b", "a"]
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
} 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::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
} else if test_num == 2 {
|
||||
assert_eq!(
|
||||
request
|
||||
.create
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|b| b.0)
|
||||
.collect::<Vec<_>>(),
|
||||
["x", "z", "y"]
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
} 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::<HashMap<_, _>>();
|
||||
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::<HashMap<_, _>>();
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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<P, E>
|
||||
where
|
||||
P: Property,
|
||||
E: Element<Property = P> + JmapObjectId,
|
||||
{
|
||||
fn get_created_id(&self, id_ref: &str) -> Option<AnyId>;
|
||||
|
||||
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<T: JmapObject> ResolveReference for GetRequest<T> {
|
||||
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::<T::Id>(reference)
|
||||
.map(|f| f.map(MaybeIdReference::Id))
|
||||
.collect::<Result<_, _>>()?,
|
||||
));
|
||||
}
|
||||
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::<T::Property>(reference)
|
||||
.map(|f| f.map(MaybeInvalid::Value))
|
||||
.collect::<Result<_, _>>()?,
|
||||
));
|
||||
}
|
||||
|
||||
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::<Id>(reference)
|
||||
.map(|f| f.map(MaybeInvalid::Value))
|
||||
.collect::<Result<_, _>>()?,
|
||||
));
|
||||
}
|
||||
|
||||
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::<Id>(reference)
|
||||
.map(|f| f.map(MaybeIdReference::Id))
|
||||
.collect::<Result<_, _>>()?,
|
||||
);
|
||||
}
|
||||
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::<Id>(reference)
|
||||
.map(|f| f.map(MaybeInvalid::Value))
|
||||
.collect::<Result<_, _>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
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<T> ResolveCreatedReference<T::Property, T::Element> for SetResponse<T>
|
||||
where
|
||||
T: JmapObject,
|
||||
{
|
||||
fn get_created_id(&self, id_ref: &str) -> Option<AnyId> {
|
||||
self.created
|
||||
.get(id_ref)
|
||||
.and_then(|v| v.as_element())
|
||||
.and_then(|v| v.as_any_id())
|
||||
}
|
||||
}
|
||||
@@ -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<Call<RequestMethod<'x>>>,
|
||||
pub created_ids: Option<HashMap<String, String>>,
|
||||
pub created_ids: Option<HashMap<String, AnyId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -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<String, String>,
|
||||
created_ids: HashMap<String, AnyId>,
|
||||
|
||||
#[serde(rename = "requestId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -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<String, String>,
|
||||
pub created_ids: HashMap<String, AnyId>,
|
||||
}
|
||||
|
||||
impl<'x> Response<'x> {
|
||||
pub fn new(session_state: u32, created_ids: HashMap<String, String>, capacity: usize) -> Self {
|
||||
pub fn new(session_state: u32, created_ids: HashMap<String, AnyId>, 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<String>) {
|
||||
pub fn push_created_id(&mut self, create_id: String, id: impl Into<AnyId>) {
|
||||
self.created_ids.insert(create_id, id.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,886 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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<Property>),
|
||||
Values(Vec<Value>),
|
||||
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<Id> {
|
||||
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<String, Vec<String>>)>,
|
||||
) -> 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<T>(
|
||||
create: &mut VecMap<String, T>,
|
||||
graph: HashMap<String, Vec<String>>,
|
||||
) -> trc::Result<VecMap<String, T>> {
|
||||
// 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::<Vec<_>>();
|
||||
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<Value>;
|
||||
|
||||
fn eval_object_references(&self, set_value: SetValue) -> Result<MaybePatchValue, SetError<P>> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub fn unwrap_ids(self, rr: &ResultReference) -> trc::Result<Vec<Id>> {
|
||||
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<Vec<MaybeReference<AnyId, String>>> {
|
||||
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<Vec<Property>> {
|
||||
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::<Vec<_>>(),
|
||||
["g", "f", "e", "d", "c", "b", "a"]
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
} 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::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
} else if test_num == 2 {
|
||||
assert_eq!(
|
||||
request
|
||||
.create
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|b| b.0)
|
||||
.collect::<Vec<_>>(),
|
||||
["x", "z", "y"]
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
} 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::<HashMap<_, _>>();
|
||||
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::<HashMap<_, _>>();
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user