/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ 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>> + Send; } impl AddressBookSet for Server { async fn address_book_set( &self, mut request: SetRequest<'_, addressbook::AddressBook>, access_token: &AccessToken, _session: &HttpSessionData, ) -> trc::Result> { 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::(), 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(account_id, &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::>(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::() .caused_by(trc::location!())?; let mut new_address_book = address_book .deserialize::() .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(account_id, &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::(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::>(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::() .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::>(); 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::>(ValueKey::archive( account_id, Collection::ContactCard, document_id, )) .await? { let card = card_ .to_unarchived::() .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::() .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, updates: Value<'_, AddressBookProperty, AddressBookValue>, address_book: &mut AddressBook, access_token: &AccessToken, account_id: u32, ) -> Result> { 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::(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::( 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) }