JMAP for Contacts implementation (closes #1576)
This commit is contained in:
@@ -384,6 +384,7 @@ impl RequestHandler for Server {
|
||||
RequestMethod::Copy(req) => match req {
|
||||
CopyRequestMethod::Email(mut req) => {
|
||||
set_account_id_if_missing(&mut req.account_id, access_token);
|
||||
set_account_id_if_missing(&mut req.from_account_id, access_token);
|
||||
|
||||
access_token
|
||||
.assert_has_access(req.account_id, Collection::Email)?
|
||||
@@ -400,7 +401,9 @@ impl RequestHandler for Server {
|
||||
self.blob_copy(req, access_token).await?.into()
|
||||
}
|
||||
CopyRequestMethod::ContactCard(mut req) => {
|
||||
set_account_id_if_missing(&mut req.from_account_id, access_token);
|
||||
set_account_id_if_missing(&mut req.account_id, access_token);
|
||||
|
||||
access_token
|
||||
.assert_has_access(req.account_id, Collection::ContactCard)?
|
||||
.assert_has_access(req.from_account_id, Collection::ContactCard)?;
|
||||
|
||||
@@ -4,32 +4,181 @@
|
||||
* 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::{
|
||||
method::copy::{CopyRequest, CopyResponse},
|
||||
object::contact::ContactCard,
|
||||
request::{Call, RequestMethod},
|
||||
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::{roaring::RoaringBitmap, write::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, ContactCard>,
|
||||
request: CopyRequest<'x, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<CopyResponse<ContactCard>>> + Send;
|
||||
) -> impl Future<Output = trc::Result<CopyResponse<contact::ContactCard>>> + Send;
|
||||
}
|
||||
|
||||
impl JmapContactCardCopy for Server {
|
||||
async fn contact_card_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, ContactCard>,
|
||||
request: CopyRequest<'x, contact::ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> trc::Result<CopyResponse<ContactCard>> {
|
||||
todo!()
|
||||
_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();
|
||||
|
||||
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, 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, 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 not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(_contact) = self
|
||||
.get_archive(account_id, Collection::ContactCard, from_contact_id)
|
||||
.await?
|
||||
else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found 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_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(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,57 @@
|
||||
* 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 store::query::Filter;
|
||||
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
|
||||
.store()
|
||||
.filter(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
vec![Filter::eq(ContactField::Uid, uid.as_bytes().to_vec())],
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !hits.results.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.results.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(()))
|
||||
}
|
||||
|
||||
@@ -5,10 +5,21 @@
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::query::{QueryRequest, QueryResponse},
|
||||
object::contact::ContactCard,
|
||||
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
|
||||
object::contact::{ContactCard, ContactCardComparator, ContactCardFilter},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use store::{SerializeInfallible, query, roaring::RoaringBitmap};
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::ContactField,
|
||||
};
|
||||
use utils::sanitize_email;
|
||||
|
||||
use crate::{JmapMethods, changes::state::JmapCacheState};
|
||||
|
||||
pub trait ContactCardQuery: Sync + Send {
|
||||
fn contact_card_query(
|
||||
@@ -24,6 +35,101 @@ impl ContactCardQuery for Server {
|
||||
mut request: QueryRequest<ContactCard>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
todo!()
|
||||
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, SyncCollection::AddressBook)
|
||||
.await?;
|
||||
let filter_mask = (access_token.is_shared(account_id))
|
||||
.then(|| cache.shared_items(access_token, [Acl::ReadItems], true));
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
ContactCardFilter::InAddressBook(MaybeInvalid::Value(id)) => {
|
||||
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.children_ids(id.document_id()),
|
||||
)))
|
||||
}
|
||||
ContactCardFilter::Uid(uid) => {
|
||||
filters.push(query::Filter::eq(ContactField::Uid, uid.into_bytes()))
|
||||
}
|
||||
ContactCardFilter::Email(email) => filters.push(query::Filter::eq(
|
||||
ContactField::Email,
|
||||
sanitize_email(&email).unwrap_or(email).into_bytes(),
|
||||
)),
|
||||
ContactCardFilter::Text(value) => filters.push(query::Filter::has_text(
|
||||
ContactField::Text,
|
||||
value.to_lowercase(),
|
||||
)),
|
||||
ContactCardFilter::CreatedBefore(before) => filters.push(query::Filter::lt(
|
||||
ContactField::Created,
|
||||
(before.timestamp() as u64).serialize(),
|
||||
)),
|
||||
ContactCardFilter::CreatedAfter(after) => filters.push(query::Filter::gt(
|
||||
ContactField::Created,
|
||||
(after.timestamp() as u64).serialize(),
|
||||
)),
|
||||
ContactCardFilter::UpdatedBefore(before) => filters.push(query::Filter::lt(
|
||||
ContactField::Updated,
|
||||
(before.timestamp() as u64).serialize(),
|
||||
)),
|
||||
ContactCardFilter::UpdatedAfter(after) => filters.push(query::Filter::gt(
|
||||
ContactField::Updated,
|
||||
(after.timestamp() as u64).serialize(),
|
||||
)),
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_set = self
|
||||
.filter(account_id, Collection::ContactCard, filters)
|
||||
.await?;
|
||||
|
||||
if let Some(filter_mask) = filter_mask {
|
||||
result_set.apply_mask(filter_mask);
|
||||
}
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(&result_set, cache.get_state(false), &request)
|
||||
.await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.and_then(|s| if !s.is_empty() { s.into() } else { None })
|
||||
.unwrap_or_else(|| vec![Comparator::descending(ContactCardComparator::Updated)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
ContactCardComparator::Created => {
|
||||
query::Comparator::field(ContactField::Created, comparator.is_ascending)
|
||||
}
|
||||
ContactCardComparator::Updated => {
|
||||
query::Comparator::field(ContactField::Updated, comparator.is_ascending)
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, comparators, paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::contact::assert_is_unique_uid;
|
||||
use calcard::jscontact::{JSContact, JSContactProperty, JSContactValue};
|
||||
use common::{DavName, Server, auth::AccessToken};
|
||||
use common::{DavName, DavResources, Server, auth::AccessToken};
|
||||
use groupware::{DestroyArchive, cache::GroupwareCache, contact::ContactCard};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
@@ -16,7 +17,7 @@ use jmap_proto::{
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Value};
|
||||
use store::{ahash::AHashSet, write::BatchBuilder};
|
||||
use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
@@ -32,6 +33,18 @@ pub trait ContactCardSet: Sync + Send {
|
||||
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_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 {
|
||||
@@ -69,95 +82,26 @@ impl ContactCardSet for Server {
|
||||
// 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,
|
||||
.create_contact_card(
|
||||
&cache,
|
||||
&mut batch,
|
||||
access_token,
|
||||
account_id,
|
||||
&can_add_address_books,
|
||||
JSContact::default(),
|
||||
object,
|
||||
)
|
||||
.await
|
||||
.await?
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => {
|
||||
response.not_created.append(id, SetError::over_quota());
|
||||
Ok(document_id) => {
|
||||
response.created(id, document_id);
|
||||
}
|
||||
Err(err) => {
|
||||
response.not_created.append(id, err);
|
||||
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
|
||||
@@ -208,6 +152,21 @@ impl ContactCardSet for Server {
|
||||
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) {
|
||||
@@ -372,6 +331,94 @@ impl ContactCardSet for Server {
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn create_contact_card(
|
||||
&self,
|
||||
cache: &DavResources,
|
||||
batch: &mut BatchBuilder,
|
||||
access_token: &AccessToken,
|
||||
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(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(
|
||||
&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)) => {
|
||||
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_id, document_id, batch)
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| Ok(document_id))
|
||||
}
|
||||
}
|
||||
|
||||
fn update_contact_card<'x>(
|
||||
|
||||
@@ -46,8 +46,9 @@ impl MailboxQuery for Server {
|
||||
Filter::Property(cond) => {
|
||||
match cond {
|
||||
MailboxFilter::ParentId(parent_id) => {
|
||||
let parent_id =
|
||||
parent_id.map(|id| id.document_id()).unwrap_or(u32::MAX);
|
||||
let parent_id = parent_id
|
||||
.and_then(|id| id.try_unwrap().map(|id| id.document_id()))
|
||||
.unwrap_or(u32::MAX);
|
||||
filters.push(query::Filter::is_in_set(
|
||||
mailboxes
|
||||
.mailboxes
|
||||
|
||||
@@ -10,6 +10,7 @@ use email::submission::UndoStatus;
|
||||
use jmap_proto::{
|
||||
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
|
||||
object::email_submission::{self, EmailSubmissionComparator, EmailSubmissionFilter},
|
||||
request::IntoValid,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
@@ -41,7 +42,7 @@ impl EmailSubmissionQuery for Server {
|
||||
Filter::Property(cond) => match cond {
|
||||
EmailSubmissionFilter::IdentityIds(ids) => {
|
||||
filters.push(query::Filter::Or);
|
||||
for id in ids {
|
||||
for id in ids.into_valid() {
|
||||
filters.push(query::Filter::eq(
|
||||
EmailSubmissionField::IdentityId,
|
||||
id.document_id().serialize(),
|
||||
@@ -51,7 +52,7 @@ impl EmailSubmissionQuery for Server {
|
||||
}
|
||||
EmailSubmissionFilter::EmailIds(ids) => {
|
||||
filters.push(query::Filter::Or);
|
||||
for id in ids {
|
||||
for id in ids.into_valid() {
|
||||
filters.push(query::Filter::eq(
|
||||
EmailSubmissionField::EmailId,
|
||||
id.id().serialize(),
|
||||
@@ -61,7 +62,7 @@ impl EmailSubmissionQuery for Server {
|
||||
}
|
||||
EmailSubmissionFilter::ThreadIds(ids) => {
|
||||
filters.push(query::Filter::Or);
|
||||
for id in ids {
|
||||
for id in ids.into_valid() {
|
||||
filters.push(query::Filter::eq(
|
||||
EmailSubmissionField::ThreadId,
|
||||
id.document_id().serialize(),
|
||||
|
||||
Reference in New Issue
Block a user