Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{changes::state::JmapCacheState, contact::set::ContactCardSet};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::{
|
||||
copy::{CopyRequest, CopyResponse},
|
||||
set::SetRequest,
|
||||
},
|
||||
object::contact,
|
||||
request::{
|
||||
Call, IntoValid, MaybeInvalid, RequestMethod, SetRequestMethod,
|
||||
method::{MethodFunction, MethodName, MethodObject},
|
||||
reference::MaybeResultReference,
|
||||
},
|
||||
types::state::State,
|
||||
};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait JmapContactCardCopy: Sync + Send {
|
||||
fn contact_card_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<CopyResponse<contact::ContactCard>>> + Send;
|
||||
}
|
||||
|
||||
impl JmapContactCardCopy for Server {
|
||||
async fn contact_card_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<CopyResponse<contact::ContactCard>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let from_account_id = request.from_account_id.document_id();
|
||||
let account = self.account(account_id).await.caused_by(trc::location!())?;
|
||||
|
||||
if account_id == from_account_id {
|
||||
return Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("From accountId is equal to fromAccountId"));
|
||||
}
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let old_state = cache.assert_state(false, &request.if_in_state)?;
|
||||
let mut response = CopyResponse {
|
||||
from_account_id: request.from_account_id,
|
||||
account_id: request.account_id,
|
||||
new_state: old_state.clone(),
|
||||
old_state,
|
||||
created: VecMap::with_capacity(request.create.len()),
|
||||
not_created: VecMap::new(),
|
||||
};
|
||||
|
||||
let from_cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
from_account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let from_contact_ids = if access_token.is_member(from_account_id) {
|
||||
from_cache.document_ids(false).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
from_cache.shared_items(access_token, [Acl::ReadItems], true)
|
||||
};
|
||||
|
||||
let can_add_address_books = if access_token.is_shared(account_id) {
|
||||
cache
|
||||
.shared_containers(access_token, [Acl::AddItems], true)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let on_success_delete = request.on_success_destroy_original.unwrap_or(false);
|
||||
let mut destroy_ids = Vec::new();
|
||||
|
||||
// Obtain quota
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
'create: for (id, create) in request.create.into_valid() {
|
||||
let from_contact_id = id.document_id();
|
||||
if !from_contact_ids.contains(from_contact_id) {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(_contact) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::ContactCard,
|
||||
from_contact_id,
|
||||
))
|
||||
.await?
|
||||
else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let contact = _contact
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
match self
|
||||
.create_contact_card(
|
||||
&cache,
|
||||
&mut batch,
|
||||
access_token,
|
||||
&account,
|
||||
account_id,
|
||||
&can_add_address_books,
|
||||
contact.card.into_jscontact(),
|
||||
create,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(document_id) => {
|
||||
response.created(id, document_id);
|
||||
|
||||
// Add to destroy list
|
||||
if on_success_delete {
|
||||
destroy_ids.push(MaybeInvalid::Value(id));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
let change_id = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
response.new_state = State::Exact(change_id);
|
||||
}
|
||||
|
||||
// Destroy ids
|
||||
if on_success_delete && !destroy_ids.is_empty() {
|
||||
*next_call = Call {
|
||||
id: String::new(),
|
||||
name: MethodName::new(MethodObject::ContactCard, MethodFunction::Set),
|
||||
method: RequestMethod::Set(SetRequestMethod::ContactCard(Box::new(SetRequest {
|
||||
account_id: request.from_account_id,
|
||||
if_in_state: request.destroy_from_if_in_state,
|
||||
create: None,
|
||||
update: None,
|
||||
destroy: MaybeResultReference::Value(destroy_ids).into(),
|
||||
arguments: Default::default(),
|
||||
}))),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::JmapCacheState;
|
||||
use calcard::jscontact::{JSContactProperty, JSContactValue, import::ConversionOptions};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::contact,
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
blob::BlobId,
|
||||
collection::{Collection, SyncCollection},
|
||||
id::Id,
|
||||
};
|
||||
|
||||
pub trait ContactCardGet: Sync + Send {
|
||||
fn contact_card_get(
|
||||
&self,
|
||||
request: GetRequest<contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<contact::ContactCard>>> + Send;
|
||||
}
|
||||
|
||||
impl ContactCardGet for Server {
|
||||
async fn contact_card_get(
|
||||
&self,
|
||||
mut request: GetRequest<contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<contact::ContactCard>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let return_all_properties = request.properties.is_none();
|
||||
let properties =
|
||||
request.unwrap_properties(&[JSContactProperty::Id, JSContactProperty::AddressBookIds]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await?;
|
||||
let contact_ids = if access_token.is_member(account_id) {
|
||||
cache.document_ids(false).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
cache.shared_items(access_token, [Acl::ReadItems], true)
|
||||
};
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
contact_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: cache.get_state(false).into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
let mut return_id = return_all_properties;
|
||||
let mut return_address_book_ids = return_all_properties;
|
||||
let mut return_converted_props = !return_all_properties;
|
||||
|
||||
if !return_all_properties {
|
||||
for property in &properties {
|
||||
match property {
|
||||
JSContactProperty::Id => {
|
||||
return_id = true;
|
||||
}
|
||||
JSContactProperty::AddressBookIds => {
|
||||
return_address_book_ids = true;
|
||||
}
|
||||
JSContactProperty::VCard => {
|
||||
return_converted_props = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
// Obtain the contact object
|
||||
let document_id = id.document_id();
|
||||
if !contact_ids.contains(document_id) {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
let _contact = if let Some(contact) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
contact
|
||||
} else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
|
||||
let contact = _contact
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let jscontact = contact
|
||||
.card
|
||||
.into_jscontact_with_options::<Id, BlobId>(
|
||||
ConversionOptions::default().include_vcard_parameters(return_converted_props),
|
||||
)
|
||||
.into_inner();
|
||||
let mut result = if return_all_properties {
|
||||
jscontact.into_object().unwrap()
|
||||
} else {
|
||||
Map::from_iter(
|
||||
jscontact
|
||||
.into_expanded_object()
|
||||
.filter(|(k, _)| k.as_property().is_some_and(|p| properties.contains(p))),
|
||||
)
|
||||
};
|
||||
|
||||
if return_id {
|
||||
result.insert_unchecked(
|
||||
JSContactProperty::Id,
|
||||
Value::Element(JSContactValue::Id(id)),
|
||||
);
|
||||
}
|
||||
|
||||
if return_address_book_ids {
|
||||
let mut obj = Map::with_capacity(contact.names.len());
|
||||
for id in contact.names.iter() {
|
||||
obj.insert_unchecked(JSContactProperty::IdValue(Id::from(id.parent_id)), true);
|
||||
}
|
||||
result.insert_unchecked(JSContactProperty::AddressBookIds, Value::Object(obj));
|
||||
}
|
||||
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use calcard::jscontact::JSContactProperty;
|
||||
use common::{DavName, DavResources, Server};
|
||||
use jmap_proto::error::set::SetError;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::ContactField, id::Id};
|
||||
|
||||
pub mod copy;
|
||||
pub mod get;
|
||||
pub mod parse;
|
||||
pub mod query;
|
||||
pub mod set;
|
||||
|
||||
pub(super) async fn assert_is_unique_uid(
|
||||
server: &Server,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
addressbook_ids: &[DavName],
|
||||
uid: Option<&str>,
|
||||
) -> trc::Result<Result<(), SetError<JSContactProperty<Id>>>> {
|
||||
if let Some(uid) = uid {
|
||||
let hits = server
|
||||
.document_ids_matching(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
ContactField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !hits.is_empty() {
|
||||
for document_id in resources
|
||||
.paths
|
||||
.iter()
|
||||
.filter(move |item| {
|
||||
item.parent_id
|
||||
.is_some_and(|id| addressbook_ids.iter().any(|ab| ab.parent_id == id))
|
||||
})
|
||||
.map(|path| resources.resources[path.resource_idx].document_id)
|
||||
{
|
||||
if hits.contains(document_id) {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::Uid)
|
||||
.with_description(format!(
|
||||
"Contact with UID {uid} already exists with id {}.",
|
||||
Id::from(document_id)
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Ok(()))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::blob::download::BlobDownload;
|
||||
use calcard::vcard::VCard;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use jmap_proto::{
|
||||
method::parse::{ParseRequest, ParseResponse},
|
||||
object::contact::ContactCard,
|
||||
request::{IntoValid, MaybeInvalid},
|
||||
};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait ContactCardParse: Sync + Send {
|
||||
fn contact_card_parse(
|
||||
&self,
|
||||
request: ParseRequest<ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<ParseResponse<ContactCard>>> + Send;
|
||||
}
|
||||
|
||||
impl ContactCardParse for Server {
|
||||
async fn contact_card_parse(
|
||||
&self,
|
||||
request: ParseRequest<ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<ParseResponse<ContactCard>> {
|
||||
if request.blob_ids.len() > self.core.jmap.contact_parse_max_items {
|
||||
return Err(trc::JmapEvent::RequestTooLarge.into_err());
|
||||
}
|
||||
let return_all_properties = request.properties.is_none();
|
||||
let properties = request
|
||||
.properties
|
||||
.map(|v| v.into_valid().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut response = ParseResponse {
|
||||
account_id: request.account_id,
|
||||
parsed: VecMap::with_capacity(request.blob_ids.len()),
|
||||
not_parsable: vec![],
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for blob_id in request.blob_ids.into_valid() {
|
||||
// Fetch raw message to parse
|
||||
let raw_vcard = match self.blob_download(&blob_id, access_token).await? {
|
||||
Some(raw_vcard) => raw_vcard,
|
||||
None => {
|
||||
response.not_found.push(MaybeInvalid::Value(blob_id));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let Ok(vcard) = VCard::parse(std::str::from_utf8(&raw_vcard).unwrap_or_default())
|
||||
else {
|
||||
response.not_parsable.push(blob_id);
|
||||
continue;
|
||||
};
|
||||
let mut js_contact = vcard.into_jscontact::<Id, BlobId>();
|
||||
|
||||
if !return_all_properties {
|
||||
js_contact
|
||||
.0
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.as_mut_vec()
|
||||
.retain(|(k, _)| k.as_property().is_some_and(|k| properties.contains(k)));
|
||||
}
|
||||
|
||||
response.parsed.append(blob_id, js_contact.into_inner());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::{
|
||||
addressbook::AddressBook,
|
||||
contact::{ContactCard, ContactCardComparator, ContactCardFilter},
|
||||
},
|
||||
request::MaybeInvalid,
|
||||
types::state::State,
|
||||
};
|
||||
use store::{
|
||||
IterateParams, U32_LEN, U64_LEN, ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
search::{ContactSearchField, SearchComparator, SearchFilter, SearchQuery},
|
||||
write::{IndexPropertyClass, SearchIndex, ValueClass, key::DeserializeBigEndian},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::ContactField,
|
||||
};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub trait ContactCardQuery: Sync + Send {
|
||||
fn contact_card_query(
|
||||
&self,
|
||||
request: QueryRequest<ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
|
||||
fn address_book_query(
|
||||
&self,
|
||||
request: QueryRequest<AddressBook>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CreatedUpdated {
|
||||
document_id: u32,
|
||||
created: u64,
|
||||
updated: u64,
|
||||
}
|
||||
|
||||
impl ContactCardQuery for Server {
|
||||
async fn contact_card_query(
|
||||
&self,
|
||||
mut request: QueryRequest<ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await?;
|
||||
let mut created_to_updated = Vec::new();
|
||||
|
||||
if request.filter.iter().any(|cond| {
|
||||
matches!(
|
||||
cond,
|
||||
Filter::Property(
|
||||
ContactCardFilter::CreatedBefore(_)
|
||||
| ContactCardFilter::CreatedAfter(_)
|
||||
| ContactCardFilter::UpdatedBefore(_)
|
||||
| ContactCardFilter::UpdatedAfter(_)
|
||||
)
|
||||
)
|
||||
}) || request.sort.as_ref().is_some_and(|v| {
|
||||
v.iter().any(|sort| {
|
||||
matches!(
|
||||
sort.property,
|
||||
ContactCardComparator::Created | ContactCardComparator::Updated
|
||||
)
|
||||
})
|
||||
}) {
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::ContactCard.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: ContactField::CreatedToUpdated.into(),
|
||||
value: 0,
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::ContactCard.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: ContactField::CreatedToUpdated.into(),
|
||||
value: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.ascending(),
|
||||
|key, value| {
|
||||
created_to_updated.push(CreatedUpdated {
|
||||
document_id: key.deserialize_be_u32(key.len() - U32_LEN)?,
|
||||
created: key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?,
|
||||
updated: value.deserialize_be_u64(0)?,
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
ContactCardFilter::InAddressBook(MaybeInvalid::Value(id)) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.children_ids(id.document_id()),
|
||||
)))
|
||||
}
|
||||
ContactCardFilter::Name(value)
|
||||
| ContactCardFilter::NameGiven(value)
|
||||
| ContactCardFilter::NameSurname(value)
|
||||
| ContactCardFilter::NameSurname2(value) => {
|
||||
filters.push(SearchFilter::has_keyword(ContactSearchField::Name, value));
|
||||
}
|
||||
ContactCardFilter::Nickname(value) => {
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Nickname,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Organization(value) => {
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Organization,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Phone(value) => {
|
||||
filters.push(SearchFilter::has_keyword(ContactSearchField::Phone, value));
|
||||
}
|
||||
ContactCardFilter::OnlineService(value) => {
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::OnlineService,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Address(value) => {
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Address,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Note(value) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
ContactSearchField::Note,
|
||||
value,
|
||||
self.core.email.default_language,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::HasMember(value) => {
|
||||
filters.push(SearchFilter::has_keyword(ContactSearchField::Member, value));
|
||||
}
|
||||
ContactCardFilter::Kind(value) => {
|
||||
filters.push(SearchFilter::eq(ContactSearchField::Kind, value));
|
||||
}
|
||||
ContactCardFilter::Uid(value) => {
|
||||
filters.push(SearchFilter::eq(ContactSearchField::Uid, value))
|
||||
}
|
||||
ContactCardFilter::Email(email) => filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Email,
|
||||
sanitize_email(&email).unwrap_or(email),
|
||||
)),
|
||||
ContactCardFilter::Text(value) => {
|
||||
filters.push(SearchFilter::Or);
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Name,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Nickname,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Organization,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Email,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Phone,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::OnlineService,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_keyword(
|
||||
ContactSearchField::Address,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
ContactSearchField::Note,
|
||||
value,
|
||||
self.core.email.default_language,
|
||||
));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
ContactCardFilter::CreatedBefore(before) => {
|
||||
let before = before.timestamp() as u64;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
created_to_updated
|
||||
.iter()
|
||||
.filter_map(|cu| (cu.created < before).then_some(cu.document_id)),
|
||||
)));
|
||||
}
|
||||
ContactCardFilter::CreatedAfter(after) => {
|
||||
let after = after.timestamp() as u64;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
created_to_updated
|
||||
.iter()
|
||||
.filter_map(|cu| (cu.created > after).then_some(cu.document_id)),
|
||||
)));
|
||||
}
|
||||
ContactCardFilter::UpdatedBefore(before) => {
|
||||
let before = before.timestamp() as u64;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
created_to_updated
|
||||
.iter()
|
||||
.filter_map(|cu| (cu.updated < before).then_some(cu.document_id)),
|
||||
)));
|
||||
}
|
||||
ContactCardFilter::UpdatedAfter(after) => {
|
||||
let after = after.timestamp() as u64;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
created_to_updated
|
||||
.iter()
|
||||
.filter_map(|cu| (cu.updated > after).then_some(cu.document_id)),
|
||||
)));
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let comparators = request
|
||||
.sort
|
||||
.take()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|comparator| match comparator.property {
|
||||
ContactCardComparator::Created => Ok(SearchComparator::sorted_set(
|
||||
created_to_updated
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, u)| (u.document_id, idx as u32))
|
||||
.collect(),
|
||||
comparator.is_ascending,
|
||||
)),
|
||||
ContactCardComparator::Updated => {
|
||||
let mut updated = created_to_updated.clone();
|
||||
updated.sort_by_key(|a| a.updated);
|
||||
Ok(SearchComparator::sorted_set(
|
||||
updated
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, u)| (u.document_id, idx as u32))
|
||||
.collect(),
|
||||
comparator.is_ascending,
|
||||
))
|
||||
}
|
||||
other => Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(other.into_string())),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let results = self
|
||||
.search_store()
|
||||
.query_account(
|
||||
SearchQuery::new(SearchIndex::Contacts)
|
||||
.with_filters(filters)
|
||||
.with_comparators(comparators)
|
||||
.with_account_id(account_id)
|
||||
.with_mask(if access_token.is_shared(account_id) {
|
||||
cache.shared_items(access_token, [Acl::ReadItems], true)
|
||||
} else {
|
||||
cache.document_ids(false).collect()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
|
||||
for document_id in results {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
|
||||
async fn address_book_query(
|
||||
&self,
|
||||
request: QueryRequest<AddressBook>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let results = cache.document_ids(true).collect::<Vec<_>>();
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len() as usize,
|
||||
self.core.jmap.query_max_results,
|
||||
State::Initial,
|
||||
&request,
|
||||
);
|
||||
|
||||
for document_id in results {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::JmapCacheState;
|
||||
use crate::contact::assert_is_unique_uid;
|
||||
use calcard::jscontact::{JSContact, JSContactProperty, JSContactValue};
|
||||
use common::{
|
||||
DavName, DavResources, Server,
|
||||
auth::{AccessToken, AccountCache},
|
||||
};
|
||||
use groupware::{DestroyArchive, cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::contact,
|
||||
request::MaybeInvalid,
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Value};
|
||||
use store::{
|
||||
ValueKey,
|
||||
ahash::AHashSet,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
blob::BlobId,
|
||||
collection::{Collection, SyncCollection, VanishedCollection},
|
||||
id::Id,
|
||||
};
|
||||
|
||||
pub trait ContactCardSet: Sync + Send {
|
||||
fn contact_card_set(
|
||||
&self,
|
||||
request: SetRequest<'_, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<contact::ContactCard>>> + Send;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_contact_card(
|
||||
&self,
|
||||
cache: &DavResources,
|
||||
batch: &mut BatchBuilder,
|
||||
access_token: &AccessToken,
|
||||
account: &AccountCache,
|
||||
account_id: u32,
|
||||
can_add_address_books: &Option<RoaringBitmap>,
|
||||
js_contact: JSContact<'_, Id, BlobId>,
|
||||
updates: Value<'_, JSContactProperty<Id>, JSContactValue<Id, BlobId>>,
|
||||
) -> impl Future<Output = trc::Result<Result<u32, SetError<JSContactProperty<Id>>>>>;
|
||||
}
|
||||
|
||||
impl ContactCardSet for Server {
|
||||
async fn contact_card_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<contact::ContactCard>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let account = self.account(account_id).await.caused_by(trc::location!())?;
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::AddressBook,
|
||||
)
|
||||
.await?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
|
||||
.with_state(cache.assert_state(false, &request.if_in_state)?);
|
||||
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
||||
|
||||
// Obtain addressBookIds
|
||||
let (can_add_address_books, can_delete_address_books, can_modify_address_books) =
|
||||
if access_token.is_shared(account_id) {
|
||||
(
|
||||
cache
|
||||
.shared_containers(access_token, [Acl::AddItems], true)
|
||||
.into(),
|
||||
cache
|
||||
.shared_containers(access_token, [Acl::RemoveItems], true)
|
||||
.into(),
|
||||
cache
|
||||
.shared_containers(access_token, [Acl::ModifyItems], true)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
// Process creates
|
||||
let mut batch = BatchBuilder::new();
|
||||
'create: for (id, object) in request.unwrap_create() {
|
||||
match self
|
||||
.create_contact_card(
|
||||
&cache,
|
||||
&mut batch,
|
||||
access_token,
|
||||
&account,
|
||||
account_id,
|
||||
&can_add_address_books,
|
||||
JSContact::default(),
|
||||
object,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(document_id) => {
|
||||
response.created(id, document_id);
|
||||
}
|
||||
Err(err) => {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update() {
|
||||
let id = match id {
|
||||
MaybeInvalid::Value(id) => id,
|
||||
invalid => {
|
||||
response.not_updated.append(invalid, SetError::not_found());
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
response.not_updated.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Obtain contact card
|
||||
let document_id = id.document_id();
|
||||
let contact_card_ = if let Some(contact_card_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
contact_card_
|
||||
} else {
|
||||
response.not_updated.append(id, SetError::not_found());
|
||||
continue 'update;
|
||||
};
|
||||
let contact_card = contact_card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_contact_card = contact_card
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut js_contact = new_contact_card.card.into_jscontact();
|
||||
|
||||
// Process changes
|
||||
if let Err(err) = update_contact_card(
|
||||
Some(id),
|
||||
object,
|
||||
&mut new_contact_card.names,
|
||||
&mut js_contact,
|
||||
) {
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Convert JSContact to vCard
|
||||
if let Some(vcard) = js_contact.into_vcard() {
|
||||
new_contact_card.size = vcard.size() as u32;
|
||||
new_contact_card.card = vcard;
|
||||
} else {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_description("Failed to convert contact to vCard."),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Validate UID
|
||||
match (new_contact_card.card.uid(), contact_card.inner.card.uid()) {
|
||||
(Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
|
||||
(None, None) | (None, Some(_)) => {}
|
||||
_ => {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::Uid)
|
||||
.with_description("You cannot change the UID of a contact."),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new addressBookIds
|
||||
for addressbook_id in new_contact_card.added_addressbook_ids(contact_card.inner) {
|
||||
if !cache.has_container_id(&addressbook_id) {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::AddressBookIds)
|
||||
.with_description(format!(
|
||||
"addressBookId {} does not exist.",
|
||||
Id::from(addressbook_id)
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
} else if can_add_address_books
|
||||
.as_ref()
|
||||
.is_some_and(|ids| !ids.contains(addressbook_id))
|
||||
{
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to add contacts to address book {}.",
|
||||
Id::from(addressbook_id)
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate deleted addressBookIds
|
||||
if let Some(can_delete_address_books) = &can_delete_address_books {
|
||||
for addressbook_id in new_contact_card.removed_addressbook_ids(contact_card.inner) {
|
||||
if !can_delete_address_books.contains(addressbook_id) {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to remove contacts from address book {}.",
|
||||
Id::from(addressbook_id)
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate changed addressBookIds
|
||||
if let Some(can_modify_address_books) = &can_modify_address_books {
|
||||
for addressbook_id in new_contact_card.unchanged_addressbook_ids(contact_card.inner)
|
||||
{
|
||||
if !can_modify_address_books.contains(addressbook_id) {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to modify address book {}.",
|
||||
Id::from(addressbook_id)
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check size and quota
|
||||
if new_contact_card.size as usize > self.core.groupware.max_vcard_size {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties().with_description(format!(
|
||||
"Contact size {} exceeds the maximum allowed size of {} bytes.",
|
||||
new_contact_card.size, self.core.groupware.max_vcard_size
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
let extra_bytes = (new_contact_card.size as u64)
|
||||
.saturating_sub(u32::from(contact_card.inner.size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
match self.has_available_quota(&account, extra_bytes).await {
|
||||
Ok(_) => {}
|
||||
Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => {
|
||||
response.not_updated.append(id, SetError::over_quota());
|
||||
continue 'update;
|
||||
}
|
||||
Err(err) => return Err(err.caused_by(trc::location!())),
|
||||
}
|
||||
}
|
||||
|
||||
// Update record
|
||||
let vanished_paths = new_contact_card
|
||||
.removed_addressbook_ids(contact_card.inner)
|
||||
.filter_map(|addressbook_id| {
|
||||
cache.format_resource_path_by_parent(document_id, addressbook_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
new_contact_card
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
contact_card,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
for path in vanished_paths {
|
||||
batch.log_vanished_item(VanishedCollection::AddressBook, path);
|
||||
}
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
'destroy: for id in will_destroy {
|
||||
let document_id = id.document_id();
|
||||
|
||||
if !cache.has_item_id(&document_id) {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(contact_card_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
continue;
|
||||
};
|
||||
|
||||
let contact_card = contact_card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACLs
|
||||
if let Some(can_delete_address_books) = &can_delete_address_books {
|
||||
for name in contact_card.inner.names.iter() {
|
||||
let parent_id = name.parent_id.to_native();
|
||||
if !can_delete_address_books.contains(parent_id) {
|
||||
response.not_destroyed.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to remove contacts from address book {}.",
|
||||
Id::from(parent_id)
|
||||
)),
|
||||
);
|
||||
continue 'destroy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete record
|
||||
DestroyArchive(contact_card)
|
||||
.delete_all(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for path in cache.format_resource_paths_by_id(document_id) {
|
||||
batch.log_vanished_item(VanishedCollection::AddressBook, path);
|
||||
}
|
||||
|
||||
response.destroyed.push(id);
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
let change_id = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.notify_task_queue();
|
||||
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn create_contact_card(
|
||||
&self,
|
||||
cache: &DavResources,
|
||||
batch: &mut BatchBuilder,
|
||||
access_token: &AccessToken,
|
||||
account: &AccountCache,
|
||||
account_id: u32,
|
||||
can_add_address_books: &Option<RoaringBitmap>,
|
||||
mut js_contact: JSContact<'_, Id, BlobId>,
|
||||
updates: Value<'_, JSContactProperty<Id>, JSContactValue<Id, BlobId>>,
|
||||
) -> trc::Result<Result<u32, SetError<JSContactProperty<Id>>>> {
|
||||
// Process changes
|
||||
let mut names = Vec::new();
|
||||
if let Err(err) = update_contact_card(None, updates, &mut names, &mut js_contact) {
|
||||
return Ok(Err(err));
|
||||
}
|
||||
|
||||
// Verify that the address book ids valid
|
||||
for name in &names {
|
||||
if !cache.has_container_id(&name.parent_id) {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::AddressBookIds)
|
||||
.with_description(format!(
|
||||
"addressBookId {} does not exist.",
|
||||
Id::from(name.parent_id)
|
||||
))));
|
||||
} else if can_add_address_books
|
||||
.as_ref()
|
||||
.is_some_and(|ids| !ids.contains(name.parent_id))
|
||||
{
|
||||
return Ok(Err(SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to add contacts to address book {}.",
|
||||
Id::from(name.parent_id)
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert JSContact to vCard
|
||||
let Some(card) = js_contact.into_vcard() else {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_description("Failed to convert contact to vCard.")));
|
||||
};
|
||||
|
||||
// Validate UID
|
||||
if let Err(err) = assert_is_unique_uid(self, cache, account_id, &names, card.uid()).await? {
|
||||
return Ok(Err(err));
|
||||
}
|
||||
|
||||
// Check size and quota
|
||||
let size = card.size();
|
||||
if size > self.core.groupware.max_vcard_size {
|
||||
return Ok(Err(SetError::invalid_properties().with_description(
|
||||
format!(
|
||||
"Contact size {} exceeds the maximum allowed size of {} bytes.",
|
||||
size, self.core.groupware.max_vcard_size
|
||||
),
|
||||
)));
|
||||
}
|
||||
match self.has_available_quota(account, size as u64).await {
|
||||
Ok(_) => {}
|
||||
Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => {
|
||||
return Ok(Err(SetError::over_quota()));
|
||||
}
|
||||
Err(err) => return Err(err.caused_by(trc::location!())),
|
||||
}
|
||||
|
||||
// Insert record
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::ContactCard, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
ContactCard {
|
||||
names,
|
||||
size: size as u32,
|
||||
card,
|
||||
..Default::default()
|
||||
}
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
batch,
|
||||
)
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| Ok(document_id))
|
||||
}
|
||||
}
|
||||
|
||||
fn update_contact_card<'x>(
|
||||
expected_id: Option<Id>,
|
||||
updates: Value<'x, JSContactProperty<Id>, JSContactValue<Id, BlobId>>,
|
||||
addressbooks: &mut Vec<DavName>,
|
||||
js_contact: &mut JSContact<'x, Id, BlobId>,
|
||||
) -> Result<(), SetError<JSContactProperty<Id>>> {
|
||||
let mut entries = js_contact.0.as_object_mut().unwrap();
|
||||
|
||||
for (property, value) in updates.into_expanded_object() {
|
||||
let Key::Property(property) = property else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.to_owned())
|
||||
.with_description("Invalid property."));
|
||||
};
|
||||
|
||||
match (property, value) {
|
||||
(JSContactProperty::AddressBookIds, value) => {
|
||||
patch_parent_ids(addressbooks, None, value)?;
|
||||
}
|
||||
(JSContactProperty::Pointer(pointer), value) => {
|
||||
if matches!(
|
||||
pointer.first(),
|
||||
Some(JsonPointerItem::Key(Key::Property(
|
||||
JSContactProperty::AddressBookIds
|
||||
)))
|
||||
) {
|
||||
let mut pointer = pointer.iter();
|
||||
pointer.next();
|
||||
patch_parent_ids(addressbooks, pointer.next(), value)?;
|
||||
} else if !js_contact.0.patch_jptr(pointer.iter(), value) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::Pointer(pointer))
|
||||
.with_description("Patch operation failed."));
|
||||
}
|
||||
entries = js_contact.0.as_object_mut().unwrap();
|
||||
}
|
||||
(JSContactProperty::Media, Value::Object(media)) => {
|
||||
for (_, value) in media.iter() {
|
||||
if value.as_object().is_some_and(|v| {
|
||||
v.keys()
|
||||
.any(|k| matches!(k, Key::Property(JSContactProperty::BlobId)))
|
||||
}) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::Media)
|
||||
.with_description("blobIds in media is not supported."));
|
||||
}
|
||||
}
|
||||
entries.insert(JSContactProperty::Media, Value::Object(media));
|
||||
}
|
||||
(JSContactProperty::Id, value) => {
|
||||
if !expected_id.is_some_and(|expected| crate::matches_id(&value, expected)) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::Id)
|
||||
.with_description("The id property is immutable."));
|
||||
}
|
||||
}
|
||||
(property, value) => {
|
||||
entries.insert(property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the contact belongs to at least one address book
|
||||
if addressbooks.is_empty() {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::AddressBookIds)
|
||||
.with_description("Contact has to belong to at least one address book."));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn patch_parent_ids(
|
||||
current: &mut Vec<DavName>,
|
||||
patch: Option<&JsonPointerItem<JSContactProperty<Id>>>,
|
||||
update: Value<'_, JSContactProperty<Id>, JSContactValue<Id, BlobId>>,
|
||||
) -> Result<(), SetError<JSContactProperty<Id>>> {
|
||||
match (patch, update) {
|
||||
(
|
||||
Some(JsonPointerItem::Key(Key::Property(JSContactProperty::IdValue(id)))),
|
||||
Value::Bool(false) | Value::Null,
|
||||
) => {
|
||||
let id = id.document_id();
|
||||
current.retain(|name| name.parent_id != id);
|
||||
Ok(())
|
||||
}
|
||||
(
|
||||
Some(JsonPointerItem::Key(Key::Property(JSContactProperty::IdValue(id)))),
|
||||
Value::Bool(true),
|
||||
) => {
|
||||
let id = id.document_id();
|
||||
if !current.iter().any(|name| name.parent_id == id) {
|
||||
current.push(DavName::new_with_rand_name(id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(None, Value::Object(object)) => {
|
||||
let mut new_ids = object
|
||||
.into_expanded_boolean_set()
|
||||
.filter_map(|id| {
|
||||
if let Key::Property(JSContactProperty::IdValue(id)) = id {
|
||||
Some(id.document_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
current.retain(|name| new_ids.remove(&name.parent_id));
|
||||
|
||||
for id in new_ids {
|
||||
current.push(DavName::new_with_rand_name(id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::AddressBookIds)
|
||||
.with_description("Invalid patch operation for addressBookIds.")),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user