diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index c35a4b22..1c1142f8 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -409,6 +409,11 @@ impl AccessToken { self.primary_id } + #[inline(always)] + pub fn tenant_id(&self) -> Option { + self.tenant.as_ref().map(|t| t.id) + } + pub fn secondary_ids(&self) -> impl Iterator { self.member_of .iter() diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 7ec54752..e7b68556 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -158,7 +158,7 @@ impl Server { AccountName = username.to_string(), SpanId = req.session_id, AccountId = principal.id(), - Type = principal.typ().as_str(), + Type = principal.typ().description(), ); return Ok(principal); @@ -316,25 +316,3 @@ impl CredentialsUsername for Credentials { } } } - -pub trait AsTenantId { - fn tenant_id(&self) -> Option; -} - -impl AsTenantId for Option { - fn tenant_id(&self) -> Option { - *self - } -} - -impl AsTenantId for AccessToken { - fn tenant_id(&self) -> Option { - self.tenant.map(|t| t.id) - } -} - -impl AsTenantId for ResourceToken { - fn tenant_id(&self) -> Option { - self.tenant.map(|t| t.id) - } -} diff --git a/crates/common/src/config/jmap/settings.rs b/crates/common/src/config/jmap/settings.rs index ca751d27..73cad268 100644 --- a/crates/common/src/config/jmap/settings.rs +++ b/crates/common/src/config/jmap/settings.rs @@ -18,6 +18,7 @@ pub struct JmapConfig { pub changes_max_results: Option, pub changes_max_history: Option, + pub share_notification_max_history: Option, pub request_max_size: usize, pub request_max_calls: usize, @@ -223,6 +224,9 @@ impl JmapConfig { changes_max_history: config .property_or_default::>("changes.max-history", "10000") .unwrap_or_default(), + share_notification_max_history: config + .property_or_default::>("sharing.max-history", "30d") + .unwrap_or_default(), snippet_max_results: config .property("jmap.protocol.search-snippet.max-results") .unwrap_or(100), diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index f1e5987a..d2938723 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -41,6 +41,7 @@ use types::{ field::{EmailField, Field}, type_state::{DataType, StateChange}, }; +use utils::snowflake::SnowflakeIdGenerator; impl Server { #[inline(always)] @@ -670,106 +671,131 @@ impl Server { Ok(assigned_ids) } - pub async fn delete_changes(&self, account_id: u32, max_entries: usize) -> trc::Result<()> { - for sync_collection in [ - SyncCollection::Email, - SyncCollection::Thread, - SyncCollection::Identity, - SyncCollection::EmailSubmission, - SyncCollection::SieveScript, - SyncCollection::FileNode, - SyncCollection::AddressBook, - SyncCollection::Calendar, - ] { - let collection = sync_collection.into(); - let from_key = LogKey { - account_id, - collection, - change_id: 0, - }; - let to_key = LogKey { - account_id, - collection, - change_id: u64::MAX, - }; + pub async fn delete_changes( + &self, + account_id: u32, + max_entries: Option, + max_duration: Option, + ) -> trc::Result<()> { + if let Some(max_entries) = max_entries { + for sync_collection in [ + SyncCollection::Email, + SyncCollection::Thread, + SyncCollection::Identity, + SyncCollection::EmailSubmission, + SyncCollection::SieveScript, + SyncCollection::FileNode, + SyncCollection::AddressBook, + SyncCollection::Calendar, + ] { + let collection = sync_collection.into(); + let from_key = LogKey { + account_id, + collection, + change_id: 0, + }; + let to_key = LogKey { + account_id, + collection, + change_id: u64::MAX, + }; - let mut first_change_id = 0; - let mut num_changes = 0; + let mut first_change_id = 0; + let mut num_changes = 0; - self.store() - .iterate( - IterateParams::new(from_key, to_key) - .descending() - .no_values(), - |key, _| { - first_change_id = key.deserialize_be_u64(key.len() - U64_LEN)?; - num_changes += 1; - - Ok(num_changes <= max_entries) - }, - ) - .await - .caused_by(trc::location!())?; - - if num_changes > max_entries { self.store() - .delete_range( - LogKey { - account_id, - collection, - change_id: 0, - }, - LogKey { - account_id, - collection, - change_id: first_change_id, + .iterate( + IterateParams::new(from_key, to_key) + .descending() + .no_values(), + |key, _| { + first_change_id = key.deserialize_be_u64(key.len() - U64_LEN)?; + num_changes += 1; + + Ok(num_changes <= max_entries) }, ) .await .caused_by(trc::location!())?; - // Delete vanished items - if let Some(vanished_collection) = - sync_collection.vanished_collection().map(u8::from) - { + if num_changes > max_entries { self.store() .delete_range( LogKey { account_id, - collection: vanished_collection, + collection, change_id: 0, }, LogKey { account_id, - collection: vanished_collection, + collection, change_id: first_change_id, }, ) .await .caused_by(trc::location!())?; - } - // Write truncation entry for cache - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id).set( - ValueClass::Any(AnyClass { - subspace: SUBSPACE_LOGS, - key: LogKey { - account_id, - collection, - change_id: first_change_id, - } - .serialize(0), - }), - Vec::new(), - ); - self.store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; + // Delete vanished items + if let Some(vanished_collection) = + sync_collection.vanished_collection().map(u8::from) + { + self.store() + .delete_range( + LogKey { + account_id, + collection: vanished_collection, + change_id: 0, + }, + LogKey { + account_id, + collection: vanished_collection, + change_id: first_change_id, + }, + ) + .await + .caused_by(trc::location!())?; + } + + // Write truncation entry for cache + let mut batch = BatchBuilder::new(); + batch.with_account_id(account_id).set( + ValueClass::Any(AnyClass { + subspace: SUBSPACE_LOGS, + key: LogKey { + account_id, + collection, + change_id: first_change_id, + } + .serialize(0), + }), + Vec::new(), + ); + self.store() + .write(batch.build_all()) + .await + .caused_by(trc::location!())?; + } } } + if let Some(max_duration) = max_duration { + self.store() + .delete_range( + LogKey { + account_id, + collection: SyncCollection::ShareNotification.into(), + change_id: 0, + }, + LogKey { + account_id, + collection: SyncCollection::ShareNotification.into(), + change_id: SnowflakeIdGenerator::from_duration(max_duration) + .unwrap_or_default(), + }, + ) + .await + .caused_by(trc::location!())?; + } Ok(()) } diff --git a/crates/common/src/sharing/mod.rs b/crates/common/src/sharing/mod.rs index 3a30e69b..43421d47 100644 --- a/crates/common/src/sharing/mod.rs +++ b/crates/common/src/sharing/mod.rs @@ -11,6 +11,7 @@ use utils::map::bitmap::Bitmap; pub mod acl; pub mod document; +pub mod notification; pub mod resources; pub trait EffectiveAcl { diff --git a/crates/common/src/sharing/notification.rs b/crates/common/src/sharing/notification.rs new file mode 100644 index 00000000..78e0a806 --- /dev/null +++ b/crates/common/src/sharing/notification.rs @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use store::{Deserialize, SerializeInfallible, U32_LEN, U64_LEN, write::key::KeySerializer}; +use types::{acl::Acl, collection::Collection}; +use utils::map::bitmap::Bitmap; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ShareNotification { + pub object_account_id: u32, + pub object_id: u32, + pub object_type: Collection, + pub changed_by: u32, + pub old_rights: Bitmap, + pub new_rights: Bitmap, + pub name: String, +} + +impl SerializeInfallible for ShareNotification { + fn serialize(&self) -> Vec { + KeySerializer::new(U64_LEN * 2 + U32_LEN * 3 + 1 + self.name.len()) + .write(self.object_account_id) + .write(self.object_id) + .write(self.object_type as u8) + .write(self.changed_by) + .write(self.old_rights.bitmap) + .write(self.new_rights.bitmap) + .write(self.name.as_bytes()) + .finalize() + } +} + +impl Deserialize for ShareNotification { + fn deserialize(bytes: &[u8]) -> trc::Result { + Self::deserialize_from_slice(bytes) + .ok_or(trc::StoreEvent::DataCorruption.caused_by(trc::location!())) + } +} + +impl ShareNotification { + fn deserialize_from_slice(bytes: &[u8]) -> Option { + Some(Self { + object_account_id: bytes + .get(..U32_LEN) + .and_then(|b| b.try_into().ok()) + .map(u32::from_be_bytes)?, + object_id: bytes + .get(U32_LEN..U32_LEN * 2) + .and_then(|b| b.try_into().ok()) + .map(u32::from_be_bytes)?, + object_type: bytes.get(U32_LEN * 2).copied().map(Collection::from)?, + changed_by: bytes + .get(U32_LEN * 2 + 1..U32_LEN * 3 + 1) + .and_then(|b| b.try_into().ok()) + .map(u32::from_be_bytes)?, + old_rights: bytes + .get(U32_LEN * 3 + 1..U32_LEN * 3 + U64_LEN + 1) + .and_then(|b| b.try_into().ok()) + .map(u64::from_be_bytes) + .map(Bitmap::from)?, + new_rights: bytes + .get(U32_LEN * 3 + U64_LEN + 1..U32_LEN * 3 + U64_LEN * 2 + 1) + .and_then(|b| b.try_into().ok()) + .map(u64::from_be_bytes) + .map(Bitmap::from)?, + name: bytes + .get(U32_LEN * 3 + U64_LEN * 2 + 1..) + .and_then(|b| String::from_utf8(b.to_vec()).ok()) + .unwrap_or_default(), + }) + } +} diff --git a/crates/common/src/storage/index.rs b/crates/common/src/storage/index.rs index 6d2d2ad3..dc29dc3e 100644 --- a/crates/common/src/storage/index.rs +++ b/crates/common/src/storage/index.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::auth::AsTenantId; +use crate::{auth::AccessToken, sharing::notification::ShareNotification}; use ahash::AHashSet; use rkyv::{ option::ArchivedOption, @@ -14,9 +14,18 @@ use rkyv::{ use std::{borrow::Cow, fmt::Debug}; use store::{ Serialize, SerializeInfallible, - write::{Archive, Archiver, BatchBuilder, BlobOp, DirectoryClass, IntoOperations, TagValue}, + write::{ + Archive, Archiver, BatchBuilder, BlobOp, DirectoryClass, IntoOperations, TagValue, + ValueClass, + }, }; -use types::{acl::AclGrant, blob_hash::BlobHash, collection::SyncCollection, field::Field}; +use types::{ + acl::AclGrant, + blob_hash::BlobHash, + collection::{Collection, SyncCollection}, + field::Field, +}; +use utils::{map::bitmap::Bitmap, snowflake::SnowflakeIdGenerator}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IndexValue<'x> { @@ -230,6 +239,7 @@ pub trait IndexableAndSerializableObject: #[derive(Debug)] pub struct ObjectIndexBuilder { + changed_by: u32, tenant_id: Option, current: Option>, changes: Option, @@ -247,6 +257,7 @@ impl ObjectIndexBuilder ObjectIndexBuilder Self { - self.tenant_id = tenant.tenant_id(); + pub fn with_access_token(mut self, access_token: &AccessToken) -> Self { + self.tenant_id = access_token.tenant.as_ref().map(|t| t.id); + self.changed_by = access_token.primary_id(); self } } @@ -291,7 +303,7 @@ impl IntoOperations (None, Some(changes)) => { // Insertion for item in changes.index_values() { - build_index(batch, item, self.tenant_id, true); + build_index(batch, item, self.changed_by, self.tenant_id, true); } if N::is_versioned() { let (offset, bytes) = Archiver::new(changes).serialize_versioned()?; @@ -305,7 +317,7 @@ impl IntoOperations batch.assert_value(Field::ARCHIVE, ¤t); for (current, change) in current.inner.index_values().zip(changes.index_values()) { if current != change { - merge_index(batch, current, change, self.tenant_id)?; + merge_index(batch, current, change, self.changed_by, self.tenant_id)?; } else { match current { IndexValue::LogContainer { sync_collection } => { @@ -332,7 +344,7 @@ impl IntoOperations // Deletion batch.assert_value(Field::ARCHIVE, ¤t); for item in current.inner.index_values() { - build_index(batch, item, self.tenant_id, false); + build_index(batch, item, self.changed_by, self.tenant_id, false); } batch.clear(Field::ARCHIVE); @@ -344,7 +356,13 @@ impl IntoOperations } } -fn build_index(batch: &mut BatchBuilder, item: IndexValue<'_>, tenant_id: Option, set: bool) { +fn build_index( + batch: &mut BatchBuilder, + item: IndexValue<'_>, + changed_by: u32, + tenant_id: Option, + set: bool, +) { match item { IndexValue::Index { field, value } => { if !value.is_empty() { @@ -381,11 +399,52 @@ fn build_index(batch: &mut BatchBuilder, item: IndexValue<'_>, tenant_id: Option } } IndexValue::Acl { value } => { + let object_account_id = batch.last_account_id().unwrap_or_default(); + let object_type = batch.last_collection().unwrap_or(Collection::None); + let object_id = batch.last_document_id().unwrap_or_default(); + let notification_id = SnowflakeIdGenerator::from_sequence_and_node_id( + object_type as u64 ^ object_account_id as u64, + None, + ) + .unwrap_or_default(); + for item in value.as_ref() { if set { batch.acl_grant(item.account_id, item.grants.bitmap.serialize()); + batch.set( + ValueClass::ShareNotification { + notification_id, + notify_account_id: item.account_id, + }, + ShareNotification { + object_account_id, + object_id, + object_type, + changed_by, + old_rights: Default::default(), + new_rights: item.grants, + name: Default::default(), + } + .serialize(), + ); } else { batch.acl_revoke(item.account_id); + batch.set( + ValueClass::ShareNotification { + notification_id, + notify_account_id: item.account_id, + }, + ShareNotification { + object_account_id, + object_id, + object_type, + changed_by, + old_rights: item.grants, + new_rights: Default::default(), + name: Default::default(), + } + .serialize(), + ); } } } @@ -432,6 +491,7 @@ fn merge_index( batch: &mut BatchBuilder, current: IndexValue<'_>, change: IndexValue<'_>, + changed_by: u32, tenant_id: Option, ) -> trc::Result<()> { match (current, change) { @@ -499,7 +559,23 @@ fn merge_index( batch.set(BlobOp::Link { hash: new_hash }, vec![]); } (IndexValue::Acl { value: old_acl }, IndexValue::Acl { value: new_acl }) => { - match (!old_acl.is_empty(), !new_acl.is_empty()) { + let has_old_acl = !old_acl.is_empty(); + let has_new_acl = !new_acl.is_empty(); + + if !has_old_acl && !has_new_acl { + return Ok(()); + } + + let object_account_id = batch.last_account_id().unwrap_or_default(); + let object_type = batch.last_collection().unwrap_or(Collection::None); + let object_id = batch.last_document_id().unwrap_or_default(); + let notification_id = SnowflakeIdGenerator::from_sequence_and_node_id( + object_type as u64 ^ object_account_id as u64, + None, + ) + .unwrap_or_default(); + + match (has_old_acl, has_new_acl) { (true, true) => { // Remove deleted ACLs for current_item in old_acl.as_ref() { @@ -508,22 +584,57 @@ fn merge_index( .any(|item| item.account_id == current_item.account_id) { batch.acl_revoke(current_item.account_id); + batch.set( + ValueClass::ShareNotification { + notification_id, + notify_account_id: current_item.account_id, + }, + ShareNotification { + object_account_id, + object_id, + object_type, + changed_by, + old_rights: current_item.grants, + new_rights: Default::default(), + name: Default::default(), + } + .serialize(), + ); } } // Update ACLs for item in new_acl.as_ref() { let mut add_item = true; + let mut old_rights = Bitmap::default(); for current_item in old_acl.as_ref() { if item.account_id == current_item.account_id { if item.grants == current_item.grants { add_item = false; + } else { + old_rights = current_item.grants; } break; } } if add_item { batch.acl_grant(item.account_id, item.grants.bitmap.serialize()); + batch.set( + ValueClass::ShareNotification { + notification_id, + notify_account_id: item.account_id, + }, + ShareNotification { + object_account_id, + object_id, + object_type, + changed_by, + old_rights, + new_rights: item.grants, + name: Default::default(), + } + .serialize(), + ); } } } @@ -531,12 +642,44 @@ fn merge_index( // Add all ACLs for item in new_acl.as_ref() { batch.acl_grant(item.account_id, item.grants.bitmap.serialize()); + batch.set( + ValueClass::ShareNotification { + notification_id, + notify_account_id: item.account_id, + }, + ShareNotification { + object_account_id, + object_id, + object_type, + changed_by, + old_rights: Default::default(), + new_rights: item.grants, + name: Default::default(), + } + .serialize(), + ); } } (true, false) => { // Remove all ACLs for item in old_acl.as_ref() { batch.acl_revoke(item.account_id); + batch.set( + ValueClass::ShareNotification { + notification_id, + notify_account_id: item.account_id, + }, + ShareNotification { + object_account_id, + object_id, + object_type, + changed_by, + old_rights: item.grants, + new_rights: Default::default(), + name: Default::default(), + } + .serialize(), + ); } } _ => {} diff --git a/crates/dav/src/calendar/scheduling.rs b/crates/dav/src/calendar/scheduling.rs index 30a74d88..7fc45edc 100644 --- a/crates/dav/src/calendar/scheduling.rs +++ b/crates/dav/src/calendar/scheduling.rs @@ -73,7 +73,11 @@ impl CalendarEventNotificationHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, SyncCollection::CalendarEventNotification) + .fetch_dav_resources( + access_token, + account_id, + SyncCollection::CalendarEventNotification, + ) .await .caused_by(trc::location!())?; let resource = resources @@ -129,7 +133,7 @@ impl CalendarEventNotificationHandler for Server { .with_etag(etag) .with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string()); - let ical = event.itip.to_string(); + let ical = event.event.to_string(); if !is_head { Ok(response.with_binary_body(ical)) @@ -154,7 +158,11 @@ impl CalendarEventNotificationHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let resources = self - .fetch_dav_resources(access_token, account_id, SyncCollection::CalendarEventNotification) + .fetch_dav_resources( + access_token, + account_id, + SyncCollection::CalendarEventNotification, + ) .await .caused_by(trc::location!())?; @@ -173,7 +181,11 @@ impl CalendarEventNotificationHandler for Server { let document_id = resource.document_id(); let event_ = self - .get_archive(account_id, Collection::CalendarEventNotification, document_id) + .get_archive( + account_id, + Collection::CalendarEventNotification, + document_id, + ) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 54556457..5971c9ca 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -28,10 +28,7 @@ use crate::{ }, }; use calcard::common::timezone::Tz; -use common::{ - DavResourcePath, DavResources, Server, - auth::{AccessToken, AsTenantId}, -}; +use common::{DavResourcePath, DavResources, Server, auth::AccessToken}; use dav_proto::{ Depth, RequestHeaders, parser::header::dav_base_uri, @@ -989,7 +986,7 @@ impl PropFindRequestHandler for Server { ) => { fields.push(DavPropertyValue::new( property.clone(), - DavValue::CData(event.inner.itip.to_string()), + DavValue::CData(event.inner.event.to_string()), )); } (CalDavProperty::ScheduleTag, ArchivedResource::CalendarEvent(event)) diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index 02724080..27e4c571 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -495,7 +495,7 @@ async fn copy_container( .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(node) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .caused_by(trc::location!())? .commit_point(); @@ -512,7 +512,7 @@ async fn copy_container( .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(node), ) .caused_by(trc::location!())? diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index 9e0c5e70..57ab90c5 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -188,7 +188,7 @@ impl FileUpdateRequestHandler for Server { ObjectIndexBuilder::new() .with_current(node) .with_changes(new_node) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .caused_by(trc::location!())?; let etag = batch.etag(); @@ -283,7 +283,7 @@ impl FileUpdateRequestHandler for Server { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(node) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .caused_by(trc::location!())?; let etag = batch.etag(); diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index b877d360..07e728cc 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -83,7 +83,9 @@ impl PrincipalPropFind for Server { response.set_namespace(Namespace::CardDav); false } - Collection::Calendar | Collection::CalendarEvent | Collection::CalendarEventNotification => { + Collection::Calendar + | Collection::CalendarEvent + | Collection::CalendarEventNotification => { response.set_namespace(Namespace::CalDav); false } diff --git a/crates/dav/src/principal/propsearch.rs b/crates/dav/src/principal/propsearch.rs index fbd6d9e8..f29071f4 100644 --- a/crates/dav/src/principal/propsearch.rs +++ b/crates/dav/src/principal/propsearch.rs @@ -5,10 +5,7 @@ */ use super::propfind::PrincipalPropFind; -use common::{ - Server, - auth::{AccessToken, AsTenantId}, -}; +use common::{Server, auth::AccessToken}; use dav_proto::schema::{ property::{DavProperty, WebDavProperty}, request::{PrincipalPropertySearch, PropFind}, diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 525c28a6..f63dbfd4 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -300,7 +300,7 @@ impl ManageDirectory for Store { trc::LimitEvent::TenantQuota .into_err() .details("Tenant principal quota exceeded") - .ctx(trc::Key::Details, principal_set.typ().as_str()) + .ctx(trc::Key::Details, principal_set.typ().description()) .ctx(trc::Key::Limit, limit) .ctx(trc::Key::Total, total) ); @@ -1533,7 +1533,7 @@ impl ManageDirectory for Store { "Principal {member:?} is not one of {}.", allowed_member_types .iter() - .map(|v| v.as_str()) + .map(|v| v.description()) .collect::>() .join(", ") ) @@ -1619,7 +1619,7 @@ impl ManageDirectory for Store { "Principal {member:?} is not one of {}.", allowed_member_types .iter() - .map(|v| v.as_str()) + .map(|v| v.description()) .collect::>() .join(", ") ) @@ -2511,7 +2511,7 @@ fn validate_member_of( "Principal {member_name:?} is not a {}.", expected_types .iter() - .map(|t| t.as_str().to_string()) + .map(|t| t.description().to_string()) .collect::>() .join(", ") ) diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index fabdb57a..ab413543 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -911,7 +911,7 @@ pub(crate) fn build_search_index( } impl Type { - pub fn to_jmap(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { Self::Individual => "individual", Self::Group => "group", @@ -927,7 +927,7 @@ impl Type { } } - pub fn as_str(&self) -> &'static str { + pub fn description(&self) -> &'static str { match self { Self::Individual => "Individual", Self::Group => "Group", @@ -997,7 +997,7 @@ impl serde::Serialize for PrincipalSet { let mut map = serializer.serialize_map(None)?; map.serialize_entry("id", &self.id)?; - map.serialize_entry("type", &self.typ.to_jmap())?; + map.serialize_entry("type", &self.typ.as_str())?; for (key, value) in &self.fields { match value { diff --git a/crates/email/src/message/delete.rs b/crates/email/src/message/delete.rs index 1c0fd5fb..60dfbd08 100644 --- a/crates/email/src/message/delete.rs +++ b/crates/email/src/message/delete.rs @@ -165,8 +165,13 @@ impl EmailDeletion for Server { } // Purge changelogs - if let Some(history) = self.core.jmap.changes_max_history - && let Err(err) = self.delete_changes(account_id, history).await + if let Err(err) = self + .delete_changes( + account_id, + self.core.jmap.changes_max_history, + self.core.jmap.share_notification_max_history, + ) + .await { trc::error!( err.details("Failed to purge changes.") diff --git a/crates/email/src/sieve/delete.rs b/crates/email/src/sieve/delete.rs index d21e5395..68dac1ac 100644 --- a/crates/email/src/sieve/delete.rs +++ b/crates/email/src/sieve/delete.rs @@ -5,7 +5,7 @@ */ use super::SieveScript; -use common::{Server, auth::ResourceToken, storage::index::ObjectIndexBuilder}; +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use store::write::BatchBuilder; use trc::AddContext; use types::{collection::Collection, field::SieveField}; @@ -13,7 +13,7 @@ use types::{collection::Collection, field::SieveField}; pub trait SieveScriptDelete: Sync + Send { fn sieve_script_delete( &self, - resource_token: &ResourceToken, + access_token: &AccessToken, document_id: u32, fail_if_active: bool, batch: &mut BatchBuilder, @@ -23,13 +23,13 @@ pub trait SieveScriptDelete: Sync + Send { impl SieveScriptDelete for Server { async fn sieve_script_delete( &self, - resource_token: &ResourceToken, + access_token: &AccessToken, document_id: u32, fail_if_active: bool, batch: &mut BatchBuilder, ) -> trc::Result> { // Fetch record - let account_id = resource_token.account_id; + let account_id = access_token.primary_id(); let obj_ = if let Some(obj) = self .get_archive(account_id, Collection::SieveScript, document_id) .await? @@ -57,7 +57,7 @@ impl SieveScriptDelete for Server { .custom( ObjectIndexBuilder::<_, ()>::new() .with_current(obj) - .with_tenant_id(resource_token), + .with_access_token(access_token), ) .caused_by(trc::location!())? .commit_point(); diff --git a/crates/groupware/src/calendar/index.rs b/crates/groupware/src/calendar/index.rs index 8193d644..416f0bd9 100644 --- a/crates/groupware/src/calendar/index.rs +++ b/crates/groupware/src/calendar/index.rs @@ -190,6 +190,10 @@ impl IndexableObject for CalendarEventNotification { field: CalendarField::Created.into(), value: self.created.into(), }, + IndexValue::Index { + field: CalendarField::EventId.into(), + value: self.event_id.unwrap_or(u32::MAX).into(), + }, IndexValue::LogItem { sync_collection: SyncCollection::CalendarEventNotification, prefix: None, @@ -209,6 +213,15 @@ impl IndexableObject for &ArchivedCalendarEventNotification { field: CalendarField::Created.into(), value: self.created.to_native().into(), }, + IndexValue::Index { + field: CalendarField::EventId.into(), + value: self + .event_id + .as_ref() + .map(|v| v.to_native()) + .unwrap_or(u32::MAX) + .into(), + }, IndexValue::LogItem { sync_collection: SyncCollection::CalendarEventNotification, prefix: None, diff --git a/crates/groupware/src/calendar/itip.rs b/crates/groupware/src/calendar/itip.rs index 62fe2920..c098481f 100644 --- a/crates/groupware/src/calendar/itip.rs +++ b/crates/groupware/src/calendar/itip.rs @@ -7,7 +7,10 @@ use crate::{ RFC_3986, cache::GroupwareCache, - calendar::{CalendarEvent, CalendarEventData, CalendarEventNotification}, + calendar::{ + CalendarEvent, CalendarEventData, CalendarEventNotification, ChangedBy, + EVENT_NOTIFICATION_IS_CHANGE, + }, scheduling::{ ItipError, ItipMessage, inbound::{ @@ -139,6 +142,13 @@ impl ItipIngest for Server { )); } + // Obtain changedBy + let changed_by = if let Some(id) = self.email_to_id(self.directory(), sender, 0).await? { + ChangedBy::PrincipalId(id) + } else { + ChangedBy::CalendarAddress(sender.into()) + }; + // Find event by UID let account_id = access_token.primary_id; let document_id = self @@ -232,8 +242,10 @@ impl ItipIngest for Server { .await .caused_by(trc::location!())?; let itip_message = CalendarEventNotification { - itip, + event: itip, + changed_by, event_id: Some(document_id), + flags: EVENT_NOTIFICATION_IS_CHANGE, size: itip_message.len() as u32, ..Default::default() }; @@ -267,6 +279,7 @@ impl ItipIngest for Server { } else { // Verify that auto-adding invitations is allowed if !self.core.groupware.itip_auto_add + && !matches!(changed_by, ChangedBy::PrincipalId(_)) && self .store() .filter( @@ -337,8 +350,9 @@ impl ItipIngest for Server { .await .caused_by(trc::location!())?; let itip_message = CalendarEventNotification { - itip, + event: itip, event_id: Some(document_id), + changed_by, size: itip_message.len() as u32, ..Default::default() }; diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 3059ef8f..c1effd4b 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -55,6 +55,24 @@ pub struct DefaultAlert { pub flags: u16, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +pub struct ParticipantIdentities { + pub identities: Vec, + pub default_name: String, + pub default: u32, +} + +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +pub struct ParticipantIdentity { + pub id: u32, + pub name: Option, + pub calendar_address: String, +} + pub const ALERT_WITH_TIME: u16 = 1; pub const ALERT_EMAIL: u16 = 1 << 1; pub const ALERT_RELATIVE_TO_END: u16 = 1 << 2; @@ -68,6 +86,9 @@ pub const EVENT_HIDE_ATTENDEES: u16 = 1 << 2; pub const EVENT_DRAFT: u16 = 1 << 3; pub const EVENT_ORIGIN: u16 = 1 << 4; +pub const EVENT_NOTIFICATION_IS_DRAFT: u16 = 1; +pub const EVENT_NOTIFICATION_IS_CHANGE: u16 = 1 << 1; + pub const PREF_USE_DEFAULT_ALERTS: u16 = 1; #[derive( @@ -90,14 +111,21 @@ pub struct CalendarEvent { rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct CalendarEventNotification { - pub itip: ICalendar, + pub event: ICalendar, pub event_id: Option, + pub changed_by: ChangedBy, pub flags: u16, pub size: u32, pub created: i64, pub modified: i64, } +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] +pub enum ChangedBy { + PrincipalId(u32), + CalendarAddress(String), +} + #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] @@ -285,3 +313,9 @@ impl ArchivedCalendarEvent { .find(|p| p.account_id == access_token.primary_id()) } } + +impl Default for ChangedBy { + fn default() -> Self { + ChangedBy::CalendarAddress("".into()) + } +} diff --git a/crates/groupware/src/calendar/storage.rs b/crates/groupware/src/calendar/storage.rs index 81742571..50f27ca9 100644 --- a/crates/groupware/src/calendar/storage.rs +++ b/crates/groupware/src/calendar/storage.rs @@ -141,7 +141,7 @@ impl CalendarEvent { ObjectIndexBuilder::new() .with_current(event) .with_changes(new_event) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -168,7 +168,7 @@ impl CalendarEvent { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(event) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|batch| { if let Some(next_alarm) = next_alarm { @@ -210,7 +210,7 @@ impl Calendar { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(calendar) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -236,7 +236,7 @@ impl Calendar { ObjectIndexBuilder::new() .with_current(calendar) .with_changes(new_calendar) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -264,7 +264,7 @@ impl CalendarEventNotification { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(event) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|batch| batch.commit_point()) } @@ -326,7 +326,7 @@ impl DestroyArchive> { .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(calendar), ) .caused_by(trc::location!())?; @@ -371,7 +371,7 @@ impl DestroyArchive> { .update_document(document_id) .custom( ObjectIndexBuilder::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(event) .with_changes(new_event), ) @@ -433,7 +433,7 @@ impl DestroyArchive> { batch .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(event), ) .caused_by(trc::location!())?; @@ -458,7 +458,7 @@ impl DestroyArchive> { .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(self.0), ) .caused_by(trc::location!())? diff --git a/crates/groupware/src/contact/storage.rs b/crates/groupware/src/contact/storage.rs index 78fd620d..ceabe792 100644 --- a/crates/groupware/src/contact/storage.rs +++ b/crates/groupware/src/contact/storage.rs @@ -34,7 +34,7 @@ impl ContactCard { ObjectIndexBuilder::new() .with_current(card) .with_changes(new_card) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -60,7 +60,7 @@ impl ContactCard { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(card) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -88,7 +88,7 @@ impl AddressBook { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(book) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -114,7 +114,7 @@ impl AddressBook { ObjectIndexBuilder::new() .with_current(book) .with_changes(new_book) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -174,7 +174,7 @@ impl DestroyArchive> { .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(book), ) .caused_by(trc::location!())?; @@ -220,7 +220,7 @@ impl DestroyArchive> { .update_document(document_id) .custom( ObjectIndexBuilder::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(card) .with_changes(new_card), ) @@ -231,7 +231,7 @@ impl DestroyArchive> { .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(card), ) .caused_by(trc::location!())?; @@ -260,7 +260,7 @@ impl DestroyArchive> { .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current(self.0), ) .caused_by(trc::location!()) diff --git a/crates/groupware/src/file/storage.rs b/crates/groupware/src/file/storage.rs index f99f6435..58b5b1b1 100644 --- a/crates/groupware/src/file/storage.rs +++ b/crates/groupware/src/file/storage.rs @@ -33,7 +33,7 @@ impl FileNode { .custom( ObjectIndexBuilder::<(), _>::new() .with_changes(node) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -56,7 +56,7 @@ impl FileNode { ObjectIndexBuilder::new() .with_current(node) .with_changes(new_node) - .with_tenant_id(access_token), + .with_access_token(access_token), ) .map(|b| b.commit_point()) } @@ -79,7 +79,7 @@ impl DestroyArchive> { .custom( ObjectIndexBuilder::<_, ()>::new() .with_current(self.0) - .with_tenant_id(access_token), + .with_access_token(access_token), )? .log_vanished_item(VanishedCollection::FileNode, path) .commit_point(); @@ -132,7 +132,7 @@ impl DestroyArchive> { .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) + .with_access_token(access_token) .with_current( node.to_unarchived::() .caused_by(trc::location!())?, diff --git a/crates/jmap-proto/src/object/calendar_event_notification.rs b/crates/jmap-proto/src/object/calendar_event_notification.rs index c0db7982..0620ae25 100644 --- a/crates/jmap-proto/src/object/calendar_event_notification.rs +++ b/crates/jmap-proto/src/object/calendar_event_notification.rs @@ -18,7 +18,7 @@ use types::{blob::BlobId, id::Id}; #[derive(Debug, Clone, Default)] pub struct CalendarEventNotification; -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct CalendarEventNotificationObject { pub id: Id, @@ -49,12 +49,15 @@ pub struct CalendarEventNotificationObject { pub event_patch: Option>, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct PersonObject { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub principal_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub calendar_address: Option, } diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs index 574a8978..bb13e9eb 100644 --- a/crates/jmap-proto/src/object/principal.rs +++ b/crates/jmap-proto/src/object/principal.rs @@ -10,7 +10,7 @@ use types::id::Id; use crate::{ object::{AnyId, JmapObject, JmapObjectId}, - request::deserialize::DeserializeArguments, + request::{capability::Capability, deserialize::DeserializeArguments}, }; #[derive(Debug, Clone, Default)] @@ -25,6 +25,9 @@ pub enum PrincipalProperty { Email, Timezone, Capabilities, + Accounts, + IdValue(Id), + Capability(Capability), } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -56,6 +59,9 @@ impl Property for PrincipalProperty { PrincipalProperty::Name => "name", PrincipalProperty::Timezone => "timezone", PrincipalProperty::Type => "type", + PrincipalProperty::Accounts => "accounts", + PrincipalProperty::Capability(cap) => cap.as_str(), + PrincipalProperty::IdValue(id) => return id.to_string().into(), } .into() } @@ -92,8 +98,9 @@ impl PrincipalProperty { b"name" => PrincipalProperty::Name, b"description" => PrincipalProperty::Description, b"email" => PrincipalProperty::Email, - b"timezone" => PrincipalProperty::Timezone, + b"timeZone" => PrincipalProperty::Timezone, b"capabilities" => PrincipalProperty::Capabilities, + b"accounts" => PrincipalProperty::Accounts, ) } } @@ -192,7 +199,7 @@ impl<'de> DeserializeArguments<'de> for PrincipalFilter { b"type" => { *self = PrincipalFilter::Type(map.next_value()?); }, - b"timezone" => { + b"timeZone" => { *self = PrincipalFilter::Timezone(map.next_value()?); }, _ => { diff --git a/crates/jmap-proto/src/object/share_notification.rs b/crates/jmap-proto/src/object/share_notification.rs index e6b78df9..f779e40d 100644 --- a/crates/jmap-proto/src/object/share_notification.rs +++ b/crates/jmap-proto/src/object/share_notification.rs @@ -148,7 +148,7 @@ impl JmapObject for ShareNotification { pub enum ShareNotificationFilter { After(UTCDate), Before(UTCDate), - ObjectType(String), + ObjectType(DataType), ObjectAccountId(Id), _T(String), } diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index 8e248c69..ba21c9d1 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -51,7 +51,7 @@ struct Account { account_capabilities: VecMap, } -#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum Capability { #[serde(rename(serialize = "urn:ietf:params:jmap:core"))] Core = 1 << 0, @@ -278,6 +278,63 @@ pub struct BaseCapabilities { pub account: VecMap, } +impl Capability { + pub fn as_str(&self) -> &'static str { + match self { + Capability::Core => "urn:ietf:params:jmap:core", + Capability::Mail => "urn:ietf:params:jmap:mail", + Capability::Submission => "urn:ietf:params:jmap:submission", + Capability::VacationResponse => "urn:ietf:params:jmap:vacationresponse", + Capability::Contacts => "urn:ietf:params:jmap:contacts", + Capability::ContactsParse => "urn:ietf:params:jmap:contacts:parse", + Capability::Calendars => "urn:ietf:params:jmap:calendars", + Capability::CalendarsParse => "urn:ietf:params:jmap:calendars:parse", + Capability::WebSocket => "urn:ietf:params:jmap:websocket", + Capability::Sieve => "urn:ietf:params:jmap:sieve", + Capability::Blob => "urn:ietf:params:jmap:blob", + Capability::Quota => "urn:ietf:params:jmap:quota", + Capability::Principals => "urn:ietf:params:jmap:principals", + Capability::PrincipalsOwner => "urn:ietf:params:jmap:principals:owner", + Capability::PrincipalsAvailability => "urn:ietf:params:jmap:principals:availability", + Capability::FileNode => "urn:ietf:params:jmap:filenode", + } + } + + pub fn all_capabilities() -> &'static [Capability] { + &[ + Capability::Core, + Capability::Mail, + Capability::Submission, + Capability::VacationResponse, + Capability::Contacts, + Capability::ContactsParse, + Capability::Calendars, + Capability::CalendarsParse, + Capability::WebSocket, + Capability::Sieve, + Capability::Blob, + Capability::Quota, + Capability::Principals, + Capability::PrincipalsOwner, + Capability::PrincipalsAvailability, + Capability::FileNode, + ] + } + + pub fn all_principal_capabilities() -> &'static [Capability] { + &[ + Capability::Mail, + Capability::Contacts, + Capability::ContactsParse, + Capability::Calendars, + Capability::CalendarsParse, + Capability::Sieve, + Capability::FileNode, + Capability::Principals, + ] + } +} + impl Session { pub fn new(base_url: impl Into, base_capabilities: &BaseCapabilities) -> Session { let base_url = base_url.into(); diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 0da9f4c3..10c536a9 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -274,7 +274,9 @@ impl RequestHandler for Server { self.vacation_response_get(req).await?.into() } - GetRequestMethod::Principal(req) => self.principal_get(req).await?.into(), + GetRequestMethod::Principal(req) => { + self.principal_get(req, access_token).await?.into() + } GetRequestMethod::Quota(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); access_token.assert_is_member(req.account_id)?; @@ -327,7 +329,7 @@ impl RequestHandler for Server { } GetRequestMethod::CalendarEventNotification(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token.assert_has_access(req.account_id, Collection::Calendar)?; + access_token.assert_is_member(req.account_id)?; self.calendar_event_notification_get(req, access_token) .await? @@ -335,17 +337,15 @@ impl RequestHandler for Server { } GetRequestMethod::ParticipantIdentity(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token.assert_has_access(req.account_id, Collection::Calendar)?; + access_token.assert_is_member(req.account_id)?; - self.participant_identity_get(req, access_token) - .await? - .into() + self.participant_identity_get(req).await?.into() } GetRequestMethod::ShareNotification(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); access_token.assert_is_member(req.account_id)?; - self.share_notification_get(req, access_token).await?.into() + self.share_notification_get(req).await?.into() } }, RequestMethod::Query(req) => match req { @@ -375,7 +375,9 @@ impl RequestHandler for Server { } QueryRequestMethod::Principal(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - self.principal_query(req, session).await?.into() + self.principal_query(req, access_token, session) + .await? + .into() } QueryRequestMethod::Quota(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -403,7 +405,7 @@ impl RequestHandler for Server { } QueryRequestMethod::CalendarEventNotification(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token.assert_has_access(req.account_id, Collection::Calendar)?; + access_token.assert_is_member(req.account_id)?; self.calendar_event_notification_query(req, access_token) .await? @@ -411,12 +413,9 @@ impl RequestHandler for Server { } QueryRequestMethod::ShareNotification(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token - .assert_has_access(req.account_id, Collection::ShareNotification)?; + access_token.assert_is_member(req.account_id)?; - self.share_notification_query(req, access_token) - .await? - .into() + self.share_notification_query(req).await?.into() } }, RequestMethod::Set(req) => match req { @@ -488,12 +487,9 @@ impl RequestHandler for Server { } SetRequestMethod::ShareNotification(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token - .assert_has_access(req.account_id, Collection::ShareNotification)?; + access_token.assert_is_member(req.account_id)?; - self.share_notification_set(req, access_token, session) - .await? - .into() + self.share_notification_set(req).await?.into() } SetRequestMethod::Calendar(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -511,7 +507,7 @@ impl RequestHandler for Server { } SetRequestMethod::CalendarEventNotification(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token.assert_has_access(req.account_id, Collection::Calendar)?; + access_token.assert_is_member(req.account_id)?; self.calendar_event_notification_set(req, access_token, session) .await? @@ -519,11 +515,9 @@ impl RequestHandler for Server { } SetRequestMethod::ParticipantIdentity(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); - access_token.assert_has_access(req.account_id, Collection::Calendar)?; + access_token.assert_is_member(req.account_id)?; - self.participant_identity_set(req, access_token, session) - .await? - .into() + self.participant_identity_set(req).await?.into() } }, RequestMethod::Changes(mut req) => { diff --git a/crates/jmap/src/calendar_event_notification/get.rs b/crates/jmap/src/calendar_event_notification/get.rs index a80eae8a..39f34857 100644 --- a/crates/jmap/src/calendar_event_notification/get.rs +++ b/crates/jmap/src/calendar_event_notification/get.rs @@ -4,18 +4,39 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::changes::state::JmapCacheState; +use calcard::{ + icalendar::{ArchivedICalendarProperty, ICalendar}, + jscalendar::import::ConversionOptions, +}; use common::{Server, auth::AccessToken}; +use groupware::{ + cache::GroupwareCache, + calendar::{ + ArchivedChangedBy, CalendarEventNotification, EVENT_NOTIFICATION_IS_CHANGE, + EVENT_NOTIFICATION_IS_DRAFT, + }, +}; use jmap_proto::{ method::get::GetRequest, object::calendar_event_notification::{ - CalendarEventNotification, CalendarEventNotificationGetResponse, + self, CalendarEventNotificationGetResponse, CalendarEventNotificationObject, + CalendarEventNotificationProperty, CalendarEventNotificationType, PersonObject, }, + types::date::UTCDate, +}; +use store::write::serialize::rkyv_deserialize; +use trc::AddContext; +use types::{ + blob::BlobId, + collection::{Collection, SyncCollection}, + id::Id, }; pub trait CalendarEventNotificationGet: Sync + Send { fn calendar_event_notification_get( &self, - request: GetRequest, + request: GetRequest, access_token: &AccessToken, ) -> impl Future> + Send; } @@ -23,9 +44,150 @@ pub trait CalendarEventNotificationGet: Sync + Send { impl CalendarEventNotificationGet for Server { async fn calendar_event_notification_get( &self, - mut request: GetRequest, + mut request: GetRequest, access_token: &AccessToken, ) -> trc::Result { - todo!() + let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; + let properties = request.unwrap_properties(&[ + CalendarEventNotificationProperty::Id, + CalendarEventNotificationProperty::Created, + CalendarEventNotificationProperty::Type, + CalendarEventNotificationProperty::ChangedBy, + ]); + let account_id = request.account_id.document_id(); + let cache = self + .fetch_dav_resources( + access_token, + account_id, + SyncCollection::CalendarEventNotification, + ) + .await + .caused_by(trc::location!())?; + + let ids = if let Some(ids) = ids { + ids + } else { + cache + .document_ids(false) + .take(self.core.jmap.get_max_objects) + .map(Into::into) + .collect::>() + }; + let mut response = CalendarEventNotificationGetResponse { + account_id: request.account_id.into(), + state: cache.get_state(false).into(), + list: Vec::with_capacity(ids.len()), + not_found: vec![], + }; + + for id in ids { + // Obtain the event object + let document_id = id.document_id(); + let _event = if let Some(event) = self + .get_archive( + account_id, + Collection::CalendarEventNotification, + document_id, + ) + .await? + { + event + } else { + response.not_found.push(id); + continue; + }; + let event = _event + .unarchive::() + .caused_by(trc::location!())?; + let mut result = CalendarEventNotificationObject { + id, + ..Default::default() + }; + for property in &properties { + match property { + CalendarEventNotificationProperty::Id => {} + CalendarEventNotificationProperty::Created => { + result.created = Some(UTCDate::from_timestamp(event.created.to_native())); + } + CalendarEventNotificationProperty::CalendarEventId => { + result.calendar_event_id = + event.event_id.as_ref().map(|id| id.to_native().into()); + } + CalendarEventNotificationProperty::ChangedBy => { + let mut changed_by = PersonObject::default(); + + match &event.changed_by { + ArchivedChangedBy::PrincipalId(id) => { + if let Ok(token) = self.get_access_token(id.to_native()).await { + changed_by.name = token.description.clone().unwrap_or_default(); + changed_by.email = token.emails.first().cloned(); + } + changed_by.principal_id = Some(id.to_native().into()); + } + ArchivedChangedBy::CalendarAddress(email) => { + changed_by.email = Some(email.to_string()); + changed_by.calendar_address = Some(format!("mailto:{email}")); + } + } + + result.changed_by = Some(changed_by); + } + CalendarEventNotificationProperty::Comment => { + result.comment = event + .event + .components + .iter() + .filter(|c| c.component_type.is_scheduling_object()) + .flat_map(|c| c.entries.iter()) + .find(|e| matches!(e.name, ArchivedICalendarProperty::Comment)) + .and_then(|e| e.values.first().and_then(|v| v.as_text())) + .map(|v| v.to_string()); + } + CalendarEventNotificationProperty::Type => { + result.notification_type = + Some(if event.flags & EVENT_NOTIFICATION_IS_CHANGE != 0 { + CalendarEventNotificationType::Updated + } else if !event.event.components.is_empty() { + CalendarEventNotificationType::Created + } else { + CalendarEventNotificationType::Destroyed + }); + } + CalendarEventNotificationProperty::IsDraft => { + result.is_draft = Some(event.flags & EVENT_NOTIFICATION_IS_DRAFT != 0); + } + CalendarEventNotificationProperty::Event => { + if event.flags & EVENT_NOTIFICATION_IS_CHANGE == 0 && result.event.is_none() + { + let js_event = rkyv_deserialize::<_, ICalendar>(&event.event) + .caused_by(trc::location!())? + .into_jscalendar_with_opt::( + ConversionOptions::default() + .include_ical_components(false) + .return_first(true), + ); + result.event = js_event.into(); + } + } + CalendarEventNotificationProperty::EventPatch => { + if event.flags & EVENT_NOTIFICATION_IS_CHANGE != 0 + && result.event_patch.is_none() + { + let js_event = rkyv_deserialize::<_, ICalendar>(&event.event) + .caused_by(trc::location!())? + .into_jscalendar_with_opt::( + ConversionOptions::default() + .include_ical_components(false) + .return_first(true), + ); + result.event_patch = js_event.into(); + } + } + } + } + response.list.push(result); + } + + Ok(response) } } diff --git a/crates/jmap/src/calendar_event_notification/query.rs b/crates/jmap/src/calendar_event_notification/query.rs index bde23d74..03c315b9 100644 --- a/crates/jmap/src/calendar_event_notification/query.rs +++ b/crates/jmap/src/calendar_event_notification/query.rs @@ -4,10 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{JmapMethods, changes::state::JmapCacheState}; use common::{Server, auth::AccessToken}; +use groupware::cache::GroupwareCache; use jmap_proto::{ - method::query::{QueryRequest, QueryResponse}, - object::calendar_event_notification::CalendarEventNotification, + method::query::{Comparator, Filter, QueryRequest, QueryResponse}, + object::calendar_event_notification::{ + CalendarEventNotification, CalendarEventNotificationComparator, + CalendarEventNotificationFilter, + }, + request::IntoValid, +}; +use store::{SerializeInfallible, query}; +use types::{ + collection::{Collection, SyncCollection}, + field::CalendarField, }; pub trait CalendarEventNotificationQuery: Sync + Send { @@ -24,6 +35,91 @@ impl CalendarEventNotificationQuery for Server { mut request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { - 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::CalendarEventNotification, + ) + .await?; + + for cond in std::mem::take(&mut request.filter) { + match cond { + Filter::Property(cond) => match cond { + CalendarEventNotificationFilter::Before(before) => { + filters.push(query::Filter::lt( + CalendarField::Created, + (before.timestamp() as u64).serialize(), + )) + } + CalendarEventNotificationFilter::After(after) => { + filters.push(query::Filter::gt( + CalendarField::Created, + (after.timestamp() as u64).serialize(), + )) + } + CalendarEventNotificationFilter::CalendarEventIds(ids) => { + let has_many = ids.len() > 1; + if has_many { + filters.push(query::Filter::Or); + } + for id in ids.into_valid() { + filters.push(query::Filter::eq( + CalendarField::EventId, + id.document_id().serialize(), + )); + } + if has_many { + filters.push(query::Filter::End); + } + } + 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 result_set = self + .filter(account_id, Collection::CalendarEventNotification, filters) + .await?; + + 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.filter(|s| !s.is_empty()).unwrap_or_else(|| { + vec![Comparator::descending( + CalendarEventNotificationComparator::Created, + )] + }) { + comparators.push(match comparator.property { + CalendarEventNotificationComparator::Created => { + query::Comparator::field(CalendarField::Created, comparator.is_ascending) + } + CalendarEventNotificationComparator::_T(unsupported) => { + return Err(trc::JmapEvent::UnsupportedSort + .into_err() + .details(unsupported)); + } + }); + } + + // Sort results + self.sort(result_set, comparators, paginate, response).await + } else { + Ok(response) + } } } diff --git a/crates/jmap/src/calendar_event_notification/set.rs b/crates/jmap/src/calendar_event_notification/set.rs index 802886b1..e6415af8 100644 --- a/crates/jmap/src/calendar_event_notification/set.rs +++ b/crates/jmap/src/calendar_event_notification/set.rs @@ -5,28 +5,107 @@ */ use common::{Server, auth::AccessToken}; +use groupware::{DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification}; use http_proto::HttpSessionData; use jmap_proto::{ + error::set::SetError, method::set::{SetRequest, SetResponse}, - object::calendar_event_notification::CalendarEventNotification, + object::calendar_event_notification, + request::IntoValid, + types::state::State, }; +use store::write::BatchBuilder; +use trc::AddContext; +use types::collection::{Collection, SyncCollection}; pub trait CalendarEventNotificationSet: Sync + Send { fn calendar_event_notification_set( &self, - request: SetRequest<'_, CalendarEventNotification>, + request: SetRequest<'_, calendar_event_notification::CalendarEventNotification>, access_token: &AccessToken, session: &HttpSessionData, - ) -> impl Future>> + Send; + ) -> impl Future< + Output = trc::Result>, + > + Send; } impl CalendarEventNotificationSet for Server { async fn calendar_event_notification_set( &self, - mut request: SetRequest<'_, CalendarEventNotification>, + mut request: SetRequest<'_, calendar_event_notification::CalendarEventNotification>, access_token: &AccessToken, _session: &HttpSessionData, - ) -> trc::Result> { - todo!() + ) -> trc::Result> { + let account_id = request.account_id.document_id(); + let cache = self + .fetch_dav_resources( + access_token, + account_id, + SyncCollection::CalendarEventNotification, + ) + .await?; + let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; + + let mut batch = BatchBuilder::new(); + for (id, _) in request.unwrap_create() { + response.not_created.append( + id, + SetError::forbidden().with_description("Cannot create event notifications."), + ); + } + + // Process updates + for (id, _) in request.unwrap_update().into_valid() { + response.not_updated.append( + id, + SetError::forbidden().with_description("Cannot update event notifications."), + ); + } + + // Process deletions + for id in request.unwrap_destroy().into_valid() { + let document_id = id.document_id(); + + if !cache.has_item_id(&document_id) { + response.not_destroyed.append(id, SetError::not_found()); + continue; + }; + + let _event = if let Some(event) = self + .get_archive( + account_id, + Collection::CalendarEventNotification, + document_id, + ) + .await? + { + event + } else { + response.not_destroyed.append(id, SetError::not_found()); + continue; + }; + let event = _event + .to_unarchived::() + .caused_by(trc::location!())?; + + DestroyArchive(event) + .delete(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) } } diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index 28782841..eff4100c 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -191,7 +191,7 @@ impl QueryChanges for Server { changes = self .changes( build_changes_request(&request), - MethodObject::FileNode, + MethodObject::CalendarEvent, access_token, ) .await? @@ -215,7 +215,7 @@ impl QueryChanges for Server { changes = self .changes( build_changes_request(&request), - MethodObject::FileNode, + MethodObject::CalendarEventNotification, access_token, ) .await? @@ -239,7 +239,7 @@ impl QueryChanges for Server { changes = self .changes( build_changes_request(&request), - MethodObject::FileNode, + MethodObject::ShareNotification, access_token, ) .await? @@ -253,9 +253,7 @@ impl QueryChanges for Server { } up_to_id = request.up_to_id; - results = self - .share_notification_query(request.into(), access_token) - .await?; + results = self.share_notification_query(request.into()).await?; } QueryChangesRequestMethod::Principal(_) => { return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); diff --git a/crates/jmap/src/participant_identity/get.rs b/crates/jmap/src/participant_identity/get.rs index c2918a74..70919075 100644 --- a/crates/jmap/src/participant_identity/get.rs +++ b/crates/jmap/src/participant_identity/get.rs @@ -4,26 +4,187 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::Server; +use directory::QueryParams; +use groupware::calendar::{ParticipantIdentities, ParticipantIdentity}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, - object::participant_identity::ParticipantIdentity, + object::participant_identity::{self, ParticipantIdentityProperty, ParticipantIdentityValue}, }; +use jmap_tools::{Map, Value}; +use store::{ + Serialize, + write::{AlignedBytes, Archive, Archiver, BatchBuilder}, +}; +use trc::AddContext; +use types::{collection::Collection, field::PrincipalField}; pub trait ParticipantIdentityGet: Sync + Send { fn participant_identity_get( &self, - request: GetRequest, - access_token: &AccessToken, - ) -> impl Future>> + Send; + request: GetRequest, + ) -> impl Future>> + Send; + + fn participant_identity_get_or_create( + &self, + account_id: u32, + ) -> impl Future>>> + Send; } impl ParticipantIdentityGet for Server { async fn participant_identity_get( &self, - mut request: GetRequest, - access_token: &AccessToken, - ) -> trc::Result> { - todo!() + mut request: GetRequest, + ) -> trc::Result> { + let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; + let properties = request.unwrap_properties(&[ + ParticipantIdentityProperty::Id, + ParticipantIdentityProperty::Name, + ParticipantIdentityProperty::CalendarAddress, + ParticipantIdentityProperty::IsDefault, + ]); + let account_id = request.account_id.document_id(); + let identities = self.participant_identity_get_or_create(account_id).await?; + + let mut response = GetResponse { + account_id: request.account_id.into(), + state: None, + list: Vec::new(), + not_found: vec![], + }; + + let Some(identities) = identities else { + response.not_found = ids.unwrap_or_default(); + return Ok(response); + }; + + let identities = identities + .unarchive::() + .caused_by(trc::location!())?; + + let ids = if let Some(ids) = ids { + ids + } else { + (0..identities.identities.len() as u32) + .take(self.core.jmap.get_max_objects) + .map(Into::into) + .collect::>() + }; + + for id in ids { + // Obtain the identity object + let document_id = id.document_id(); + let Some(identity) = identities.identities.iter().find(|i| i.id == document_id) else { + response.not_found.push(id); + continue; + }; + let _identity = if let Some(identity) = self + .get_archive(account_id, Collection::Identity, document_id) + .await? + { + identity + } else { + response.not_found.push(id); + continue; + }; + + let mut result = Map::with_capacity(properties.len()); + for property in &properties { + let value = match &property { + ParticipantIdentityProperty::Id => { + Value::Element(ParticipantIdentityValue::Id(id)) + } + ParticipantIdentityProperty::Name => Value::Str( + identity + .name + .as_ref() + .map(|n| n.as_str()) + .unwrap_or(identities.default_name.as_str()) + .to_string() + .into(), + ), + ParticipantIdentityProperty::CalendarAddress => { + Value::Str(identity.calendar_address.to_string().into()) + } + ParticipantIdentityProperty::IsDefault => { + Value::Bool(identities.default == document_id) + } + }; + result.insert_unchecked(property.clone(), value); + } + response.list.push(result.into()); + } + + Ok(response) + } + + async fn participant_identity_get_or_create( + &self, + account_id: u32, + ) -> trc::Result>> { + if let Some(identities) = self + .get_archive_by_property( + account_id, + Collection::Principal, + 0, + PrincipalField::ParticipantIdentities.into(), + ) + .await? + { + return Ok(Some(identities)); + } + + // Obtain principal + let principal = if let Some(principal) = self + .core + .storage + .directory + .query(QueryParams::id(account_id).with_return_member_of(false)) + .await + .caused_by(trc::location!())? + { + principal + } else { + return Ok(None); + }; + let num_emails = principal.emails.len(); + if num_emails == 0 { + return Ok(None); + } + + // Build identities + let identities = Archiver::new(ParticipantIdentities { + identities: principal + .emails + .iter() + .enumerate() + .map(|(id, email)| ParticipantIdentity { + id: id as u32, + name: None, + calendar_address: format!("mailto:{email}"), + }) + .collect(), + default: 0, + default_name: principal.description.unwrap_or(principal.name), + }) + .serialize() + .caused_by(trc::location!())?; + + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Principal) + .update_document(0) + .set(PrincipalField::ParticipantIdentities, identities); + + self.commit_batch(batch).await.caused_by(trc::location!())?; + + self.get_archive_by_property( + account_id, + Collection::Principal, + 0, + PrincipalField::ParticipantIdentities.into(), + ) + .await } } diff --git a/crates/jmap/src/participant_identity/set.rs b/crates/jmap/src/participant_identity/set.rs index d4aa5040..8a705e5a 100644 --- a/crates/jmap/src/participant_identity/set.rs +++ b/crates/jmap/src/participant_identity/set.rs @@ -4,29 +4,240 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use http_proto::HttpSessionData; +use crate::participant_identity::get::ParticipantIdentityGet; +use common::Server; +use directory::QueryParams; +use groupware::calendar::{ParticipantIdentities, ParticipantIdentity}; use jmap_proto::{ + error::set::SetError, method::set::{SetRequest, SetResponse}, - object::participant_identity::ParticipantIdentity, + object::participant_identity::{self, ParticipantIdentityProperty, ParticipantIdentityValue}, + request::{IntoValid, reference::MaybeIdReference}, }; +use jmap_tools::{Key, Value}; +use store::{ + Serialize, + write::{Archiver, BatchBuilder}, +}; +use trc::AddContext; +use types::{collection::Collection, field::PrincipalField}; +use utils::sanitize_email; pub trait ParticipantIdentitySet: Sync + Send { fn participant_identity_set( &self, - request: SetRequest<'_, ParticipantIdentity>, - access_token: &AccessToken, - session: &HttpSessionData, - ) -> impl Future>> + Send; + request: SetRequest<'_, participant_identity::ParticipantIdentity>, + ) -> impl Future>> + Send; } impl ParticipantIdentitySet for Server { async fn participant_identity_set( &self, - mut request: SetRequest<'_, ParticipantIdentity>, - access_token: &AccessToken, - _session: &HttpSessionData, - ) -> trc::Result> { - todo!() + mut request: SetRequest<'_, participant_identity::ParticipantIdentity>, + ) -> trc::Result> { + let account_id = request.account_id.document_id(); + let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; + let will_destroy = request.unwrap_destroy().into_valid().collect::>(); + let (identity_archive, mut identities) = + match self.participant_identity_get_or_create(account_id).await? { + Some(archive) => { + let identities = archive + .deserialize::() + .caused_by(trc::location!())?; + + (Some(archive), identities) + } + None => (None, ParticipantIdentities::default()), + }; + + // Obtain allowed emails + let allowed_emails = self + .directory() + .query(QueryParams::id(account_id).with_return_member_of(false)) + .await? + .map(|p| p.emails) + .unwrap_or_default(); + + // Process creates + let mut has_changes = false; + 'create: for (id, object) in request.unwrap_create() { + let mut identity = ParticipantIdentity::default(); + + if let Err(err) = validate_identity_value(object, &mut identity, &allowed_emails) { + response.not_created.append(id, err); + continue 'create; + } + + if identities + .identities + .iter() + .any(|i| i.calendar_address == identity.calendar_address) + { + response.not_created.append( + id, + SetError::invalid_properties() + .with_property(ParticipantIdentityProperty::CalendarAddress) + .with_description("Calendar address already in use.".to_string()), + ); + continue 'create; + } + + let document_id = identities + .identities + .iter() + .map(|i| i.id) + .max() + .unwrap_or_default() + + 1; + identity.id = document_id; + identities.identities.push(identity); + + if let Some(MaybeIdReference::Reference(id_ref)) = + &request.arguments.on_success_set_is_default + && id_ref == &id + { + identities.default = document_id; + } + + has_changes = true; + 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; + } + + let Some(identity) = identities + .identities + .iter_mut() + .find(|i| i.id == id.document_id()) + else { + response.not_updated.append(id, SetError::not_found()); + continue 'update; + }; + + if let Err(err) = validate_identity_value(object, identity, &allowed_emails) { + response.not_updated.append(id, err); + continue 'update; + } + + has_changes = true; + response.updated.append(id, None); + } + + // Process deletions + for id in &will_destroy { + let document_id = id.document_id(); + if identities.identities.iter().any(|i| i.id == document_id) { + response.destroyed.push(*id); + } else { + response.not_destroyed.append(*id, SetError::not_found()); + } + } + if !response.destroyed.is_empty() { + has_changes = true; + identities + .identities + .retain(|i| !response.destroyed.iter().any(|id| id.document_id() == i.id)); + } + + if let Some(MaybeIdReference::Id(id)) = request.arguments.on_success_set_is_default { + let id = id.document_id(); + if identities.identities.iter().any(|i| i.id == id) { + identities.default = id; + has_changes = true; + } + } + + // Write changes + if has_changes { + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Principal) + .update_document(0); + if let Some(archive) = identity_archive { + batch.assert_value(PrincipalField::ParticipantIdentities, archive); + } + batch.set( + PrincipalField::ParticipantIdentities, + Archiver::new(identities) + .serialize() + .caused_by(trc::location!())?, + ); + + self.commit_batch(batch).await.caused_by(trc::location!())?; + } + + Ok(response) + } +} + +fn validate_identity_value( + update: Value<'_, ParticipantIdentityProperty, ParticipantIdentityValue>, + identity: &mut ParticipantIdentity, + allowed_emails: &[String], +) -> Result<(), SetError> { + let mut changed_address = false; + for (property, value) in update.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) { + (ParticipantIdentityProperty::Name, Value::Str(value)) if value.len() < 255 => { + identity.name = value.into_owned().into(); + } + (ParticipantIdentityProperty::CalendarAddress, Value::Str(value)) => { + if identity.calendar_address != value { + changed_address = true; + identity.calendar_address = value.into_owned(); + } + } + (property, _) => { + return Err(SetError::invalid_properties() + .with_property(property.clone()) + .with_description("Field could not be set.")); + } + } + } + // Validate email address + if !identity.calendar_address.is_empty() { + if !changed_address { + return Ok(()); + } + + let email = if let Some(email) = identity.calendar_address.strip_prefix("mailto:") { + sanitize_email(email) + } else { + sanitize_email(&identity.calendar_address) + }; + + if let Some(email) = email { + if allowed_emails.iter().any(|e| e == &email) { + identity.calendar_address = format!("mailto:{email}"); + Ok(()) + } else { + Err(SetError::invalid_properties() + .with_property(ParticipantIdentityProperty::CalendarAddress) + .with_description( + "Calendar address not configured for this account.".to_string(), + )) + } + } else { + Err(SetError::invalid_properties() + .with_property(ParticipantIdentityProperty::CalendarAddress) + .with_description("Invalid or missing calendar address.".to_string())) + } + } else { + Err(SetError::invalid_properties() + .with_property(ParticipantIdentityProperty::CalendarAddress) + .with_description("Missing calendar address.")) } } diff --git a/crates/jmap/src/principal/availability.rs b/crates/jmap/src/principal/availability.rs index a2ddf60a..7be7b1c9 100644 --- a/crates/jmap/src/principal/availability.rs +++ b/crates/jmap/src/principal/availability.rs @@ -109,7 +109,7 @@ impl PrincipalGetAvailability for Server { } else { PrincipalAddresses::Shared(access_token) }; - + let max_instances = self.core.groupware.max_ical_instances; let filter = TimeRange { start: request.utc_start.timestamp(), end: request.utc_end.timestamp(), @@ -250,13 +250,19 @@ impl PrincipalGetAvailability for Server { let Some(busy_status) = matching_component_ids.get(&expansion.comp_id) else { continue; }; - periods.push(FreeBusyResult { - utc_start: expansion.start, - utc_end: expansion.end, - busy_status: *busy_status, - expansion_id: expansion.comp_id, - document_id, - }); + if periods.len() < max_instances { + periods.push(FreeBusyResult { + utc_start: expansion.start, + utc_end: expansion.end, + busy_status: *busy_status, + expansion_id: expansion.comp_id, + document_id, + }); + } else { + return Err(trc::JmapEvent::RequestTooLarge + .into_err() + .details("The number of expanded instances exceeds the server limit")); + } } } diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index eb456c3c..b1f75073 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -4,21 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::Server; -use directory::QueryParams; +use common::{Server, auth::AccessToken}; +use directory::{QueryParams, Type, backend::internal::manage::ManageDirectory}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, - object::principal::{Principal, PrincipalProperty, PrincipalValue}, + object::principal::{Principal, PrincipalProperty, PrincipalType, PrincipalValue}, + request::capability::Capability, types::state::State, }; -use jmap_tools::{Map, Value}; +use jmap_tools::{Key, Map, Value}; use std::future::Future; -use types::collection::Collection; +use store::roaring::RoaringBitmap; +use types::{collection::Collection, id::Id}; pub trait PrincipalGet: Sync + Send { fn principal_get( &self, request: GetRequest, + access_token: &AccessToken, ) -> impl Future>> + Send; } @@ -26,6 +29,7 @@ impl PrincipalGet for Server { async fn principal_get( &self, mut request: GetRequest, + access_token: &AccessToken, ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ @@ -34,13 +38,20 @@ impl PrincipalGet for Server { PrincipalProperty::Name, PrincipalProperty::Description, PrincipalProperty::Email, - //PrincipalProperty::Timezone, - //PrincipalProperty::Capabilities, ]); - let principal_ids = self - .get_document_ids(u32::MAX, Collection::Principal) - .await? - .unwrap_or_default(); + let principal_ids = if access_token.tenant.is_some() { + self.store() + .list_principals(None, access_token.tenant.map(|t| t.id), &[], false, 0, 0) + .await? + .items + .into_iter() + .map(|p| p.id()) + .collect::() + } else { + self.get_document_ids(u32::MAX, Collection::Principal) + .await? + .unwrap_or_default() + }; let ids = if let Some(ids) = ids { ids } else { @@ -59,12 +70,14 @@ impl PrincipalGet for Server { for id in ids { // Obtain the principal - let principal = if let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::id(id.document_id()).with_return_member_of(false)) - .await? + let document_id = id.document_id(); + let principal = if principal_ids.contains(document_id) + && let Some(principal) = self + .core + .storage + .directory + .query(QueryParams::id(document_id).with_return_member_of(false)) + .await? { principal } else { @@ -77,7 +90,13 @@ impl PrincipalGet for Server { let value = match property { PrincipalProperty::Id => Value::Element(PrincipalValue::Id(id)), PrincipalProperty::Type => { - Value::Str(principal.typ().to_jmap().to_string().into()) + Value::Element(PrincipalValue::Type(match principal.typ() { + Type::Individual => PrincipalType::Individual, + Type::Group => PrincipalType::Group, + Type::Resource => PrincipalType::Resource, + Type::Location => PrincipalType::Location, + _ => PrincipalType::Other, + })) } PrincipalProperty::Name => Value::Str(principal.name().to_string().into()), PrincipalProperty::Description => principal @@ -89,6 +108,11 @@ impl PrincipalGet for Server { .first() .map(|email| Value::Str(email.to_string().into())) .unwrap_or(Value::Null), + PrincipalProperty::Accounts => Value::Object(Map::from(vec![( + Key::Property(PrincipalProperty::IdValue(id)), + build_account(id, principal.name().to_string(), true, false), + )])), + PrincipalProperty::Capabilities => all_capabilities(None), _ => Value::Null, }; @@ -100,3 +124,51 @@ impl PrincipalGet for Server { Ok(response) } } + +fn build_account( + id: Id, + name: String, + is_personal: bool, + is_readonly: bool, +) -> Value<'static, PrincipalProperty, PrincipalValue> { + let mut account = Map::with_capacity(4); + account.insert_unchecked( + Key::Property(PrincipalProperty::Name), + Value::Str(name.into()), + ); + account.insert_unchecked(Key::Borrowed("isPersonal"), Value::Bool(is_personal)); + account.insert_unchecked(Key::Borrowed("isReadOnly"), Value::Bool(is_readonly)); + account.insert_unchecked( + Key::Borrowed("accountCapabilities"), + all_capabilities(id.into()), + ); + Value::Object(account) +} + +fn all_capabilities(id: Option) -> Value<'static, PrincipalProperty, PrincipalValue> { + Value::Object(Map::from_iter( + Capability::all_principal_capabilities() + .iter() + .map(|cap| { + ( + Key::Property(PrincipalProperty::Capability(*cap)), + Value::Object(Map::new()), + ) + }) + .chain(id.map(|id| { + ( + Key::Property(PrincipalProperty::Capability(Capability::PrincipalsOwner)), + Value::Object(Map::from(vec![ + ( + Key::Borrowed("accountIdForPrincipal"), + Value::Element(PrincipalValue::Id(id)), + ), + ( + Key::Borrowed("principalId"), + Value::Element(PrincipalValue::Id(id)), + ), + ])), + ) + })), + )) +} diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index c75dccbe..a371399f 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -5,12 +5,12 @@ */ use crate::JmapMethods; -use common::Server; -use directory::QueryParams; +use common::{Server, auth::AccessToken}; +use directory::{QueryParams, Type, backend::internal::manage::ManageDirectory}; use http_proto::HttpSessionData; use jmap_proto::{ method::query::{Filter, QueryRequest, QueryResponse}, - object::principal::{Principal, PrincipalFilter}, + object::principal::{Principal, PrincipalFilter, PrincipalType}, types::state::State, }; use std::future::Future; @@ -21,6 +21,7 @@ pub trait PrincipalQuery: Sync + Send { fn principal_query( &self, request: QueryRequest, + access_token: &AccessToken, session: &HttpSessionData, ) -> impl Future> + Send; } @@ -29,6 +30,7 @@ impl PrincipalQuery for Server { async fn principal_query( &self, mut request: QueryRequest, + access_token: &AccessToken, session: &HttpSessionData, ) -> trc::Result { let account_id = request.account_id.document_id(); @@ -38,7 +40,19 @@ impl PrincipalQuery for Server { results: RoaringBitmap::new(), }; let mut is_set = true; - let todo = "implement other search criteria"; + let all_ids = if access_token.tenant.is_some() { + self.store() + .list_principals(None, access_token.tenant.map(|t| t.id), &[], false, 0, 0) + .await? + .items + .into_iter() + .map(|p| p.id()) + .collect::() + } else { + self.get_document_ids(u32::MAX, Collection::Principal) + .await? + .unwrap_or_default() + }; for cond in std::mem::take(&mut request.filter) { match cond { @@ -65,7 +79,7 @@ impl PrincipalQuery for Server { PrincipalFilter::Email(email) => { let mut ids = RoaringBitmap::new(); if let Some(id) = self - .email_to_id(&self.core.storage.directory, &email, session.session_id) + .email_to_id(self.directory(), &email, session.session_id) .await? { ids.insert(id); @@ -77,8 +91,76 @@ impl PrincipalQuery for Server { result_set.results &= ids; } } - PrincipalFilter::_T(other) => { - return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other)); + PrincipalFilter::AccountIds(ids) => { + let ids = ids + .into_iter() + .filter_map(|id| { + let id = id.document_id(); + if all_ids.contains(id) { Some(id) } else { None } + }) + .collect::(); + if is_set { + result_set.results = ids; + is_set = false; + } else { + result_set.results &= ids; + } + } + PrincipalFilter::Text(text) => { + let ids = self + .store() + .list_principals( + Some(text.as_str()), + access_token.tenant.map(|t| t.id), + &[], + false, + 0, + 0, + ) + .await? + .items + .into_iter() + .map(|p| p.id()) + .collect::(); + + if is_set { + result_set.results = ids; + is_set = false; + } else { + result_set.results &= ids; + } + } + PrincipalFilter::Type(principal_type) => { + let typ = match principal_type { + PrincipalType::Individual => Type::Individual, + PrincipalType::Group => Type::Group, + PrincipalType::Resource => Type::Resource, + PrincipalType::Location => Type::Location, + PrincipalType::Other => Type::Other, + }; + + let ids = self + .store() + .list_principals( + None, + access_token.tenant.map(|t| t.id), + &[typ], + false, + 0, + 0, + ) + .await? + .items + .into_iter() + .map(|p| p.id()) + .collect::(); + + if is_set { + result_set.results = ids; + is_set = false; + } else { + result_set.results &= ids; + } } other => { return Err(trc::JmapEvent::UnsupportedFilter @@ -95,10 +177,9 @@ impl PrincipalQuery for Server { } if is_set { - result_set.results = self - .get_document_ids(u32::MAX, Collection::Principal) - .await? - .unwrap_or_default(); + result_set.results = all_ids; + } else { + result_set.results &= all_ids; } let (response, paginate) = self diff --git a/crates/jmap/src/share_notification/get.rs b/crates/jmap/src/share_notification/get.rs index 60e2f669..93c16bee 100644 --- a/crates/jmap/src/share_notification/get.rs +++ b/crates/jmap/src/share_notification/get.rs @@ -4,26 +4,240 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{Server, sharing::notification::ShareNotification}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, - object::share_notification::ShareNotification, + object::{ + JmapRight, + addressbook::AddressBookRight, + calendar::CalendarRight, + file_node::FileNodeRight, + mailbox::MailboxRight, + share_notification::{self, ShareNotificationProperty, ShareNotificationValue}, + }, + request::IntoValid, + types::{date::UTCDate, state::State}, }; +use jmap_tools::{Key, Map, Value}; +use std::time::Duration; +use store::{ + Deserialize, IterateParams, LogKey, U64_LEN, ahash::AHashSet, write::key::DeserializeBigEndian, +}; +use trc::AddContext; +use types::{ + acl::Acl, + collection::{Collection, SyncCollection}, + id::Id, + type_state::DataType, +}; +use utils::{map::bitmap::Bitmap, snowflake::SnowflakeIdGenerator}; pub trait ShareNotificationGet: Sync + Send { fn share_notification_get( &self, - request: GetRequest, - access_token: &AccessToken, - ) -> impl Future>> + Send; + request: GetRequest, + ) -> impl Future>> + Send; } impl ShareNotificationGet for Server { async fn share_notification_get( &self, - mut request: GetRequest, - access_token: &AccessToken, - ) -> trc::Result> { - todo!() + mut request: GetRequest, + ) -> trc::Result> { + let properties = request.unwrap_properties(&[ + ShareNotificationProperty::Id, + ShareNotificationProperty::Name, + ShareNotificationProperty::ChangedBy, + ShareNotificationProperty::Created, + ShareNotificationProperty::ObjectAccountId, + ShareNotificationProperty::ObjectId, + ShareNotificationProperty::ObjectType, + ShareNotificationProperty::OldRights, + ShareNotificationProperty::NewRights, + ShareNotificationProperty::Name, + ]); + + let account_id = request.account_id.document_id(); + let mut min_id = u64::MAX; + let mut max_id = 0u64; + + let mut ids = if let Some(ids) = request.ids.take() { + let ids = ids.unwrap(); + if ids.len() <= self.core.jmap.get_max_objects { + ids.into_valid() + .map(|id| { + let id_num = *id.as_ref(); + if id_num < min_id { + min_id = id_num; + } + if id_num > max_id { + max_id = id_num; + } + id_num + }) + .collect::>() + } else { + return Err(trc::JmapEvent::RequestTooLarge.into_err()); + } + } else { + AHashSet::new() + }; + let has_ids = !ids.is_empty(); + + if min_id == u64::MAX { + min_id = SnowflakeIdGenerator::from_duration( + self.core + .jmap + .share_notification_max_history + .unwrap_or(Duration::from_secs(30 * 86400)), + ) + .unwrap_or_default(); + } + + if max_id == 0 { + max_id = u64::MAX; + } + + let mut response = GetResponse { + account_id: request.account_id.into(), + state: None, + list: Vec::with_capacity(ids.len()), + not_found: vec![], + }; + + self.store() + .iterate( + IterateParams::new( + LogKey { + account_id, + collection: SyncCollection::ShareNotification.into(), + change_id: min_id, + }, + LogKey { + account_id, + collection: SyncCollection::ShareNotification.into(), + change_id: max_id + 1, + }, + ) + .descending(), + |key, value| { + let change_id = key.deserialize_be_u64(key.len() - U64_LEN)?; + if response.state.is_none() { + response.state = Some(State::Exact(change_id)); + } + + if !has_ids || ids.remove(&change_id) { + let notification = + ShareNotification::deserialize(value).caused_by(trc::location!())?; + response.list.push(build_share_notification( + change_id, + notification, + &properties, + )); + } + + Ok((!has_ids || !ids.is_empty()) + && response.list.len() < self.core.jmap.get_max_objects) + }, + ) + .await + .caused_by(trc::location!())?; + + response + .not_found + .extend(ids.into_iter().map(Id::from).collect::>()); + + Ok(response) } } + +fn build_share_notification( + id: u64, + mut notification: ShareNotification, + properties: &[ShareNotificationProperty], +) -> Value<'static, ShareNotificationProperty, ShareNotificationValue> { + let mut result = Map::with_capacity(properties.len()); + for property in properties { + let value = match property { + ShareNotificationProperty::Id => Value::Element(ShareNotificationValue::Id(id.into())), + ShareNotificationProperty::Created => Value::Element(ShareNotificationValue::Date( + UTCDate::from_timestamp(SnowflakeIdGenerator::to_timestamp(id) as i64), + )), + ShareNotificationProperty::ChangedBy => Value::Object(Map::from(vec![ + ( + Key::Property(ShareNotificationProperty::ChangedByPrincipalId), + Value::Element(ShareNotificationValue::Id(notification.changed_by.into())), + ), + ( + Key::Property(ShareNotificationProperty::ChangedByName), + Value::Str("".into()), + ), + ])), + ShareNotificationProperty::ObjectType => DataType::try_from(notification.object_type) + .ok() + .map(|typ| Value::Element(ShareNotificationValue::ObjectType(typ))) + .unwrap_or(Value::Null), + ShareNotificationProperty::ObjectAccountId => Value::Element( + ShareNotificationValue::Id(notification.object_account_id.into()), + ), + ShareNotificationProperty::ObjectId => { + Value::Element(ShareNotificationValue::Id(notification.object_id.into())) + } + ShareNotificationProperty::OldRights => { + map_rights(notification.object_type, notification.old_rights) + } + ShareNotificationProperty::NewRights => { + map_rights(notification.object_type, notification.new_rights) + } + ShareNotificationProperty::Name => { + Value::Str(std::mem::take(&mut notification.name).into()) + } + _ => Value::Null, + }; + + result.insert_unchecked(property.clone(), value); + } + + Value::Object(result) +} + +fn map_rights( + object_type: Collection, + rights: Bitmap, +) -> Value<'static, ShareNotificationProperty, ShareNotificationValue> { + let mut obj = Map::with_capacity(3); + + match object_type { + Collection::Calendar | Collection::CalendarEvent => { + for acl in rights.into_iter() { + for right in CalendarRight::from_acl(acl) { + obj.insert_unchecked(Key::Borrowed(right.as_str()), Value::Bool(true)); + } + } + } + Collection::AddressBook | Collection::ContactCard => { + for acl in rights.into_iter() { + for right in AddressBookRight::from_acl(acl) { + obj.insert_unchecked(Key::Borrowed(right.as_str()), Value::Bool(true)); + } + } + } + Collection::FileNode => { + for acl in rights.into_iter() { + for right in FileNodeRight::from_acl(acl) { + obj.insert_unchecked(Key::Borrowed(right.as_str()), Value::Bool(true)); + } + } + } + Collection::Mailbox | Collection::Email => { + for acl in rights.into_iter() { + for right in MailboxRight::from_acl(acl) { + obj.insert_unchecked(Key::Borrowed(right.as_str()), Value::Bool(true)); + } + } + } + _ => {} + } + + Value::Object(obj) +} diff --git a/crates/jmap/src/share_notification/query.rs b/crates/jmap/src/share_notification/query.rs index 22b6c44f..23c09554 100644 --- a/crates/jmap/src/share_notification/query.rs +++ b/crates/jmap/src/share_notification/query.rs @@ -4,26 +4,138 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use std::time::Duration; + +use crate::{JmapMethods, UpdateResults}; +use common::{Server, sharing::notification::ShareNotification}; use jmap_proto::{ - method::query::{QueryRequest, QueryResponse}, - object::share_notification::ShareNotification, + method::query::{Filter, QueryRequest, QueryResponse}, + object::share_notification::{self, ShareNotificationFilter}, + types::state::State, }; +use store::{ + Deserialize, IterateParams, LogKey, U64_LEN, query::ResultSet, write::key::DeserializeBigEndian, +}; +use trc::AddContext; +use types::{ + collection::{Collection, SyncCollection}, + id::Id, +}; +use utils::snowflake::SnowflakeIdGenerator; pub trait ShareNotificationQuery: Sync + Send { fn share_notification_query( &self, - request: QueryRequest, - access_token: &AccessToken, + request: QueryRequest, ) -> impl Future> + Send; } impl ShareNotificationQuery for Server { async fn share_notification_query( &self, - mut request: QueryRequest, - access_token: &AccessToken, + mut request: QueryRequest, ) -> trc::Result { - todo!() + let account_id = request.account_id.document_id(); + let mut from_change_id = SnowflakeIdGenerator::from_duration( + self.core + .jmap + .share_notification_max_history + .unwrap_or(Duration::from_secs(30 * 86400)), + ) + .unwrap_or_default(); + let mut to_change_id = u64::MAX; + let mut collection = None; + let mut object_type = None; + + for cond in std::mem::take(&mut request.filter) { + match cond { + Filter::Property(cond) => match cond { + ShareNotificationFilter::After(utcdate) => { + from_change_id = + SnowflakeIdGenerator::from_timestamp(utcdate.timestamp() as u64) + .unwrap_or(0); + } + ShareNotificationFilter::Before(utcdate) => { + to_change_id = + SnowflakeIdGenerator::from_timestamp(utcdate.timestamp() as u64) + .unwrap_or(u64::MAX); + } + ShareNotificationFilter::ObjectType(typ) => { + collection = Collection::try_from(typ).ok(); + } + ShareNotificationFilter::ObjectAccountId(id) => { + object_type = Some(id.document_id()); + } + ShareNotificationFilter::_T(other) => { + return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other)); + } + }, + Filter::And | Filter::Or | Filter::Not | Filter::Close => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details("Logical operators are not supported")); + } + } + } + + let mut results = Vec::new(); + let mut result_set = ResultSet { + account_id, + collection: Collection::None, + results: Default::default(), + }; + + self.store() + .iterate( + IterateParams::new( + LogKey { + account_id, + collection: SyncCollection::ShareNotification.into(), + change_id: from_change_id, + }, + LogKey { + account_id, + collection: SyncCollection::ShareNotification.into(), + change_id: to_change_id, + }, + ) + .descending(), + |key, value| { + let change_id = key.deserialize_be_u64(key.len() - U64_LEN)?; + + if collection.is_some() || object_type.is_some() { + let notification = + ShareNotification::deserialize(value).caused_by(trc::location!())?; + if collection.is_some_and(|c| c != notification.object_type) + || object_type.is_some_and(|o| o != notification.object_account_id) + { + return Ok(true); + } + } + + result_set.results.insert(results.len() as u32); + results.push(Id::from(change_id)); + + Ok(true) + }, + ) + .await + .caused_by(trc::location!())?; + + let (mut response, paginate) = self + .build_query_response(&result_set, State::Initial, &request) + .await?; + + if let Some(mut paginate) = paginate { + for result in results { + if !paginate.add_id(result) { + break; + } + } + + response.update_results(paginate.build()); + } + + Ok(response) } } diff --git a/crates/jmap/src/share_notification/set.rs b/crates/jmap/src/share_notification/set.rs index 16b14e5c..3cf30b9c 100644 --- a/crates/jmap/src/share_notification/set.rs +++ b/crates/jmap/src/share_notification/set.rs @@ -4,19 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use http_proto::HttpSessionData; +use common::Server; use jmap_proto::{ + error::set::SetError, method::set::{SetRequest, SetResponse}, object::share_notification::ShareNotification, + request::IntoValid, }; +use store::write::{BatchBuilder, ValueClass}; +use trc::AddContext; pub trait ShareNotificationSet: Sync + Send { fn share_notification_set( &self, request: SetRequest<'_, ShareNotification>, - access_token: &AccessToken, - session: &HttpSessionData, ) -> impl Future>> + Send; } @@ -24,9 +25,41 @@ impl ShareNotificationSet for Server { async fn share_notification_set( &self, mut request: SetRequest<'_, ShareNotification>, - access_token: &AccessToken, - _session: &HttpSessionData, ) -> trc::Result> { - todo!() + let account_id = request.account_id.document_id(); + let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; + + for (id, _) in request.unwrap_create() { + response.not_created.append( + id, + SetError::forbidden().with_description("Cannot create share notifications."), + ); + } + + // Process updates + for (id, _) in request.unwrap_update().into_valid() { + response.not_updated.append( + id, + SetError::forbidden().with_description("Cannot update share notifications."), + ); + } + + // Process deletions + let mut batch = BatchBuilder::new(); + batch.with_account_id(account_id); + for id in request.unwrap_destroy().into_valid() { + batch.clear(ValueClass::ShareNotification { + notification_id: id.id(), + notify_account_id: account_id, + }); + response.destroyed.push(id); + } + + // Write changes + if !batch.is_empty() { + self.commit_batch(batch).await.caused_by(trc::location!())?; + } + + Ok(response) } } diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index bafc879d..f7c5aebf 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -131,7 +131,7 @@ impl SieveScriptSet for Server { .with_account_id(account_id) .with_collection(Collection::SieveScript) .create_document(document_id) - .custom(builder.with_tenant_id(&ctx.resource_token)) + .custom(builder.with_access_token(ctx.access_token)) .caused_by(trc::location!())? .commit_point(); @@ -239,7 +239,7 @@ impl SieveScriptSet for Server { // Write record batch - .custom(builder.with_tenant_id(&ctx.resource_token)) + .custom(builder.with_access_token(ctx.access_token)) .caused_by(trc::location!())? .commit_point(); @@ -269,7 +269,7 @@ impl SieveScriptSet for Server { let document_id = id.document_id(); if sieve_ids.contains(document_id) { match self - .sieve_script_delete(&ctx.resource_token, document_id, true, &mut batch) + .sieve_script_delete(ctx.access_token, document_id, true, &mut batch) .await? { Some(true) => { diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index c8592e08..9999f266 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -62,7 +62,6 @@ impl VacationResponseSet for Server { ) .await?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); - let resource_token = self.get_resource_token(access_token, account_id).await?; // Process set or update requests let mut create_id = None; @@ -260,7 +259,7 @@ impl VacationResponseSet for Server { let mut obj = ObjectIndexBuilder::new() .with_current_opt(prev_sieve) .with_changes(sieve) - .with_tenant_id(&resource_token); + .with_access_token(access_token); // Update id let document_id = if let Some(document_id) = document_id { @@ -327,7 +326,7 @@ impl VacationResponseSet for Server { if id.is_singleton() && let Some(document_id) = self.get_vacation_sieve_script_id(account_id).await? { - self.sieve_script_delete(&resource_token, document_id, false, &mut batch) + self.sieve_script_delete(access_token, document_id, false, &mut batch) .await?; response.destroyed.push(id); continue; diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs index e721786b..e5b40a7b 100644 --- a/crates/managesieve/src/op/deletescript.rs +++ b/crates/managesieve/src/op/deletescript.rs @@ -4,17 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Instant; - +use crate::core::{Command, ResponseCode, Session, StatusResponse}; use common::listener::SessionStream; use directory::Permission; use email::sieve::delete::SieveScriptDelete; use imap_proto::receiver::Request; +use std::time::Instant; use store::write::BatchBuilder; use trc::AddContext; -use crate::core::{Command, ResponseCode, Session, StatusResponse}; - impl Session { pub async fn handle_deletescript(&mut self, request: Request) -> trc::Result> { // Validate access @@ -40,12 +38,7 @@ impl Session { match self .server - .sieve_script_delete( - &access_token.as_resource_token(), - document_id, - true, - &mut batch, - ) + .sieve_script_delete(access_token, document_id, true, &mut batch) .await .caused_by(trc::location!())? { diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index 826796cd..d9642a91 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -47,10 +47,10 @@ impl Session { let script_size = script_bytes.len() as i64; // Check quota - let resource_token = self.state.access_token().as_resource_token(); - let account_id = resource_token.account_id; + let access_token = self.state.access_token(); + let account_id = access_token.primary_id(); self.server - .has_available_quota(&resource_token, script_bytes.len() as u64) + .has_available_quota(&access_token.as_resource_token(), script_bytes.len() as u64) .await .caused_by(trc::location!())?; @@ -141,7 +141,7 @@ impl Session { .with_blob_hash(blob_hash.clone()), ) .with_current(script) - .with_tenant_id(&resource_token), + .with_access_token(access_token), ) .caused_by(trc::location!())?; @@ -185,7 +185,7 @@ impl Session { .with_is_active(false) .with_size(script_size as u32), ) - .with_tenant_id(&resource_token), + .with_access_token(access_token), ) .caused_by(trc::location!())?; diff --git a/crates/store/src/query/log.rs b/crates/store/src/query/log.rs index 20a94d2d..fe03dbe4 100644 --- a/crates/store/src/query/log.rs +++ b/crates/store/src/query/log.rs @@ -66,6 +66,10 @@ impl Store { collection: LogCollection, query: Query, ) -> trc::Result { + let is_share_log = matches!( + collection, + LogCollection::Sync(SyncCollection::ShareNotification) + ); let collection = u8::from(collection); let (is_inclusive, from_change_id, to_change_id) = match query { @@ -102,15 +106,19 @@ impl Store { changelog.from_change_id = change_id; } changelog.to_change_id = change_id; - let (has_container_changes, has_item_changes) = - changelog.deserialize(value).ok_or_else(|| { - trc::Error::corrupted_key(key, value.into(), trc::location!()) - })?; - if has_container_changes { - changelog.container_change_id = Some(change_id); - } - if has_item_changes { - changelog.item_change_id = Some(change_id); + if !is_share_log { + let (has_container_changes, has_item_changes) = + changelog.deserialize(value).ok_or_else(|| { + trc::Error::corrupted_key(key, value.into(), trc::location!()) + })?; + if has_container_changes { + changelog.container_change_id = Some(change_id); + } + if has_item_changes { + changelog.item_change_id = Some(change_id); + } + } else { + changelog.changes.push(Change::InsertItem(change_id)); } } Ok(true) diff --git a/crates/store/src/query/sort.rs b/crates/store/src/query/sort.rs index 4fc78504..b3497075 100644 --- a/crates/store/src/query/sort.rs +++ b/crates/store/src/query/sort.rs @@ -321,8 +321,13 @@ impl<'x> Pagination<'x> { self } + #[inline(always)] pub fn add(&mut self, prefix_id: u32, document_id: u32) -> bool { - let id = Id::from_parts(prefix_id, document_id); + self.add_id(Id::from_parts(prefix_id, document_id)) + } + + pub fn add_id(&mut self, id: Id) -> bool { + let document_id = id.document_id(); // Pagination if !self.has_anchor { diff --git a/crates/store/src/write/batch.rs b/crates/store/src/write/batch.rs index a7b81ca6..93cfb77a 100644 --- a/crates/store/src/write/batch.rs +++ b/crates/store/src/write/batch.rs @@ -447,6 +447,14 @@ impl BatchBuilder { self.current_account_id } + pub fn last_collection(&self) -> Option { + self.current_collection + } + + pub fn last_document_id(&self) -> Option { + self.current_document_id + } + pub fn commit_points(&mut self) -> CommitPointIterator { self.serialize_changes(); CommitPointIterator { diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 07d4ab1c..1cc7fb18 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -5,7 +5,7 @@ */ use std::convert::TryInto; -use types::blob_hash::BLOB_HASH_LEN; +use types::{blob_hash::BLOB_HASH_LEN, collection::SyncCollection}; use utils::codec::leb128::Leb128_; use crate::{ @@ -435,6 +435,13 @@ impl ValueClass { }, ValueClass::DocumentId => serializer.write(account_id).write(collection), ValueClass::ChangeId => serializer.write(account_id), + ValueClass::ShareNotification { + notification_id, + notify_account_id, + } => serializer + .write(*notify_account_id) + .write(u8::from(SyncCollection::ShareNotification)) + .write(*notification_id), ValueClass::Any(any) => serializer.write(any.key.as_slice()), } .finalize() @@ -626,6 +633,7 @@ impl ValueClass { }, ValueClass::DocumentId => U32_LEN + 1, ValueClass::ChangeId => U32_LEN, + ValueClass::ShareNotification { .. } => U32_LEN + U64_LEN + 1, ValueClass::Any(v) => v.key.len(), } } @@ -673,6 +681,7 @@ impl ValueClass { TelemetryClass::Metric { .. } => SUBSPACE_TELEMETRY_METRIC, }, ValueClass::DocumentId | ValueClass::ChangeId => SUBSPACE_COUNTER, + ValueClass::ShareNotification { .. } => SUBSPACE_LOGS, ValueClass::Any(any) => any.subspace, } } diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 31aa623f..c4dc1ae5 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -194,6 +194,10 @@ pub enum ValueClass { Report(ReportClass), Telemetry(TelemetryClass), Any(AnyClass), + ShareNotification { + notification_id: u64, + notify_account_id: u32, + }, DocumentId, ChangeId, } diff --git a/crates/types/src/collection.rs b/crates/types/src/collection.rs index ae4a84a2..10e4bb72 100644 --- a/crates/types/src/collection.rs +++ b/crates/types/src/collection.rs @@ -29,9 +29,8 @@ pub enum Collection { ContactCard = 11, FileNode = 12, CalendarEventNotification = 13, - ShareNotification = 14, #[default] - None = 15, + None = 14, } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)] @@ -77,7 +76,6 @@ impl Collection { Collection::ContactCard => Some(Collection::AddressBook), Collection::FileNode => Some(Collection::FileNode), Collection::CalendarEventNotification => Some(Collection::CalendarEventNotification), - Collection::ShareNotification => Some(Collection::ShareNotification), _ => None, } } @@ -89,7 +87,6 @@ impl Collection { Collection::AddressBook => Some(Collection::ContactCard), Collection::FileNode => Some(Collection::FileNode), Collection::CalendarEventNotification => Some(Collection::CalendarEventNotification), - Collection::ShareNotification => Some(Collection::ShareNotification), _ => None, } } @@ -125,8 +122,7 @@ impl SyncCollection { SyncCollection::EmailSubmission => Collection::EmailSubmission, SyncCollection::SieveScript => Collection::SieveScript, SyncCollection::CalendarEventNotification => Collection::CalendarEventNotification, - SyncCollection::ShareNotification => Collection::ShareNotification, - SyncCollection::None => Collection::None, + SyncCollection::ShareNotification | SyncCollection::None => Collection::None, } } @@ -158,7 +154,6 @@ impl From for SyncCollection { Collection::AddressBook => SyncCollection::AddressBook, Collection::ContactCard => SyncCollection::AddressBook, Collection::FileNode => SyncCollection::FileNode, - Collection::ShareNotification => SyncCollection::ShareNotification, _ => SyncCollection::None, } } @@ -181,7 +176,6 @@ impl From for Collection { 11 => Collection::ContactCard, 12 => Collection::FileNode, 13 => Collection::CalendarEventNotification, - 14 => Collection::ShareNotification, _ => Collection::None, } } @@ -240,7 +234,6 @@ impl From for Collection { 11 => Collection::ContactCard, 12 => Collection::FileNode, 13 => Collection::CalendarEventNotification, - 14 => Collection::ShareNotification, _ => Collection::None, } } @@ -295,7 +288,30 @@ impl TryFrom for DataType { Collection::ContactCard => Ok(DataType::ContactCard), Collection::FileNode => Ok(DataType::FileNode), Collection::CalendarEventNotification => Ok(DataType::CalendarEventNotification), - Collection::ShareNotification => Ok(DataType::ShareNotification), + _ => Err(()), + } + } +} + +impl TryFrom for Collection { + type Error = (); + + fn try_from(value: DataType) -> Result { + match value { + DataType::Email => Ok(Collection::Email), + DataType::Mailbox => Ok(Collection::Mailbox), + DataType::Thread => Ok(Collection::Thread), + DataType::Identity => Ok(Collection::Identity), + DataType::EmailSubmission => Ok(Collection::EmailSubmission), + DataType::SieveScript => Ok(Collection::SieveScript), + DataType::PushSubscription => Ok(Collection::PushSubscription), + DataType::Principal => Ok(Collection::Principal), + DataType::Calendar => Ok(Collection::Calendar), + DataType::CalendarEvent => Ok(Collection::CalendarEvent), + DataType::AddressBook => Ok(Collection::AddressBook), + DataType::ContactCard => Ok(Collection::ContactCard), + DataType::FileNode => Ok(Collection::FileNode), + DataType::CalendarEventNotification => Ok(Collection::CalendarEventNotification), _ => Err(()), } } @@ -324,7 +340,6 @@ impl Collection { Collection::ContactCard => "contactCard", Collection::FileNode => "fileNode", Collection::CalendarEventNotification => "calendarEventNotification", - Collection::ShareNotification => "shareNotification", Collection::None => "", } } @@ -349,7 +364,6 @@ impl FromStr for Collection { "contactCard" => Collection::ContactCard, "fileNode" => Collection::FileNode, "calendarEventNotification" => Collection::CalendarEventNotification, - "shareNotification" => Collection::ShareNotification, ) .ok_or(()) } diff --git a/crates/types/src/field.rs b/crates/types/src/field.rs index 9a36567d..63051cc5 100644 --- a/crates/types/src/field.rs +++ b/crates/types/src/field.rs @@ -31,6 +31,7 @@ pub enum CalendarField { Updated, Start, Text, + EventId, Archive, } @@ -84,6 +85,7 @@ pub enum EmailSubmissionField { pub enum PrincipalField { Archive, EncryptionKeys, + ParticipantIdentities, } impl From for u8 { @@ -107,6 +109,7 @@ impl From for u8 { CalendarField::Created => 2, CalendarField::Updated => 3, CalendarField::Start => 4, + CalendarField::EventId => 5, CalendarField::Archive => ARCHIVE_FIELD, } } @@ -168,6 +171,7 @@ impl From for u8 { impl From for u8 { fn from(value: PrincipalField) -> Self { match value { + PrincipalField::ParticipantIdentities => 45, PrincipalField::EncryptionKeys => 46, PrincipalField::Archive => ARCHIVE_FIELD, } diff --git a/crates/utils/src/snowflake.rs b/crates/utils/src/snowflake.rs index dc670882..706c7d46 100644 --- a/crates/utils/src/snowflake.rs +++ b/crates/utils/src/snowflake.rs @@ -56,6 +56,24 @@ impl SnowflakeIdGenerator { .and_then(|diff| Self::from_duration(Duration::from_secs(diff))) } + pub fn from_sequence_and_node_id(sequence: u64, node_id: Option) -> Option { + let node_id = node_id.unwrap_or_else(rand::random::); + let sequence = sequence & SEQUENCE_MASK; + + (SystemTime::UNIX_EPOCH + Duration::from_secs(DEFAULT_EPOCH)) + .elapsed() + .ok() + .map(|elapsed| { + ((elapsed.as_millis() as u64) << (SEQUENCE_LEN + NODE_ID_LEN)) + | (sequence << NODE_ID_LEN) + | (node_id & NODE_ID_MASK) + }) + } + + pub fn to_timestamp(id: u64) -> u64 { + (id >> (SEQUENCE_LEN + NODE_ID_LEN)) + DEFAULT_EPOCH + } + pub fn with_node_id(node_id: u64) -> Self { Self { epoch: SystemTime::UNIX_EPOCH + Duration::from_secs(DEFAULT_EPOCH), // 52 years after UNIX_EPOCH diff --git a/tests/src/store/query.rs b/tests/src/store/query.rs index 77132f87..39dc6486 100644 --- a/tests/src/store/query.rs +++ b/tests/src/store/query.rs @@ -469,7 +469,7 @@ pub async fn test_filter(db: Store, fts: FtsStore) { db.get_value::(ValueKey { account_id: 0, collection: COLLECTION_ID.into(), - document_id: document_id as u32, + document_id: document_id.document_id(), class: ValueClass::Property(fields_u8["accession_number"]), }) .await @@ -559,7 +559,7 @@ pub async fn test_sort(db: Store) { db.get_value::(ValueKey { account_id: 0, collection: COLLECTION_ID.into(), - document_id: document_id as u32, + document_id: document_id.document_id(), class: ValueClass::Property(fields["accession_number"]), }) .await diff --git a/tests/src/webdav/cal_query.rs b/tests/src/webdav/cal_query.rs index 9123a73f..8c704196 100644 --- a/tests/src/webdav/cal_query.rs +++ b/tests/src/webdav/cal_query.rs @@ -6,17 +6,14 @@ use super::WebDavTest; use ahash::AHashSet; -use calcard::{ - common::timezone::Tz, - icalendar::{ICalendar, dates::CalendarEvent}, -}; -use dav_proto::schema::property::TimeRange; +use calcard::{common::timezone::Tz, icalendar::ICalendar}; use groupware::{ DavResourceName, - calendar::{CalendarEventData, alarm::ExpandAlarm}, + calendar::{CalendarEventData, alarm::ExpandAlarm, expand::CalendarEventExpansion}, }; use hyper::StatusCode; use store::write::serialize::rkyv_unarchive; +use types::TimeRange; pub async fn test(test: &WebDavTest) { println!("Running REPORT calendar-query & free-busy-query tests..."); @@ -219,7 +216,8 @@ fn roundtrip_expansion(ics: &str, ignore_errors: bool) { let mut events = expanded .events .into_iter() - .map(|e| { + .enumerate() + .map(|(i, e)| { let e = e.try_into_date_time().unwrap(); let start = e.start.timestamp(); let end = e.end.timestamp(); @@ -247,8 +245,9 @@ fn roundtrip_expansion(ics: &str, ignore_errors: bool) { if max > max_utc { max_utc = max; } - CalendarEvent { + CalendarEventExpansion { comp_id: e.comp_id, + expansion_id: i as u32, start, end, } diff --git a/tests/src/webdav/cal_scheduling.rs b/tests/src/webdav/cal_scheduling.rs index a09bcf7b..b08e2762 100644 --- a/tests/src/webdav/cal_scheduling.rs +++ b/tests/src/webdav/cal_scheduling.rs @@ -199,7 +199,7 @@ pub async fn test(test: &WebDavTest) { .fetch_dav_resources( &access_token, client.account_id, - SyncCollection::CalendarScheduling, + SyncCollection::CalendarEventNotification, ) .await .unwrap(); diff --git a/tests/src/webdav/prop.rs b/tests/src/webdav/prop.rs index ec74e5b3..8b34a58d 100644 --- a/tests/src/webdav/prop.rs +++ b/tests/src/webdav/prop.rs @@ -7,12 +7,12 @@ use super::{DavResponse, DummyWebDavClient, WebDavTest}; use crate::webdav::{GenerateTestDavResource, TEST_ICAL_2, TEST_VTIMEZONE_1}; use ahash::{AHashMap, AHashSet}; -use dav_proto::schema::{ - property::{CalDavProperty, CardDavProperty, DavProperty, PrincipalProperty, WebDavProperty}, - request::DeadElementTag, +use dav_proto::schema::property::{ + CalDavProperty, CardDavProperty, DavProperty, PrincipalProperty, WebDavProperty, }; use groupware::DavResourceName; use hyper::StatusCode; +use types::dead_property::DeadElementTag; pub async fn test(test: &WebDavTest, assisted_discovery: bool) { let client = test.client("jane");