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.
502 lines
18 KiB
Rust
502 lines
18 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::api::acl::{JmapAcl, JmapRights};
|
|
use crate::changes::state::JmapCacheState;
|
|
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
|
use groupware::{
|
|
DestroyArchive,
|
|
cache::GroupwareCache,
|
|
contact::{AddressBook, AddressBookPreferences, ContactCard},
|
|
};
|
|
use http_proto::HttpSessionData;
|
|
use jmap_proto::{
|
|
error::set::SetError,
|
|
method::set::{SetRequest, SetResponse},
|
|
object::addressbook::{self, AddressBookProperty, AddressBookValue},
|
|
request::{MaybeInvalid, reference::MaybeIdReference},
|
|
types::state::State,
|
|
};
|
|
use jmap_tools::{JsonPointerItem, Key, Value};
|
|
use rand::{RngExt, distr::Alphanumeric};
|
|
use store::{
|
|
SerializeInfallible, ValueKey,
|
|
ahash::AHashSet,
|
|
write::{AlignedBytes, Archive, BatchBuilder, ValueClass},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
acl::Acl,
|
|
collection::{Collection, SyncCollection},
|
|
field::PrincipalField,
|
|
id::Id,
|
|
};
|
|
|
|
pub trait AddressBookSet: Sync + Send {
|
|
fn address_book_set(
|
|
&self,
|
|
request: SetRequest<'_, addressbook::AddressBook>,
|
|
access_token: &AccessToken,
|
|
session: &HttpSessionData,
|
|
) -> impl Future<Output = trc::Result<SetResponse<addressbook::AddressBook>>> + Send;
|
|
}
|
|
|
|
impl AddressBookSet for Server {
|
|
async fn address_book_set(
|
|
&self,
|
|
mut request: SetRequest<'_, addressbook::AddressBook>,
|
|
access_token: &AccessToken,
|
|
_session: &HttpSessionData,
|
|
) -> trc::Result<SetResponse<addressbook::AddressBook>> {
|
|
let account_id = request.account_id.document_id();
|
|
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(true, &request.if_in_state)?);
|
|
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
|
let is_shared = access_token.is_shared(account_id);
|
|
let mut set_default = None;
|
|
|
|
// Process creates
|
|
let mut batch = BatchBuilder::new();
|
|
'create: for (id, object) in request.unwrap_create() {
|
|
if is_shared {
|
|
response.not_created.append(
|
|
id,
|
|
SetError::forbidden()
|
|
.with_description("Cannot create address books in a shared account."),
|
|
);
|
|
continue 'create;
|
|
}
|
|
|
|
let mut address_book = AddressBook {
|
|
name: rand::rng()
|
|
.sample_iter(Alphanumeric)
|
|
.take(10)
|
|
.map(char::from)
|
|
.collect::<String>(),
|
|
preferences: vec![AddressBookPreferences {
|
|
account_id,
|
|
name: "Address Book".to_string(),
|
|
..Default::default()
|
|
}],
|
|
..Default::default()
|
|
};
|
|
|
|
// Process changes
|
|
if let Err(err) =
|
|
update_address_book(None, object, &mut address_book, access_token, account_id)
|
|
{
|
|
response.not_created.append(id, err);
|
|
continue 'create;
|
|
}
|
|
|
|
// Validate ACLs
|
|
if !address_book.acls.is_empty() {
|
|
if let Err(err) = self.acl_validate(&address_book.acls).await {
|
|
response.not_created.append(id, err.into());
|
|
continue 'create;
|
|
}
|
|
|
|
self.refresh_acls(&address_book.acls, None)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
// Insert record
|
|
let document_id = self
|
|
.store()
|
|
.assign_document_ids(account_id, Collection::AddressBook, 1)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
address_book
|
|
.insert(
|
|
access_token.account_tenant_ids(),
|
|
account_id,
|
|
document_id,
|
|
&mut batch,
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
|
|
if let Some(MaybeIdReference::Reference(id_ref)) =
|
|
&request.arguments.on_success_set_is_default
|
|
&& id_ref == &id
|
|
{
|
|
set_default = Some(document_id);
|
|
}
|
|
|
|
response.created(id, document_id);
|
|
}
|
|
|
|
// 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 address book
|
|
let document_id = id.document_id();
|
|
let address_book_ = if let Some(address_book_) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::AddressBook,
|
|
document_id,
|
|
))
|
|
.await?
|
|
{
|
|
address_book_
|
|
} else {
|
|
response.not_updated.append(id, SetError::not_found());
|
|
continue 'update;
|
|
};
|
|
let address_book = address_book_
|
|
.to_unarchived::<AddressBook>()
|
|
.caused_by(trc::location!())?;
|
|
let mut new_address_book = address_book
|
|
.deserialize::<AddressBook>()
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Apply changes
|
|
let has_acl_changes = match update_address_book(
|
|
Some(id),
|
|
object,
|
|
&mut new_address_book,
|
|
access_token,
|
|
account_id,
|
|
) {
|
|
Ok(has_acl_changes_) => has_acl_changes_,
|
|
Err(err) => {
|
|
response.not_updated.append(id, err);
|
|
continue 'update;
|
|
}
|
|
};
|
|
|
|
// Validate ACL
|
|
if is_shared {
|
|
let acl = address_book.inner.acls.effective_acl(access_token);
|
|
if !acl.contains(Acl::Modify) || (has_acl_changes && !acl.contains(Acl::Share)) {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::forbidden()
|
|
.with_description("You are not allowed to modify this address book."),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
if has_acl_changes {
|
|
if let Err(err) = self.acl_validate(&new_address_book.acls).await {
|
|
response.not_updated.append(id, err.into());
|
|
continue 'update;
|
|
}
|
|
self.refresh_archived_acls(
|
|
&new_address_book.acls,
|
|
address_book.inner.acls.as_slice(),
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
// Update record
|
|
new_address_book
|
|
.update(
|
|
access_token.account_tenant_ids(),
|
|
address_book,
|
|
account_id,
|
|
document_id,
|
|
&mut batch,
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
response.updated.append(id, None);
|
|
}
|
|
|
|
// Process deletions
|
|
let mut reset_default_address_book = false;
|
|
if !will_destroy.is_empty() {
|
|
let mut destroy_children = AHashSet::new();
|
|
let mut destroy_parents = AHashSet::new();
|
|
let default_address_book_id = self
|
|
.store()
|
|
.get_value::<u32>(ValueKey {
|
|
account_id,
|
|
collection: Collection::Principal.into(),
|
|
document_id: 0,
|
|
class: ValueClass::Property(PrincipalField::DefaultAddressBookId.into()),
|
|
})
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
let on_destroy_remove_contents = request
|
|
.arguments
|
|
.on_destroy_remove_contents
|
|
.unwrap_or(false);
|
|
|
|
for id in will_destroy {
|
|
let document_id = id.document_id();
|
|
|
|
if !cache.has_container_id(&document_id) {
|
|
response.not_destroyed.append(id, SetError::not_found());
|
|
continue;
|
|
};
|
|
|
|
let Some(address_book_) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::AddressBook,
|
|
document_id,
|
|
))
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
else {
|
|
response.not_destroyed.append(id, SetError::not_found());
|
|
continue;
|
|
};
|
|
|
|
let address_book = address_book_
|
|
.to_unarchived::<AddressBook>()
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Validate ACLs
|
|
if is_shared
|
|
&& !address_book
|
|
.inner
|
|
.acls
|
|
.effective_acl(access_token)
|
|
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
|
|
{
|
|
response.not_destroyed.append(
|
|
id,
|
|
SetError::forbidden()
|
|
.with_description("You are not allowed to delete this address book."),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Obtain children ids
|
|
let children_ids = cache.children_ids(document_id).collect::<Vec<_>>();
|
|
if !children_ids.is_empty() && !on_destroy_remove_contents {
|
|
response
|
|
.not_destroyed
|
|
.append(id, SetError::address_book_has_contents());
|
|
continue;
|
|
}
|
|
destroy_children.extend(children_ids.iter().copied());
|
|
destroy_parents.insert(document_id);
|
|
|
|
// Delete record
|
|
let delete_path = cache
|
|
.container_resource_path_by_id(document_id)
|
|
.map(|resource| cache.format_resource(resource));
|
|
DestroyArchive(address_book)
|
|
.delete(
|
|
access_token.account_tenant_ids(),
|
|
account_id,
|
|
document_id,
|
|
delete_path,
|
|
&mut batch,
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
|
|
if default_address_book_id == Some(document_id) {
|
|
reset_default_address_book = true;
|
|
}
|
|
|
|
response.destroyed.push(id);
|
|
}
|
|
|
|
// Delete children
|
|
if !destroy_children.is_empty() {
|
|
for document_id in destroy_children {
|
|
if let Some(card_) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::ContactCard,
|
|
document_id,
|
|
))
|
|
.await?
|
|
{
|
|
let card = card_
|
|
.to_unarchived::<ContactCard>()
|
|
.caused_by(trc::location!())?;
|
|
|
|
if card
|
|
.inner
|
|
.names
|
|
.iter()
|
|
.all(|n| destroy_parents.contains(&n.parent_id.to_native()))
|
|
{
|
|
// Card only belongs to address books being deleted, delete it
|
|
DestroyArchive(card).delete_all(
|
|
access_token.account_tenant_ids(),
|
|
account_id,
|
|
document_id,
|
|
&mut batch,
|
|
)?;
|
|
} else {
|
|
// Unlink addressbook id from card
|
|
let mut new_card = card
|
|
.deserialize::<ContactCard>()
|
|
.caused_by(trc::location!())?;
|
|
new_card
|
|
.names
|
|
.retain(|n| !destroy_parents.contains(&n.parent_id));
|
|
new_card.update(
|
|
access_token.account_tenant_ids(),
|
|
card,
|
|
account_id,
|
|
document_id,
|
|
&mut batch,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set default address book
|
|
if let Some(MaybeIdReference::Id(id)) = &request.arguments.on_success_set_is_default {
|
|
set_default = Some(id.document_id());
|
|
}
|
|
if let Some(default_address_book_id) = set_default {
|
|
if response.not_created.is_empty()
|
|
&& response.not_updated.is_empty()
|
|
&& response.not_destroyed.is_empty()
|
|
{
|
|
batch
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::Principal)
|
|
.with_document(0)
|
|
.set(
|
|
PrincipalField::DefaultAddressBookId,
|
|
default_address_book_id.serialize(),
|
|
);
|
|
}
|
|
} else if reset_default_address_book {
|
|
batch
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::Principal)
|
|
.with_document(0)
|
|
.clear(PrincipalField::DefaultAddressBookId);
|
|
}
|
|
|
|
// Write changes
|
|
if !batch.is_empty()
|
|
&& let Ok(change_id) = self
|
|
.commit_batch(batch)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.last_change_id(account_id)
|
|
{
|
|
self.notify_task_queue();
|
|
response.new_state = State::Exact(change_id).into();
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
}
|
|
|
|
fn update_address_book(
|
|
expected_id: Option<Id>,
|
|
updates: Value<'_, AddressBookProperty, AddressBookValue>,
|
|
address_book: &mut AddressBook,
|
|
access_token: &AccessToken,
|
|
account_id: u32,
|
|
) -> Result<bool, SetError<AddressBookProperty>> {
|
|
let personal_id = access_token.personal_id(account_id, Collection::AddressBook);
|
|
let mut has_acl_changes = false;
|
|
|
|
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) {
|
|
(AddressBookProperty::Name, Value::Str(value)) if (1..=255).contains(&value.len()) => {
|
|
address_book.preferences_mut(personal_id).name = value.into_owned();
|
|
}
|
|
(AddressBookProperty::Description, Value::Str(value)) if value.len() < 255 => {
|
|
address_book.preferences_mut(personal_id).description = value.into_owned().into();
|
|
}
|
|
(AddressBookProperty::Description, Value::Null) => {
|
|
address_book.preferences_mut(personal_id).description = None;
|
|
}
|
|
(AddressBookProperty::SortOrder, Value::Number(value)) => {
|
|
address_book.preferences_mut(personal_id).sort_order = value.cast_to_u64() as u32;
|
|
}
|
|
(AddressBookProperty::IsSubscribed, Value::Bool(subscribe)) => {
|
|
if subscribe {
|
|
if !address_book.subscribers.contains(&personal_id) {
|
|
address_book.subscribers.push(personal_id);
|
|
}
|
|
} else {
|
|
address_book.subscribers.retain(|id| *id != personal_id);
|
|
}
|
|
}
|
|
(AddressBookProperty::ShareWith, value) => {
|
|
address_book.acls = JmapRights::acl_set::<addressbook::AddressBook>(value)?;
|
|
has_acl_changes = true;
|
|
}
|
|
(AddressBookProperty::Pointer(pointer), value)
|
|
if matches!(
|
|
pointer.first(),
|
|
Some(JsonPointerItem::Key(Key::Property(
|
|
AddressBookProperty::ShareWith
|
|
)))
|
|
) =>
|
|
{
|
|
let mut pointer = pointer.iter();
|
|
pointer.next();
|
|
|
|
address_book.acls = JmapRights::acl_patch::<addressbook::AddressBook>(
|
|
std::mem::take(&mut address_book.acls),
|
|
pointer,
|
|
value,
|
|
)?;
|
|
has_acl_changes = true;
|
|
}
|
|
(AddressBookProperty::Id, value) => {
|
|
if !expected_id.is_some_and(|expected| crate::matches_id(&value, expected)) {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(AddressBookProperty::Id)
|
|
.with_description("The id property is immutable."));
|
|
}
|
|
}
|
|
(property, _) => {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(property.clone())
|
|
.with_description("Field could not be set."));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate name
|
|
if address_book.preferences(personal_id).name.is_empty() {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(AddressBookProperty::Name)
|
|
.with_description("Missing name."));
|
|
}
|
|
|
|
Ok(has_acl_changes)
|
|
}
|