JMAP for Contacts implementation (part 2)
This commit is contained in:
@@ -4,26 +4,170 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use crate::{api::acl::JmapRights, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{cache::GroupwareCache, contact::AddressBook};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::addressbook::AddressBook,
|
||||
object::addressbook::{self, AddressBookProperty, AddressBookValue},
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use store::roaring::RoaringBitmap;
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub trait AddressBookGet: Sync + Send {
|
||||
fn address_book_get(
|
||||
&self,
|
||||
request: GetRequest<AddressBook>,
|
||||
request: GetRequest<addressbook::AddressBook>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<AddressBook>>> + Send;
|
||||
) -> impl Future<Output = trc::Result<GetResponse<addressbook::AddressBook>>> + Send;
|
||||
}
|
||||
|
||||
impl AddressBookGet for Server {
|
||||
async fn address_book_get(
|
||||
&self,
|
||||
mut request: GetRequest<AddressBook>,
|
||||
mut request: GetRequest<addressbook::AddressBook>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<AddressBook>> {
|
||||
todo!()
|
||||
) -> trc::Result<GetResponse<addressbook::AddressBook>> {
|
||||
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
AddressBookProperty::Id,
|
||||
AddressBookProperty::Name,
|
||||
AddressBookProperty::Description,
|
||||
AddressBookProperty::SortOrder,
|
||||
AddressBookProperty::IsDefault,
|
||||
AddressBookProperty::IsSubscribed,
|
||||
AddressBookProperty::MyRights,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook)
|
||||
.await?;
|
||||
let address_book_ids = if access_token.is_member(account_id) {
|
||||
cache.document_ids(true).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true)
|
||||
};
|
||||
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
address_book_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(true).into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the address_book object
|
||||
let document_id = id.document_id();
|
||||
if !address_book_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
}
|
||||
let _address_book = if let Some(address_book) = self
|
||||
.get_archive(account_id, Collection::AddressBook, document_id)
|
||||
.await?
|
||||
{
|
||||
address_book
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
};
|
||||
let address_book = _address_book
|
||||
.unarchive::<AddressBook>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
match property {
|
||||
AddressBookProperty::Id => {
|
||||
result.insert_unchecked(AddressBookProperty::Id, AddressBookValue::Id(id));
|
||||
}
|
||||
AddressBookProperty::Name => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::Name,
|
||||
address_book.preferences(access_token).name.to_string(),
|
||||
);
|
||||
}
|
||||
AddressBookProperty::Description => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::Description,
|
||||
address_book
|
||||
.preferences(access_token)
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|v| v.to_string()),
|
||||
);
|
||||
}
|
||||
AddressBookProperty::SortOrder => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::SortOrder,
|
||||
address_book
|
||||
.preferences(access_token)
|
||||
.sort_order
|
||||
.to_native(),
|
||||
);
|
||||
}
|
||||
AddressBookProperty::IsDefault => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::IsDefault,
|
||||
address_book.is_default,
|
||||
);
|
||||
}
|
||||
AddressBookProperty::IsSubscribed => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::IsSubscribed,
|
||||
address_book
|
||||
.subscribers
|
||||
.iter()
|
||||
.any(|account_id| *account_id == access_token.primary_id()),
|
||||
);
|
||||
}
|
||||
AddressBookProperty::ShareWith => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::ShareWith,
|
||||
JmapRights::share_with::<addressbook::AddressBook>(
|
||||
account_id,
|
||||
access_token,
|
||||
&address_book
|
||||
.acls
|
||||
.iter()
|
||||
.map(AclGrant::from)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
AddressBookProperty::MyRights => {
|
||||
result.insert_unchecked(
|
||||
AddressBookProperty::IsDefault,
|
||||
if access_token.is_shared(account_id) {
|
||||
JmapRights::rights::<addressbook::AddressBook>(
|
||||
address_book.acls.effective_acl(access_token),
|
||||
)
|
||||
} else {
|
||||
JmapRights::all_rights::<addressbook::AddressBook>()
|
||||
},
|
||||
);
|
||||
}
|
||||
property => {
|
||||
result.insert_unchecked(property.clone(), Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,348 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use crate::api::acl::{JmapAcl, JmapRights};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{
|
||||
DestroyArchive,
|
||||
cache::GroupwareCache,
|
||||
contact::{AddressBook, AddressBookPreferences},
|
||||
};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::addressbook::AddressBook,
|
||||
object::addressbook::{self, AddressBookProperty, AddressBookValue},
|
||||
request::IntoValid,
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{JsonPointerItem, Key, Value};
|
||||
use rand::{Rng, distr::Alphanumeric};
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub trait AddressBookSet: Sync + Send {
|
||||
fn address_book_set(
|
||||
&self,
|
||||
request: SetRequest<'_, AddressBook>,
|
||||
request: SetRequest<'_, addressbook::AddressBook>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<AddressBook>>> + Send;
|
||||
) -> impl Future<Output = trc::Result<SetResponse<addressbook::AddressBook>>> + Send;
|
||||
}
|
||||
|
||||
impl AddressBookSet for Server {
|
||||
async fn address_book_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, AddressBook>,
|
||||
mut request: SetRequest<'_, addressbook::AddressBook>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<AddressBook>> {
|
||||
todo!()
|
||||
_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, SyncCollection::AddressBook)
|
||||
.await?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = request.unwrap_destroy().into_valid().collect::<Vec<_>>();
|
||||
let is_shared = access_token.is_shared(account_id);
|
||||
|
||||
// TODO: Implement onSuccessSetIsDefault
|
||||
|
||||
// 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(object, &mut address_book, access_token) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
response.created(id, document_id);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update().into_valid() {
|
||||
// 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
|
||||
.get_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(object, &mut new_address_book, access_token) {
|
||||
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::Administer))
|
||||
{
|
||||
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_acls(
|
||||
&new_address_book.acls,
|
||||
Some(
|
||||
address_book
|
||||
.inner
|
||||
.acls
|
||||
.iter()
|
||||
.map(AclGrant::from)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Update record
|
||||
new_address_book
|
||||
.update(
|
||||
access_token,
|
||||
address_book,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
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
|
||||
.get_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;
|
||||
}
|
||||
|
||||
// Delete record
|
||||
DestroyArchive(address_book)
|
||||
.delete_with_cards(
|
||||
self,
|
||||
access_token,
|
||||
account_id,
|
||||
document_id,
|
||||
children_ids,
|
||||
None,
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
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!())?;
|
||||
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_address_book(
|
||||
updates: Value<'_, AddressBookProperty, AddressBookValue>,
|
||||
address_book: &mut AddressBook,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<bool, SetError<AddressBookProperty>> {
|
||||
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(access_token).name = value.into_owned();
|
||||
}
|
||||
(AddressBookProperty::Description, Value::Str(value)) if value.len() < 255 => {
|
||||
address_book.preferences_mut(access_token).description = value.into_owned().into();
|
||||
}
|
||||
(AddressBookProperty::Description, Value::Null) => {
|
||||
address_book.preferences_mut(access_token).description = None;
|
||||
}
|
||||
(AddressBookProperty::SortOrder, Value::Number(value)) => {
|
||||
address_book.preferences_mut(access_token).sort_order = value.cast_to_u64() as u32;
|
||||
}
|
||||
(AddressBookProperty::IsSubscribed, Value::Bool(subscribe)) => {
|
||||
let account_id = access_token.primary_id();
|
||||
if subscribe {
|
||||
if !address_book.subscribers.contains(&account_id) {
|
||||
address_book.subscribers.push(account_id);
|
||||
}
|
||||
} else {
|
||||
address_book.subscribers.retain(|id| *id != account_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;
|
||||
}
|
||||
(property, _) => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.clone())
|
||||
.with_description("Field could not be set."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if address_book.preferences(access_token).name.is_empty() {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(AddressBookProperty::Name)
|
||||
.with_description("Missing name."));
|
||||
}
|
||||
|
||||
Ok(has_acl_changes)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{MessageStoreCache, Server};
|
||||
use common::{DavResources, MessageStoreCache, Server};
|
||||
use jmap_proto::types::state::State;
|
||||
use std::future::Future;
|
||||
use trc::AddContext;
|
||||
@@ -25,10 +25,18 @@ pub trait StateManager: Sync + Send {
|
||||
) -> impl Future<Output = trc::Result<State>> + Send;
|
||||
}
|
||||
|
||||
pub trait MessageCacheState: Sync + Send {
|
||||
fn get_state(&self, is_mailbox: bool) -> State;
|
||||
pub trait JmapCacheState: Sync + Send {
|
||||
fn get_state(&self, is_container: bool) -> State;
|
||||
|
||||
fn assert_state(&self, is_mailbox: bool, if_in_state: &Option<State>) -> trc::Result<State>;
|
||||
fn assert_state(&self, is_container: bool, if_in_state: &Option<State>) -> trc::Result<State> {
|
||||
let old_state: State = self.get_state(is_container);
|
||||
if let Some(if_in_state) = if_in_state
|
||||
&& &old_state != if_in_state
|
||||
{
|
||||
return Err(trc::JmapEvent::StateMismatch.into_err());
|
||||
}
|
||||
Ok(old_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl StateManager for Server {
|
||||
@@ -59,22 +67,22 @@ impl StateManager for Server {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageCacheState for MessageStoreCache {
|
||||
fn get_state(&self, is_mailbox: bool) -> State {
|
||||
if is_mailbox {
|
||||
impl JmapCacheState for MessageStoreCache {
|
||||
fn get_state(&self, is_container: bool) -> State {
|
||||
if is_container {
|
||||
State::from(self.mailboxes.change_id)
|
||||
} else {
|
||||
State::from(self.emails.change_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_state(&self, is_mailbox: bool, if_in_state: &Option<State>) -> trc::Result<State> {
|
||||
let old_state: State = self.get_state(is_mailbox);
|
||||
if let Some(if_in_state) = if_in_state
|
||||
&& &old_state != if_in_state
|
||||
{
|
||||
return Err(trc::JmapEvent::StateMismatch.into_err());
|
||||
impl JmapCacheState for DavResources {
|
||||
fn get_state(&self, is_container: bool) -> State {
|
||||
if is_container {
|
||||
State::from(self.container_change_id)
|
||||
} else {
|
||||
State::from(self.item_change_id)
|
||||
}
|
||||
Ok(old_state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,128 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::JmapCacheState;
|
||||
use calcard::jscontact::{JSContactProperty, JSContactValue};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::{cache::GroupwareCache, contact::ContactCard};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::contact::ContactCard,
|
||||
object::contact,
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use store::roaring::RoaringBitmap;
|
||||
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<ContactCard>,
|
||||
request: GetRequest<contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<ContactCard>>> + Send;
|
||||
) -> impl Future<Output = trc::Result<GetResponse<contact::ContactCard>>> + Send;
|
||||
}
|
||||
|
||||
impl ContactCardGet for Server {
|
||||
async fn contact_card_get(
|
||||
&self,
|
||||
mut request: GetRequest<ContactCard>,
|
||||
mut request: GetRequest<contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<ContactCard>> {
|
||||
todo!()
|
||||
) -> trc::Result<GetResponse<contact::ContactCard>> {
|
||||
let 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, SyncCollection::AddressBook)
|
||||
.await?;
|
||||
let contact_ids = if access_token.is_member(account_id) {
|
||||
cache.document_ids(false).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
cache.shared_containers(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: vec![],
|
||||
};
|
||||
let return_id = return_all_properties || properties.contains(&JSContactProperty::Id);
|
||||
let return_address_book_ids =
|
||||
return_all_properties || properties.contains(&JSContactProperty::AddressBookIds);
|
||||
|
||||
for id in ids {
|
||||
// Obtain the contact object
|
||||
let document_id = id.document_id();
|
||||
if !contact_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
}
|
||||
|
||||
let _contact = if let Some(contact) = self
|
||||
.get_archive(account_id, Collection::ContactCard, document_id)
|
||||
.await?
|
||||
{
|
||||
contact
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
};
|
||||
|
||||
let contact = _contact
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut result = if return_all_properties {
|
||||
contact
|
||||
.card
|
||||
.into_jscontact::<Id, BlobId>()
|
||||
.into_inner()
|
||||
.into_object()
|
||||
.unwrap()
|
||||
} else {
|
||||
Map::from_iter(
|
||||
contact
|
||||
.card
|
||||
.into_jscontact::<Id, BlobId>()
|
||||
.into_inner()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
* 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,
|
||||
};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait ContactCardParse: Sync + Send {
|
||||
fn contact_card_parse(
|
||||
@@ -24,6 +29,50 @@ impl ContactCardParse for Server {
|
||||
request: ParseRequest<ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<ParseResponse<ContactCard>> {
|
||||
todo!()
|
||||
if request.blob_ids.len() > self.core.jmap.mail_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(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,474 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use calcard::jscontact::{JSContact, JSContactProperty, JSContactValue};
|
||||
use common::{DavName, Server, auth::AccessToken};
|
||||
use groupware::{DestroyArchive, cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::contact::ContactCard,
|
||||
object::contact,
|
||||
request::IntoValid,
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Value};
|
||||
use store::{ahash::AHashSet, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
blob::BlobId,
|
||||
collection::{Collection, SyncCollection},
|
||||
id::Id,
|
||||
};
|
||||
|
||||
pub trait ContactCardSet: Sync + Send {
|
||||
fn contact_card_set(
|
||||
&self,
|
||||
request: SetRequest<'_, ContactCard>,
|
||||
request: SetRequest<'_, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<ContactCard>>> + Send;
|
||||
) -> impl Future<Output = trc::Result<SetResponse<contact::ContactCard>>> + Send;
|
||||
}
|
||||
|
||||
impl ContactCardSet for Server {
|
||||
async fn contact_card_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, ContactCard>,
|
||||
mut request: SetRequest<'_, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<ContactCard>> {
|
||||
todo!()
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<contact::ContactCard>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(access_token, account_id, SyncCollection::AddressBook)
|
||||
.await?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = request.unwrap_destroy().into_valid().collect::<Vec<_>>();
|
||||
|
||||
// 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() {
|
||||
let mut names = Vec::new();
|
||||
let mut js_contact = JSContact::default();
|
||||
|
||||
// Process changes
|
||||
if let Err(err) = update_contact_card(object, &mut names, &mut js_contact) {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Verify that the address book ids valid
|
||||
for name in &names {
|
||||
if !cache.has_container_id(&name.parent_id) {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::AddressBookIds)
|
||||
.with_description(format!(
|
||||
"addressBookId {} does not exist.",
|
||||
Id::from(name.parent_id)
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
} else if can_add_address_books
|
||||
.as_ref()
|
||||
.is_some_and(|ids| !ids.contains(name.parent_id))
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to add contacts to address book {}.",
|
||||
Id::from(name.parent_id)
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert JSContact to vCard
|
||||
let Some(card) = js_contact.into_vcard() else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_description("Failed to convert contact to vCard."),
|
||||
);
|
||||
continue 'create;
|
||||
};
|
||||
|
||||
// Check size and quota
|
||||
let size = card.size();
|
||||
if size > self.core.groupware.max_vcard_size {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties().with_description(format!(
|
||||
"Contact size {} exceeds the maximum allowed size of {} bytes.",
|
||||
size, self.core.groupware.max_vcard_size
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
match self
|
||||
.has_available_quota(
|
||||
&self.get_resource_token(access_token, account_id).await?,
|
||||
size as u64,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => {
|
||||
response.not_created.append(id, SetError::over_quota());
|
||||
continue 'create;
|
||||
}
|
||||
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_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
response.created(id, document_id);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update().into_valid() {
|
||||
// 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
|
||||
.get_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(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 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(
|
||||
&self.get_resource_token(access_token, account_id).await?,
|
||||
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
|
||||
new_contact_card
|
||||
.update(
|
||||
access_token,
|
||||
contact_card,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
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(contact_card_) = self
|
||||
.get_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete record
|
||||
DestroyArchive(contact_card)
|
||||
.delete_all(access_token, account_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
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!())?;
|
||||
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_contact_card<'x>(
|
||||
updates: Value<'x, JSContactProperty<Id>, JSContactValue<Id, BlobId>>,
|
||||
addressbooks: &mut Vec<DavName>,
|
||||
js_contact: &mut JSContact<'x, Id, BlobId>,
|
||||
) -> Result<(), SetError<JSContactProperty<Id>>> {
|
||||
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."));
|
||||
}
|
||||
}
|
||||
(property, value) => {
|
||||
js_contact
|
||||
.0
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.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.")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
changes::state::MessageCacheState,
|
||||
changes::state::JmapCacheState,
|
||||
email::{PatchResult, handle_email_patch, ingested_into_object},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::{
|
||||
headers::IntoForm,
|
||||
};
|
||||
use crate::{
|
||||
blob::download::BlobDownload, changes::state::MessageCacheState, email::headers::HeaderToValue,
|
||||
blob::download::BlobDownload, changes::state::JmapCacheState, email::headers::HeaderToValue,
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use email::{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
blob::download::BlobDownload, changes::state::MessageCacheState, email::ingested_into_object,
|
||||
blob::download::BlobDownload, changes::state::JmapCacheState, email::ingested_into_object,
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use email::{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{JmapMethods, changes::state::MessageCacheState};
|
||||
use crate::{JmapMethods, changes::state::JmapCacheState};
|
||||
use common::{MessageStoreCache, Server, auth::AccessToken};
|
||||
use email::cache::{MessageCacheFetch, email::MessageCacheAccess};
|
||||
use jmap_proto::{
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::headers::{BuildHeader, ValueToHeader};
|
||||
use crate::{
|
||||
JmapMethods,
|
||||
blob::download::BlobDownload,
|
||||
changes::state::MessageCacheState,
|
||||
changes::state::JmapCacheState,
|
||||
email::{PatchResult, handle_email_patch, ingested_into_object},
|
||||
};
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
@@ -47,6 +47,7 @@ use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection, VanishedCollection},
|
||||
id::Id,
|
||||
keyword::Keyword,
|
||||
type_state::{DataType, StateChange},
|
||||
};
|
||||
@@ -76,14 +77,16 @@ impl EmailSet for Server {
|
||||
let can_train_spam = self.email_bayes_can_train(access_token);
|
||||
|
||||
// Obtain mailboxIds
|
||||
let (can_add_mailbox_ids, can_delete_mailbox_ids, can_modify_message_ids) =
|
||||
let (can_add_mailbox_ids, can_delete_mailbox_ids, can_modify_mailbox_ids) =
|
||||
if access_token.is_shared(account_id) {
|
||||
(
|
||||
cache.shared_mailboxes(access_token, Acl::AddItems).into(),
|
||||
cache
|
||||
.shared_mailboxes(access_token, Acl::RemoveItems)
|
||||
.into(),
|
||||
cache.shared_messages(access_token, Acl::ModifyItems).into(),
|
||||
cache
|
||||
.shared_mailboxes(access_token, Acl::ModifyItems)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
@@ -694,14 +697,21 @@ impl EmailSet for Server {
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(EmailProperty::MailboxIds)
|
||||
.with_description(format!("mailboxId {mailbox_id} does not exist.")),
|
||||
.with_description(format!(
|
||||
"mailboxId {} does not exist.",
|
||||
Id::from(*mailbox_id)
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
} else if matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(*mailbox_id)) {
|
||||
} else if can_add_mailbox_ids
|
||||
.as_ref()
|
||||
.is_some_and(|ids| !ids.contains(*mailbox_id))
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to add messages to mailbox {mailbox_id}."
|
||||
"You are not allowed to add messages to mailbox {}.",
|
||||
Id::from(*mailbox_id)
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
@@ -867,7 +877,12 @@ impl EmailSet for Server {
|
||||
// Process keywords
|
||||
if has_keyword_changes {
|
||||
// Verify permissions on shared accounts
|
||||
if matches!(&can_modify_message_ids, Some(ids) if !ids.contains(document_id)) {
|
||||
if can_modify_mailbox_ids.as_ref().is_some_and(|ids| {
|
||||
!new_data
|
||||
.mailboxes
|
||||
.iter()
|
||||
.any(|mb| ids.contains(mb.mailbox_id))
|
||||
}) {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
@@ -907,7 +922,9 @@ impl EmailSet for Server {
|
||||
for mailbox_id in new_data.added_mailboxes(data.inner) {
|
||||
if cache.has_mailbox_id(&mailbox_id.mailbox_id) {
|
||||
// Verify permissions on shared accounts
|
||||
if !matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(mailbox_id.mailbox_id))
|
||||
if can_add_mailbox_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(mailbox_id.mailbox_id))
|
||||
{
|
||||
changed_mailboxes.insert(mailbox_id.mailbox_id, Vec::new());
|
||||
} else {
|
||||
@@ -915,7 +932,7 @@ impl EmailSet for Server {
|
||||
id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You are not allowed to add messages to mailbox {}.",
|
||||
mailbox_id.mailbox_id
|
||||
Id::from(mailbox_id.mailbox_id)
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
@@ -927,7 +944,7 @@ impl EmailSet for Server {
|
||||
.with_property(EmailProperty::MailboxIds)
|
||||
.with_description(format!(
|
||||
"mailboxId {} does not exist.",
|
||||
mailbox_id.mailbox_id
|
||||
Id::from(mailbox_id.mailbox_id)
|
||||
)),
|
||||
);
|
||||
continue 'update;
|
||||
@@ -937,7 +954,9 @@ impl EmailSet for Server {
|
||||
// Add all removed mailboxes to change list
|
||||
for mailbox_id in new_data.removed_mailboxes(data.inner) {
|
||||
// Verify permissions on shared accounts
|
||||
if !matches!(&can_delete_mailbox_ids, Some(ids) if !ids.contains(u32::from(mailbox_id.mailbox_id)))
|
||||
if can_delete_mailbox_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(u32::from(mailbox_id.mailbox_id)))
|
||||
{
|
||||
changed_mailboxes
|
||||
.entry(mailbox_id.mailbox_id.to_native())
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{JmapMethods, changes::state::MessageCacheState};
|
||||
use crate::{JmapMethods, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use email::cache::{MessageCacheFetch, mailbox::MailboxCacheAccess};
|
||||
use jmap_proto::{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::{
|
||||
JmapMethods,
|
||||
api::acl::{JmapAcl, JmapRights},
|
||||
changes::state::MessageCacheState,
|
||||
changes::state::JmapCacheState,
|
||||
};
|
||||
use common::{
|
||||
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
|
||||
@@ -391,7 +391,6 @@ impl MailboxSet for Server {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Key::Property(MailboxProperty::Pointer(pointer)), value)
|
||||
if matches!(
|
||||
pointer.first(),
|
||||
|
||||
Reference in New Issue
Block a user