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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+203
View File
@@ -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)
}
}
+159
View File
@@ -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)
}
}
+59
View File
@@ -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(()))
}
+78
View File
@@ -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)
}
}
+371
View File
@@ -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()
}
}
+595
View File
@@ -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.")),
}
}