From b7df05dd3beab758de9064101f59ac6c371f24c2 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Sun, 5 Oct 2025 16:06:57 +0200 Subject: [PATCH] JMAP for Calendars implementation (part 1) --- Cargo.lock | 2 - crates/common/src/auth/access_token.rs | 12 +- crates/common/src/auth/roles.rs | 10 +- crates/common/src/lib.rs | 16 +- crates/dav/src/calendar/copy_move.rs | 2 +- crates/dav/src/calendar/scheduling.rs | 22 +- crates/dav/src/common/mod.rs | 46 +- crates/dav/src/common/propfind.rs | 26 +- crates/dav/src/principal/propfind.rs | 2 +- crates/dav/src/request.rs | 2 +- .../directory/src/backend/internal/manage.rs | 12 +- crates/directory/src/core/mod.rs | 51 ++ crates/directory/src/core/principal.rs | 29 +- crates/directory/src/lib.rs | 48 +- crates/groupware/src/cache/calcard.rs | 6 +- crates/groupware/src/cache/mod.rs | 6 +- crates/groupware/src/calendar/index.rs | 20 +- crates/groupware/src/calendar/itip.rs | 10 +- crates/groupware/src/calendar/mod.rs | 14 +- crates/groupware/src/calendar/storage.rs | 24 +- crates/groupware/src/lib.rs | 6 +- crates/jmap-proto/Cargo.toml | 2 +- crates/jmap-proto/src/method/availability.rs | 91 +++ crates/jmap-proto/src/method/copy.rs | 5 +- crates/jmap-proto/src/method/lookup.rs | 2 +- crates/jmap-proto/src/method/mod.rs | 1 + crates/jmap-proto/src/method/set.rs | 2 +- crates/jmap-proto/src/object/addressbook.rs | 44 +- crates/jmap-proto/src/object/blob.rs | 40 +- crates/jmap-proto/src/object/calendar.rs | 484 ++++++++++++++++ .../jmap-proto/src/object/calendar_event.rs | 363 ++++++++++++ .../src/object/calendar_event_notification.rs | 397 +++++++++++++ crates/jmap-proto/src/object/contact.rs | 87 ++- crates/jmap-proto/src/object/email.rs | 85 ++- .../jmap-proto/src/object/email_submission.rs | 44 +- crates/jmap-proto/src/object/file_node.rs | 54 +- crates/jmap-proto/src/object/identity.rs | 41 +- crates/jmap-proto/src/object/mailbox.rs | 53 +- crates/jmap-proto/src/object/mod.rs | 23 +- .../src/object/participant_identity.rs | 190 +++++++ crates/jmap-proto/src/object/principal.rs | 40 +- .../src/object/push_subscription.rs | 41 +- crates/jmap-proto/src/object/quota.rs | 40 +- .../src/object/share_notification.rs | 302 ++++++++++ crates/jmap-proto/src/object/sieve.rs | 44 +- crates/jmap-proto/src/object/thread.rs | 41 +- .../src/object/vacation_response.rs | 41 +- crates/jmap-proto/src/references/eval.rs | 97 +++- crates/jmap-proto/src/references/jsptr.rs | 70 ++- crates/jmap-proto/src/references/resolve.rs | 37 +- crates/jmap-proto/src/request/capability.rs | 77 ++- crates/jmap-proto/src/request/method.rs | 73 +++ crates/jmap-proto/src/request/mod.rs | 43 +- crates/jmap-proto/src/request/reference.rs | 6 +- crates/jmap-proto/src/response/mod.rs | 139 ++++- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/addressbook/set.rs | 2 +- crates/jmap/src/api/auth.rs | 42 +- crates/jmap/src/api/request.rs | 149 ++++- crates/jmap/src/blob/copy.rs | 19 +- crates/jmap/src/blob/get.rs | 117 ++-- crates/jmap/src/calendar/get.rs | 163 ++++++ crates/jmap/src/calendar/mod.rs | 8 + crates/jmap/src/calendar/set.rs | 342 +++++++++++ crates/jmap/src/calendar_event/copy.rs | 190 +++++++ crates/jmap/src/calendar_event/get.rs | 134 +++++ crates/jmap/src/calendar_event/mod.rs | 11 + crates/jmap/src/calendar_event/parse.rs | 81 +++ crates/jmap/src/calendar_event/query.rs | 137 +++++ crates/jmap/src/calendar_event/set.rs | 537 ++++++++++++++++++ .../src/calendar_event_notification/get.rs | 31 + .../src/calendar_event_notification/mod.rs | 9 + .../src/calendar_event_notification/query.rs | 29 + .../src/calendar_event_notification/set.rs | 32 ++ crates/jmap/src/changes/get.rs | 29 +- crates/jmap/src/changes/query.rs | 78 ++- crates/jmap/src/contact/get.rs | 1 + crates/jmap/src/file/set.rs | 2 + crates/jmap/src/lib.rs | 5 + crates/jmap/src/participant_identity/get.rs | 29 + crates/jmap/src/participant_identity/mod.rs | 8 + crates/jmap/src/participant_identity/set.rs | 32 ++ crates/jmap/src/principal/availability.rs | 27 + crates/jmap/src/principal/mod.rs | 1 + crates/jmap/src/share_notification/get.rs | 29 + crates/jmap/src/share_notification/mod.rs | 9 + crates/jmap/src/share_notification/query.rs | 29 + crates/jmap/src/share_notification/set.rs | 32 ++ crates/migration/src/principal.rs | 4 +- crates/types/Cargo.toml | 2 +- crates/types/src/acl.rs | 5 +- crates/types/src/collection.rs | 50 +- crates/types/src/type_state.rs | 17 +- crates/utils/proc-macros/src/lib.rs | 6 +- 94 files changed, 5352 insertions(+), 543 deletions(-) create mode 100644 crates/jmap-proto/src/method/availability.rs create mode 100644 crates/jmap-proto/src/object/calendar.rs create mode 100644 crates/jmap-proto/src/object/calendar_event.rs create mode 100644 crates/jmap-proto/src/object/calendar_event_notification.rs create mode 100644 crates/jmap-proto/src/object/participant_identity.rs create mode 100644 crates/jmap-proto/src/object/share_notification.rs create mode 100644 crates/jmap/src/calendar/get.rs create mode 100644 crates/jmap/src/calendar/mod.rs create mode 100644 crates/jmap/src/calendar/set.rs create mode 100644 crates/jmap/src/calendar_event/copy.rs create mode 100644 crates/jmap/src/calendar_event/get.rs create mode 100644 crates/jmap/src/calendar_event/mod.rs create mode 100644 crates/jmap/src/calendar_event/parse.rs create mode 100644 crates/jmap/src/calendar_event/query.rs create mode 100644 crates/jmap/src/calendar_event/set.rs create mode 100644 crates/jmap/src/calendar_event_notification/get.rs create mode 100644 crates/jmap/src/calendar_event_notification/mod.rs create mode 100644 crates/jmap/src/calendar_event_notification/query.rs create mode 100644 crates/jmap/src/calendar_event_notification/set.rs create mode 100644 crates/jmap/src/participant_identity/get.rs create mode 100644 crates/jmap/src/participant_identity/mod.rs create mode 100644 crates/jmap/src/participant_identity/set.rs create mode 100644 crates/jmap/src/principal/availability.rs create mode 100644 crates/jmap/src/share_notification/get.rs create mode 100644 crates/jmap/src/share_notification/mod.rs create mode 100644 crates/jmap/src/share_notification/query.rs create mode 100644 crates/jmap/src/share_notification/set.rs diff --git a/Cargo.lock b/Cargo.lock index 759894e3..41bb1be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3939,8 +3939,6 @@ dependencies = [ [[package]] name = "jmap-tools" version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91222e1ff860c06e6a48a317b67ffe6012ae08d40a5e8c6bf75987ae7644b4b1" dependencies = [ "hashify", "rkyv", diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index d4636054..c35a4b22 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -51,9 +51,11 @@ impl Server { // Add principal permissions for permission in principal.permissions() { if permission.grant { - role_permissions.enabled.set(permission.permission.id()); + role_permissions.enabled.set(permission.permission as usize); } else { - role_permissions.disabled.set(permission.permission.id()); + role_permissions + .disabled + .set(permission.permission as usize); } } @@ -390,7 +392,7 @@ impl AccessToken { } pub fn with_permission(mut self, permission: Permission) -> Self { - self.permissions.set(permission.id()); + self.permissions.set(permission.id() as usize); self } @@ -445,7 +447,7 @@ impl AccessToken { #[inline(always)] pub fn has_permission(&self, permission: Permission) -> bool { - self.permissions.get(permission.id()) + self.permissions.get(permission.id() as usize) } pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { @@ -470,7 +472,7 @@ impl AccessToken { let item = USIZE_MASK - bytes.leading_zeros(); bytes ^= 1 << item; if let Some(permission) = - Permission::from_id((block_num * USIZE_BITS) + item as usize) + Permission::from_id(((block_num * USIZE_BITS) + item as usize) as u32) { permissions.push(permission); } diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs index cf07d073..6c3a9c6a 100644 --- a/crates/common/src/auth/roles.rs +++ b/crates/common/src/auth/roles.rs @@ -110,9 +110,11 @@ impl Server { // Add permissions for permission in principal.permissions() { if permission.grant { - role_permissions.enabled.set(permission.permission.id()); + role_permissions.enabled.set(permission.permission as usize); } else { - role_permissions.disabled.set(permission.permission.id()); + role_permissions + .disabled + .set(permission.permission as usize); } } @@ -168,7 +170,7 @@ fn tenant_admin_permissions() -> Arc { let mut permissions = RolePermissions::default(); for permission_id in 0..Permission::COUNT { - let permission = Permission::from_id(permission_id).unwrap(); + let permission = Permission::from_id(permission_id as u32).unwrap(); if permission.is_tenant_admin_permission() { permissions.enabled.set(permission_id); } @@ -181,7 +183,7 @@ fn user_permissions() -> Arc { let mut permissions = RolePermissions::default(); for permission_id in 0..Permission::COUNT { - let permission = Permission::from_id(permission_id).unwrap(); + let permission = Permission::from_id(permission_id as u32).unwrap(); if permission.is_user_permission() { permissions.enabled.set(permission_id); } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 717c11fe..ee6d8f24 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -309,7 +309,7 @@ pub enum DavResourceMetadata { start: i64, duration: u32, }, - CalendarScheduling { + CalendarEventNotification { names: TinyVec<[DavName; 2]>, }, AddressBook { @@ -685,7 +685,7 @@ impl DavResource { DavResourceMetadata::ContactCard { names } => { names.iter().any(|name| name.parent_id == parent_id) } - DavResourceMetadata::CalendarScheduling { names } => { + DavResourceMetadata::CalendarEventNotification { names } => { names.is_empty() && parent_id == SCHEDULE_INBOX_ID } _ => false, @@ -699,7 +699,7 @@ impl DavResource { names.first().map(|name| name.parent_id) } DavResourceMetadata::ContactCard { names } => names.first().map(|name| name.parent_id), - DavResourceMetadata::CalendarScheduling { names } if names.is_empty() => { + DavResourceMetadata::CalendarEventNotification { names } if names.is_empty() => { Some(SCHEDULE_INBOX_ID) } _ => None, @@ -710,7 +710,7 @@ impl DavResource { match &self.data { DavResourceMetadata::CalendarEvent { names, .. } => Some(names.as_slice()), DavResourceMetadata::ContactCard { names } => Some(names.as_slice()), - DavResourceMetadata::CalendarScheduling { names } if !names.is_empty() => { + DavResourceMetadata::CalendarEventNotification { names } if !names.is_empty() => { Some(names.as_slice()) } _ => None, @@ -722,7 +722,7 @@ impl DavResource { DavResourceMetadata::File { name, .. } => Some(name.as_str()), DavResourceMetadata::Calendar { name, .. } => Some(name.as_str()), DavResourceMetadata::AddressBook { name, .. } => Some(name.as_str()), - DavResourceMetadata::CalendarScheduling { names } if names.is_empty() => { + DavResourceMetadata::CalendarEventNotification { names } if names.is_empty() => { Some(if self.document_id == SCHEDULE_INBOX_ID { "inbox" } else { @@ -764,8 +764,8 @@ impl DavResource { DavResourceMetadata::ContactCard { names: b, .. }, ) => a != b, ( - DavResourceMetadata::CalendarScheduling { names: a, .. }, - DavResourceMetadata::CalendarScheduling { names: b, .. }, + DavResourceMetadata::CalendarEventNotification { names: a, .. }, + DavResourceMetadata::CalendarEventNotification { names: b, .. }, ) => a != b, _ => unreachable!(), } @@ -791,7 +791,7 @@ impl DavResource { match &self.data { DavResourceMetadata::File { size, .. } => size.is_none(), DavResourceMetadata::Calendar { .. } | DavResourceMetadata::AddressBook { .. } => true, - DavResourceMetadata::CalendarScheduling { names } => names.is_empty(), + DavResourceMetadata::CalendarEventNotification { names } => names.is_empty(), _ => false, } } diff --git a/crates/dav/src/calendar/copy_move.rs b/crates/dav/src/calendar/copy_move.rs index 32a46aed..1e6b88c3 100644 --- a/crates/dav/src/calendar/copy_move.rs +++ b/crates/dav/src/calendar/copy_move.rs @@ -804,12 +804,12 @@ async fn copy_container( let preference = calendar.preferences.into_iter().next().unwrap(); calendar.name = new_name.to_string(); - calendar.default_alerts.clear(); calendar.acls.clear(); calendar.preferences = vec![CalendarPreferences { account_id: to_account_id, name: preference.name, description: preference.description, + default_alerts: preference.default_alerts, sort_order: 0, color: preference.color, flags: 0, diff --git a/crates/dav/src/calendar/scheduling.rs b/crates/dav/src/calendar/scheduling.rs index 55638f52..30a74d88 100644 --- a/crates/dav/src/calendar/scheduling.rs +++ b/crates/dav/src/calendar/scheduling.rs @@ -29,7 +29,7 @@ use dav_proto::{ response::{CalCondition, Href, ScheduleResponse, ScheduleResponseItem}, }, }; -use groupware::{DestroyArchive, cache::GroupwareCache, calendar::CalendarScheduling}; +use groupware::{DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification}; use http_proto::HttpResponse; use hyper::StatusCode; use store::{ahash::AHashMap, write::BatchBuilder}; @@ -37,7 +37,7 @@ use trc::AddContext; use types::collection::{Collection, SyncCollection}; use utils::sanitize_email; -pub(crate) trait CalendarSchedulingHandler: Sync + Send { +pub(crate) trait CalendarEventNotificationHandler: Sync + Send { fn handle_scheduling_get_request( &self, access_token: &AccessToken, @@ -59,7 +59,7 @@ pub(crate) trait CalendarSchedulingHandler: Sync + Send { ) -> impl Future> + Send; } -impl CalendarSchedulingHandler for Server { +impl CalendarEventNotificationHandler for Server { async fn handle_scheduling_get_request( &self, access_token: &AccessToken, @@ -73,7 +73,7 @@ impl CalendarSchedulingHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let resources = self - .fetch_dav_resources(access_token, account_id, SyncCollection::CalendarScheduling) + .fetch_dav_resources(access_token, account_id, SyncCollection::CalendarEventNotification) .await .caused_by(trc::location!())?; let resource = resources @@ -96,14 +96,14 @@ impl CalendarSchedulingHandler for Server { let event_ = self .get_archive( account_id, - Collection::CalendarScheduling, + Collection::CalendarEventNotification, resource.document_id(), ) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let event = event_ - .unarchive::() + .unarchive::() .caused_by(trc::location!())?; // Validate headers @@ -113,7 +113,7 @@ impl CalendarSchedulingHandler for Server { headers, vec![ResourceState { account_id, - collection: Collection::CalendarScheduling, + collection: Collection::CalendarEventNotification, document_id: resource.document_id().into(), etag: etag.clone().into(), path: resource_.resource.unwrap(), @@ -154,7 +154,7 @@ impl CalendarSchedulingHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let resources = self - .fetch_dav_resources(access_token, account_id, SyncCollection::CalendarScheduling) + .fetch_dav_resources(access_token, account_id, SyncCollection::CalendarEventNotification) .await .caused_by(trc::location!())?; @@ -173,7 +173,7 @@ impl CalendarSchedulingHandler for Server { let document_id = resource.document_id(); let event_ = self - .get_archive(account_id, Collection::CalendarScheduling, document_id) + .get_archive(account_id, Collection::CalendarEventNotification, document_id) .await .caused_by(trc::location!())? .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -184,7 +184,7 @@ impl CalendarSchedulingHandler for Server { headers, vec![ResourceState { account_id, - collection: Collection::CalendarScheduling, + collection: Collection::CalendarEventNotification, document_id: document_id.into(), etag: event_.etag().into(), path: delete_path, @@ -196,7 +196,7 @@ impl CalendarSchedulingHandler for Server { .await?; let event = event_ - .to_unarchived::() + .to_unarchived::() .caused_by(trc::location!())?; // Delete event diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 313d310d..4f93cc62 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -22,8 +22,8 @@ use dav_proto::{ }; use groupware::{ calendar::{ - ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarScheduling, Calendar, - CalendarEvent, CalendarScheduling, + ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarEventNotification, Calendar, + CalendarEvent, CalendarEventNotification, }, contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, file::{ArchivedFileNode, FileNode}, @@ -135,7 +135,7 @@ pub(crate) trait DavCollection { impl DavCollection for Collection { fn namespace(&self) -> Namespace { match self { - Collection::Calendar | Collection::CalendarEvent | Collection::CalendarScheduling => { + Collection::Calendar | Collection::CalendarEvent | Collection::CalendarEventNotification => { Namespace::CalDav } Collection::AddressBook | Collection::ContactCard => Namespace::CardDav, @@ -308,8 +308,8 @@ impl<'x> DavQuery<'x> { pub(crate) enum ArchivedResource<'x> { Calendar(Archive<&'x ArchivedCalendar>), CalendarEvent(Archive<&'x ArchivedCalendarEvent>), - CalendarScheduling(Archive<&'x ArchivedCalendarScheduling>), - CalendarSchedulingCollection(bool), + CalendarEventNotification(Archive<&'x ArchivedCalendarEventNotification>), + CalendarEventNotificationCollection(bool), AddressBook(Archive<&'x ArchivedAddressBook>), ContactCard(Archive<&'x ArchivedContactCard>), FileNode(Archive<&'x ArchivedFileNode>), @@ -327,9 +327,9 @@ impl<'x> ArchivedResource<'x> { Collection::CalendarEvent => archive .to_unarchived::() .map(ArchivedResource::CalendarEvent), - Collection::CalendarScheduling => archive - .to_unarchived::() - .map(ArchivedResource::CalendarScheduling), + Collection::CalendarEventNotification => archive + .to_unarchived::() + .map(ArchivedResource::CalendarEventNotification), Collection::AddressBook => archive .to_unarchived::() .map(ArchivedResource::AddressBook), @@ -359,8 +359,8 @@ impl<'x> ArchivedResource<'x> { ArchivedResource::AddressBook(archive) => archive.inner.created.to_native(), ArchivedResource::ContactCard(archive) => archive.inner.created.to_native(), ArchivedResource::FileNode(archive) => archive.inner.created.to_native(), - ArchivedResource::CalendarScheduling(archive) => archive.inner.created.to_native(), - ArchivedResource::CalendarSchedulingCollection(_) => 1634515200, + ArchivedResource::CalendarEventNotification(archive) => archive.inner.created.to_native(), + ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200, } } @@ -371,8 +371,8 @@ impl<'x> ArchivedResource<'x> { ArchivedResource::AddressBook(archive) => archive.inner.modified.to_native(), ArchivedResource::ContactCard(archive) => archive.inner.modified.to_native(), ArchivedResource::FileNode(archive) => archive.inner.modified.to_native(), - ArchivedResource::CalendarScheduling(archive) => archive.inner.modified.to_native(), - ArchivedResource::CalendarSchedulingCollection(_) => 1634515200, + ArchivedResource::CalendarEventNotification(archive) => archive.inner.modified.to_native(), + ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200, } } @@ -383,8 +383,8 @@ impl<'x> ArchivedResource<'x> { ArchivedResource::AddressBook(archive) => Some(&archive.inner.dead_properties), ArchivedResource::ContactCard(archive) => Some(&archive.inner.dead_properties), ArchivedResource::FileNode(archive) => Some(&archive.inner.dead_properties), - ArchivedResource::CalendarScheduling(_) - | ArchivedResource::CalendarSchedulingCollection(_) => None, + ArchivedResource::CalendarEventNotification(_) + | ArchivedResource::CalendarEventNotificationCollection(_) => None, } } @@ -394,11 +394,11 @@ impl<'x> ArchivedResource<'x> { archive.inner.file.as_ref().map(|f| f.size.to_native()) } ArchivedResource::CalendarEvent(archive) => archive.inner.size.to_native().into(), - ArchivedResource::CalendarScheduling(archive) => archive.inner.size.to_native().into(), + ArchivedResource::CalendarEventNotification(archive) => archive.inner.size.to_native().into(), ArchivedResource::ContactCard(archive) => archive.inner.size.to_native().into(), ArchivedResource::AddressBook(_) | ArchivedResource::Calendar(_) - | ArchivedResource::CalendarSchedulingCollection(_) => None, + | ArchivedResource::CalendarEventNotificationCollection(_) => None, } } @@ -409,13 +409,13 @@ impl<'x> ArchivedResource<'x> { .file .as_ref() .and_then(|f| f.media_type.as_deref()), - ArchivedResource::CalendarEvent(_) | ArchivedResource::CalendarScheduling(_) => { + ArchivedResource::CalendarEvent(_) | ArchivedResource::CalendarEventNotification(_) => { "text/calendar".into() } ArchivedResource::ContactCard(_) => "text/vcard".into(), ArchivedResource::AddressBook(_) | ArchivedResource::Calendar(_) - | ArchivedResource::CalendarSchedulingCollection(_) => None, + | ArchivedResource::CalendarEventNotificationCollection(_) => None, } } @@ -430,8 +430,8 @@ impl<'x> ArchivedResource<'x> { } ArchivedResource::ContactCard(archive) => archive.inner.display_name.as_deref(), ArchivedResource::FileNode(archive) => archive.inner.display_name.as_deref(), - ArchivedResource::CalendarScheduling(_) - | ArchivedResource::CalendarSchedulingCollection(_) => None, + ArchivedResource::CalendarEventNotification(_) + | ArchivedResource::CalendarEventNotificationCollection(_) => None, } } @@ -462,7 +462,7 @@ impl<'x> ArchivedResource<'x> { ReportSet::PrincipalMatch, ] .into(), - ArchivedResource::CalendarSchedulingCollection(_) => vec![ + ArchivedResource::CalendarEventNotificationCollection(_) => vec![ ReportSet::SyncCollection, ReportSet::CalendarQuery, ReportSet::CalendarMultiGet, @@ -483,10 +483,10 @@ impl<'x> ArchivedResource<'x> { ArchivedResource::FileNode(archive) if archive.inner.file.is_none() => { vec![ResourceType::Collection].into() } - ArchivedResource::CalendarSchedulingCollection(true) => { + ArchivedResource::CalendarEventNotificationCollection(true) => { vec![ResourceType::Collection, ResourceType::ScheduleInbox].into() } - ArchivedResource::CalendarSchedulingCollection(false) => { + ArchivedResource::CalendarEventNotificationCollection(false) => { vec![ResourceType::Collection, ResourceType::ScheduleOutbox].into() } _ => None, diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index a92c04fe..70a988a9 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -139,7 +139,7 @@ impl PropFindRequestHandler for Server { { true } - Collection::CalendarScheduling if resource.account_id.is_some() => true, + Collection::CalendarEventNotification if resource.account_id.is_some() => true, _ => { return Err(DavErrorCondition::new( StatusCode::FORBIDDEN, @@ -156,13 +156,13 @@ impl PropFindRequestHandler for Server { Collection::FileNode | Collection::Calendar | Collection::AddressBook - | Collection::CalendarScheduling => { + | Collection::CalendarEventNotification => { // Validate permissions access_token.assert_has_permission(match resource.collection { Collection::FileNode => Permission::DavFilePropFind, Collection::Calendar | Collection::CalendarEvent - | Collection::CalendarScheduling => Permission::DavCalPropFind, + | Collection::CalendarEventNotification => Permission::DavCalPropFind, Collection::AddressBook | Collection::ContactCard => { Permission::DavCardPropFind } @@ -236,7 +236,7 @@ impl PropFindRequestHandler for Server { Collection::FileNode => Permission::DavFilePropFind, Collection::Calendar | Collection::CalendarEvent - | Collection::CalendarScheduling => Permission::DavCalPropFind, + | Collection::CalendarEventNotification => Permission::DavCalPropFind, Collection::AddressBook | Collection::ContactCard => { Permission::DavCardPropFind } @@ -363,7 +363,7 @@ impl PropFindRequestHandler for Server { Collection::FileNode => { (FILE_CONTAINER_PROPS.as_slice(), FILE_ITEM_PROPS.as_slice()) } - Collection::Calendar | Collection::CalendarScheduling => ( + Collection::Calendar | Collection::CalendarEventNotification => ( CALENDAR_CONTAINER_PROPS.as_slice(), CALENDAR_ITEM_PROPS.as_slice(), ), @@ -408,7 +408,7 @@ impl PropFindRequestHandler for Server { PropFind::Prop(items) => items.clone(), }; - let is_scheduling = collection_container == Collection::CalendarScheduling; + let is_scheduling = collection_container == Collection::CalendarEventNotification; 'outer: for item in paths { let account_id = item.account_id; let document_id = item.document_id; @@ -422,7 +422,7 @@ impl PropFindRequestHandler for Server { let archive_; let archive = if is_scheduling && item.is_container { archive_ = Archive::default(); - ArchivedResource::CalendarSchedulingCollection( + ArchivedResource::CalendarEventNotificationCollection( item.document_id == SCHEDULE_INBOX_ID, ) } else if let Some(archive) = self @@ -683,8 +683,8 @@ impl PropFindRequestHandler for Server { property.clone(), vec![SupportedPrivilege::all_scheduling_privileges(matches!( archive, - ArchivedResource::CalendarScheduling(_) - | ArchivedResource::CalendarSchedulingCollection(true) + ArchivedResource::CalendarEventNotification(_) + | ArchivedResource::CalendarEventNotificationCollection(true) ))], )); } @@ -694,8 +694,8 @@ impl PropFindRequestHandler for Server { Privilege::scheduling( matches!( archive, - ArchivedResource::CalendarScheduling(_) - | ArchivedResource::CalendarSchedulingCollection(true) + ArchivedResource::CalendarEventNotification(_) + | ArchivedResource::CalendarEventNotificationCollection(true) ), access_token.is_member(account_id), ) @@ -979,7 +979,7 @@ impl PropFindRequestHandler for Server { } ( CalDavProperty::CalendarData(_), - ArchivedResource::CalendarScheduling(event), + ArchivedResource::CalendarEventNotification(event), ) => { fields.push(DavPropertyValue::new( property.clone(), @@ -1008,7 +1008,7 @@ impl PropFindRequestHandler for Server { } ( CalDavProperty::ScheduleDefaultCalendarURL, - ArchivedResource::CalendarSchedulingCollection(true), + ArchivedResource::CalendarEventNotificationCollection(true), ) => { if let Some(default_cal) = &self.core.groupware.default_calendar_name { fields.push(DavPropertyValue::new( diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 9bd1f45f..b877d360 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -83,7 +83,7 @@ impl PrincipalPropFind for Server { response.set_namespace(Namespace::CardDav); false } - Collection::Calendar | Collection::CalendarEvent | Collection::CalendarScheduling => { + Collection::Calendar | Collection::CalendarEvent | Collection::CalendarEventNotification => { response.set_namespace(Namespace::CalDav); false } diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index 91fb1777..dd6bda6a 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -10,7 +10,7 @@ use crate::{ copy_move::CalendarCopyMoveRequestHandler, delete::CalendarDeleteRequestHandler, freebusy::CalendarFreebusyRequestHandler, get::CalendarGetRequestHandler, mkcol::CalendarMkColRequestHandler, proppatch::CalendarPropPatchRequestHandler, - query::CalendarQueryRequestHandler, scheduling::CalendarSchedulingHandler, + query::CalendarQueryRequestHandler, scheduling::CalendarEventNotificationHandler, update::CalendarUpdateRequestHandler, }, card::{ diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 187f2889..525c28a6 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -506,7 +506,7 @@ impl ManageDirectory for Store { permissions .into_iter() .map(|(k, v)| PermissionGrant { - permission: k, + permission: k.id(), grant: !v, }) .collect(), @@ -1725,7 +1725,7 @@ impl ManageDirectory for Store { if !permissions.is_empty() { principal.add_permissions(permissions.into_iter().map(|permission| { PermissionGrant { - permission, + permission: permission.id(), grant: !is_disabled, } })); @@ -2218,13 +2218,17 @@ impl ManageDirectory for Store { if has_enabled { result.append_str( PrincipalField::EnabledPermissions, - grant.permission.name(), + Permission::from_id(grant.permission) + .map(|f| f.name()) + .unwrap_or("unknown"), ); } } else if has_disabled { result.append_str( PrincipalField::DisabledPermissions, - grant.permission.name(), + Permission::from_id(grant.permission) + .map(|f| f.name()) + .unwrap_or("unknown"), ); } } diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs index ed0d6aa3..790f6c9e 100644 --- a/crates/directory/src/core/mod.rs +++ b/crates/directory/src/core/mod.rs @@ -268,6 +268,57 @@ impl Permission { Permission::JmapFileNodeChanges => "Track file node changes via JMAP", Permission::JmapFileNodeQuery => "Search for file nodes matching criteria via JMAP", Permission::JmapFileNodeQueryChanges => "Track file node query changes via JMAP", + Permission::JmapPrincipalGetAvailability => { + "Retrieve availability information via JMAP" + } + Permission::JmapPrincipalChanges => "Track principal changes via JMAP", + Permission::JmapShareNotificationGet => "Retrieve share notifications via JMAP", + Permission::JmapShareNotificationSet => "Create or update share notifications via JMAP", + Permission::JmapShareNotificationChanges => "Track share notification changes via JMAP", + Permission::JmapShareNotificationQuery => { + "Search for share notifications matching criteria via JMAP" + } + Permission::JmapShareNotificationQueryChanges => { + "Track share notification query changes via JMAP" + } + Permission::JmapCalendarGet => "Retrieve calendars via JMAP", + Permission::JmapCalendarSet => "Create or update calendars via JMAP", + Permission::JmapCalendarChanges => "Track calendar changes via JMAP", + Permission::JmapCalendarEventGet => "Retrieve calendar events via JMAP", + Permission::JmapCalendarEventSet => "Create or update calendar events via JMAP", + Permission::JmapCalendarEventChanges => "Track calendar event changes via JMAP", + Permission::JmapCalendarEventQuery => { + "Search for calendar events matching criteria via JMAP" + } + Permission::JmapCalendarEventQueryChanges => { + "Track calendar event query changes via JMAP" + } + Permission::JmapCalendarEventCopy => "Copy calendar events to new locations via JMAP", + Permission::JmapCalendarEventParse => "Parse calendar events via JMAP", + Permission::JmapCalendarEventNotificationGet => { + "Retrieve calendar event notifications via JMAP" + } + Permission::JmapCalendarEventNotificationSet => { + "Create or update calendar event notifications via JMAP" + } + Permission::JmapCalendarEventNotificationChanges => { + "Track calendar event notification changes via JMAP" + } + Permission::JmapCalendarEventNotificationQuery => { + "Search for calendar event notifications matching criteria via JMAP" + } + Permission::JmapCalendarEventNotificationQueryChanges => { + "Track calendar event notification query changes via JMAP" + } + Permission::JmapParticipantIdentityGet => { + "Retrieve participant identity information via JMAP" + } + Permission::JmapParticipantIdentitySet => { + "Create or update participant identities via JMAP" + } + Permission::JmapParticipantIdentityChanges => { + "Track participant identity changes via JMAP" + } } } } diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 31f236ef..fabdb57a 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -196,6 +196,7 @@ impl Principal { } pub fn add_permission(&mut self, permission: Permission, grant: bool) { + let permission = permission.id(); if let Some(permissions) = self.data.iter_mut().find_map(|item| { if let PrincipalData::Permissions(permissions) = item { Some(permissions) @@ -232,6 +233,7 @@ impl Principal { } pub fn remove_permission(&mut self, permission: Permission, grant: bool) { + let permission = permission.id(); if let Some(permissions) = self.data.iter_mut().find_map(|item| { if let PrincipalData::Permissions(permissions) = item { Some(permissions) @@ -1323,7 +1325,7 @@ impl<'de> serde::Deserialize<'de> for StringOrMany { impl Permission { pub fn all() -> impl Iterator { - (0..Permission::COUNT).filter_map(Permission::from_id) + (0..Permission::COUNT as u32).filter_map(Permission::from_id) } pub const fn is_user_permission(&self) -> bool { @@ -1480,6 +1482,31 @@ impl Permission { | Permission::JmapFileNodeChanges | Permission::JmapFileNodeQuery | Permission::JmapFileNodeQueryChanges + | Permission::JmapPrincipalGetAvailability + | Permission::JmapPrincipalChanges + | Permission::JmapShareNotificationGet + | Permission::JmapShareNotificationSet + | Permission::JmapShareNotificationChanges + | Permission::JmapShareNotificationQuery + | Permission::JmapShareNotificationQueryChanges + | Permission::JmapCalendarGet + | Permission::JmapCalendarSet + | Permission::JmapCalendarChanges + | Permission::JmapCalendarEventGet + | Permission::JmapCalendarEventSet + | Permission::JmapCalendarEventChanges + | Permission::JmapCalendarEventQuery + | Permission::JmapCalendarEventQueryChanges + | Permission::JmapCalendarEventCopy + | Permission::JmapCalendarEventParse + | Permission::JmapCalendarEventNotificationGet + | Permission::JmapCalendarEventNotificationSet + | Permission::JmapCalendarEventNotificationChanges + | Permission::JmapCalendarEventNotificationQuery + | Permission::JmapCalendarEventNotificationQueryChanges + | Permission::JmapParticipantIdentityGet + | Permission::JmapParticipantIdentitySet + | Permission::JmapParticipantIdentityChanges ) } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index ca0592b9..759be326 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -75,7 +75,7 @@ pub struct MemberOf { #[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] pub struct PermissionGrant { - pub permission: Permission, + pub permission: u32, pub grant: bool, } @@ -109,18 +109,7 @@ pub enum Type { } #[derive( - rkyv::Archive, - rkyv::Deserialize, - rkyv::Serialize, - Debug, - Clone, - Copy, - PartialEq, - Eq, - Hash, - serde::Serialize, - serde::Deserialize, - EnumMethods, + Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods, )] #[serde(rename_all = "kebab-case")] pub enum Permission { @@ -397,10 +386,41 @@ pub enum Permission { JmapFileNodeChanges, JmapFileNodeQuery, JmapFileNodeQueryChanges, + + JmapPrincipalGetAvailability, + JmapPrincipalChanges, + + JmapShareNotificationGet, + JmapShareNotificationSet, + JmapShareNotificationChanges, + JmapShareNotificationQuery, + JmapShareNotificationQueryChanges, + + JmapCalendarGet, + JmapCalendarSet, + JmapCalendarChanges, + + JmapCalendarEventGet, + JmapCalendarEventSet, + JmapCalendarEventChanges, + JmapCalendarEventQuery, + JmapCalendarEventQueryChanges, + JmapCalendarEventCopy, + JmapCalendarEventParse, + + JmapCalendarEventNotificationGet, + JmapCalendarEventNotificationSet, + JmapCalendarEventNotificationChanges, + JmapCalendarEventNotificationQuery, + JmapCalendarEventNotificationQueryChanges, + + JmapParticipantIdentityGet, + JmapParticipantIdentitySet, + JmapParticipantIdentityChanges, // WARNING: add new ids at the end (TODO: use static ids) } -pub const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); +pub const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); pub type Permissions = Bitset; pub const ROLE_ADMIN: u32 = u32::MAX; diff --git a/crates/groupware/src/cache/calcard.rs b/crates/groupware/src/cache/calcard.rs index 860f5344..ef92925f 100644 --- a/crates/groupware/src/cache/calcard.rs +++ b/crates/groupware/src/cache/calcard.rs @@ -183,7 +183,7 @@ pub(super) async fn build_scheduling_resources( .core .storage .data - .get_last_change_id(account_id, SyncCollection::CalendarScheduling.into()) + .get_last_change_id(account_id, SyncCollection::CalendarEventNotification.into()) .await .caused_by(trc::location!())? .unwrap_or_default(); @@ -196,7 +196,7 @@ pub(super) async fn build_scheduling_resources( .unwrap_or_else(|| format!("_{account_id}")); let item_ids = server - .get_document_ids(account_id, Collection::CalendarScheduling) + .get_document_ids(account_id, Collection::CalendarEventNotification) .await .caused_by(trc::location!())? .unwrap_or_default(); @@ -326,7 +326,7 @@ pub(super) fn resource_from_event(event: &ArchivedCalendarEvent, document_id: u3 pub(super) fn resource_from_scheduling(document_id: u32, is_container: bool) -> DavResource { DavResource { document_id, - data: DavResourceMetadata::CalendarScheduling { + data: DavResourceMetadata::CalendarEventNotification { names: if !is_container { [DavName { name: format!("{document_id}.ics"), diff --git a/crates/groupware/src/cache/mod.rs b/crates/groupware/src/cache/mod.rs index 192bc5ea..4a28513f 100644 --- a/crates/groupware/src/cache/mod.rs +++ b/crates/groupware/src/cache/mod.rs @@ -77,7 +77,7 @@ impl GroupwareCache for Server { SyncCollection::Calendar => &self.inner.cache.events, SyncCollection::AddressBook => &self.inner.cache.contacts, SyncCollection::FileNode => &self.inner.cache.files, - SyncCollection::CalendarScheduling => &self.inner.cache.scheduling, + SyncCollection::CalendarEventNotification => &self.inner.cache.scheduling, _ => unreachable!(), }; let cache_ = match cache_store.get_value_or_guard_async(&account_id).await { @@ -178,7 +178,7 @@ impl GroupwareCache for Server { } let num_changes = changes.changes.len(); - let cache = if !matches!(collection, SyncCollection::CalendarScheduling) { + let cache = if !matches!(collection, SyncCollection::CalendarEventNotification) { let mut updated_resources = AHashMap::with_capacity(8); let has_no_children = collection == SyncCollection::FileNode; @@ -517,7 +517,7 @@ async fn full_cache_build( .await } SyncCollection::FileNode => build_file_resources(server, account_id, update_lock).await, - SyncCollection::CalendarScheduling => { + SyncCollection::CalendarEventNotification => { build_scheduling_resources(server, account_id, update_lock).await } _ => unreachable!(), diff --git a/crates/groupware/src/calendar/index.rs b/crates/groupware/src/calendar/index.rs index 6e39a64a..7bd1a005 100644 --- a/crates/groupware/src/calendar/index.rs +++ b/crates/groupware/src/calendar/index.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::calendar::{ArchivedCalendarScheduling, CalendarScheduling}; +use crate::calendar::{ArchivedCalendarEventNotification, CalendarEventNotification}; use super::{ ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert, @@ -22,7 +22,6 @@ impl IndexableObject for Calendar { IndexValue::Quota { used: self.dead_properties.size() as u32 + self.preferences.iter().map(|p| p.size()).sum::() as u32 - + self.default_alerts.iter().map(|a| a.size()).sum::() as u32 + self.name.len() as u32, }, IndexValue::LogContainer { @@ -47,7 +46,6 @@ impl IndexableObject for &ArchivedCalendar { IndexValue::Quota { used: self.dead_properties.size() as u32 + self.preferences.iter().map(|p| p.size()).sum::() as u32 - + self.default_alerts.iter().map(|a| a.size()).sum::() as u32 + self.name.len() as u32, }, IndexValue::LogContainer { @@ -114,7 +112,7 @@ impl IndexableAndSerializableObject for CalendarEvent { } } -impl IndexableObject for CalendarScheduling { +impl IndexableObject for CalendarEventNotification { fn index_values(&self) -> impl Iterator> { [ IndexValue::Quota { used: self.size }, @@ -123,7 +121,7 @@ impl IndexableObject for CalendarScheduling { value: self.created.into(), }, IndexValue::LogItem { - sync_collection: SyncCollection::CalendarScheduling, + sync_collection: SyncCollection::CalendarEventNotification, prefix: None, }, ] @@ -131,7 +129,7 @@ impl IndexableObject for CalendarScheduling { } } -impl IndexableObject for &ArchivedCalendarScheduling { +impl IndexableObject for &ArchivedCalendarEventNotification { fn index_values(&self) -> impl Iterator> { [ IndexValue::Quota { @@ -142,7 +140,7 @@ impl IndexableObject for &ArchivedCalendarScheduling { value: self.created.to_native().into(), }, IndexValue::LogItem { - sync_collection: SyncCollection::CalendarScheduling, + sync_collection: SyncCollection::CalendarEventNotification, prefix: None, }, ] @@ -150,7 +148,7 @@ impl IndexableObject for &ArchivedCalendarScheduling { } } -impl IndexableAndSerializableObject for CalendarScheduling { +impl IndexableAndSerializableObject for CalendarEventNotification { fn is_versioned() -> bool { false } @@ -159,6 +157,7 @@ impl IndexableAndSerializableObject for CalendarScheduling { impl CalendarPreferences { pub fn size(&self) -> usize { self.name.len() + + self.default_alerts.iter().map(|a| a.size()).sum::() + self.description.as_ref().map_or(0, |n| n.len()) + self.color.as_ref().map_or(0, |n| n.len()) + self.time_zone.size() @@ -168,6 +167,7 @@ impl CalendarPreferences { impl ArchivedCalendarPreferences { pub fn size(&self) -> usize { self.name.len() + + self.default_alerts.iter().map(|a| a.size()).sum::() + self.description.as_ref().map_or(0, |n| n.len()) + self.color.as_ref().map_or(0, |n| n.len()) + self.time_zone.size() @@ -196,12 +196,12 @@ impl ArchivedTimezone { impl DefaultAlert { pub fn size(&self) -> usize { - self.alert.size() + self.id.len() + std::mem::size_of::() + self.id.len() } } impl ArchivedDefaultAlert { pub fn size(&self) -> usize { - self.alert.size() + self.id.len() + std::mem::size_of::() + self.id.len() } } diff --git a/crates/groupware/src/calendar/itip.rs b/crates/groupware/src/calendar/itip.rs index 3e3105e0..0992c961 100644 --- a/crates/groupware/src/calendar/itip.rs +++ b/crates/groupware/src/calendar/itip.rs @@ -7,7 +7,7 @@ use crate::{ RFC_3986, cache::GroupwareCache, - calendar::{CalendarEvent, CalendarEventData, CalendarScheduling}, + calendar::{CalendarEvent, CalendarEventData, CalendarEventNotification}, scheduling::{ ItipError, ItipMessage, inbound::{ @@ -224,10 +224,10 @@ impl ItipIngest for Server { // Build event for schedule inbox let itip_document_id = self .store() - .assign_document_ids(account_id, Collection::CalendarScheduling, 1) + .assign_document_ids(account_id, Collection::CalendarEventNotification, 1) .await .caused_by(trc::location!())?; - let itip_message = CalendarScheduling { + let itip_message = CalendarEventNotification { itip, event_id: Some(document_id), size: itip_message.len() as u32, @@ -329,10 +329,10 @@ impl ItipIngest for Server { .caused_by(trc::location!())?; let itip_document_id = self .store() - .assign_document_ids(account_id, Collection::CalendarScheduling, 1) + .assign_document_ids(account_id, Collection::CalendarEventNotification, 1) .await .caused_by(trc::location!())?; - let itip_message = CalendarScheduling { + let itip_message = CalendarEventNotification { itip, event_id: Some(document_id), size: itip_message.len() as u32, diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 6879b92e..60fc0818 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -11,7 +11,7 @@ pub mod index; pub mod itip; pub mod storage; -use calcard::icalendar::ICalendar; +use calcard::icalendar::{ICalendar, ICalendarDuration}; use common::{DavName, auth::AccessToken}; use dav_proto::schema::request::DeadProperty; use types::acl::AclGrant; @@ -22,7 +22,6 @@ use types::acl::AclGrant; pub struct Calendar { pub name: String, pub preferences: Vec, - pub default_alerts: Vec, pub acls: Vec, pub dead_properties: DeadProperty, pub created: i64, @@ -46,6 +45,7 @@ pub struct CalendarPreferences { pub color: Option, pub flags: u16, pub time_zone: Timezone, + pub default_alerts: Vec, } #[derive( @@ -54,10 +54,14 @@ pub struct CalendarPreferences { pub struct DefaultAlert { pub account_id: u32, pub id: String, - pub alert: ICalendar, - pub with_time: bool, + pub offset: ICalendarDuration, + pub flags: u16, } +pub const ALERT_WITH_TIME: u16 = 1; +pub const ALERT_EMAIL: u16 = 1 << 1; +pub const ALERT_RELATIVE_TO_END: u16 = 1 << 2; + pub const SCHEDULE_INBOX_ID: u32 = u32::MAX - 1; pub const SCHEDULE_OUTBOX_ID: u32 = u32::MAX - 2; @@ -86,7 +90,7 @@ pub struct CalendarEvent { #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] -pub struct CalendarScheduling { +pub struct CalendarEventNotification { pub itip: ICalendar, pub event_id: Option, pub flags: u16, diff --git a/crates/groupware/src/calendar/storage.rs b/crates/groupware/src/calendar/storage.rs index 75b3d23f..ebc79520 100644 --- a/crates/groupware/src/calendar/storage.rs +++ b/crates/groupware/src/calendar/storage.rs @@ -10,7 +10,7 @@ use super::{ }; use crate::{ DavResourceName, DestroyArchive, RFC_3986, - calendar::{ArchivedCalendarScheduling, CalendarScheduling}, + calendar::{ArchivedCalendarEventNotification, CalendarEventNotification}, scheduling::{ItipMessages, event_cancel::itip_cancel}, }; use calcard::common::timezone::Tz; @@ -47,14 +47,14 @@ impl ItipAutoExpunge for Server { IterateParams::new( IndexKey { account_id, - collection: Collection::CalendarScheduling.into(), + collection: Collection::CalendarEventNotification.into(), document_id: 0, field: CalendarField::Created.into(), key: 0u64.serialize(), }, IndexKey { account_id, - collection: Collection::CalendarScheduling.into(), + collection: Collection::CalendarEventNotification.into(), document_id: u32::MAX, field: CalendarField::Created.into(), key: now().saturating_sub(hold_period).serialize(), @@ -81,7 +81,7 @@ impl ItipAutoExpunge for Server { trc::event!( Purge(trc::PurgeEvent::AutoExpunge), AccountId = account_id, - Collection = Collection::CalendarScheduling.as_str(), + Collection = Collection::CalendarEventNotification.as_str(), Total = destroy_ids.len(), ); @@ -95,12 +95,16 @@ impl ItipAutoExpunge for Server { for document_id in destroy_ids { // Fetch event if let Some(event_) = self - .get_archive(account_id, Collection::CalendarScheduling, document_id) + .get_archive( + account_id, + Collection::CalendarEventNotification, + document_id, + ) .await .caused_by(trc::location!())? { let event = event_ - .to_unarchived::() + .to_unarchived::() .caused_by(trc::location!())?; DestroyArchive(event) .delete(&access_token, account_id, document_id, &mut batch) @@ -238,7 +242,7 @@ impl Calendar { } } -impl CalendarScheduling { +impl CalendarEventNotification { pub fn insert<'x>( self, access_token: &AccessToken, @@ -255,7 +259,7 @@ impl CalendarScheduling { // Prepare write batch batch .with_account_id(account_id) - .with_collection(Collection::CalendarScheduling) + .with_collection(Collection::CalendarEventNotification) .create_document(document_id) .custom( ObjectIndexBuilder::<(), _>::new() @@ -421,7 +425,7 @@ impl DestroyArchive> { } } -impl DestroyArchive> { +impl DestroyArchive> { #[allow(clippy::too_many_arguments)] pub fn delete( self, @@ -433,7 +437,7 @@ impl DestroyArchive> { // Delete event batch .with_account_id(account_id) - .with_collection(Collection::CalendarScheduling) + .with_collection(Collection::CalendarEventNotification) .delete_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index d59216ac..ea3f9ab7 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -106,7 +106,7 @@ impl From for Collection { DavResourceName::Cal => Collection::Calendar, DavResourceName::File => Collection::FileNode, DavResourceName::Principal => Collection::Principal, - DavResourceName::Scheduling => Collection::CalendarScheduling, + DavResourceName::Scheduling => Collection::CalendarEventNotification, } } } @@ -118,7 +118,7 @@ impl From for DavResourceName { Collection::Calendar => DavResourceName::Cal, Collection::FileNode => DavResourceName::File, Collection::Principal => DavResourceName::Principal, - Collection::CalendarScheduling => DavResourceName::Scheduling, + Collection::CalendarEventNotification => DavResourceName::Scheduling, _ => unreachable!(), } } @@ -130,7 +130,7 @@ impl From for DavResourceName { SyncCollection::AddressBook => DavResourceName::Card, SyncCollection::Calendar => DavResourceName::Cal, SyncCollection::FileNode => DavResourceName::File, - SyncCollection::CalendarScheduling => DavResourceName::Scheduling, + SyncCollection::CalendarEventNotification => DavResourceName::Scheduling, _ => unreachable!(), } } diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index ddcb1276..a36fd069 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -9,7 +9,7 @@ store = { path = "../store" } utils = { path = "../utils" } types = { path = "../types" } trc = { path = "../trc" } -jmap-tools = { version = "0.1" } +jmap-tools = { path = "/Users/me/code/jmap-tool" } calcard = { path = "/Users/me/code/calcard" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } serde = { version = "1.0", features = ["derive"]} diff --git a/crates/jmap-proto/src/method/availability.rs b/crates/jmap-proto/src/method/availability.rs new file mode 100644 index 00000000..c487258c --- /dev/null +++ b/crates/jmap-proto/src/method/availability.rs @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + request::{ + MaybeInvalid, + deserialize::{DeserializeArguments, deserialize_request}, + }, + types::date::UTCDate, +}; +use calcard::jscalendar::{JSCalendar, JSCalendarProperty}; +use serde::{Deserialize, Deserializer, Serialize}; +use types::id::Id; + +#[derive(Debug, Clone, Default)] +pub struct GetAvailabilityRequest { + pub account_id: Id, + pub id: Id, + pub utc_start: UTCDate, + pub utc_end: UTCDate, + pub show_details: bool, + pub event_properties: Option>>>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GetAvailabilityResponse { + pub list: Vec, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BusyPeriod { + pub utc_start: UTCDate, + pub utc_end: UTCDate, + pub busy_status: Option, + pub event: Option>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum BusyStatus { + Confirmed, + Tentative, + Unavailable, +} + +impl<'de> DeserializeArguments<'de> for GetAvailabilityRequest { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"accountId" => { + self.account_id = map.next_value()?; + }, + b"utcStart" => { + self.utc_start = map.next_value()?; + }, + b"utcEnd" => { + self.utc_end = map.next_value()?; + }, + b"id" => { + self.id = map.next_value()?; + }, + b"showDetails" => { + self.show_details = map.next_value()?; + }, + b"eventProperties" => { + self.event_properties = map.next_value()?; + }, + _ => { + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl<'de> Deserialize<'de> for GetAvailabilityRequest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_request(deserializer) + } +} diff --git a/crates/jmap-proto/src/method/copy.rs b/crates/jmap-proto/src/method/copy.rs index 1a5cef90..de3e7610 100644 --- a/crates/jmap-proto/src/method/copy.rs +++ b/crates/jmap-proto/src/method/copy.rs @@ -74,7 +74,7 @@ pub struct CopyBlobResponse { #[serde(rename = "notCopied")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub not_copied: VecMap, SetError>, + pub not_copied: VecMap>, } impl<'de, T: JmapObject> DeserializeArguments<'de> for CopyRequest<'de, T> { @@ -171,11 +171,12 @@ impl<'de, T: JmapObject> Default for CopyRequest<'de, T> { impl CopyResponse { pub fn created(&mut self, id: Id, document_id: impl Into) { + let document_id = document_id.into(); self.created.append( id, Value::Object(Map::from(vec![( Key::Property(T::ID_PROPERTY), - Value::Element(T::Element::from(document_id.into())), + Value::Element(document_id.into()), )])), ); } diff --git a/crates/jmap-proto/src/method/lookup.rs b/crates/jmap-proto/src/method/lookup.rs index 94085139..e5ce00d2 100644 --- a/crates/jmap-proto/src/method/lookup.rs +++ b/crates/jmap-proto/src/method/lookup.rs @@ -28,7 +28,7 @@ pub struct BlobLookupResponse { pub list: Vec, #[serde(rename = "notFound")] - pub not_found: Vec>, + pub not_found: Vec, } #[derive(Debug, Clone, Default, serde::Serialize)] diff --git a/crates/jmap-proto/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs index 61611ccb..4aa8f47b 100644 --- a/crates/jmap-proto/src/method/mod.rs +++ b/crates/jmap-proto/src/method/mod.rs @@ -12,6 +12,7 @@ use serde::{ }; use std::{borrow::Cow, fmt, str::FromStr}; +pub mod availability; pub mod changes; pub mod copy; pub mod get; diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs index db86563c..fa4172ad 100644 --- a/crates/jmap-proto/src/method/set.rs +++ b/crates/jmap-proto/src/method/set.rs @@ -216,7 +216,7 @@ impl SetResponse { id, Value::Object(Map::from(vec![( Key::Property(T::ID_PROPERTY), - Value::Element(T::Element::from(document_id.into())), + Value::Element(document_id.into().into()), )])), ); } diff --git a/crates/jmap-proto/src/object/addressbook.rs b/crates/jmap-proto/src/object/addressbook.rs index c73ff666..765029d7 100644 --- a/crates/jmap-proto/src/object/addressbook.rs +++ b/crates/jmap-proto/src/object/addressbook.rs @@ -281,17 +281,13 @@ impl JmapObjectId for AddressBookValue { None } } -} -impl TryFrom for AddressBookValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - if let AnyId::Id(id) = value { - Ok(AddressBookValue::Id(id)) - } else { - Err(()) + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(new_id) = new_id { + *self = AddressBookValue::Id(new_id); + return true; } + false } } @@ -331,3 +327,33 @@ impl From for AddressBookProperty { AddressBookProperty::Rights(right) } } + +impl JmapObjectId for AddressBookProperty { + fn as_id(&self) -> Option { + if let AddressBookProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let AddressBookProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(new_id) = new_id { + *self = AddressBookProperty::IdValue(new_id); + return true; + } + false + } +} diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index 05ed7ab0..3ede7363 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -155,15 +155,6 @@ impl<'de> DeserializeArguments<'de> for BlobGetArguments { } } -impl serde::Serialize for BlobProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl JmapObject for Blob { type Property = BlobProperty; @@ -213,15 +204,30 @@ impl JmapObjectId for BlobValue { None } } -} -impl TryFrom for BlobValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::BlobId(id) => Ok(BlobValue::BlobId(id)), - _ => Err(()), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::BlobId(id) = new_id { + *self = BlobValue::BlobId(id); + return true; } + false + } +} + +impl JmapObjectId for BlobProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false } } diff --git a/crates/jmap-proto/src/object/calendar.rs b/crates/jmap-proto/src/object/calendar.rs new file mode 100644 index 00000000..d5a2bba7 --- /dev/null +++ b/crates/jmap-proto/src/object/calendar.rs @@ -0,0 +1,484 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{ + AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref, + }, + request::{deserialize::DeserializeArguments, reference::MaybeIdReference}, + types::date::UTCDate, +}; +use calcard::{ + common::{IanaParse, timezone::Tz}, + icalendar::ICalendarDuration, + jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType}, +}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::{acl::Acl, id::Id}; + +#[derive(Debug, Clone, Default)] +pub struct Calendar; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CalendarProperty { + Id, + Name, + Description, + Color, + SortOrder, + IsSubscribed, + IsVisible, + IsDefault, + IncludeInAvailability, + DefaultAlertsWithTime, + DefaultAlertsWithoutTime, + TimeZone, + ShareWith, + MyRights, + + // Alert object properties + When, + Trigger, + Offset, + RelativeTo, + Action, + Type, + + // Other + IdValue(Id), + Rights(CalendarRight), + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CalendarRight { + MayReadFreeBusy, + MayReadItems, + MayWriteAll, + MayWriteOwn, + MayUpdatePrivate, + MayRSVP, + MayShare, + MayDelete, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CalendarValue { + Id(Id), + IdReference(String), + IncludeInAvailability(IncludeInAvailability), + Date(UTCDate), + Timezone(Tz), + Action(JSCalendarAlertAction), + RelativeTo(JSCalendarRelativeTo), + Type(JSCalendarType), + Duration(ICalendarDuration), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IncludeInAvailability { + All, + Attending, + None, +} + +impl Property for CalendarProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + let allow_patch = key.is_none(); + if let Some(Key::Property(key)) = key { + match key.patch_or_prop() { + CalendarProperty::ShareWith => { + Id::from_str(value).ok().map(CalendarProperty::IdValue) + } + _ => CalendarProperty::parse(value, allow_patch), + } + } else { + CalendarProperty::parse(value, allow_patch) + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + CalendarProperty::Id => "id", + CalendarProperty::Name => "name", + CalendarProperty::Description => "description", + CalendarProperty::Color => "color", + CalendarProperty::SortOrder => "sortOrder", + CalendarProperty::IsSubscribed => "isSubscribed", + CalendarProperty::IsVisible => "isVisible", + CalendarProperty::IsDefault => "isDefault", + CalendarProperty::IncludeInAvailability => "includeInAvailability", + CalendarProperty::DefaultAlertsWithTime => "defaultAlertsWithTime", + CalendarProperty::DefaultAlertsWithoutTime => "defaultAlertsWithoutTime", + CalendarProperty::TimeZone => "timeZone", + CalendarProperty::ShareWith => "shareWith", + CalendarProperty::MyRights => "myRights", + CalendarProperty::When => "when", + CalendarProperty::Trigger => "trigger", + CalendarProperty::Offset => "offset", + CalendarProperty::RelativeTo => "relativeTo", + CalendarProperty::Action => "action", + CalendarProperty::Type => "@type", + CalendarProperty::Rights(calendar_right) => calendar_right.as_str(), + CalendarProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + CalendarProperty::IdValue(id) => return id.to_string().into(), + } + .into() + } +} + +impl CalendarRight { + pub fn as_str(&self) -> &'static str { + match self { + CalendarRight::MayReadFreeBusy => "mayReadFreeBusy", + CalendarRight::MayReadItems => "mayReadItems", + CalendarRight::MayWriteAll => "mayWriteAll", + CalendarRight::MayWriteOwn => "mayWriteOwn", + CalendarRight::MayUpdatePrivate => "mayUpdatePrivate", + CalendarRight::MayRSVP => "mayRSVP", + CalendarRight::MayShare => "mayShare", + CalendarRight::MayDelete => "mayDelete", + } + } +} + +impl IncludeInAvailability { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"all" => IncludeInAvailability::All, + b"attending" => IncludeInAvailability::Attending, + b"none" => IncludeInAvailability::None, + ) + } + + pub fn as_str(&self) -> &'static str { + match self { + IncludeInAvailability::All => "all", + IncludeInAvailability::Attending => "attending", + IncludeInAvailability::None => "none", + } + } +} + +impl Element for CalendarValue { + type Property = CalendarProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop { + CalendarEventNotificationProperty::Id + | CalendarEventNotificationProperty::CalendarEventId => Id::from_str(value) + .ok() + .map(CalendarEventNotificationValue::Id), + CalendarEventNotificationProperty::Created => UTCDate::from_str(value) + .ok() + .map(CalendarEventNotificationValue::Date), + CalendarEventNotificationProperty::Type => { + CalendarEventNotificationType::parse(value) + .map(CalendarEventNotificationValue::Type) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + CalendarEventNotificationValue::Id(id) => id.to_string().into(), + CalendarEventNotificationValue::Date(date) => date.to_string().into(), + CalendarEventNotificationValue::Type(t) => t.as_str().into(), + } + } +} + +impl CalendarEventNotificationType { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"created" => CalendarEventNotificationType::Created, + b"updated" => CalendarEventNotificationType::Updated, + b"destroyed" => CalendarEventNotificationType::Destroyed, + ) + } + + pub fn as_str(&self) -> &'static str { + match self { + CalendarEventNotificationType::Created => "created", + CalendarEventNotificationType::Updated => "updated", + CalendarEventNotificationType::Destroyed => "destroyed", + } + } +} + +impl CalendarEventNotificationProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => CalendarEventNotificationProperty::Id, + b"created" => CalendarEventNotificationProperty::Created, + b"changedBy" => CalendarEventNotificationProperty::ChangedBy, + b"comment" => CalendarEventNotificationProperty::Comment, + b"type" => CalendarEventNotificationProperty::Type, + b"calendarEventId" => CalendarEventNotificationProperty::CalendarEventId, + b"isDraft" => CalendarEventNotificationProperty::IsDraft, + b"event" => CalendarEventNotificationProperty::Event, + b"eventPatch" => CalendarEventNotificationProperty::EventPatch + ) + } +} + +impl FromStr for CalendarEventNotificationProperty { + type Err = (); + + fn from_str(s: &str) -> Result { + CalendarEventNotificationProperty::parse(s).ok_or(()) + } +} + +impl JmapObject for CalendarEventNotification { + type Property = CalendarEventNotificationProperty; + + type Element = CalendarEventNotificationValue; + + type Id = Id; + + type Filter = CalendarEventNotificationFilter; + + type Comparator = CalendarEventNotificationComparator; + + type GetArguments = (); + + type SetArguments<'de> = (); + + type QueryArguments = (); + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = CalendarEventNotificationProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CalendarEventNotificationFilter { + After(UTCDate), + Before(UTCDate), + Type(CalendarEventNotificationType), + CalendarEventIds(Vec>), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CalendarEventNotificationComparator { + Created, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for CalendarEventNotificationFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"after" => { + *self = CalendarEventNotificationFilter::After(map.next_value()?); + }, + b"before" => { + *self = CalendarEventNotificationFilter::Before(map.next_value()?); + }, + b"type" => { + *self = CalendarEventNotificationFilter::Type(map.next_value()?); + }, + b"calendarEventIds" => { + *self = CalendarEventNotificationFilter::CalendarEventIds(map.next_value()?); + }, + _ => { + *self = CalendarEventNotificationFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for CalendarEventNotificationComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"created" => { + *self = CalendarEventNotificationComparator::Created; + }, + _ => { + *self = CalendarEventNotificationComparator::_T(value.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + Ok(()) + } +} + +impl<'de> serde::Deserialize<'de> for CalendarEventNotificationType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + CalendarEventNotificationType::parse(<&str>::deserialize(deserializer)?) + .ok_or_else(|| serde::de::Error::custom("invalid CalendarEventNotificationType")) + } +} + +impl CalendarEventNotificationFilter { + pub fn into_string(self) -> Cow<'static, str> { + match self { + CalendarEventNotificationFilter::After(_) => "after", + CalendarEventNotificationFilter::Before(_) => "before", + CalendarEventNotificationFilter::Type(_) => "type", + CalendarEventNotificationFilter::CalendarEventIds(_) => "calendarEventIds", + CalendarEventNotificationFilter::_T(s) => return Cow::Owned(s), + } + .into() + } +} + +impl CalendarEventNotificationComparator { + pub fn into_string(self) -> Cow<'static, str> { + match self { + CalendarEventNotificationComparator::Created => "created", + CalendarEventNotificationComparator::_T(s) => return Cow::Owned(s), + } + .into() + } +} + +impl Default for CalendarEventNotificationFilter { + fn default() -> Self { + CalendarEventNotificationFilter::_T(String::new()) + } +} + +impl Default for CalendarEventNotificationComparator { + fn default() -> Self { + CalendarEventNotificationComparator::_T(String::new()) + } +} + +impl TryFrom for Id { + type Error = (); + + fn try_from(_: CalendarEventNotificationProperty) -> Result { + Err(()) + } +} + +impl From for CalendarEventNotificationValue { + fn from(id: Id) -> Self { + CalendarEventNotificationValue::Id(id) + } +} + +impl JmapObjectId for CalendarEventNotificationValue { + fn as_id(&self) -> Option { + if let CalendarEventNotificationValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let CalendarEventNotificationValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} + +impl JmapObjectId for CalendarEventNotificationProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} + +impl serde::Serialize for CalendarEventNotificationType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} diff --git a/crates/jmap-proto/src/object/contact.rs b/crates/jmap-proto/src/object/contact.rs index 5c6fc49a..d215afd6 100644 --- a/crates/jmap-proto/src/object/contact.rs +++ b/crates/jmap-proto/src/object/contact.rs @@ -10,6 +10,7 @@ use crate::{ types::date::UTCDate, }; use calcard::jscontact::{JSContactProperty, JSContactValue}; +use jmap_tools::{JsonPointerItem, Key}; use std::borrow::Cow; use types::{blob::BlobId, id::Id}; @@ -58,19 +59,24 @@ impl JmapObjectId for JSContactValue { } fn as_id_ref(&self) -> Option<&str> { - None - } -} - -impl TryFrom for JSContactValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(JSContactValue::Id(id)), - AnyId::BlobId(id) => Ok(JSContactValue::BlobId(id)), + match self { + JSContactValue::IdReference(r) => Some(r), + _ => None, } } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + match new_id { + AnyId::Id(id) => { + *self = JSContactValue::Id(id); + } + AnyId::BlobId(id) => { + *self = JSContactValue::BlobId(id); + } + } + + true + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -114,7 +120,7 @@ impl<'de> DeserializeArguments<'de> for ContactCardFilter { A: serde::de::MapAccess<'de>, { hashify::fnc_map!(key.as_bytes(), - b"inAddressBook" => { + b"inContactCard" => { *self = ContactCardFilter::InAddressBook(map.next_value()?); }, b"uid" => { @@ -220,7 +226,7 @@ impl<'de> DeserializeArguments<'de> for ContactCardComparator { impl ContactCardFilter { pub fn into_string(self) -> Cow<'static, str> { match self { - ContactCardFilter::InAddressBook(_) => "inAddressBook", + ContactCardFilter::InAddressBook(_) => "inContactCard", ContactCardFilter::Uid(_) => "uid", ContactCardFilter::HasMember(_) => "hasMember", ContactCardFilter::Kind(_) => "kind", @@ -271,3 +277,58 @@ impl Default for ContactCardComparator { ContactCardComparator::_T(String::new()) } } + +impl JmapObjectId for JSContactProperty { + fn as_id(&self) -> Option { + if let JSContactProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let JSContactProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + match self { + JSContactProperty::IdReference(r) => Some(r), + JSContactProperty::Pointer(value) => { + let value = value.as_slice(); + match (value.first(), value.get(1)) { + ( + Some(JsonPointerItem::Key(Key::Property( + JSContactProperty::AddressBookIds, + ))), + Some(JsonPointerItem::Key(Key::Property(JSContactProperty::IdReference( + r, + )))), + ) => Some(r), + _ => None, + } + } + _ => None, + } + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + if let JSContactProperty::Pointer(value) = self { + let value = value.as_mut_slice(); + if let Some(value) = value.get_mut(1) { + *value = JsonPointerItem::Key(Key::Property(JSContactProperty::IdValue(id))); + return true; + } + } else { + *self = JSContactProperty::IdValue(id); + return true; + } + } + false + } +} diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index 7ff2b680..de239fe5 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -77,6 +77,7 @@ pub enum EmailProperty { // Other Keyword(Keyword), IdValue(Id), + IdReference(String), Pointer(JsonPointer), } @@ -112,7 +113,11 @@ impl Property for EmailProperty { if let Some(Key::Property(key)) = key { match key.patch_or_prop() { EmailProperty::Keywords => EmailProperty::Keyword(Keyword::parse(value)).into(), - EmailProperty::MailboxIds => Id::from_str(value).ok().map(EmailProperty::IdValue), + EmailProperty::MailboxIds => match parse_ref(value) { + MaybeReference::Value(v) => Some(EmailProperty::IdValue(v)), + MaybeReference::Reference(v) => Some(EmailProperty::IdReference(v)), + MaybeReference::ParseError => None, + }, _ => EmailProperty::parse(value, allow_patch), } } else { @@ -166,6 +171,7 @@ impl Property for EmailProperty { EmailProperty::Keyword(keyword) => return keyword.to_string().into(), EmailProperty::IdValue(id) => return id.to_string().into(), EmailProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + EmailProperty::IdReference(r) => return format!("#{r}").into(), } .into() } @@ -480,15 +486,6 @@ impl<'de> DeserializeArguments<'de> for EmailParseArguments { } } -impl serde::Serialize for EmailProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl JmapObject for Email { type Property = EmailProperty; @@ -885,16 +882,17 @@ impl JmapObjectId for EmailValue { None } } -} -impl TryFrom for EmailValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(EmailValue::Id(id)), - AnyId::BlobId(id) => Ok(EmailValue::BlobId(id)), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + match new_id { + AnyId::Id(id) => { + *self = EmailValue::Id(id); + } + AnyId::BlobId(id) => { + *self = EmailValue::BlobId(id); + } } + true } } @@ -915,3 +913,54 @@ impl From for EmailValue { EmailValue::Date(date) } } + +impl JmapObjectId for EmailProperty { + fn as_id(&self) -> Option { + if let EmailProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let EmailProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + match self { + EmailProperty::IdReference(r) => Some(r), + EmailProperty::Pointer(value) => { + let value = value.as_slice(); + match (value.first(), value.get(1)) { + ( + Some(JsonPointerItem::Key(Key::Property(EmailProperty::MailboxIds))), + Some(JsonPointerItem::Key(Key::Property(EmailProperty::IdReference(r)))), + ) => Some(r), + _ => None, + } + } + _ => None, + } + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + if let EmailProperty::Pointer(value) = self { + let value = value.as_mut_slice(); + if let Some(value) = value.get_mut(1) { + *value = JsonPointerItem::Key(Key::Property(EmailProperty::IdValue(id))); + return true; + } + } else { + *self = EmailProperty::IdValue(id); + return true; + } + } + false + } +} diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index ee2ba11e..c52780f5 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -284,15 +284,6 @@ impl<'x> DeserializeArguments<'x> for EmailSubmissionSetArguments<'x> { } } -impl serde::Serialize for EmailSubmissionProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for EmailSubmissionProperty { type Err = (); @@ -459,15 +450,34 @@ impl JmapObjectId for EmailSubmissionValue { None } } -} -impl TryFrom for EmailSubmissionValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(EmailSubmissionValue::Id(id)), - AnyId::BlobId(blob_id) => Ok(EmailSubmissionValue::BlobId(blob_id)), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + match new_id { + AnyId::Id(id) => { + *self = EmailSubmissionValue::Id(id); + } + AnyId::BlobId(blob_id) => { + *self = EmailSubmissionValue::BlobId(blob_id); + } } + true + } +} + +impl JmapObjectId for EmailSubmissionProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false } } diff --git a/crates/jmap-proto/src/object/file_node.rs b/crates/jmap-proto/src/object/file_node.rs index 36acc4e1..a62dffd7 100644 --- a/crates/jmap-proto/src/object/file_node.rs +++ b/crates/jmap-proto/src/object/file_node.rs @@ -222,15 +222,6 @@ impl<'x> DeserializeArguments<'x> for FileNodeQueryArguments { } } -impl serde::Serialize for FileNodeProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for FileNodeProperty { type Err = (); @@ -491,15 +482,13 @@ impl JmapObjectId for FileNodeValue { None } } -} -impl TryFrom for FileNodeValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(FileNodeValue::Id(id)), - AnyId::BlobId(blob_id) => Ok(FileNodeValue::BlobId(blob_id)), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = FileNodeValue::Id(id); + true + } else { + false } } } @@ -588,3 +577,34 @@ impl TryFrom for FileNodeRight { } } } + +impl JmapObjectId for FileNodeProperty { + fn as_id(&self) -> Option { + if let FileNodeProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let FileNodeProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = FileNodeProperty::IdValue(id); + true + } else { + false + } + } +} diff --git a/crates/jmap-proto/src/object/identity.rs b/crates/jmap-proto/src/object/identity.rs index 72840796..4f9b43dd 100644 --- a/crates/jmap-proto/src/object/identity.rs +++ b/crates/jmap-proto/src/object/identity.rs @@ -106,15 +106,6 @@ impl IdentityProperty { } } -impl serde::Serialize for IdentityProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for IdentityProperty { type Err = (); @@ -169,15 +160,31 @@ impl JmapObjectId for IdentityValue { fn as_id_ref(&self) -> Option<&str> { None } -} -impl TryFrom for IdentityValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(IdentityValue::Id(id)), - _ => Err(()), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = IdentityValue::Id(id); + true + } else { + false } } } + +impl JmapObjectId for IdentityProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index 6c0f79e4..2b2e1a90 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -242,15 +242,6 @@ impl FromStr for MailboxProperty { } } -impl serde::Serialize for MailboxProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl JmapObject for Mailbox { type Property = MailboxProperty; @@ -444,16 +435,13 @@ impl JmapObjectId for MailboxValue { None } } -} -impl TryFrom for MailboxValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - if let AnyId::Id(id) = value { - Ok(MailboxValue::Id(id)) + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = MailboxValue::Id(id); + true } else { - Err(()) + false } } } @@ -510,3 +498,34 @@ impl From for MailboxProperty { MailboxProperty::Rights(right) } } + +impl JmapObjectId for MailboxProperty { + fn as_id(&self) -> Option { + if let MailboxProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let MailboxProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = MailboxProperty::IdValue(id); + true + } else { + false + } + } +} diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index 21faa647..ebab1663 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -12,29 +12,29 @@ use types::{acl::Acl, blob::BlobId, id::Id}; pub mod addressbook; pub mod blob; +pub mod calendar; +pub mod calendar_event; +pub mod calendar_event_notification; pub mod contact; pub mod email; pub mod email_submission; pub mod file_node; pub mod identity; pub mod mailbox; +pub mod participant_identity; pub mod principal; pub mod push_subscription; pub mod quota; pub mod search_snippet; +pub mod share_notification; pub mod sieve; pub mod thread; pub mod vacation_response; pub trait JmapObject: std::fmt::Debug { - type Property: Property + FromStr + Debug + Sync + Send; - type Element: Element - + From - + JmapObjectId - + Debug - + Sync - + Send; - type Id: FromStr + TryFrom + Serialize + Debug + Sync + Send; + type Property: Property + JmapObjectId + FromStr + Debug + Sync + Send; + type Element: Element + JmapObjectId + Debug + Sync + Send; + type Id: FromStr + TryFrom + Into + Serialize + Debug + Sync + Send; type Filter: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; type Comparator: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; @@ -67,10 +67,11 @@ pub enum AnyId { BlobId(BlobId), } -pub trait JmapObjectId: TryFrom { +pub trait JmapObjectId { fn as_id(&self) -> Option; fn as_any_id(&self) -> Option; fn as_id_ref(&self) -> Option<&str>; + fn try_set_id(&mut self, new_id: AnyId) -> bool; } #[derive(Debug, Clone, PartialEq, Eq)] @@ -199,6 +200,10 @@ impl JmapObjectId for Null { fn as_id_ref(&self) -> Option<&str> { unreachable!() } + + fn try_set_id(&mut self, _: AnyId) -> bool { + unreachable!() + } } impl TryFrom for Null { diff --git a/crates/jmap-proto/src/object/participant_identity.rs b/crates/jmap-proto/src/object/participant_identity.rs new file mode 100644 index 00000000..ebe862f9 --- /dev/null +++ b/crates/jmap-proto/src/object/participant_identity.rs @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::{deserialize::DeserializeArguments, reference::MaybeIdReference}, +}; +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, Default)] +pub struct ParticipantIdentity; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ParticipantIdentityProperty { + Id, + Name, + CalendarAddress, + IsDefault, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ParticipantIdentityValue { + Id(Id), +} + +impl Property for ParticipantIdentityProperty { + fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option { + ParticipantIdentityProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + ParticipantIdentityProperty::Id => "id", + ParticipantIdentityProperty::Name => "name", + ParticipantIdentityProperty::CalendarAddress => "calendarAddress", + ParticipantIdentityProperty::IsDefault => "isDefault", + } + .into() + } +} + +impl Element for ParticipantIdentityValue { + type Property = ParticipantIdentityProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop { + ParticipantIdentityProperty::Id => { + Id::from_str(value).ok().map(ParticipantIdentityValue::Id) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + ParticipantIdentityValue::Id(id) => id.to_string().into(), + } + } +} + +impl ParticipantIdentityProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => ParticipantIdentityProperty::Id, + b"name" => ParticipantIdentityProperty::Name, + b"calendarAddress" => ParticipantIdentityProperty::CalendarAddress, + b"isDefault" => ParticipantIdentityProperty::IsDefault + ) + } +} + +#[derive(Debug, Clone, Default)] +pub struct ParticipantIdentitySetArguments { + pub on_success_set_is_default: Option>, +} + +impl<'de> DeserializeArguments<'de> for ParticipantIdentitySetArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"onSuccessSetIsDefault" => { + self.on_success_set_is_default = map.next_value()?; + }, + _ => { + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl FromStr for ParticipantIdentityProperty { + type Err = (); + + fn from_str(s: &str) -> Result { + ParticipantIdentityProperty::parse(s).ok_or(()) + } +} + +impl JmapObject for ParticipantIdentity { + type Property = ParticipantIdentityProperty; + + type Element = ParticipantIdentityValue; + + type Id = Id; + + type Filter = (); + + type Comparator = (); + + type GetArguments = (); + + type SetArguments<'de> = ParticipantIdentitySetArguments; + + type QueryArguments = (); + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = ParticipantIdentityProperty::Id; +} + +impl TryFrom for Id { + type Error = (); + + fn try_from(_: ParticipantIdentityProperty) -> Result { + Err(()) + } +} + +impl From for ParticipantIdentityValue { + fn from(id: Id) -> Self { + ParticipantIdentityValue::Id(id) + } +} + +impl JmapObjectId for ParticipantIdentityValue { + fn as_id(&self) -> Option { + let ParticipantIdentityValue::Id(id) = self; + Some(*id) + } + + fn as_any_id(&self) -> Option { + let ParticipantIdentityValue::Id(id) = self; + Some(AnyId::Id(*id)) + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(new_id) = new_id { + *self = ParticipantIdentityValue::Id(new_id); + return true; + } + false + } +} + +impl JmapObjectId for ParticipantIdentityProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs index 4bdc0297..574a8978 100644 --- a/crates/jmap-proto/src/object/principal.rs +++ b/crates/jmap-proto/src/object/principal.rs @@ -120,15 +120,6 @@ impl PrincipalType { } } -impl serde::Serialize for PrincipalProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for PrincipalProperty { type Err = (); @@ -291,16 +282,13 @@ impl JmapObjectId for PrincipalValue { fn as_id_ref(&self) -> Option<&str> { None } -} -impl TryFrom for PrincipalValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - if let AnyId::Id(id) = value { - Ok(PrincipalValue::Id(id)) + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = PrincipalValue::Id(id); + true } else { - Err(()) + false } } } @@ -318,3 +306,21 @@ impl Display for PrincipalFilter { }) } } + +impl JmapObjectId for PrincipalProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/push_subscription.rs b/crates/jmap-proto/src/object/push_subscription.rs index 542ab2a1..cd3e7798 100644 --- a/crates/jmap-proto/src/object/push_subscription.rs +++ b/crates/jmap-proto/src/object/push_subscription.rs @@ -126,15 +126,6 @@ impl Element for PushSubscriptionValue { } } -impl serde::Serialize for PushSubscriptionProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for PushSubscriptionProperty { type Err = (); @@ -191,15 +182,31 @@ impl JmapObjectId for PushSubscriptionValue { fn as_id_ref(&self) -> Option<&str> { None } -} -impl TryFrom for PushSubscriptionValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(PushSubscriptionValue::Id(id)), - _ => Err(()), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = PushSubscriptionValue::Id(id); + true + } else { + false } } } + +impl JmapObjectId for PushSubscriptionProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/quota.rs b/crates/jmap-proto/src/object/quota.rs index 8b09d038..172294e4 100644 --- a/crates/jmap-proto/src/object/quota.rs +++ b/crates/jmap-proto/src/object/quota.rs @@ -97,15 +97,6 @@ impl Element for QuotaValue { } } -impl serde::Serialize for QuotaProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for QuotaProperty { type Err = (); @@ -246,16 +237,31 @@ impl JmapObjectId for QuotaValue { fn as_id_ref(&self) -> Option<&str> { None } -} -impl TryFrom for QuotaValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - if let AnyId::Id(id) = value { - Ok(QuotaValue::Id(id)) + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = QuotaValue::Id(id); + true } else { - Err(()) + false } } } + +impl JmapObjectId for QuotaProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/share_notification.rs b/crates/jmap-proto/src/object/share_notification.rs new file mode 100644 index 00000000..e6b78df9 --- /dev/null +++ b/crates/jmap-proto/src/object/share_notification.rs @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::deserialize::DeserializeArguments, + types::date::UTCDate, +}; +use jmap_tools::{Element, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::{id::Id, type_state::DataType}; + +#[derive(Debug, Clone, Default)] +pub struct ShareNotification; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ShareNotificationProperty { + Id, + Created, + ChangedBy, + ChangedByName, + ChangedByEmail, + ChangedByPrincipalId, + ObjectType, + ObjectAccountId, + ObjectId, + OldRights, + NewRights, + Name, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ShareNotificationValue { + Id(Id), + Date(UTCDate), + ObjectType(DataType), +} + +impl Property for ShareNotificationProperty { + fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option { + ShareNotificationProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + ShareNotificationProperty::Id => "id", + ShareNotificationProperty::Created => "created", + ShareNotificationProperty::ChangedBy => "changedBy", + ShareNotificationProperty::ChangedByName => "name", + ShareNotificationProperty::ChangedByEmail => "email", + ShareNotificationProperty::ChangedByPrincipalId => "principalId", + ShareNotificationProperty::ObjectType => "objectType", + ShareNotificationProperty::ObjectAccountId => "objectAccountId", + ShareNotificationProperty::ObjectId => "objectId", + ShareNotificationProperty::OldRights => "oldRights", + ShareNotificationProperty::NewRights => "newRights", + ShareNotificationProperty::Name => "name", + } + .into() + } +} + +impl Element for ShareNotificationValue { + type Property = ShareNotificationProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop { + ShareNotificationProperty::Id + | ShareNotificationProperty::ChangedByPrincipalId + | ShareNotificationProperty::ObjectAccountId + | ShareNotificationProperty::ObjectId => { + Id::from_str(value).ok().map(ShareNotificationValue::Id) + } + ShareNotificationProperty::Created => UTCDate::from_str(value) + .ok() + .map(ShareNotificationValue::Date), + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + ShareNotificationValue::Id(id) => id.to_string().into(), + ShareNotificationValue::Date(date) => date.to_string().into(), + ShareNotificationValue::ObjectType(ty) => ty.as_str().into(), + } + } +} + +impl ShareNotificationProperty { + fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => ShareNotificationProperty::Id, + b"created" => ShareNotificationProperty::Created, + b"changedBy" => ShareNotificationProperty::ChangedBy, + b"name" => ShareNotificationProperty::ChangedByName, + b"email" => ShareNotificationProperty::ChangedByEmail, + b"principalId" => ShareNotificationProperty::ChangedByPrincipalId, + b"objectType" => ShareNotificationProperty::ObjectType, + b"objectAccountId" => ShareNotificationProperty::ObjectAccountId, + b"objectId" => ShareNotificationProperty::ObjectId, + b"oldRights" => ShareNotificationProperty::OldRights, + b"newRights" => ShareNotificationProperty::NewRights + ) + } +} + +impl FromStr for ShareNotificationProperty { + type Err = (); + + fn from_str(s: &str) -> Result { + ShareNotificationProperty::parse(s).ok_or(()) + } +} + +impl JmapObject for ShareNotification { + type Property = ShareNotificationProperty; + + type Element = ShareNotificationValue; + + type Id = Id; + + type Filter = ShareNotificationFilter; + + type Comparator = ShareNotificationComparator; + + type GetArguments = (); + + type SetArguments<'de> = (); + + type QueryArguments = (); + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = ShareNotificationProperty::Id; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ShareNotificationFilter { + After(UTCDate), + Before(UTCDate), + ObjectType(String), + ObjectAccountId(Id), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ShareNotificationComparator { + Created, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for ShareNotificationFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"after" => { + *self = ShareNotificationFilter::After(map.next_value()?); + }, + b"before" => { + *self = ShareNotificationFilter::Before(map.next_value()?); + }, + b"objectType" => { + *self = ShareNotificationFilter::ObjectType(map.next_value()?); + }, + b"objectAccountId" => { + *self = ShareNotificationFilter::ObjectAccountId(map.next_value()?); + }, + _ => { + *self = ShareNotificationFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for ShareNotificationComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"created" => { + *self = ShareNotificationComparator::Created; + }, + _ => { + *self = ShareNotificationComparator::_T(value.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + Ok(()) + } +} + +impl ShareNotificationFilter { + pub fn into_string(self) -> Cow<'static, str> { + match self { + ShareNotificationFilter::After(_) => "after", + ShareNotificationFilter::Before(_) => "before", + ShareNotificationFilter::ObjectType(_) => "objectType", + ShareNotificationFilter::ObjectAccountId(_) => "objectAccountId", + ShareNotificationFilter::_T(s) => return Cow::Owned(s), + } + .into() + } +} + +impl ShareNotificationComparator { + pub fn into_string(self) -> Cow<'static, str> { + match self { + ShareNotificationComparator::Created => "created", + ShareNotificationComparator::_T(s) => return Cow::Owned(s), + } + .into() + } +} + +impl Default for ShareNotificationFilter { + fn default() -> Self { + ShareNotificationFilter::_T(String::new()) + } +} + +impl Default for ShareNotificationComparator { + fn default() -> Self { + ShareNotificationComparator::_T(String::new()) + } +} + +impl TryFrom for Id { + type Error = (); + + fn try_from(_: ShareNotificationProperty) -> Result { + Err(()) + } +} + +impl From for ShareNotificationValue { + fn from(id: Id) -> Self { + ShareNotificationValue::Id(id) + } +} + +impl JmapObjectId for ShareNotificationValue { + fn as_id(&self) -> Option { + if let ShareNotificationValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let ShareNotificationValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} + +impl JmapObjectId for ShareNotificationProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index 89271bdf..adaf835c 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -124,15 +124,6 @@ impl FromStr for SieveProperty { } } -impl serde::Serialize for SieveProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl JmapObject for Sieve { type Property = SieveProperty; @@ -260,15 +251,34 @@ impl JmapObjectId for SieveValue { None } } -} -impl TryFrom for SieveValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(SieveValue::Id(id)), - AnyId::BlobId(id) => Ok(SieveValue::BlobId(id)), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + match new_id { + AnyId::Id(id) => { + *self = SieveValue::Id(id); + } + AnyId::BlobId(id) => { + *self = SieveValue::BlobId(id); + } } + true + } +} + +impl JmapObjectId for SieveProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false } } diff --git a/crates/jmap-proto/src/object/thread.rs b/crates/jmap-proto/src/object/thread.rs index bef0783c..4815a842 100644 --- a/crates/jmap-proto/src/object/thread.rs +++ b/crates/jmap-proto/src/object/thread.rs @@ -65,15 +65,6 @@ impl ThreadProperty { } } -impl serde::Serialize for ThreadProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for ThreadProperty { type Err = (); @@ -126,15 +117,31 @@ impl JmapObjectId for ThreadValue { fn as_id_ref(&self) -> Option<&str> { None } -} -impl TryFrom for ThreadValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(ThreadValue::Id(id)), - _ => Err(()), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = ThreadValue::Id(id); + true + } else { + false } } } + +impl JmapObjectId for ThreadProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/object/vacation_response.rs b/crates/jmap-proto/src/object/vacation_response.rs index a451f5d5..be1a9ffb 100644 --- a/crates/jmap-proto/src/object/vacation_response.rs +++ b/crates/jmap-proto/src/object/vacation_response.rs @@ -94,15 +94,6 @@ impl VacationResponseProperty { } } -impl serde::Serialize for VacationResponseProperty { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.to_cow().as_ref()) - } -} - impl FromStr for VacationResponseProperty { type Err = (); @@ -159,15 +150,31 @@ impl JmapObjectId for VacationResponseValue { fn as_id_ref(&self) -> Option<&str> { None } -} -impl TryFrom for VacationResponseValue { - type Error = (); - - fn try_from(value: AnyId) -> Result { - match value { - AnyId::Id(id) => Ok(VacationResponseValue::Id(id)), - _ => Err(()), + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = VacationResponseValue::Id(id); + true + } else { + false } } } + +impl JmapObjectId for VacationResponseProperty { + fn as_id(&self) -> Option { + None + } + + fn as_any_id(&self) -> Option { + None + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, _: AnyId) -> bool { + false + } +} diff --git a/crates/jmap-proto/src/references/eval.rs b/crates/jmap-proto/src/references/eval.rs index ca51603b..da7f1eb3 100644 --- a/crates/jmap-proto/src/references/eval.rs +++ b/crates/jmap-proto/src/references/eval.rs @@ -14,7 +14,7 @@ use crate::{ response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod}, }; use compact_str::format_compact; -use jmap_tools::{Element, Property, Value}; +use jmap_tools::{Element, Key, Property, Value}; use types::id::Id; impl Response<'_> { @@ -66,6 +66,24 @@ impl Response<'_> { GetResponseMethod::FileNode(response) => { response.eval_jptr(path, &mut results) } + GetResponseMethod::Calendar(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::CalendarEvent(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::CalendarEventNotification(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::ParticipantIdentity(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::ShareNotification(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::PrincipalAvailability(response) => { + response.eval_jptr(path, &mut results) + } }, ResponseMethod::Changes(response) => match response { ChangesResponseMethod::Email(response) => { @@ -95,6 +113,15 @@ impl Response<'_> { ChangesResponseMethod::FileNode(response) => { response.eval_jptr(path, &mut results) } + ChangesResponseMethod::CalendarEvent(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::CalendarEventNotification(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::ShareNotification(response) => { + response.eval_jptr(path, &mut results) + } }, ResponseMethod::Query(response) => response.eval_jptr(path, &mut results), ResponseMethod::QueryChanges(response) => { @@ -134,39 +161,52 @@ pub(crate) trait EvalObjectReferences { &mut self, response: &Response<'_>, graph: &mut Graph<'_>, + depth: usize, ) -> trc::Result<()>; } impl<'x, P, E> EvalObjectReferences for Value<'x, P, E> where - P: Property, - E: Element + JmapObjectId + TryFrom, + P: Property + JmapObjectId, + E: Element + JmapObjectId, { fn eval_object_references( &mut self, response: &Response<'_>, graph: &mut Graph<'_>, + depth: usize, ) -> trc::Result<()> { let Value::Object(obj) = self else { return Ok(()); }; - for (_, value) in obj.as_mut_vec() { + for (key, value) in obj.as_mut_vec() { + // Resolve patch with references (e.g. mailboxIds/#idRef) + if depth == 0 + && let Key::Property(property) = key + && let Some(id_ref) = property.as_id_ref() + { + if let Some(id) = response.created_ids.get(id_ref) { + if !property.try_set_id(id.clone()) { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details("Id reference points to invalid type.")); + } + } else { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!("Id reference {id_ref:?} not found."))); + } + } + match value { Value::Element(element) => { if let Some(id_ref) = element.as_id_ref() { if let Some(id) = response.created_ids.get(id_ref) { - match E::try_from(id.clone()) { - Ok(eid) => { - *element = eid; - } - Err(_) => { - return Err(trc::JmapEvent::InvalidResultReference - .into_err() - .details(format_compact!( - "Id reference {id_ref:?} points to invalid type." - ))); - } + if !element.try_set_id(id.clone()) { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details("Id reference points to invalid type.")); } } else if let Graph::Some { child_id, graph } = graph { graph @@ -180,9 +220,32 @@ where } } } - Value::Array(items) => { + Value::Array(items) if depth == 0 => { + // Resolve references in arrays (e.g. emailIds: [#idRef1, #idRef2]) for item in items { - item.eval_object_references(response, graph)?; + item.eval_object_references(response, graph, depth + 1)?; + } + } + Value::Object(items) if depth == 0 => { + // Resolve references in JMAP sets (e.g. mailboxIds: { "#idRef1": true, "#idRef2": true }) + for (key, _) in items.as_mut_vec() { + if let Key::Property(property) = key + && let Some(id_ref) = property.as_id_ref() + { + if let Some(id) = response.created_ids.get(id_ref) { + if !property.try_set_id(id.clone()) { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details("Id reference points to invalid type.")); + } + } else { + return Err(trc::JmapEvent::InvalidResultReference + .into_err() + .details(format_compact!( + "Id reference {id_ref:?} not found." + ))); + } + } } } _ => {} diff --git a/crates/jmap-proto/src/references/jsptr.rs b/crates/jmap-proto/src/references/jsptr.rs index b66b7c78..ed2eeb1b 100644 --- a/crates/jmap-proto/src/references/jsptr.rs +++ b/crates/jmap-proto/src/references/jsptr.rs @@ -7,12 +7,18 @@ use crate::{ method::{ PropertyWrapper, + availability::{BusyPeriod, GetAvailabilityResponse}, changes::ChangesResponse, get::GetResponse, query::QueryResponse, query_changes::{AddedItem, QueryChangesResponse}, }, - object::{AnyId, JmapObject, JmapObjectId}, + object::{ + AnyId, JmapObject, JmapObjectId, + calendar_event_notification::{ + CalendarEventNotificationGetResponse, CalendarEventNotificationObject, + }, + }, request::reference::ResultReference, }; use compact_str::format_compact; @@ -218,6 +224,68 @@ impl ResponsePtr for AddedItem { } } +impl ResponsePtr for CalendarEventNotificationGetResponse { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("list") => { + self.list.eval_jptr(pointer, results); + true + } + _ => false, + } + } +} + +impl ResponsePtr for CalendarEventNotificationObject { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("id") => { + results.0.push(EvalResult::Id(AnyId::Id(self.id))); + true + } + Some("calendarEventId") => { + if let Some(id) = &self.calendar_event_id { + results.0.push(EvalResult::Id(AnyId::Id(*id))); + } + true + } + Some("event") => { + if let Some(event) = &self.event { + event.0.eval_jptr(pointer, results); + } + true + } + _ => false, + } + } +} + +impl ResponsePtr for GetAvailabilityResponse { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("list") => { + self.list.eval_jptr(pointer, results); + true + } + _ => false, + } + } +} + +impl ResponsePtr for BusyPeriod { + fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { + match pointer.next().and_then(|item| item.as_string_key()) { + Some("event") => { + if let Some(event) = &self.event { + event.0.eval_jptr(pointer, results); + } + true + } + _ => false, + } + } +} + impl EvalResults { pub fn into_ids>( self, diff --git a/crates/jmap-proto/src/references/resolve.rs b/crates/jmap-proto/src/references/resolve.rs index 773ce8fc..b7af9f17 100644 --- a/crates/jmap-proto/src/references/resolve.rs +++ b/crates/jmap-proto/src/references/resolve.rs @@ -45,6 +45,16 @@ impl Response<'_> { GetRequestMethod::AddressBook(request) => request.resolve_references(self)?, GetRequestMethod::ContactCard(request) => request.resolve_references(self)?, GetRequestMethod::FileNode(request) => request.resolve_references(self)?, + GetRequestMethod::ShareNotification(request) => request.resolve_references(self)?, + GetRequestMethod::Calendar(request) => request.resolve_references(self)?, + GetRequestMethod::CalendarEvent(request) => request.resolve_references(self)?, + GetRequestMethod::CalendarEventNotification(request) => { + request.resolve_references(self)? + } + GetRequestMethod::ParticipantIdentity(request) => { + request.resolve_references(self)? + } + GetRequestMethod::PrincipalAvailability(_) => (), }, RequestMethod::Set(request) => match request { SetRequestMethod::Email(request) => request.resolve_references(self)?, @@ -57,9 +67,19 @@ impl Response<'_> { SetRequestMethod::AddressBook(request) => request.resolve_references(self)?, SetRequestMethod::ContactCard(request) => request.resolve_references(self)?, SetRequestMethod::FileNode(request) => request.resolve_references(self)?, + SetRequestMethod::ShareNotification(request) => request.resolve_references(self)?, + SetRequestMethod::Calendar(request) => request.resolve_references(self)?, + SetRequestMethod::CalendarEvent(request) => request.resolve_references(self)?, + SetRequestMethod::CalendarEventNotification(request) => { + request.resolve_references(self)? + } + SetRequestMethod::ParticipantIdentity(request) => { + request.resolve_references(self)? + } }, RequestMethod::Copy(request) => match request { CopyRequestMethod::Email(request) => request.resolve_references(self)?, + CopyRequestMethod::CalendarEvent(request) => request.resolve_references(self)?, CopyRequestMethod::ContactCard(request) => request.resolve_references(self)?, CopyRequestMethod::Blob(_) => (), }, @@ -85,15 +105,9 @@ where Value::Element(element) => { if let Some(id_ref) = element.as_id_ref() { if let Some(id) = self.get_created_id(id_ref) { - match E::try_from(id) { - Ok(eid) => { - *element = eid; - } - Err(_) => { - return Err(SetError::invalid_properties().with_description( - format!("Id reference {id_ref:?} points to invalid type."), - )); - } + if !element.try_set_id(id) { + return Err(SetError::invalid_properties() + .with_description("Id reference points to invalid type.")); } } else { return Err(SetError::not_found() @@ -180,6 +194,7 @@ impl<'x, T: JmapObject> ResolveReference for SetRequest<'x, T> { child_id: &*id, graph: &mut graph, }, + 0, )?; } @@ -192,7 +207,7 @@ impl<'x, T: JmapObject> ResolveReference for SetRequest<'x, T> { // Resolve update references if let Some(update) = &mut self.update { for obj in update.values_mut() { - obj.eval_object_references(response, &mut Graph::None)?; + obj.eval_object_references(response, &mut Graph::None, 0)?; } } @@ -215,7 +230,7 @@ impl<'x, T: JmapObject> ResolveReference for CopyRequest<'x, T> { fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> { // Resolve create references for (id, obj) in self.create.iter_mut() { - obj.eval_object_references(response, &mut Graph::None)?; + obj.eval_object_references(response, &mut Graph::None, 0)?; if let MaybeIdReference::Reference(ir) = id { *id = MaybeIdReference::Id(response.eval_id_reference(ir)?); diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index ecdbe1cd..8e248c69 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -6,7 +6,10 @@ use std::fmt; -use crate::{object::file_node::FileNodeComparator, response::serialize::serialize_hex}; +use crate::{ + object::file_node::FileNodeComparator, response::serialize::serialize_hex, types::date::UTCDate, +}; +use calcard::icalendar::ICalendarDuration; use serde::{Deserialize, Deserializer}; use types::{id::Id, type_state::DataType}; use utils::map::vec_map::VecMap; @@ -60,22 +63,28 @@ pub enum Capability { VacationResponse = 1 << 3, #[serde(rename(serialize = "urn:ietf:params:jmap:contacts"))] Contacts = 1 << 4, + #[serde(rename(serialize = "urn:ietf:params:jmap:contacts:parse"))] + ContactsParse = 1 << 5, #[serde(rename(serialize = "urn:ietf:params:jmap:calendars"))] - Calendars = 1 << 5, + Calendars = 1 << 6, + #[serde(rename(serialize = "urn:ietf:params:jmap:calendars:parse"))] + CalendarsParse = 1 << 7, #[serde(rename(serialize = "urn:ietf:params:jmap:websocket"))] - WebSocket = 1 << 6, + WebSocket = 1 << 8, #[serde(rename(serialize = "urn:ietf:params:jmap:sieve"))] - Sieve = 1 << 7, + Sieve = 1 << 9, #[serde(rename(serialize = "urn:ietf:params:jmap:blob"))] - Blob = 1 << 8, + Blob = 1 << 10, #[serde(rename(serialize = "urn:ietf:params:jmap:quota"))] - Quota = 1 << 9, + Quota = 1 << 11, #[serde(rename(serialize = "urn:ietf:params:jmap:principals"))] - Principals = 1 << 10, + Principals = 1 << 12, #[serde(rename(serialize = "urn:ietf:params:jmap:principals:owner"))] - PrincipalsOwner = 1 << 11, + PrincipalsOwner = 1 << 13, + #[serde(rename(serialize = "urn:ietf:params:jmap:principals:availability"))] + PrincipalsAvailability = 1 << 14, #[serde(rename(serialize = "urn:ietf:params:jmap:filenode"))] - FileNode = 1 << 12, + FileNode = 1 << 15, } #[derive(Debug, Clone, Copy, Default)] @@ -94,8 +103,11 @@ pub enum Capabilities { SieveSession(SieveSessionCapabilities), Blob(BlobCapabilities), Contacts(ContactsCapabilities), - Principals(PrincipalsCapabilities), - PrincipalsOwner(PrincipalsOwnerCapabilities), + Principals(PrincipalCapabilities), + PrincipalsOwner(PrincipalOwnerCapabilities), + PrincipalsAvailability(PrincipalAvailabilityCapabilities), + PrincipalCalendar(PrincipalCalendarCapabilities), + Calendar(CalendarCapabilities), FileNode(FileNodeCapabilities), Empty(EmptyCapabilities), } @@ -188,6 +200,22 @@ pub struct BlobCapabilities { pub supported_digest_algorithms: Vec<&'static str>, } +#[derive(Debug, Clone, serde::Serialize)] +pub struct CalendarCapabilities { + #[serde(rename(serialize = "maxCalendarsPerEvent"))] + pub max_calendars_per_event: Option, + #[serde(rename(serialize = "minDateTime"))] + pub min_date_time: UTCDate, + #[serde(rename(serialize = "maxDateTime"))] + pub max_date_time: UTCDate, + #[serde(rename(serialize = "maxExpandedQueryDuration"))] + pub max_expanded_query_duration: String, + #[serde(rename(serialize = "maxParticipantsPerEvent"))] + pub max_participants_per_event: Option, + #[serde(rename(serialize = "mayCreateCalendar"))] + pub may_create_calendar: bool, +} + #[derive(Debug, Clone, serde::Serialize)] pub struct ContactsCapabilities { #[serde(rename(serialize = "maxAddressBooksPerCard"))] @@ -197,13 +225,19 @@ pub struct ContactsCapabilities { } #[derive(Debug, Clone, serde::Serialize)] -pub struct PrincipalsCapabilities { +pub struct PrincipalAvailabilityCapabilities { + #[serde(rename(serialize = "maxAvailabilityDuration"))] + pub max_availability_duration: ICalendarDuration, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct PrincipalCapabilities { #[serde(rename(serialize = "currentUserPrincipalId"))] pub current_user_principal_id: Option, } #[derive(Debug, Clone, serde::Serialize)] -pub struct PrincipalsOwnerCapabilities { +pub struct PrincipalOwnerCapabilities { #[serde(rename(serialize = "accountIdForPrincipal"))] pub account_id_for_principal: Id, @@ -211,6 +245,18 @@ pub struct PrincipalsOwnerCapabilities { pub principal_id: Id, } +#[derive(Debug, Clone, serde::Serialize)] +pub struct PrincipalCalendarCapabilities { + #[serde(rename(serialize = "accountIdForPrincipal"))] + pub account_id_for_principal: Option, + #[serde(rename(serialize = "mayGetAvailability"))] + pub may_get_availability: bool, + #[serde(rename(serialize = "mayShareWith"))] + pub may_share_with: bool, + #[serde(rename(serialize = "calendarAddress"))] + pub calendar_address: String, +} + #[derive(Debug, Clone, serde::Serialize)] pub struct FileNodeCapabilities { #[serde(rename(serialize = "maxFileNodeDepth"))] @@ -379,7 +425,10 @@ impl Capability { "urn:ietf:params:jmap:quota" => Capability::Quota, "urn:ietf:params:jmap:principals" => Capability::Principals, "urn:ietf:params:jmap:principals:owner" => Capability::PrincipalsOwner, - "urn:ietf:params:jmap:filenode" => Capability::FileNode + "urn:ietf:params:jmap:filenode" => Capability::FileNode, + "urn:ietf:params:jmap:principals:availability" => Capability::PrincipalsAvailability, + "urn:ietf:params:jmap:contacts:parse" => Capability::ContactsParse, + "urn:ietf:params:jmap:calendars:parse" => Capability::CalendarsParse, ) } } diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs index 471f5c35..cf23b24d 100644 --- a/crates/jmap-proto/src/request/method.rs +++ b/crates/jmap-proto/src/request/method.rs @@ -27,9 +27,14 @@ pub enum MethodObject { SieveScript, Principal, Quota, + Calendar, + CalendarEvent, + CalendarEventNotification, AddressBook, ContactCard, FileNode, + ParticipantIdentity, + ShareNotification, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -46,6 +51,7 @@ pub enum MethodFunction { Lookup, Upload, Echo, + GetAvailability, } impl Display for MethodName { @@ -114,6 +120,9 @@ impl MethodName { (MethodFunction::Get, MethodObject::Principal) => "Principal/get", (MethodFunction::Set, MethodObject::Principal) => "Principal/set", (MethodFunction::Query, MethodObject::Principal) => "Principal/query", + (MethodFunction::Changes, MethodObject::Principal) => "Principal/changes", + (MethodFunction::QueryChanges, MethodObject::Principal) => "Principal/queryChanges", + (MethodFunction::GetAvailability, MethodObject::Principal) => "Principal/getAvailability", (MethodFunction::Get, MethodObject::Quota) => "Quota/get", (MethodFunction::Changes, MethodObject::Quota) => "Quota/changes", @@ -143,6 +152,34 @@ impl MethodName { (MethodFunction::QueryChanges, MethodObject::FileNode) => "FileNode/queryChanges", (MethodFunction::Set, MethodObject::FileNode) => "FileNode/set", + (MethodFunction::Get, MethodObject::ShareNotification) => "ShareNotification/get", + (MethodFunction::Changes, MethodObject::ShareNotification) => "ShareNotification/changes", + (MethodFunction::Query, MethodObject::ShareNotification) => "ShareNotification/query", + (MethodFunction::QueryChanges, MethodObject::ShareNotification) => "ShareNotification/queryChanges", + (MethodFunction::Set, MethodObject::ShareNotification) => "ShareNotification/set", + + (MethodFunction::Get, MethodObject::Calendar) => "Calendar/get", + (MethodFunction::Changes, MethodObject::Calendar) => "Calendar/changes", + (MethodFunction::Set, MethodObject::Calendar) => "Calendar/set", + + (MethodFunction::Get, MethodObject::CalendarEvent) => "CalendarEvent/get", + (MethodFunction::Changes, MethodObject::CalendarEvent) => "CalendarEvent/changes", + (MethodFunction::Query, MethodObject::CalendarEvent) => "CalendarEvent/query", + (MethodFunction::QueryChanges, MethodObject::CalendarEvent) => "CalendarEvent/queryChanges", + (MethodFunction::Set, MethodObject::CalendarEvent) => "CalendarEvent/set", + (MethodFunction::Copy, MethodObject::CalendarEvent) => "CalendarEvent/copy", + (MethodFunction::Parse, MethodObject::CalendarEvent) => "CalendarEvent/parse", + + (MethodFunction::Get, MethodObject::CalendarEventNotification) => "CalendarEventNotification/get", + (MethodFunction::Changes, MethodObject::CalendarEventNotification) => "CalendarEventNotification/changes", + (MethodFunction::Query, MethodObject::CalendarEventNotification) => "CalendarEventNotification/query", + (MethodFunction::QueryChanges, MethodObject::CalendarEventNotification) => "CalendarEventNotification/queryChanges", + (MethodFunction::Set, MethodObject::CalendarEventNotification) => "CalendarEventNotification/set", + + (MethodFunction::Get, MethodObject::ParticipantIdentity) => "ParticipantIdentity/get", + (MethodFunction::Changes, MethodObject::ParticipantIdentity) => "ParticipantIdentity/changes", + (MethodFunction::Set, MethodObject::ParticipantIdentity) => "ParticipantIdentity/set", + (MethodFunction::Echo, MethodObject::Core) => "Core/echo", _ => "error", } @@ -194,6 +231,9 @@ impl MethodName { "Principal/get" => (MethodObject::Principal, MethodFunction::Get), "Principal/set" => (MethodObject::Principal, MethodFunction::Set), "Principal/query" => (MethodObject::Principal, MethodFunction::Query), + "Principal/changes" => (MethodObject::Principal, MethodFunction::Changes), + "Principal/queryChanges" => (MethodObject::Principal, MethodFunction::QueryChanges), + "Principal/getAvailability" => (MethodObject::Principal, MethodFunction::GetAvailability), "Quota/get" => (MethodObject::Quota, MethodFunction::Get), "Quota/changes" => (MethodObject::Quota, MethodFunction::Changes), @@ -223,6 +263,34 @@ impl MethodName { "FileNode/queryChanges" => (MethodObject::FileNode, MethodFunction::QueryChanges), "FileNode/set" => (MethodObject::FileNode, MethodFunction::Set), + "ShareNotification/get" => (MethodObject::ShareNotification, MethodFunction::Get), + "ShareNotification/changes" => (MethodObject::ShareNotification, MethodFunction::Changes), + "ShareNotification/set" => (MethodObject::ShareNotification, MethodFunction::Set), + "ShareNotification/query" => (MethodObject::ShareNotification, MethodFunction::Query), + "ShareNotification/queryChanges" => (MethodObject::ShareNotification, MethodFunction::QueryChanges), + + "Calendar/get" => (MethodObject::Calendar, MethodFunction::Get), + "Calendar/changes" => (MethodObject::Calendar, MethodFunction::Changes), + "Calendar/set" => (MethodObject::Calendar, MethodFunction::Set), + + "CalendarEvent/get" => (MethodObject::CalendarEvent, MethodFunction::Get), + "CalendarEvent/changes" => (MethodObject::CalendarEvent, MethodFunction::Changes), + "CalendarEvent/query" => (MethodObject::CalendarEvent, MethodFunction::Query), + "CalendarEvent/queryChanges" => (MethodObject::CalendarEvent, MethodFunction::QueryChanges), + "CalendarEvent/set" => (MethodObject::CalendarEvent, MethodFunction::Set), + "CalendarEvent/copy" => (MethodObject::CalendarEvent, MethodFunction::Copy), + "CalendarEvent/parse" => (MethodObject::CalendarEvent, MethodFunction::Parse), + + "CalendarEventNotification/get" => (MethodObject::CalendarEventNotification, MethodFunction::Get), + "CalendarEventNotification/changes" => (MethodObject::CalendarEventNotification, MethodFunction::Changes), + "CalendarEventNotification/set" => (MethodObject::CalendarEventNotification, MethodFunction::Set), + "CalendarEventNotification/query" => (MethodObject::CalendarEventNotification, MethodFunction::Query), + "CalendarEventNotification/queryChanges" => (MethodObject::CalendarEventNotification, MethodFunction::QueryChanges), + + "ParticipantIdentity/get" => (MethodObject::ParticipantIdentity, MethodFunction::Get), + "ParticipantIdentity/changes" => (MethodObject::ParticipantIdentity, MethodFunction::Changes), + "ParticipantIdentity/set" => (MethodObject::ParticipantIdentity, MethodFunction::Set), + "Core/echo" => (MethodObject::Core, MethodFunction::Echo), ).map(|(obj, fnc)| MethodName { obj, fnc }) @@ -249,6 +317,11 @@ impl Display for MethodObject { MethodObject::AddressBook => "AddressBook", MethodObject::ContactCard => "ContactCard", MethodObject::FileNode => "FileNode", + MethodObject::ParticipantIdentity => "ParticipantIdentity", + MethodObject::Calendar => "Calendar", + MethodObject::CalendarEvent => "CalendarEvent", + MethodObject::CalendarEventNotification => "CalendarEventNotification", + MethodObject::ShareNotification => "ShareNotification", }) } } diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index ccc8bf48..6193912e 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -14,6 +14,7 @@ pub mod websocket; use self::method::MethodName; use crate::{ method::{ + availability::GetAvailabilityRequest, changes::ChangesRequest, copy::{CopyBlobRequest, CopyRequest}, get::GetRequest, @@ -28,10 +29,13 @@ use crate::{ validate::ValidateSieveScriptRequest, }, object::{ - AnyId, addressbook::AddressBook, blob::Blob, contact::ContactCard, email::Email, - email_submission::EmailSubmission, file_node::FileNode, identity::Identity, - mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota, - sieve::Sieve, thread::Thread, vacation_response::VacationResponse, + AnyId, addressbook::AddressBook, blob::Blob, calendar::Calendar, + calendar_event::CalendarEvent, calendar_event_notification::CalendarEventNotification, + contact::ContactCard, email::Email, email_submission::EmailSubmission, file_node::FileNode, + identity::Identity, mailbox::Mailbox, participant_identity::ParticipantIdentity, + principal::Principal, push_subscription::PushSubscription, quota::Quota, + share_notification::ShareNotification, sieve::Sieve, thread::Thread, + vacation_response::VacationResponse, }, request::{capability::CapabilityIds, reference::MaybeIdReference}, }; @@ -82,11 +86,17 @@ pub enum GetRequestMethod { Sieve(GetRequest), VacationResponse(GetRequest), Principal(GetRequest), + PrincipalAvailability(GetAvailabilityRequest), Quota(GetRequest), Blob(GetRequest), AddressBook(GetRequest), ContactCard(GetRequest), FileNode(GetRequest), + Calendar(GetRequest), + CalendarEvent(GetRequest), + CalendarEventNotification(GetRequest), + ParticipantIdentity(GetRequest), + ShareNotification(GetRequest), } #[derive(Debug)] @@ -101,12 +111,18 @@ pub enum SetRequestMethod<'x> { AddressBook(SetRequest<'x, AddressBook>), ContactCard(SetRequest<'x, ContactCard>), FileNode(SetRequest<'x, FileNode>), + ShareNotification(SetRequest<'x, ShareNotification>), + Calendar(SetRequest<'x, Calendar>), + CalendarEvent(SetRequest<'x, CalendarEvent>), + CalendarEventNotification(SetRequest<'x, CalendarEventNotification>), + ParticipantIdentity(SetRequest<'x, ParticipantIdentity>), } #[derive(Debug)] pub enum CopyRequestMethod<'x> { Email(CopyRequest<'x, Email>), ContactCard(CopyRequest<'x, ContactCard>), + CalendarEvent(CopyRequest<'x, CalendarEvent>), Blob(CopyBlobRequest), } @@ -120,6 +136,9 @@ pub enum QueryRequestMethod { Quota(QueryRequest), ContactCard(QueryRequest), FileNode(QueryRequest), + CalendarEvent(QueryRequest), + CalendarEventNotification(QueryRequest), + ShareNotification(QueryRequest), } #[derive(Debug)] @@ -132,12 +151,16 @@ pub enum QueryChangesRequestMethod { Quota(QueryChangesRequest), ContactCard(QueryChangesRequest), FileNode(QueryChangesRequest), + CalendarEvent(QueryChangesRequest), + CalendarEventNotification(QueryChangesRequest), + ShareNotification(QueryChangesRequest), } #[derive(Debug)] pub enum ParseRequestMethod { Email(ParseRequest), ContactCard(ParseRequest), + CalendarEvent(ParseRequest), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -161,18 +184,6 @@ impl<'de, V: FromStr> serde::Deserialize<'de> for MaybeInvalid { } } -impl serde::Serialize for MaybeInvalid { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - MaybeInvalid::Value(id) => id.serialize(serializer), - MaybeInvalid::Invalid(str) => serializer.serialize_str(str), - } - } -} - impl Default for MaybeInvalid { fn default() -> Self { MaybeInvalid::Invalid("".to_string()) diff --git a/crates/jmap-proto/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs index 82159802..4460b12d 100644 --- a/crates/jmap-proto/src/request/reference.rs +++ b/crates/jmap-proto/src/request/reference.rs @@ -16,7 +16,7 @@ pub struct ResultReference { pub path: JsonPointer, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum MaybeIdReference { Id(V), Reference(String), @@ -86,13 +86,13 @@ impl FromStr for MaybeIdReference { } } -impl serde::Serialize for MaybeIdReference { +impl serde::Serialize for MaybeIdReference { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { - MaybeIdReference::Id(id) => id.serialize(serializer), + MaybeIdReference::Id(id) => serializer.serialize_str(&id.to_string()), MaybeIdReference::Reference(str) => serializer.serialize_str(&format!("#{}", str)), MaybeIdReference::Invalid(str) => serializer.serialize_str(str), } diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs index d84203a7..928cccbb 100644 --- a/crates/jmap-proto/src/response/mod.rs +++ b/crates/jmap-proto/src/response/mod.rs @@ -11,6 +11,7 @@ use self::serialize::serialize_hex; use crate::{ error::method::MethodErrorWrapper, method::{ + availability::GetAvailabilityResponse, changes::ChangesResponse, copy::{CopyBlobResponse, CopyResponse}, get::GetResponse, @@ -25,10 +26,28 @@ use crate::{ validate::ValidateSieveScriptResponse, }, object::{ - AnyId, addressbook::AddressBook, blob::Blob, contact::ContactCard, email::Email, - email_submission::EmailSubmission, file_node::FileNode, identity::Identity, - mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota, - sieve::Sieve, thread::Thread, vacation_response::VacationResponse, + AnyId, + addressbook::AddressBook, + blob::Blob, + calendar::Calendar, + calendar_event::CalendarEvent, + calendar_event_notification::{ + CalendarEventNotification, CalendarEventNotificationGetResponse, + }, + contact::ContactCard, + email::Email, + email_submission::EmailSubmission, + file_node::FileNode, + identity::Identity, + mailbox::Mailbox, + participant_identity::ParticipantIdentity, + principal::Principal, + push_subscription::PushSubscription, + quota::Quota, + share_notification::ShareNotification, + sieve::Sieve, + thread::Thread, + vacation_response::VacationResponse, }, request::{Call, method::MethodName}, }; @@ -66,11 +85,17 @@ pub enum GetResponseMethod { Sieve(GetResponse), VacationResponse(GetResponse), Principal(GetResponse), + PrincipalAvailability(GetAvailabilityResponse), Quota(GetResponse), Blob(GetResponse), AddressBook(GetResponse), ContactCard(GetResponse), FileNode(GetResponse), + Calendar(GetResponse), + CalendarEvent(GetResponse), + CalendarEventNotification(CalendarEventNotificationGetResponse), + ParticipantIdentity(GetResponse), + ShareNotification(GetResponse), } #[derive(Debug, serde::Serialize)] @@ -86,6 +111,11 @@ pub enum SetResponseMethod { AddressBook(SetResponse), ContactCard(SetResponse), FileNode(SetResponse), + ShareNotification(SetResponse), + Calendar(SetResponse), + CalendarEvent(SetResponse), + CalendarEventNotification(SetResponse), + ParticipantIdentity(SetResponse), } #[derive(Debug, serde::Serialize)] @@ -100,6 +130,9 @@ pub enum ChangesResponseMethod { AddressBook(ChangesResponse), ContactCard(ChangesResponse), FileNode(ChangesResponse), + CalendarEvent(ChangesResponse), + CalendarEventNotification(ChangesResponse), + ShareNotification(ChangesResponse), } #[derive(Debug, serde::Serialize)] @@ -107,6 +140,7 @@ pub enum ChangesResponseMethod { pub enum CopyResponseMethod { Email(CopyResponse), ContactCard(CopyResponse), + CalendarEvent(CopyResponse), Blob(CopyBlobResponse), } @@ -115,6 +149,7 @@ pub enum CopyResponseMethod { pub enum ParseResponseMethod { Email(ParseResponse), ContactCard(ParseResponse), + CalendarEvent(ParseResponse), } #[derive(Debug, serde::Serialize)] @@ -456,3 +491,99 @@ impl From> for ResponseMethod<'_> { ResponseMethod::Changes(ChangesResponseMethod::FileNode(response)) } } + +impl From for ResponseMethod<'_> { + fn from(response: GetAvailabilityResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::PrincipalAvailability(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::Calendar(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::Calendar(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::CalendarEvent(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::CalendarEventNotification(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::CalendarEvent(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::ParticipantIdentity(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::ParticipantIdentity(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::ShareNotification(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::ShareNotification(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::ShareNotification(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(response: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::CalendarEvent(response)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(value: ParseResponse) -> Self { + ResponseMethod::Parse(ParseResponseMethod::CalendarEvent(value)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(value: CopyResponse) -> Self { + ResponseMethod::Copy(CopyResponseMethod::CalendarEvent(value)) + } +} + +impl From for ResponseMethod<'_> { + fn from(value: CalendarEventNotificationGetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::CalendarEventNotification(value)) + } +} + +impl From> for ResponseMethod<'_> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::CalendarEventNotification(value)) + } +} diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index aaf4ae2d..6eca082b 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -26,7 +26,7 @@ mail-builder = { version = "0.4" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } mail-auth = { version = "0.7.1", features = ["generate"] } sieve-rs = { version = "0.7", features = ["rkyv"] } -jmap-tools = { version = "0.1", features = ["rkyv"] } +jmap-tools = { path = "/Users/me/code/jmap-tool", features = ["rkyv"] } serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } diff --git a/crates/jmap/src/addressbook/set.rs b/crates/jmap/src/addressbook/set.rs index cd2d694e..8ace54a2 100644 --- a/crates/jmap/src/addressbook/set.rs +++ b/crates/jmap/src/addressbook/set.rs @@ -52,7 +52,7 @@ impl AddressBookSet for Server { let will_destroy = request.unwrap_destroy().into_valid().collect::>(); let is_shared = access_token.is_shared(account_id); - // TODO: Implement onSuccessSetIsDefault + let todo = " Implement onSuccessSetIsDefault + Sieve"; // Process creates let mut batch = BatchBuilder::new(); diff --git a/crates/jmap/src/api/auth.rs b/crates/jmap/src/api/auth.rs index 53fd5591..569435e6 100644 --- a/crates/jmap/src/api/auth.rs +++ b/crates/jmap/src/api/auth.rs @@ -70,6 +70,16 @@ impl JmapAuthorization for AccessToken { GetRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet, GetRequestMethod::ContactCard(_) => Permission::JmapContactCardGet, GetRequestMethod::FileNode(_) => Permission::JmapFileNodeGet, + GetRequestMethod::PrincipalAvailability(_) => { + Permission::JmapPrincipalGetAvailability + } + GetRequestMethod::Calendar(_) => Permission::JmapCalendarGet, + GetRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventGet, + GetRequestMethod::CalendarEventNotification(_) => { + Permission::JmapCalendarEventNotificationGet + } + GetRequestMethod::ParticipantIdentity(_) => Permission::JmapParticipantIdentityGet, + GetRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationGet, }, RequestMethod::Set(m) => match &m { SetRequestMethod::Email(_) => Permission::JmapEmailSet, @@ -82,6 +92,13 @@ impl JmapAuthorization for AccessToken { SetRequestMethod::AddressBook(_) => Permission::JmapAddressBookSet, SetRequestMethod::ContactCard(_) => Permission::JmapContactCardSet, SetRequestMethod::FileNode(_) => Permission::JmapFileNodeSet, + SetRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationSet, + SetRequestMethod::Calendar(_) => Permission::JmapCalendarSet, + SetRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventSet, + SetRequestMethod::CalendarEventNotification(_) => { + Permission::JmapCalendarEventNotificationSet + } + SetRequestMethod::ParticipantIdentity(_) => Permission::JmapParticipantIdentitySet, }, RequestMethod::Changes(_) => match object { MethodObject::Email => Permission::JmapEmailChanges, @@ -92,24 +109,33 @@ impl JmapAuthorization for AccessToken { MethodObject::Quota => Permission::JmapQuotaChanges, MethodObject::ContactCard => Permission::JmapContactCardChanges, MethodObject::FileNode => Permission::JmapFileNodeChanges, + MethodObject::Calendar => Permission::JmapCalendarChanges, + MethodObject::CalendarEvent => Permission::JmapCalendarEventChanges, + MethodObject::CalendarEventNotification => { + Permission::JmapCalendarEventNotificationChanges + } + MethodObject::ParticipantIdentity => Permission::JmapParticipantIdentityChanges, + MethodObject::ShareNotification => Permission::JmapShareNotificationChanges, + MethodObject::Principal => Permission::JmapPrincipalChanges, MethodObject::Core | MethodObject::Blob | MethodObject::PushSubscription | MethodObject::SearchSnippet | MethodObject::VacationResponse | MethodObject::SieveScript - | MethodObject::Principal | MethodObject::AddressBook => Permission::JmapEmailChanges, }, RequestMethod::Copy(m) => match &m { CopyRequestMethod::Email(_) => Permission::JmapEmailCopy, CopyRequestMethod::Blob(_) => Permission::JmapBlobCopy, CopyRequestMethod::ContactCard(_) => Permission::JmapContactCardCopy, + CopyRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventCopy, }, RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, RequestMethod::Parse(m) => match &m { ParseRequestMethod::Email(_) => Permission::JmapEmailParse, ParseRequestMethod::ContactCard(_) => Permission::JmapContactCardParse, + ParseRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventParse, }, RequestMethod::QueryChanges(m) => match m { QueryChangesRequestMethod::Email(_) => Permission::JmapEmailQueryChanges, @@ -124,6 +150,15 @@ impl JmapAuthorization for AccessToken { Permission::JmapContactCardQueryChanges } QueryChangesRequestMethod::FileNode(_) => Permission::JmapFileNodeQueryChanges, + QueryChangesRequestMethod::CalendarEvent(_) => { + Permission::JmapCalendarEventQueryChanges + } + QueryChangesRequestMethod::CalendarEventNotification(_) => { + Permission::JmapCalendarEventNotificationQueryChanges + } + QueryChangesRequestMethod::ShareNotification(_) => { + Permission::JmapShareNotificationQueryChanges + } }, RequestMethod::Query(m) => match m { QueryRequestMethod::Email(_) => Permission::JmapEmailQuery, @@ -134,6 +169,11 @@ impl JmapAuthorization for AccessToken { QueryRequestMethod::Quota(_) => Permission::JmapQuotaQuery, QueryRequestMethod::ContactCard(_) => Permission::JmapContactCardQuery, QueryRequestMethod::FileNode(_) => Permission::JmapFileNodeQuery, + QueryRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventQuery, + QueryRequestMethod::CalendarEventNotification(_) => { + Permission::JmapCalendarEventNotificationQuery + } + QueryRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationQuery, }, RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 0d08f01a..7b97182e 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -8,6 +8,15 @@ use crate::{ addressbook::{get::AddressBookGet, set::AddressBookSet}, api::auth::JmapAuthorization, blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload}, + calendar::{get::CalendarGet, set::CalendarSet}, + calendar_event::{ + copy::JmapCalendarEventCopy, get::CalendarEventGet, parse::CalendarEventParse, + query::CalendarEventQuery, set::CalendarEventSet, + }, + calendar_event_notification::{ + get::CalendarEventNotificationGet, query::CalendarEventNotificationQuery, + set::CalendarEventNotificationSet, + }, changes::{get::ChangesLookup, query::QueryChanges}, contact::{ copy::JmapContactCardCopy, get::ContactCardGet, parse::ContactCardParse, @@ -20,9 +29,13 @@ use crate::{ file::{get::FileNodeGet, query::FileNodeQuery, set::FileNodeSet}, identity::{get::IdentityGet, set::IdentitySet}, mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet}, - principal::{get::PrincipalGet, query::PrincipalQuery}, + participant_identity::{get::ParticipantIdentityGet, set::ParticipantIdentitySet}, + principal::{availability::PrincipalGetAvailability, get::PrincipalGet, query::PrincipalQuery}, push::{get::PushSubscriptionFetch, set::PushSubscriptionSet}, quota::{get::QuotaGet, query::QuotaQuery}, + share_notification::{ + get::ShareNotificationGet, query::ShareNotificationQuery, set::ShareNotificationSet, + }, sieve::{ get::SieveScriptGet, query::SieveScriptQuery, set::SieveScriptSet, validate::SieveScriptValidate, @@ -139,6 +152,19 @@ impl RequestHandler for Server { SetResponseMethod::FileNode(set_response) => { set_response.update_created_ids(&mut response); } + SetResponseMethod::ShareNotification(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::Calendar(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::CalendarEvent(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::ParticipantIdentity(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::CalendarEventNotification(_) => {} } } ResponseMethod::ImportEmail(import_response) => { @@ -279,6 +305,47 @@ impl RequestHandler for Server { self.file_node_get(req, access_token).await?.into() } + GetRequestMethod::PrincipalAvailability(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + + self.principal_get_availability(req, access_token) + .await? + .into() + } + GetRequestMethod::Calendar(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::Calendar)?; + + self.calendar_get(req, access_token).await?.into() + } + GetRequestMethod::CalendarEvent(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?; + + self.calendar_event_get(req, access_token).await?.into() + } + 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)?; + + self.calendar_event_notification_get(req, access_token) + .await? + .into() + } + 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)?; + + self.participant_identity_get(req, access_token) + .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() + } }, RequestMethod::Query(req) => match req { QueryRequestMethod::Email(mut req) => { @@ -327,6 +394,29 @@ impl RequestHandler for Server { self.file_node_query(req, access_token).await?.into() } + QueryRequestMethod::CalendarEvent(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?; + + self.calendar_event_query(req, access_token).await?.into() + } + 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)?; + + self.calendar_event_notification_query(req, access_token) + .await? + .into() + } + 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)?; + + self.share_notification_query(req, access_token) + .await? + .into() + } }, RequestMethod::Set(req) => match req { SetRequestMethod::Email(mut req) => { @@ -395,6 +485,45 @@ impl RequestHandler for Server { self.file_node_set(req, access_token, session).await?.into() } + 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)?; + + self.share_notification_set(req, access_token, session) + .await? + .into() + } + SetRequestMethod::Calendar(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::Calendar)?; + + self.calendar_set(req, access_token, session).await?.into() + } + SetRequestMethod::CalendarEvent(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?; + + self.calendar_event_set(req, access_token, session) + .await? + .into() + } + 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)?; + + self.calendar_event_notification_set(req, access_token, session) + .await? + .into() + } + 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)?; + + self.participant_identity_set(req, access_token, session) + .await? + .into() + } }, RequestMethod::Changes(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -434,6 +563,18 @@ impl RequestHandler for Server { .await? .into() } + CopyRequestMethod::CalendarEvent(mut req) => { + set_account_id_if_missing(&mut req.from_account_id, access_token); + set_account_id_if_missing(&mut req.account_id, access_token); + + access_token + .assert_has_access(req.account_id, Collection::CalendarEvent)? + .assert_has_access(req.from_account_id, Collection::CalendarEvent)?; + + self.calendar_event_copy(req, access_token, next_call, session) + .await? + .into() + } }, RequestMethod::ImportEmail(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -454,6 +595,12 @@ impl RequestHandler for Server { self.contact_card_parse(req, access_token).await?.into() } + ParseRequestMethod::CalendarEvent(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?; + + self.calendar_event_parse(req, access_token).await?.into() + } }, RequestMethod::QueryChanges(req) => self.query_changes(req, access_token).await?.into(), RequestMethod::SearchSnippet(mut req) => { diff --git a/crates/jmap/src/blob/copy.rs b/crates/jmap/src/blob/copy.rs index 307ff6cc..2ad64780 100644 --- a/crates/jmap/src/blob/copy.rs +++ b/crates/jmap/src/blob/copy.rs @@ -10,7 +10,7 @@ use directory::Permission; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::copy::{CopyBlobRequest, CopyBlobResponse}, - request::MaybeInvalid, + request::IntoValid, }; use std::future::Future; use store::{ @@ -43,18 +43,7 @@ impl BlobCopy for Server { }; let account_id = request.account_id.document_id(); - for blob_id in request.blob_ids { - let blob_id = match blob_id { - MaybeInvalid::Value(blob_id) => blob_id, - MaybeInvalid::Invalid(_) => { - response.not_copied.append( - blob_id, - SetError::invalid_properties().with_description("Invalid blobId."), - ); - continue; - } - }; - + for blob_id in request.blob_ids.into_valid() { if self.has_access_blob(&blob_id, access_token).await? { // Enforce quota let used = self @@ -72,7 +61,7 @@ impl BlobCopy for Server { && !access_token.has_permission(Permission::UnlimitedUploads) { response.not_copied.append( - MaybeInvalid::Value(blob_id), + blob_id, SetError::over_quota().with_description(format!( "You have exceeded the blob quota of {} files or {} bytes.", self.core.jmap.upload_tmp_quota_amount, @@ -108,7 +97,7 @@ impl BlobCopy for Server { response.copied.append(blob_id, dest_blob_id); } else { response.not_copied.append( - MaybeInvalid::Value(blob_id), + blob_id, SetError::new(SetErrorType::BlobNotFound).with_description( "blobId does not exist or not enough permissions to access it.", ), diff --git a/crates/jmap/src/blob/get.rs b/crates/jmap/src/blob/get.rs index 1fd7dec9..6303dc23 100644 --- a/crates/jmap/src/blob/get.rs +++ b/crates/jmap/src/blob/get.rs @@ -13,7 +13,7 @@ use jmap_proto::{ lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse}, }, object::blob::{Blob, BlobProperty, BlobValue, DataProperty, DigestProperty}, - request::MaybeInvalid, + request::{IntoValid, MaybeInvalid}, }; use jmap_tools::{Map, Value}; use mail_builder::encoders::base64::base64_encode; @@ -191,75 +191,66 @@ impl BlobOperations for Server { not_found: vec![], }; - for id in request.ids { - match id { - MaybeInvalid::Value(id) => { - let mut matched_ids = VecMap::new(); + for id in request.ids.into_valid() { + let mut matched_ids = VecMap::new(); - match &id.class { - BlobClass::Linked { - account_id, - collection, - document_id, - } if *account_id == req_account_id => { - let collection = Collection::from(*collection); - if collection == Collection::Email { - if let Some(data_) = self - .get_archive(req_account_id, Collection::Email, *document_id) - .await? - { - let data = data_ - .unarchive::() - .caused_by(trc::location!())?; - if include_email { - matched_ids.append( - DataType::Email, - vec![Id::from_parts( - u32::from(data.thread_id), - *document_id, - )], - ); - } - if include_thread { - matched_ids.append( - DataType::Thread, - vec![Id::from(u32::from(data.thread_id))], - ); - } - if include_mailbox { - matched_ids.append( - DataType::Mailbox, - data.mailboxes - .iter() - .map(|m| { - debug_assert!(m.uid != 0); - Id::from(u32::from(m.mailbox_id)) - }) - .collect::>(), - ); - } - } - } else { - match DataType::try_from(collection) { - Ok(data_type) if type_names.contains(&data_type) => { - matched_ids.append(data_type, vec![Id::from(*document_id)]); - } - _ => (), - } + match &id.class { + BlobClass::Linked { + account_id, + collection, + document_id, + } if *account_id == req_account_id => { + let collection = Collection::from(*collection); + if collection == Collection::Email { + if let Some(data_) = self + .get_archive(req_account_id, Collection::Email, *document_id) + .await? + { + let data = data_ + .unarchive::() + .caused_by(trc::location!())?; + if include_email { + matched_ids.append( + DataType::Email, + vec![Id::from_parts(u32::from(data.thread_id), *document_id)], + ); + } + if include_thread { + matched_ids.append( + DataType::Thread, + vec![Id::from(u32::from(data.thread_id))], + ); + } + if include_mailbox { + matched_ids.append( + DataType::Mailbox, + data.mailboxes + .iter() + .map(|m| { + debug_assert!(m.uid != 0); + Id::from(u32::from(m.mailbox_id)) + }) + .collect::>(), + ); } } - BlobClass::Reserved { account_id, .. } if *account_id == req_account_id => { - } - _ => { - response.not_found.push(MaybeInvalid::Value(id)); - continue; + } else { + match DataType::try_from(collection) { + Ok(data_type) if type_names.contains(&data_type) => { + matched_ids.append(data_type, vec![Id::from(*document_id)]); + } + _ => (), } } - - response.list.push(BlobInfo { id, matched_ids }); } - _ => response.not_found.push(id), + BlobClass::Reserved { account_id, .. } if *account_id == req_account_id => {} + _ => { + response.not_found.push(id); + continue; + } } + + response.list.push(BlobInfo { id, matched_ids }); } Ok(response) diff --git a/crates/jmap/src/calendar/get.rs b/crates/jmap/src/calendar/get.rs new file mode 100644 index 00000000..45a26bab --- /dev/null +++ b/crates/jmap/src/calendar/get.rs @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{api::acl::JmapRights, changes::state::JmapCacheState}; +use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; +use groupware::{cache::GroupwareCache, calendar::Calendar}; +use jmap_proto::{ + method::get::{GetRequest, GetResponse}, + object::calendar::{self, CalendarProperty, CalendarValue}, +}; +use jmap_tools::{Map, Value}; +use store::roaring::RoaringBitmap; +use trc::AddContext; +use types::{ + acl::{Acl, AclGrant}, + collection::{Collection, SyncCollection}, +}; + +pub trait CalendarGet: Sync + Send { + fn calendar_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl CalendarGet for Server { + async fn calendar_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(&[ + CalendarProperty::Id, + CalendarProperty::Name, + CalendarProperty::Description, + CalendarProperty::SortOrder, + CalendarProperty::IsDefault, + CalendarProperty::IsSubscribed, + CalendarProperty::MyRights, + ]); + let account_id = request.account_id.document_id(); + let cache = self + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) + .await?; + let calendar_ids = if access_token.is_member(account_id) { + cache.document_ids(true).collect::() + } else { + cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true) + }; + + let ids = if let Some(ids) = ids { + ids + } else { + calendar_ids + .iter() + .take(self.core.jmap.get_max_objects) + .map(Into::into) + .collect::>() + }; + let mut response = GetResponse { + account_id: request.account_id.into(), + state: cache.get_state(true).into(), + list: Vec::with_capacity(ids.len()), + not_found: vec![], + }; + + for id in ids { + // Obtain the calendar object + let document_id = id.document_id(); + if !calendar_ids.contains(document_id) { + response.not_found.push(id); + continue; + } + let _calendar = if let Some(calendar) = self + .get_archive(account_id, Collection::Calendar, document_id) + .await? + { + calendar + } else { + response.not_found.push(id); + continue; + }; + let calendar = _calendar + .unarchive::() + .caused_by(trc::location!())?; + let mut result = Map::with_capacity(properties.len()); + for property in &properties { + match property { + CalendarProperty::Id => { + result.insert_unchecked(CalendarProperty::Id, CalendarValue::Id(id)); + } + CalendarProperty::Name => { + result.insert_unchecked( + CalendarProperty::Name, + calendar.preferences(access_token).name.to_string(), + ); + } + CalendarProperty::Description => { + result.insert_unchecked( + CalendarProperty::Description, + calendar + .preferences(access_token) + .description + .as_ref() + .map(|v| v.to_string()), + ); + } + CalendarProperty::SortOrder => { + result.insert_unchecked( + CalendarProperty::SortOrder, + calendar.preferences(access_token).sort_order.to_native(), + ); + } + /*CalendarProperty::IsDefault => { + result.insert_unchecked(CalendarProperty::IsDefault, calendar.is_default); + } + CalendarProperty::IsSubscribed => { + result.insert_unchecked( + CalendarProperty::IsSubscribed, + calendar + .subscribers + .iter() + .any(|account_id| *account_id == access_token.primary_id()), + ); + }*/ + CalendarProperty::ShareWith => { + result.insert_unchecked( + CalendarProperty::ShareWith, + JmapRights::share_with::( + account_id, + access_token, + &calendar.acls.iter().map(AclGrant::from).collect::>(), + ), + ); + } + CalendarProperty::MyRights => { + result.insert_unchecked( + CalendarProperty::MyRights, + if access_token.is_shared(account_id) { + JmapRights::rights::( + calendar.acls.effective_acl(access_token), + ) + } else { + JmapRights::all_rights::() + }, + ); + } + property => { + result.insert_unchecked(property.clone(), Value::Null); + } + } + } + response.list.push(result.into()); + } + + Ok(response) + } +} diff --git a/crates/jmap/src/calendar/mod.rs b/crates/jmap/src/calendar/mod.rs new file mode 100644 index 00000000..f460abaa --- /dev/null +++ b/crates/jmap/src/calendar/mod.rs @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod get; +pub mod set; diff --git a/crates/jmap/src/calendar/set.rs b/crates/jmap/src/calendar/set.rs new file mode 100644 index 00000000..61d7000a --- /dev/null +++ b/crates/jmap/src/calendar/set.rs @@ -0,0 +1,342 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::api::acl::{JmapAcl, JmapRights}; +use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; +use groupware::{DestroyArchive, cache::GroupwareCache}; +use http_proto::HttpSessionData; +use jmap_proto::{ + error::set::SetError, + method::set::{SetRequest, SetResponse}, + object::calendar::{self, CalendarProperty, CalendarValue}, + request::IntoValid, + types::state::State, +}; +use jmap_tools::{JsonPointerItem, Key, Value}; +use rand::{Rng, distr::Alphanumeric}; +use store::write::BatchBuilder; +use trc::AddContext; +use types::{ + acl::{Acl, AclGrant}, + collection::{Collection, SyncCollection}, +}; + +pub trait CalendarSet: Sync + Send { + fn calendar_set( + &self, + request: SetRequest<'_, calendar::Calendar>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl CalendarSet for Server { + async fn calendar_set( + &self, + mut request: SetRequest<'_, calendar::Calendar>, + access_token: &AccessToken, + _session: &HttpSessionData, + ) -> trc::Result> { + todo!() + /*let account_id = request.account_id.document_id(); + let cache = self + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) + .await?; + let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; + let will_destroy = request.unwrap_destroy().into_valid().collect::>(); + let is_shared = access_token.is_shared(account_id); + + let todo = " Implement onSuccessSetIsDefault + Sieve"; + + // Process creates + let mut batch = BatchBuilder::new(); + 'create: for (id, object) in request.unwrap_create() { + if is_shared { + response.not_created.append( + id, + SetError::forbidden() + .with_description("Cannot create calendars in a shared account."), + ); + continue 'create; + } + + let mut calendar = Calendar { + name: rand::rng() + .sample_iter(Alphanumeric) + .take(10) + .map(char::from) + .collect::(), + preferences: vec![CalendarPreferences { + account_id, + name: "Address Book".to_string(), + ..Default::default() + }], + ..Default::default() + }; + + // Process changes + if let Err(err) = update_calendar(object, &mut calendar, access_token) { + response.not_created.append(id, err); + continue 'create; + } + + // Validate ACLs + if !calendar.acls.is_empty() { + if let Err(err) = self.acl_validate(&calendar.acls).await { + response.not_created.append(id, err.into()); + continue 'create; + } + + self.refresh_acls(&calendar.acls, None).await; + } + + // Insert record + let document_id = self + .store() + .assign_document_ids(account_id, Collection::Calendar, 1) + .await + .caused_by(trc::location!())?; + calendar + .insert(access_token, account_id, document_id, &mut batch) + .caused_by(trc::location!())?; + response.created(id, document_id); + } + + // Process updates + 'update: for (id, object) in request.unwrap_update().into_valid() { + // Make sure id won't be destroyed + if will_destroy.contains(&id) { + response.not_updated.append(id, SetError::will_destroy()); + continue 'update; + } + + // Obtain calendar + let document_id = id.document_id(); + let calendar_ = if let Some(calendar_) = self + .get_archive(account_id, Collection::Calendar, document_id) + .await? + { + calendar_ + } else { + response.not_updated.append(id, SetError::not_found()); + continue 'update; + }; + let calendar = calendar_ + .to_unarchived::() + .caused_by(trc::location!())?; + let mut new_calendar = calendar + .deserialize::() + .caused_by(trc::location!())?; + + // Apply changes + let has_acl_changes = match update_calendar(object, &mut new_calendar, access_token) { + Ok(has_acl_changes_) => has_acl_changes_, + Err(err) => { + response.not_updated.append(id, err); + continue 'update; + } + }; + + // Validate ACL + if is_shared { + let acl = calendar.inner.acls.effective_acl(access_token); + if !acl.contains(Acl::Modify) || (has_acl_changes && !acl.contains(Acl::Administer)) + { + response.not_updated.append( + id, + SetError::forbidden() + .with_description("You are not allowed to modify this calendar."), + ); + continue 'update; + } + } + if has_acl_changes { + if let Err(err) = self.acl_validate(&new_calendar.acls).await { + response.not_updated.append(id, err.into()); + continue 'update; + } + self.refresh_acls( + &new_calendar.acls, + Some( + calendar + .inner + .acls + .iter() + .map(AclGrant::from) + .collect::>() + .as_slice(), + ), + ) + .await; + } + + // Update record + new_calendar + .update(access_token, calendar, account_id, document_id, &mut batch) + .caused_by(trc::location!())?; + response.updated.append(id, None); + } + + // Process deletions + let on_destroy_remove_contents = request + .arguments + .on_destroy_remove_contents + .unwrap_or(false); + for id in will_destroy { + let document_id = id.document_id(); + + if !cache.has_container_id(&document_id) { + response.not_destroyed.append(id, SetError::not_found()); + continue; + }; + + let Some(calendar_) = self + .get_archive(account_id, Collection::Calendar, document_id) + .await + .caused_by(trc::location!())? + else { + response.not_destroyed.append(id, SetError::not_found()); + continue; + }; + + let calendar = calendar_ + .to_unarchived::() + .caused_by(trc::location!())?; + + // Validate ACLs + if is_shared + && !calendar + .inner + .acls + .effective_acl(access_token) + .contains_all([Acl::Delete, Acl::RemoveItems].into_iter()) + { + response.not_destroyed.append( + id, + SetError::forbidden() + .with_description("You are not allowed to delete this calendar."), + ); + continue; + } + + // Obtain children ids + let children_ids = cache.children_ids(document_id).collect::>(); + if !children_ids.is_empty() && !on_destroy_remove_contents { + response + .not_destroyed + .append(id, SetError::calendar_has_contents()); + continue; + } + + // Delete record + DestroyArchive(calendar) + .delete_with_cards( + self, + access_token, + account_id, + document_id, + children_ids, + None, + &mut batch, + ) + .await + .caused_by(trc::location!())?; + + response.destroyed.push(id); + } + + // Write changes + if !batch.is_empty() { + let change_id = self + .commit_batch(batch) + .await + .and_then(|ids| ids.last_change_id(account_id)) + .caused_by(trc::location!())?; + + response.new_state = State::Exact(change_id).into(); + } + + Ok(response)*/ + } +} + +/*fn update_calendar( + updates: Value<'_, CalendarProperty, CalendarValue>, + calendar: &mut Calendar, + access_token: &AccessToken, +) -> Result> { + let mut has_acl_changes = false; + + for (property, value) in updates.into_expanded_object() { + let Key::Property(property) = property else { + return Err(SetError::invalid_properties() + .with_property(property.to_owned()) + .with_description("Invalid property.")); + }; + + match (property, value) { + (CalendarProperty::Name, Value::Str(value)) if (1..=255).contains(&value.len()) => { + calendar.preferences_mut(access_token).name = value.into_owned(); + } + (CalendarProperty::Description, Value::Str(value)) if value.len() < 255 => { + calendar.preferences_mut(access_token).description = value.into_owned().into(); + } + (CalendarProperty::Description, Value::Null) => { + calendar.preferences_mut(access_token).description = None; + } + (CalendarProperty::SortOrder, Value::Number(value)) => { + calendar.preferences_mut(access_token).sort_order = value.cast_to_u64() as u32; + } + (CalendarProperty::IsSubscribed, Value::Bool(subscribe)) => { + let account_id = access_token.primary_id(); + if subscribe { + if !calendar.subscribers.contains(&account_id) { + calendar.subscribers.push(account_id); + } + } else { + calendar.subscribers.retain(|id| *id != account_id); + } + } + (CalendarProperty::ShareWith, value) => { + calendar.acls = JmapRights::acl_set::(value)?; + has_acl_changes = true; + } + (CalendarProperty::Pointer(pointer), value) + if matches!( + pointer.first(), + Some(JsonPointerItem::Key(Key::Property( + CalendarProperty::ShareWith + ))) + ) => + { + let mut pointer = pointer.iter(); + pointer.next(); + + calendar.acls = JmapRights::acl_patch::( + std::mem::take(&mut calendar.acls), + pointer, + value, + )?; + has_acl_changes = true; + } + (property, _) => { + return Err(SetError::invalid_properties() + .with_property(property.clone()) + .with_description("Field could not be set.")); + } + } + } + + // Validate name + if calendar.preferences(access_token).name.is_empty() { + return Err(SetError::invalid_properties() + .with_property(CalendarProperty::Name) + .with_description("Missing name.")); + } + + Ok(has_acl_changes) +} +*/ diff --git a/crates/jmap/src/calendar_event/copy.rs b/crates/jmap/src/calendar_event/copy.rs new file mode 100644 index 00000000..83239930 --- /dev/null +++ b/crates/jmap/src/calendar_event/copy.rs @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{calendar_event::set::CalendarEventSet, changes::state::JmapCacheState}; +use common::{Server, auth::AccessToken}; +use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; +use http_proto::HttpSessionData; +use jmap_proto::{ + error::set::SetError, + method::{ + copy::{CopyRequest, CopyResponse}, + set::SetRequest, + }, + object::calendar_event, + request::{ + Call, IntoValid, MaybeInvalid, RequestMethod, SetRequestMethod, + method::{MethodFunction, MethodName, MethodObject}, + reference::MaybeResultReference, + }, + types::state::State, +}; +use store::{roaring::RoaringBitmap, write::BatchBuilder}; +use trc::AddContext; +use types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; +use utils::map::vec_map::VecMap; + +pub trait JmapCalendarEventCopy: Sync + Send { + fn calendar_event_copy<'x>( + &self, + request: CopyRequest<'x, calendar_event::CalendarEvent>, + access_token: &AccessToken, + next_call: &mut Option>>, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl JmapCalendarEventCopy for Server { + async fn calendar_event_copy<'x>( + &self, + request: CopyRequest<'x, calendar_event::CalendarEvent>, + access_token: &AccessToken, + next_call: &mut Option>>, + _session: &HttpSessionData, + ) -> trc::Result> { + todo!() + + /*let account_id = request.account_id.document_id(); + let from_account_id = request.from_account_id.document_id(); + + if account_id == from_account_id { + return Err(trc::JmapEvent::InvalidArguments + .into_err() + .details("From accountId is equal to fromAccountId")); + } + let cache = self + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) + .await + .caused_by(trc::location!())?; + let old_state = cache.assert_state(false, &request.if_in_state)?; + let mut response = CopyResponse { + from_account_id: request.from_account_id, + account_id: request.account_id, + new_state: old_state.clone(), + old_state, + created: VecMap::with_capacity(request.create.len()), + not_created: VecMap::new(), + }; + + let from_cache = self + .fetch_dav_resources(access_token, from_account_id, SyncCollection::Calendar) + .await + .caused_by(trc::location!())?; + let from_calendar_event_ids = if access_token.is_member(from_account_id) { + from_cache.document_ids(false).collect::() + } else { + from_cache.shared_items(access_token, [Acl::ReadItems], true) + }; + + let can_add_address_books = if access_token.is_shared(account_id) { + cache + .shared_containers(access_token, [Acl::AddItems], true) + .into() + } else { + None + }; + let on_success_delete = request.on_success_destroy_original.unwrap_or(false); + let mut destroy_ids = Vec::new(); + + // Obtain quota + let mut batch = BatchBuilder::new(); + + 'create: for (id, create) in request.create.into_valid() { + let from_calendar_event_id = id.document_id(); + if !from_calendar_event_ids.contains(from_calendar_event_id) { + response.not_created.append( + id, + SetError::not_found().with_description(format!( + "Item {} not found not found in account {}.", + id, response.from_account_id + )), + ); + continue; + } + + let Some(_calendar_event) = self + .get_archive( + account_id, + Collection::CalendarEvent, + from_calendar_event_id, + ) + .await? + else { + response.not_created.append( + id, + SetError::not_found().with_description(format!( + "Item {} not found not found in account {}.", + id, response.from_account_id + )), + ); + continue; + }; + + let calendar_event = _calendar_event + .deserialize::() + .caused_by(trc::location!())?; + + match self + .create_calendar_event( + &cache, + &mut batch, + access_token, + account_id, + &can_add_address_books, + calendar_event.card.into_jscalendar(), + create, + ) + .await? + { + Ok(document_id) => { + response.created(id, document_id); + + // Add to destroy list + if on_success_delete { + destroy_ids.push(MaybeInvalid::Value(id)); + } + } + Err(err) => { + response.not_created.append(id, err); + continue 'create; + } + } + } + + // Write changes + if !batch.is_empty() { + let change_id = self + .commit_batch(batch) + .await + .and_then(|ids| ids.last_change_id(account_id)) + .caused_by(trc::location!())?; + + response.new_state = State::Exact(change_id); + } + + // Destroy ids + if on_success_delete && !destroy_ids.is_empty() { + *next_call = Call { + id: String::new(), + name: MethodName::new(MethodObject::CalendarEvent, MethodFunction::Set), + method: RequestMethod::Set(SetRequestMethod::CalendarEvent(SetRequest { + account_id: request.from_account_id, + if_in_state: request.destroy_from_if_in_state, + create: None, + update: None, + destroy: MaybeResultReference::Value(destroy_ids).into(), + arguments: Default::default(), + })), + } + .into(); + } + + Ok(response)*/ + } +} diff --git a/crates/jmap/src/calendar_event/get.rs b/crates/jmap/src/calendar_event/get.rs new file mode 100644 index 00000000..aa6f2872 --- /dev/null +++ b/crates/jmap/src/calendar_event/get.rs @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::changes::state::JmapCacheState; +use calcard::jscalendar::{JSCalendarProperty, JSCalendarValue}; +use common::{Server, auth::AccessToken}; +use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; +use jmap_proto::{ + method::get::{GetRequest, GetResponse}, + object::calendar_event, + request::IntoValid, +}; +use jmap_tools::{Map, Value}; +use store::roaring::RoaringBitmap; +use trc::AddContext; +use types::{ + acl::Acl, + blob::BlobId, + collection::{Collection, SyncCollection}, + id::Id, +}; + +pub trait CalendarEventGet: Sync + Send { + fn calendar_event_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl CalendarEventGet for Server { + async fn calendar_event_get( + &self, + mut request: GetRequest, + access_token: &AccessToken, + ) -> trc::Result> { + todo!() + + /*let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; + let return_all_properties = request.properties.is_none(); + let properties = + request.unwrap_properties(&[JSCalendarProperty::Id, JSCalendarProperty::CalendarIds]); + let account_id = request.account_id.document_id(); + let cache = self + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) + .await?; + let calendar_event_ids = if access_token.is_member(account_id) { + cache.document_ids(false).collect::() + } else { + cache.shared_containers(access_token, [Acl::ReadItems], true) + }; + let ids = if let Some(ids) = ids { + ids + } else { + calendar_event_ids + .iter() + .take(self.core.jmap.get_max_objects) + .map(Into::into) + .collect::>() + }; + let mut response = GetResponse { + account_id: request.account_id.into(), + state: cache.get_state(false).into(), + list: Vec::with_capacity(ids.len()), + not_found: vec![], + }; + let return_id = return_all_properties || properties.contains(&JSCalendarProperty::Id); + let return_address_book_ids = + return_all_properties || properties.contains(&JSCalendarProperty::CalendarIds); + + for id in ids { + // Obtain the calendar_event object + let document_id = id.document_id(); + if !calendar_event_ids.contains(document_id) { + response.not_found.push(id); + continue; + } + + let _calendar_event = if let Some(calendar_event) = self + .get_archive(account_id, Collection::CalendarEvent, document_id) + .await? + { + calendar_event + } else { + response.not_found.push(id); + continue; + }; + + let calendar_event = _calendar_event + .deserialize::() + .caused_by(trc::location!())?; + + let mut result = if return_all_properties { + calendar_event + .card + .into_jscalendar::() + .into_inner() + .into_object() + .unwrap() + } else { + Map::from_iter( + calendar_event + .card + .into_jscalendar::() + .into_inner() + .into_expanded_object() + .filter(|(k, _)| k.as_property().is_some_and(|p| properties.contains(p))), + ) + }; + + if return_id { + result.insert_unchecked( + JSCalendarProperty::Id, + Value::Element(JSCalendarValue::Id(id)), + ); + } + + if return_address_book_ids { + let mut obj = Map::with_capacity(calendar_event.names.len()); + for id in calendar_event.names.iter() { + obj.insert_unchecked(JSCalendarProperty::IdValue(Id::from(id.parent_id)), true); + } + result.insert_unchecked(JSCalendarProperty::CalendarIds, Value::Object(obj)); + } + + response.list.push(result.into()); + } + + Ok(response)*/ + } +} diff --git a/crates/jmap/src/calendar_event/mod.rs b/crates/jmap/src/calendar_event/mod.rs new file mode 100644 index 00000000..58b319ed --- /dev/null +++ b/crates/jmap/src/calendar_event/mod.rs @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod copy; +pub mod get; +pub mod parse; +pub mod query; +pub mod set; diff --git a/crates/jmap/src/calendar_event/parse.rs b/crates/jmap/src/calendar_event/parse.rs new file mode 100644 index 00000000..94600b7c --- /dev/null +++ b/crates/jmap/src/calendar_event/parse.rs @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::blob::download::BlobDownload; +use calcard::icalendar::ICalendar; +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::parse::{ParseRequest, ParseResponse}, + object::calendar_event::CalendarEvent, + request::IntoValid, +}; +use types::id::Id; +use utils::map::vec_map::VecMap; + +pub trait CalendarEventParse: Sync + Send { + fn calendar_event_parse( + &self, + request: ParseRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl CalendarEventParse for Server { + async fn calendar_event_parse( + &self, + request: ParseRequest, + access_token: &AccessToken, + ) -> trc::Result> { + let todo = "user calendar parse specific limit, same for addressbooks"; + if request.blob_ids.len() > self.core.jmap.mail_parse_max_items { + return Err(trc::JmapEvent::RequestTooLarge.into_err()); + } + let return_all_properties = request.properties.is_none(); + let properties = request + .properties + .map(|v| v.into_valid().collect::>()) + .unwrap_or_default(); + + let mut response = ParseResponse { + account_id: request.account_id, + parsed: VecMap::with_capacity(request.blob_ids.len()), + not_parsable: vec![], + not_found: vec![], + }; + + for blob_id in request.blob_ids.into_valid() { + // Fetch raw message to parse + let raw_vcard = match self.blob_download(&blob_id, access_token).await? { + Some(raw_vcard) => raw_vcard, + None => { + response.not_found.push(blob_id); + continue; + } + }; + let Ok(vcard) = ICalendar::parse(std::str::from_utf8(&raw_vcard).unwrap_or_default()) + else { + response.not_parsable.push(blob_id); + continue; + }; + let mut js_calendar_event = vcard.into_jscalendar::(); + + if !return_all_properties { + js_calendar_event + .0 + .as_object_mut() + .unwrap() + .as_mut_vec() + .retain(|(k, _)| k.as_property().is_some_and(|k| properties.contains(k))); + } + + response + .parsed + .append(blob_id, js_calendar_event.into_inner()); + } + + Ok(response) + } +} diff --git a/crates/jmap/src/calendar_event/query.rs b/crates/jmap/src/calendar_event/query.rs new file mode 100644 index 00000000..9f6ef01e --- /dev/null +++ b/crates/jmap/src/calendar_event/query.rs @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use groupware::cache::GroupwareCache; +use jmap_proto::{ + method::query::{Comparator, Filter, QueryRequest, QueryResponse}, + object::calendar_event::{CalendarEvent, CalendarEventComparator, CalendarEventFilter}, + request::MaybeInvalid, +}; +use store::{SerializeInfallible, query, roaring::RoaringBitmap}; +use types::{ + acl::Acl, + collection::{Collection, SyncCollection}, + field::ContactField, +}; +use utils::sanitize_email; + +use crate::{JmapMethods, changes::state::JmapCacheState}; + +pub trait CalendarEventQuery: Sync + Send { + fn calendar_event_query( + &self, + request: QueryRequest, + access_token: &AccessToken, + ) -> impl Future> + Send; +} + +impl CalendarEventQuery for Server { + async fn calendar_event_query( + &self, + 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::Calendar) + .await?; + let filter_mask = (access_token.is_shared(account_id)) + .then(|| cache.shared_items(access_token, [Acl::ReadItems], true)); + + for cond in std::mem::take(&mut request.filter) { + match cond { + Filter::Property(cond) => match cond { + CalendarEventFilter::InCalendar(MaybeInvalid::Value(id)) => { + filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( + cache.children_ids(id.document_id()), + ))) + } + CalendarEventFilter::Uid(uid) => { + filters.push(query::Filter::eq(ContactField::Uid, uid.into_bytes())) + } + CalendarEventFilter::Email(email) => filters.push(query::Filter::eq( + ContactField::Email, + sanitize_email(&email).unwrap_or(email).into_bytes(), + )), + CalendarEventFilter::Text(value) => filters.push(query::Filter::has_text( + ContactField::Text, + value.to_lowercase(), + )), + CalendarEventFilter::CreatedBefore(before) => filters.push(query::Filter::lt( + ContactField::Created, + (before.timestamp() as u64).serialize(), + )), + CalendarEventFilter::CreatedAfter(after) => filters.push(query::Filter::gt( + ContactField::Created, + (after.timestamp() as u64).serialize(), + )), + CalendarEventFilter::UpdatedBefore(before) => filters.push(query::Filter::lt( + ContactField::Updated, + (before.timestamp() as u64).serialize(), + )), + CalendarEventFilter::UpdatedAfter(after) => filters.push(query::Filter::gt( + ContactField::Updated, + (after.timestamp() as u64).serialize(), + )), + unsupported => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details(unsupported.into_string())); + } + }, + + Filter::And | Filter::Or | Filter::Not | Filter::Close => { + filters.push(cond.into()); + } + } + } + + let mut result_set = self + .filter(account_id, Collection::CalendarEvent, filters) + .await?; + + if let Some(filter_mask) = filter_mask { + result_set.apply_mask(filter_mask); + } + + let (response, paginate) = self + .build_query_response(&result_set, cache.get_state(false), &request) + .await?; + + if let Some(paginate) = paginate { + // Parse sort criteria + let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len())); + for comparator in request + .sort + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| vec![Comparator::descending(CalendarEventComparator::Updated)]) + { + comparators.push(match comparator.property { + CalendarEventComparator::Created => { + query::Comparator::field(ContactField::Created, comparator.is_ascending) + } + CalendarEventComparator::Updated => { + query::Comparator::field(ContactField::Updated, comparator.is_ascending) + } + unsupported => { + return Err(trc::JmapEvent::UnsupportedSort + .into_err() + .details(unsupported.into_string())); + } + }); + } + + // Sort results + self.sort(result_set, comparators, paginate, response).await + } else { + Ok(response) + }*/ + } +} diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs new file mode 100644 index 00000000..af54c75e --- /dev/null +++ b/crates/jmap/src/calendar_event/set.rs @@ -0,0 +1,537 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use calcard::jscalendar::{JSCalendar, JSCalendarProperty, JSCalendarValue}; +use common::{DavName, DavResources, Server, auth::AccessToken}; +use groupware::{DestroyArchive, cache::GroupwareCache}; +use http_proto::HttpSessionData; +use jmap_proto::{ + error::set::SetError, + method::set::{SetRequest, SetResponse}, + object::calendar_event, + request::IntoValid, + types::state::State, +}; +use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Value}; +use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder}; +use trc::AddContext; +use types::{ + acl::Acl, + blob::BlobId, + collection::{Collection, SyncCollection}, + id::Id, +}; + +pub trait CalendarEventSet: Sync + Send { + fn calendar_event_set( + &self, + request: SetRequest<'_, calendar_event::CalendarEvent>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future>> + Send; + + #[allow(clippy::too_many_arguments)] + fn create_calendar_event( + &self, + cache: &DavResources, + batch: &mut BatchBuilder, + access_token: &AccessToken, + account_id: u32, + can_add_address_books: &Option, + js_calendar_event: JSCalendar<'_, Id>, + updates: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) -> impl Future>>>>; +} + +impl CalendarEventSet for Server { + async fn calendar_event_set( + &self, + mut request: SetRequest<'_, calendar_event::CalendarEvent>, + access_token: &AccessToken, + _session: &HttpSessionData, + ) -> trc::Result> { + todo!() + /*let account_id = request.account_id.document_id(); + let cache = self + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) + .await?; + let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; + let will_destroy = request.unwrap_destroy().into_valid().collect::>(); + + // Obtain calendarIds + let (can_add_address_books, can_delete_address_books, can_modify_address_books) = + if access_token.is_shared(account_id) { + ( + cache + .shared_containers(access_token, [Acl::AddItems], true) + .into(), + cache + .shared_containers(access_token, [Acl::RemoveItems], true) + .into(), + cache + .shared_containers(access_token, [Acl::ModifyItems], true) + .into(), + ) + } else { + (None, None, None) + }; + + // Process creates + let mut batch = BatchBuilder::new(); + 'create: for (id, object) in request.unwrap_create() { + match self + .create_calendar_event( + &cache, + &mut batch, + access_token, + account_id, + &can_add_address_books, + JSCalendar::default(), + object, + ) + .await? + { + Ok(document_id) => { + response.created(id, document_id); + } + Err(err) => { + response.not_created.append(id, err); + continue 'create; + } + } + } + + // Process updates + 'update: for (id, object) in request.unwrap_update().into_valid() { + // Make sure id won't be destroyed + if will_destroy.contains(&id) { + response.not_updated.append(id, SetError::will_destroy()); + continue 'update; + } + + // Obtain calendar_event card + let document_id = id.document_id(); + let calendar_event_ = if let Some(calendar_event_) = self + .get_archive(account_id, Collection::CalendarEvent, document_id) + .await? + { + calendar_event_ + } else { + response.not_updated.append(id, SetError::not_found()); + continue 'update; + }; + let calendar_event = calendar_event_ + .to_unarchived::() + .caused_by(trc::location!())?; + let mut new_calendar_event = calendar_event + .deserialize::() + .caused_by(trc::location!())?; + let mut js_calendar_event = new_calendar_event.card.into_jscalendar(); + + // Process changes + if let Err(err) = update_calendar_event( + object, + &mut new_calendar_event.names, + &mut js_calendar_event, + ) { + response.not_updated.append(id, err); + continue 'update; + } + + // Convert JSCalendar to vCard + if let Some(vcard) = js_calendar_event.into_vcard() { + new_calendar_event.size = vcard.size() as u32; + new_calendar_event.card = vcard; + } else { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_description("Failed to convert calendar_event to vCard."), + ); + continue 'update; + } + + // Validate UID + match ( + new_calendar_event.card.uid(), + calendar_event.inner.card.uid(), + ) { + (Some(old_uid), Some(new_uid)) if old_uid == new_uid => {} + (None, None) | (None, Some(_)) => {} + _ => { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::Uid) + .with_description("You cannot change the UID of a calendar_event."), + ); + continue 'update; + } + } + + // Validate new calendarIds + for addressbook_id in new_calendar_event.added_addressbook_ids(calendar_event.inner) { + if !cache.has_container_id(&addressbook_id) { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::CalendarIds) + .with_description(format!( + "calendarId {} does not exist.", + Id::from(addressbook_id) + )), + ); + continue 'update; + } else if can_add_address_books + .as_ref() + .is_some_and(|ids| !ids.contains(addressbook_id)) + { + response.not_updated.append( + id, + SetError::forbidden().with_description(format!( + "You are not allowed to add calendar_events to calendar {}.", + Id::from(addressbook_id) + )), + ); + continue 'update; + } + } + + // Validate deleted calendarIds + if let Some(can_delete_address_books) = &can_delete_address_books { + for addressbook_id in + new_calendar_event.removed_addressbook_ids(calendar_event.inner) + { + if !can_delete_address_books.contains(addressbook_id) { + response.not_updated.append( + id, + SetError::forbidden().with_description(format!( + "You are not allowed to remove calendar_events from calendar {}.", + Id::from(addressbook_id) + )), + ); + continue 'update; + } + } + } + + // Validate changed calendarIds + if let Some(can_modify_address_books) = &can_modify_address_books { + for addressbook_id in + new_calendar_event.unchanged_addressbook_ids(calendar_event.inner) + { + if !can_modify_address_books.contains(addressbook_id) { + response.not_updated.append( + id, + SetError::forbidden().with_description(format!( + "You are not allowed to modify calendar {}.", + Id::from(addressbook_id) + )), + ); + continue 'update; + } + } + } + + // Check size and quota + if new_calendar_event.size as usize > self.core.groupware.max_vcard_size { + response.not_updated.append( + id, + SetError::invalid_properties().with_description(format!( + "Contact size {} exceeds the maximum allowed size of {} bytes.", + new_calendar_event.size, self.core.groupware.max_vcard_size + )), + ); + continue 'update; + } + let extra_bytes = (new_calendar_event.size as u64) + .saturating_sub(u32::from(calendar_event.inner.size) as u64); + if extra_bytes > 0 { + match self + .has_available_quota( + &self.get_resource_token(access_token, account_id).await?, + extra_bytes, + ) + .await + { + Ok(_) => {} + Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => { + response.not_updated.append(id, SetError::over_quota()); + continue 'update; + } + Err(err) => return Err(err.caused_by(trc::location!())), + } + } + + // Update record + new_calendar_event + .update( + access_token, + calendar_event, + account_id, + document_id, + &mut batch, + ) + .caused_by(trc::location!())?; + response.updated.append(id, None); + } + + // Process deletions + for id in will_destroy { + let document_id = id.document_id(); + + if !cache.has_container_id(&document_id) { + response.not_destroyed.append(id, SetError::not_found()); + continue; + }; + + let Some(calendar_event_) = self + .get_archive(account_id, Collection::CalendarEvent, document_id) + .await + .caused_by(trc::location!())? + else { + response.not_destroyed.append(id, SetError::not_found()); + continue; + }; + + let calendar_event = calendar_event_ + .to_unarchived::() + .caused_by(trc::location!())?; + + // Validate ACLs + if let Some(can_delete_address_books) = &can_delete_address_books { + for name in calendar_event.inner.names.iter() { + let parent_id = name.parent_id.to_native(); + if !can_delete_address_books.contains(parent_id) { + response.not_destroyed.append( + id, + SetError::forbidden().with_description(format!( + "You are not allowed to remove calendar_events from calendar {}.", + Id::from(parent_id) + )), + ); + continue; + } + } + } + + // Delete record + DestroyArchive(calendar_event) + .delete_all(access_token, account_id, document_id, &mut batch) + .caused_by(trc::location!())?; + + response.destroyed.push(id); + } + + // Write changes + if !batch.is_empty() { + let change_id = self + .commit_batch(batch) + .await + .and_then(|ids| ids.last_change_id(account_id)) + .caused_by(trc::location!())?; + + response.new_state = State::Exact(change_id).into(); + } + + Ok(response)*/ + } + + async fn create_calendar_event( + &self, + cache: &DavResources, + batch: &mut BatchBuilder, + access_token: &AccessToken, + account_id: u32, + can_add_address_books: &Option, + mut js_calendar_event: JSCalendar<'_, Id>, + updates: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) -> trc::Result>>> { + todo!() + /* + // Process changes + let mut names = Vec::new(); + if let Err(err) = update_calendar_event(updates, &mut names, &mut js_calendar_event) { + return Ok(Err(err)); + } + + // Verify that the calendar ids valid + for name in &names { + if !cache.has_container_id(&name.parent_id) { + return Ok(Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::CalendarIds) + .with_description(format!( + "calendarId {} does not exist.", + Id::from(name.parent_id) + )))); + } else if can_add_address_books + .as_ref() + .is_some_and(|ids| !ids.contains(name.parent_id)) + { + return Ok(Err(SetError::forbidden().with_description(format!( + "You are not allowed to add calendar_events to calendar {}.", + Id::from(name.parent_id) + )))); + } + } + + // Convert JSCalendar to vCard + let Some(card) = js_calendar_event.into_vcard() else { + return Ok(Err(SetError::invalid_properties() + .with_description("Failed to convert calendar_event to vCard."))); + }; + + // Validate UID + if let Err(err) = assert_is_unique_uid(self, cache, account_id, &names, card.uid()).await? { + return Ok(Err(err)); + } + + // Check size and quota + let size = card.size(); + if size > self.core.groupware.max_vcard_size { + return Ok(Err(SetError::invalid_properties().with_description( + format!( + "Contact size {} exceeds the maximum allowed size of {} bytes.", + size, self.core.groupware.max_vcard_size + ), + ))); + } + match self + .has_available_quota( + &self.get_resource_token(access_token, account_id).await?, + size as u64, + ) + .await + { + Ok(_) => {} + Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => { + return Ok(Err(SetError::over_quota())); + } + Err(err) => return Err(err.caused_by(trc::location!())), + } + + // Insert record + let document_id = self + .store() + .assign_document_ids(account_id, Collection::CalendarEvent, 1) + .await + .caused_by(trc::location!())?; + CalendarEvent { + names, + size: size as u32, + card, + ..Default::default() + } + .insert(access_token, account_id, document_id, batch) + .caused_by(trc::location!()) + .map(|_| Ok(document_id))*/ + } +} + +/* +fn update_calendar_event<'x>( + updates: Value<'x, JSCalendarProperty, JSCalendarValue>, + addressbooks: &mut Vec, + js_calendar_event: &mut JSCalendar<'x, Id>, +) -> Result<(), SetError>> { + for (property, value) in updates.into_expanded_object() { + let Key::Property(property) = property else { + return Err(SetError::invalid_properties() + .with_property(property.to_owned()) + .with_description("Invalid property.")); + }; + + match (property, value) { + (JSCalendarProperty::CalendarIds, value) => { + patch_parent_ids(addressbooks, None, value)?; + } + (JSCalendarProperty::Pointer(pointer), value) => { + if matches!( + pointer.first(), + Some(JsonPointerItem::Key(Key::Property( + JSCalendarProperty::CalendarIds + ))) + ) { + let mut pointer = pointer.iter(); + pointer.next(); + patch_parent_ids(addressbooks, pointer.next(), value)?; + } else if !js_calendar_event.0.patch_jptr(pointer.iter(), value) { + return Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::Pointer(pointer)) + .with_description("Patch operation failed.")); + } + } + (property, value) => { + js_calendar_event + .0 + .as_object_mut() + .unwrap() + .insert(property, value); + } + } + } + + // Make sure the calendar_event belongs to at least one calendar + if addressbooks.is_empty() { + return Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::CalendarIds) + .with_description("Contact has to belong to at least one calendar.")); + } + + Ok(()) +} + +fn patch_parent_ids( + current: &mut Vec, + patch: Option<&JsonPointerItem>>, + update: Value<'_, JSCalendarProperty, JSCalendarValue>, +) -> Result<(), SetError>> { + match (patch, update) { + ( + Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdValue(id)))), + Value::Bool(false) | Value::Null, + ) => { + let id = id.document_id(); + current.retain(|name| name.parent_id != id); + Ok(()) + } + ( + Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdValue(id)))), + Value::Bool(true), + ) => { + let id = id.document_id(); + if !current.iter().any(|name| name.parent_id == id) { + current.push(DavName::new_with_rand_name(id)); + } + Ok(()) + } + (None, Value::Object(object)) => { + let mut new_ids = object + .into_expanded_boolean_set() + .filter_map(|id| { + if let Key::Property(JSCalendarProperty::IdValue(id)) = id { + Some(id.document_id()) + } else { + None + } + }) + .collect::>(); + + current.retain(|name| !new_ids.remove(&name.parent_id)); + + for id in new_ids { + current.push(DavName::new_with_rand_name(id)); + } + + Ok(()) + } + _ => Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::CalendarIds) + .with_description("Invalid patch operation for calendarIds.")), + } +} + +*/ diff --git a/crates/jmap/src/calendar_event_notification/get.rs b/crates/jmap/src/calendar_event_notification/get.rs new file mode 100644 index 00000000..a80eae8a --- /dev/null +++ b/crates/jmap/src/calendar_event_notification/get.rs @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::get::GetRequest, + object::calendar_event_notification::{ + CalendarEventNotification, CalendarEventNotificationGetResponse, + }, +}; + +pub trait CalendarEventNotificationGet: Sync + Send { + fn calendar_event_notification_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future> + Send; +} + +impl CalendarEventNotificationGet for Server { + async fn calendar_event_notification_get( + &self, + mut request: GetRequest, + access_token: &AccessToken, + ) -> trc::Result { + todo!() + } +} diff --git a/crates/jmap/src/calendar_event_notification/mod.rs b/crates/jmap/src/calendar_event_notification/mod.rs new file mode 100644 index 00000000..c036acc9 --- /dev/null +++ b/crates/jmap/src/calendar_event_notification/mod.rs @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod get; +pub mod query; +pub mod set; diff --git a/crates/jmap/src/calendar_event_notification/query.rs b/crates/jmap/src/calendar_event_notification/query.rs new file mode 100644 index 00000000..bde23d74 --- /dev/null +++ b/crates/jmap/src/calendar_event_notification/query.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::query::{QueryRequest, QueryResponse}, + object::calendar_event_notification::CalendarEventNotification, +}; + +pub trait CalendarEventNotificationQuery: Sync + Send { + fn calendar_event_notification_query( + &self, + request: QueryRequest, + access_token: &AccessToken, + ) -> impl Future> + Send; +} + +impl CalendarEventNotificationQuery for Server { + async fn calendar_event_notification_query( + &self, + mut request: QueryRequest, + access_token: &AccessToken, + ) -> trc::Result { + todo!() + } +} diff --git a/crates/jmap/src/calendar_event_notification/set.rs b/crates/jmap/src/calendar_event_notification/set.rs new file mode 100644 index 00000000..802886b1 --- /dev/null +++ b/crates/jmap/src/calendar_event_notification/set.rs @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use http_proto::HttpSessionData; +use jmap_proto::{ + method::set::{SetRequest, SetResponse}, + object::calendar_event_notification::CalendarEventNotification, +}; + +pub trait CalendarEventNotificationSet: Sync + Send { + fn calendar_event_notification_set( + &self, + request: SetRequest<'_, CalendarEventNotification>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl CalendarEventNotificationSet for Server { + async fn calendar_event_notification_set( + &self, + mut request: SetRequest<'_, CalendarEventNotification>, + access_token: &AccessToken, + _session: &HttpSessionData, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index 982e750a..8a54249e 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -75,13 +75,25 @@ impl ChangesLookup for Server { (SyncCollection::FileNode, true) } - _ => { + MethodObject::CalendarEvent => { + access_token.assert_has_access(request.account_id, Collection::Calendar)?; + + (SyncCollection::Calendar, false) + } + MethodObject::CalendarEventNotification => { access_token.assert_is_member(request.account_id)?; + (SyncCollection::CalendarEventNotification, false) + } + MethodObject::ShareNotification => { + access_token.assert_is_member(request.account_id)?; + + (SyncCollection::ShareNotification, false) + } + _ => { return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); } }; - let max_changes = std::cmp::min( request .max_changes @@ -246,7 +258,18 @@ impl IntermediateChangesResponse { MethodObject::FileNode => { ChangesResponseMethod::FileNode(transmute_response(self.response)) } - MethodObject::Core + MethodObject::CalendarEvent => { + ChangesResponseMethod::CalendarEvent(transmute_response(self.response)) + } + MethodObject::CalendarEventNotification => { + ChangesResponseMethod::CalendarEventNotification(transmute_response(self.response)) + } + MethodObject::ShareNotification => { + ChangesResponseMethod::ShareNotification(transmute_response(self.response)) + } + MethodObject::ParticipantIdentity + | MethodObject::Calendar + | MethodObject::Core | MethodObject::Blob | MethodObject::PushSubscription | MethodObject::SearchSnippet diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index f4bc9488..28782841 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -6,8 +6,10 @@ use super::get::ChangesLookup; use crate::{ - api::request::set_account_id_if_missing, contact::query::ContactCardQuery, - email::query::EmailQuery, file::query::FileNodeQuery, mailbox::query::MailboxQuery, + api::request::set_account_id_if_missing, calendar_event::query::CalendarEventQuery, + calendar_event_notification::query::CalendarEventNotificationQuery, + contact::query::ContactCardQuery, email::query::EmailQuery, file::query::FileNodeQuery, + mailbox::query::MailboxQuery, share_notification::query::ShareNotificationQuery, sieve::query::SieveScriptQuery, submission::query::EmailSubmissionQuery, }; use common::{Server, auth::AccessToken}; @@ -183,6 +185,78 @@ impl QueryChanges for Server { up_to_id = request.up_to_id; results = self.file_node_query(request.into(), access_token).await?; } + QueryChangesRequestMethod::CalendarEvent(mut request) => { + // Query changes + set_account_id_if_missing(&mut request.account_id, access_token); + changes = self + .changes( + build_changes_request(&request), + MethodObject::FileNode, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self + .calendar_event_query(request.into(), access_token) + .await?; + } + QueryChangesRequestMethod::CalendarEventNotification(mut request) => { + // Query changes + set_account_id_if_missing(&mut request.account_id, access_token); + changes = self + .changes( + build_changes_request(&request), + MethodObject::FileNode, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self + .calendar_event_notification_query(request.into(), access_token) + .await?; + } + QueryChangesRequestMethod::ShareNotification(mut request) => { + // Query changes + set_account_id_if_missing(&mut request.account_id, access_token); + changes = self + .changes( + build_changes_request(&request), + MethodObject::FileNode, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self + .share_notification_query(request.into(), access_token) + .await?; + } QueryChangesRequestMethod::Principal(_) => { return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); } diff --git a/crates/jmap/src/contact/get.rs b/crates/jmap/src/contact/get.rs index 44fed5f4..0b615763 100644 --- a/crates/jmap/src/contact/get.rs +++ b/crates/jmap/src/contact/get.rs @@ -11,6 +11,7 @@ use groupware::{cache::GroupwareCache, contact::ContactCard}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::contact, + request::IntoValid, }; use jmap_tools::{Map, Value}; use store::roaring::RoaringBitmap; diff --git a/crates/jmap/src/file/set.rs b/crates/jmap/src/file/set.rs index f004cc27..9f10d794 100644 --- a/crates/jmap/src/file/set.rs +++ b/crates/jmap/src/file/set.rs @@ -49,6 +49,8 @@ impl FileNodeSet for Server { let will_destroy = request.unwrap_destroy().into_valid().collect::>(); let is_shared = access_token.is_shared(account_id); + let todo = "validate blob permissions"; + // Process creates let mut batch = BatchBuilder::new(); 'create: for (id, object) in request.unwrap_create() { diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 4dc5759d..49635e63 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -27,15 +27,20 @@ use types::collection::Collection; pub mod addressbook; pub mod api; pub mod blob; +pub mod calendar; +pub mod calendar_event; +pub mod calendar_event_notification; pub mod changes; pub mod contact; pub mod email; pub mod file; pub mod identity; pub mod mailbox; +pub mod participant_identity; pub mod principal; pub mod push; pub mod quota; +pub mod share_notification; pub mod sieve; pub mod submission; pub mod thread; diff --git a/crates/jmap/src/participant_identity/get.rs b/crates/jmap/src/participant_identity/get.rs new file mode 100644 index 00000000..c2918a74 --- /dev/null +++ b/crates/jmap/src/participant_identity/get.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::get::{GetRequest, GetResponse}, + object::participant_identity::ParticipantIdentity, +}; + +pub trait ParticipantIdentityGet: Sync + Send { + fn participant_identity_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl ParticipantIdentityGet for Server { + async fn participant_identity_get( + &self, + mut request: GetRequest, + access_token: &AccessToken, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/participant_identity/mod.rs b/crates/jmap/src/participant_identity/mod.rs new file mode 100644 index 00000000..f460abaa --- /dev/null +++ b/crates/jmap/src/participant_identity/mod.rs @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod get; +pub mod set; diff --git a/crates/jmap/src/participant_identity/set.rs b/crates/jmap/src/participant_identity/set.rs new file mode 100644 index 00000000..d4aa5040 --- /dev/null +++ b/crates/jmap/src/participant_identity/set.rs @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use http_proto::HttpSessionData; +use jmap_proto::{ + method::set::{SetRequest, SetResponse}, + object::participant_identity::ParticipantIdentity, +}; + +pub trait ParticipantIdentitySet: Sync + Send { + fn participant_identity_set( + &self, + request: SetRequest<'_, ParticipantIdentity>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> 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!() + } +} diff --git a/crates/jmap/src/principal/availability.rs b/crates/jmap/src/principal/availability.rs new file mode 100644 index 00000000..19a0c1b3 --- /dev/null +++ b/crates/jmap/src/principal/availability.rs @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::method::availability::{GetAvailabilityRequest, GetAvailabilityResponse}; +use std::future::Future; + +pub trait PrincipalGetAvailability: Sync + Send { + fn principal_get_availability( + &self, + request: GetAvailabilityRequest, + access_token: &AccessToken, + ) -> impl Future> + Send; +} + +impl PrincipalGetAvailability for Server { + async fn principal_get_availability( + &self, + mut request: GetAvailabilityRequest, + access_token: &AccessToken, + ) -> trc::Result { + todo!() + } +} diff --git a/crates/jmap/src/principal/mod.rs b/crates/jmap/src/principal/mod.rs index 4a00acaa..db9e3b2e 100644 --- a/crates/jmap/src/principal/mod.rs +++ b/crates/jmap/src/principal/mod.rs @@ -4,5 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +pub mod availability; pub mod get; pub mod query; diff --git a/crates/jmap/src/share_notification/get.rs b/crates/jmap/src/share_notification/get.rs new file mode 100644 index 00000000..60e2f669 --- /dev/null +++ b/crates/jmap/src/share_notification/get.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::get::{GetRequest, GetResponse}, + object::share_notification::ShareNotification, +}; + +pub trait ShareNotificationGet: Sync + Send { + fn share_notification_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl ShareNotificationGet for Server { + async fn share_notification_get( + &self, + mut request: GetRequest, + access_token: &AccessToken, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/share_notification/mod.rs b/crates/jmap/src/share_notification/mod.rs new file mode 100644 index 00000000..c036acc9 --- /dev/null +++ b/crates/jmap/src/share_notification/mod.rs @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod get; +pub mod query; +pub mod set; diff --git a/crates/jmap/src/share_notification/query.rs b/crates/jmap/src/share_notification/query.rs new file mode 100644 index 00000000..22b6c44f --- /dev/null +++ b/crates/jmap/src/share_notification/query.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::query::{QueryRequest, QueryResponse}, + object::share_notification::ShareNotification, +}; + +pub trait ShareNotificationQuery: Sync + Send { + fn share_notification_query( + &self, + request: QueryRequest, + access_token: &AccessToken, + ) -> impl Future> + Send; +} + +impl ShareNotificationQuery for Server { + async fn share_notification_query( + &self, + mut request: QueryRequest, + access_token: &AccessToken, + ) -> trc::Result { + todo!() + } +} diff --git a/crates/jmap/src/share_notification/set.rs b/crates/jmap/src/share_notification/set.rs new file mode 100644 index 00000000..16b14e5c --- /dev/null +++ b/crates/jmap/src/share_notification/set.rs @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use http_proto::HttpSessionData; +use jmap_proto::{ + method::set::{SetRequest, SetResponse}, + object::share_notification::ShareNotification, +}; + +pub trait ShareNotificationSet: Sync + Send { + fn share_notification_set( + &self, + request: SetRequest<'_, ShareNotification>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl ShareNotificationSet for Server { + async fn share_notification_set( + &self, + mut request: SetRequest<'_, ShareNotification>, + access_token: &AccessToken, + _session: &HttpSessionData, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/migration/src/principal.rs b/crates/migration/src/principal.rs index 805d5655..71be6c75 100644 --- a/crates/migration/src/principal.rs +++ b/crates/migration/src/principal.rs @@ -236,8 +236,8 @@ impl FromLegacy for Principal { let is_disabled = field == PrincipalField::DisabledPermissions; if let Some(ids) = legacy.take_int_array(field) { for id in ids { - if let Some(permission) = Permission::from_id(id as usize) { - permissions.insert(permission, is_disabled); + if Permission::from_id(id as u32).is_some() { + permissions.insert(id as u32, is_disabled); } } } diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 3103509e..588aa768 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -7,7 +7,7 @@ resolver = "2" [dependencies] utils = { path = "../utils" } trc = { path = "../trc" } -jmap-tools = { version = "0.1" } +jmap-tools = { path = "/Users/me/code/jmap-tool" } hashify = "0.2" serde = { version = "1.0", features = ["derive"]} rkyv = { version = "0.8.10", features = ["little_endian"] } diff --git a/crates/types/src/acl.rs b/crates/types/src/acl.rs index d3559536..9f9c568d 100644 --- a/crates/types/src/acl.rs +++ b/crates/types/src/acl.rs @@ -38,7 +38,8 @@ pub enum Acl { SchedulingReply = 12, ModifyItemsOwn = 13, ModifyPrivateProperties = 14, - None = 15, + ModifyRSVP = 15, + None = 16, } #[derive( @@ -77,6 +78,7 @@ impl Acl { Acl::SchedulingReadFreeBusy => "schedulingReadFreeBusy", Acl::SchedulingInvite => "schedulingInvite", Acl::SchedulingReply => "schedulingReply", + Acl::ModifyRSVP => "modifyRSVP", } } } @@ -130,6 +132,7 @@ impl From for Acl { 12 => Acl::SchedulingReply, 13 => Acl::ModifyItemsOwn, 14 => Acl::ModifyPrivateProperties, + 15 => Acl::ModifyRSVP, _ => Acl::None, } } diff --git a/crates/types/src/collection.rs b/crates/types/src/collection.rs index 8291dd3f..ae4a84a2 100644 --- a/crates/types/src/collection.rs +++ b/crates/types/src/collection.rs @@ -28,9 +28,10 @@ pub enum Collection { AddressBook = 10, ContactCard = 11, FileNode = 12, - CalendarScheduling = 13, + CalendarEventNotification = 13, + ShareNotification = 14, #[default] - None = 14, + None = 15, } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)] @@ -44,9 +45,10 @@ pub enum SyncCollection { Identity = 5, EmailSubmission = 6, SieveScript = 7, - CalendarScheduling = 8, + CalendarEventNotification = 8, + ShareNotification = 9, #[default] - None = 9, + None = 10, } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] @@ -74,7 +76,8 @@ impl Collection { Collection::CalendarEvent => Some(Collection::Calendar), Collection::ContactCard => Some(Collection::AddressBook), Collection::FileNode => Some(Collection::FileNode), - Collection::CalendarScheduling => Some(Collection::CalendarScheduling), + Collection::CalendarEventNotification => Some(Collection::CalendarEventNotification), + Collection::ShareNotification => Some(Collection::ShareNotification), _ => None, } } @@ -85,7 +88,8 @@ impl Collection { Collection::Calendar => Some(Collection::CalendarEvent), Collection::AddressBook => Some(Collection::ContactCard), Collection::FileNode => Some(Collection::FileNode), - Collection::CalendarScheduling => Some(Collection::CalendarScheduling), + Collection::CalendarEventNotification => Some(Collection::CalendarEventNotification), + Collection::ShareNotification => Some(Collection::ShareNotification), _ => None, } } @@ -120,7 +124,8 @@ impl SyncCollection { SyncCollection::Identity => Collection::Identity, SyncCollection::EmailSubmission => Collection::EmailSubmission, SyncCollection::SieveScript => Collection::SieveScript, - SyncCollection::CalendarScheduling => Collection::CalendarScheduling, + SyncCollection::CalendarEventNotification => Collection::CalendarEventNotification, + SyncCollection::ShareNotification => Collection::ShareNotification, SyncCollection::None => Collection::None, } } @@ -149,10 +154,11 @@ impl From for SyncCollection { Collection::Principal => SyncCollection::None, Collection::Calendar => SyncCollection::Calendar, Collection::CalendarEvent => SyncCollection::Calendar, - Collection::CalendarScheduling => SyncCollection::CalendarScheduling, + Collection::CalendarEventNotification => SyncCollection::CalendarEventNotification, Collection::AddressBook => SyncCollection::AddressBook, Collection::ContactCard => SyncCollection::AddressBook, Collection::FileNode => SyncCollection::FileNode, + Collection::ShareNotification => SyncCollection::ShareNotification, _ => SyncCollection::None, } } @@ -174,7 +180,8 @@ impl From for Collection { 10 => Collection::AddressBook, 11 => Collection::ContactCard, 12 => Collection::FileNode, - 13 => Collection::CalendarScheduling, + 13 => Collection::CalendarEventNotification, + 14 => Collection::ShareNotification, _ => Collection::None, } } @@ -191,7 +198,8 @@ impl From for SyncCollection { 5 => SyncCollection::Identity, 6 => SyncCollection::EmailSubmission, 7 => SyncCollection::SieveScript, - 8 => SyncCollection::CalendarScheduling, + 8 => SyncCollection::CalendarEventNotification, + 9 => SyncCollection::ShareNotification, _ => SyncCollection::None, } } @@ -208,7 +216,8 @@ impl From for SyncCollection { 5 => SyncCollection::Identity, 6 => SyncCollection::EmailSubmission, 7 => SyncCollection::SieveScript, - 8 => SyncCollection::CalendarScheduling, + 8 => SyncCollection::CalendarEventNotification, + 9 => SyncCollection::ShareNotification, _ => SyncCollection::None, } } @@ -230,7 +239,8 @@ impl From for Collection { 10 => Collection::AddressBook, 11 => Collection::ContactCard, 12 => Collection::FileNode, - 13 => Collection::CalendarScheduling, + 13 => Collection::CalendarEventNotification, + 14 => Collection::ShareNotification, _ => Collection::None, } } @@ -278,6 +288,14 @@ impl TryFrom for DataType { Collection::EmailSubmission => Ok(DataType::EmailSubmission), Collection::SieveScript => Ok(DataType::SieveScript), Collection::PushSubscription => Ok(DataType::PushSubscription), + Collection::Principal => Ok(DataType::Principal), + Collection::Calendar => Ok(DataType::Calendar), + Collection::CalendarEvent => Ok(DataType::CalendarEvent), + Collection::AddressBook => Ok(DataType::AddressBook), + Collection::ContactCard => Ok(DataType::ContactCard), + Collection::FileNode => Ok(DataType::FileNode), + Collection::CalendarEventNotification => Ok(DataType::CalendarEventNotification), + Collection::ShareNotification => Ok(DataType::ShareNotification), _ => Err(()), } } @@ -305,7 +323,8 @@ impl Collection { Collection::AddressBook => "addressBook", Collection::ContactCard => "contactCard", Collection::FileNode => "fileNode", - Collection::CalendarScheduling => "calendarScheduling", + Collection::CalendarEventNotification => "calendarEventNotification", + Collection::ShareNotification => "shareNotification", Collection::None => "", } } @@ -329,6 +348,8 @@ impl FromStr for Collection { "addressBook" => Collection::AddressBook, "contactCard" => Collection::ContactCard, "fileNode" => Collection::FileNode, + "calendarEventNotification" => Collection::CalendarEventNotification, + "shareNotification" => Collection::ShareNotification, ) .ok_or(()) } @@ -371,7 +392,8 @@ impl SyncCollection { SyncCollection::Identity => "identity", SyncCollection::EmailSubmission => "emailSubmission", SyncCollection::SieveScript => "sieveScript", - SyncCollection::CalendarScheduling => "calendarScheduling", + SyncCollection::CalendarEventNotification => "calendarEventNotification", + SyncCollection::ShareNotification => "shareNotification", SyncCollection::None => "", } } diff --git a/crates/types/src/type_state.rs b/crates/types/src/type_state.rs index d02627ac..bc49ba7f 100644 --- a/crates/types/src/type_state.rs +++ b/crates/types/src/type_state.rs @@ -51,7 +51,13 @@ pub enum DataType { ContactCard = 17, #[serde(rename = "FileNode")] FileNode = 18, - None = 19, + #[serde(rename = "Principal")] + Principal = 19, + #[serde(rename = "ShareNotification")] + ShareNotification = 20, + #[serde(rename = "ParticipantIdentity")] + ParticipantIdentity = 21, + None = 22, } #[derive(Debug, Clone, Copy)] @@ -116,6 +122,9 @@ impl From for DataType { 16 => DataType::AddressBook, 17 => DataType::ContactCard, 18 => DataType::FileNode, + 19 => DataType::Principal, + 20 => DataType::ShareNotification, + 21 => DataType::ParticipantIdentity, _ => { debug_assert!(false, "Invalid type_state value: {}", value); DataType::None @@ -171,6 +180,9 @@ impl DataType { b"AddressBook" => DataType::AddressBook, b"ContactCard" => DataType::ContactCard, b"FileNode" => DataType::FileNode, + b"Principal" => DataType::Principal, + b"ShareNotification" => DataType::ShareNotification, + b"ParticipantIdentity" => DataType::ParticipantIdentity, ) } @@ -195,6 +207,9 @@ impl DataType { DataType::AddressBook => "AddressBook", DataType::ContactCard => "ContactCard", DataType::FileNode => "FileNode", + DataType::Principal => "Principal", + DataType::ShareNotification => "ShareNotification", + DataType::ParticipantIdentity => "ParticipantIdentity", DataType::None => "", } } diff --git a/crates/utils/proc-macros/src/lib.rs b/crates/utils/proc-macros/src/lib.rs index 56adffa2..64ea8f1d 100644 --- a/crates/utils/proc-macros/src/lib.rs +++ b/crates/utils/proc-macros/src/lib.rs @@ -20,7 +20,7 @@ pub fn enum_id(input: TokenStream) -> TokenStream { let variant_count = variants.len(); let variant_names: Vec<_> = variants.iter().map(|v| &v.ident).collect(); - let variant_ids: Vec = (0..variant_count).collect(); + let variant_ids: Vec = (0..(variant_count as u32)).collect(); let snake_case_names: Vec = variant_names .iter() .map(|name| to_snake_case(&name.to_string())) @@ -30,13 +30,13 @@ pub fn enum_id(input: TokenStream) -> TokenStream { impl #name { pub const COUNT: usize = #variant_count; - pub const fn id(&self) -> usize { + pub const fn id(&self) -> u32 { match self { #(#name::#variant_names => #variant_ids,)* } } - pub fn from_id(id: usize) -> Option { + pub fn from_id(id: u32) -> Option { match id { #(#variant_ids => Some(#name::#variant_names),)* _ => None,

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + CalendarProperty::Id => match parse_ref(value) { + MaybeReference::Value(v) => Some(CalendarValue::Id(v)), + MaybeReference::Reference(v) => Some(CalendarValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + CalendarProperty::TimeZone => Tz::from_str(value).ok().map(CalendarValue::Timezone), + CalendarProperty::IncludeInAvailability => { + IncludeInAvailability::parse(value).map(CalendarValue::IncludeInAvailability) + } + CalendarProperty::Action => JSCalendarAlertAction::from_str(value) + .ok() + .map(CalendarValue::Action), + CalendarProperty::RelativeTo => JSCalendarRelativeTo::from_str(value) + .ok() + .map(CalendarValue::RelativeTo), + CalendarProperty::When => UTCDate::from_str(value).ok().map(CalendarValue::Date), + CalendarProperty::Offset => { + ICalendarDuration::parse(value.as_bytes()).map(CalendarValue::Duration) + } + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + CalendarValue::Id(id) => id.to_string().into(), + CalendarValue::IdReference(r) => format!("#{r}").into(), + CalendarValue::IncludeInAvailability(include) => include.as_str().into(), + CalendarValue::Date(date) => date.to_string().into(), + CalendarValue::Action(action) => action.as_str().into(), + CalendarValue::RelativeTo(relative) => relative.as_str().into(), + CalendarValue::Type(typ) => typ.as_str().into(), + CalendarValue::Duration(dur) => dur.to_string().into(), + CalendarValue::Timezone(tz) => tz.name().unwrap_or_default(), + } + } +} + +impl CalendarProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => CalendarProperty::Id, + b"name" => CalendarProperty::Name, + b"description" => CalendarProperty::Description, + b"color" => CalendarProperty::Color, + b"sortOrder" => CalendarProperty::SortOrder, + b"isSubscribed" => CalendarProperty::IsSubscribed, + b"isVisible" => CalendarProperty::IsVisible, + b"isDefault" => CalendarProperty::IsDefault, + b"includeInAvailability" => CalendarProperty::IncludeInAvailability, + b"defaultAlertsWithTime" => CalendarProperty::DefaultAlertsWithTime, + b"defaultAlertsWithoutTime" => CalendarProperty::DefaultAlertsWithoutTime, + b"timeZone" => CalendarProperty::TimeZone, + b"shareWith" => CalendarProperty::ShareWith, + b"myRights" => CalendarProperty::MyRights, + b"mayReadFreeBusy" => CalendarProperty::Rights(CalendarRight::MayReadFreeBusy), + b"mayReadItems" => CalendarProperty::Rights(CalendarRight::MayReadItems), + b"mayWriteAll" => CalendarProperty::Rights(CalendarRight::MayWriteAll), + b"mayWriteOwn" => CalendarProperty::Rights(CalendarRight::MayWriteOwn), + b"mayUpdatePrivate" => CalendarProperty::Rights(CalendarRight::MayUpdatePrivate), + b"mayRSVP" => CalendarProperty::Rights(CalendarRight::MayRSVP), + b"mayShare" => CalendarProperty::Rights(CalendarRight::MayShare), + b"mayDelete" => CalendarProperty::Rights(CalendarRight::MayDelete), + b"@type" => CalendarProperty::Type, + b"when" => CalendarProperty::When, + b"trigger" => CalendarProperty::Trigger, + b"offset" => CalendarProperty::Offset, + b"relativeTo" => CalendarProperty::RelativeTo, + b"action" => CalendarProperty::Action, + ) + .or_else(|| { + if allow_patch && value.contains('/') { + CalendarProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &CalendarProperty { + if let CalendarProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct CalendarSetArguments { + pub on_destroy_remove_events: Option, + pub on_success_set_is_default: Option>, +} + +impl<'de> DeserializeArguments<'de> for CalendarSetArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"onDestroyRemoveEvents" => { + self.on_destroy_remove_events = map.next_value()?; + }, + b"onSuccessSetIsDefault" => { + self.on_success_set_is_default = map.next_value()?; + }, + _ => { + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl FromStr for CalendarProperty { + type Err = (); + + fn from_str(s: &str) -> Result { + CalendarProperty::parse(s, false).ok_or(()) + } +} + +impl JmapObject for Calendar { + type Property = CalendarProperty; + + type Element = CalendarValue; + + type Id = Id; + + type Filter = (); + + type Comparator = (); + + type GetArguments = (); + + type SetArguments<'de> = CalendarSetArguments; + + type QueryArguments = (); + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = CalendarProperty::Id; +} + +impl JmapSharedObject for Calendar { + type Right = CalendarRight; + + const SHARE_WITH_PROPERTY: Self::Property = CalendarProperty::ShareWith; +} + +impl From for CalendarProperty { + fn from(id: Id) -> Self { + CalendarProperty::IdValue(id) + } +} + +impl TryFrom for Id { + type Error = (); + + fn try_from(value: CalendarProperty) -> Result { + if let CalendarProperty::IdValue(id) = value { + Ok(id) + } else { + Err(()) + } + } +} + +impl TryFrom for CalendarRight { + type Error = (); + + fn try_from(value: CalendarProperty) -> Result { + if let CalendarProperty::Rights(right) = value { + Ok(right) + } else { + Err(()) + } + } +} + +impl From for CalendarValue { + fn from(id: Id) -> Self { + CalendarValue::Id(id) + } +} + +impl JmapObjectId for CalendarValue { + fn as_id(&self) -> Option { + if let CalendarValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let CalendarValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let CalendarValue::IdReference(r) = self { + Some(r) + } else { + None + } + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(new_id) = new_id { + *self = CalendarValue::Id(new_id); + return true; + } + false + } +} + +impl JmapRight for CalendarRight { + fn from_acl(acl: Acl) -> &'static [Self] { + match acl { + Acl::ReadItems => &[CalendarRight::MayReadItems], + Acl::RemoveItems => &[CalendarRight::MayWriteAll], + Acl::ModifyItems => &[CalendarRight::MayWriteAll], + Acl::AddItems => &[CalendarRight::MayWriteAll], + Acl::Delete => &[CalendarRight::MayDelete], + Acl::Administer => &[CalendarRight::MayShare], + Acl::SchedulingReadFreeBusy => &[CalendarRight::MayReadFreeBusy], + Acl::ModifyItemsOwn => &[CalendarRight::MayWriteOwn], + Acl::ModifyPrivateProperties => &[CalendarRight::MayUpdatePrivate], + Acl::ModifyRSVP => &[CalendarRight::MayRSVP], + _ => &[], + } + } + + fn to_acl(&self) -> &'static [Acl] { + match self { + CalendarRight::MayReadFreeBusy => &[Acl::SchedulingReadFreeBusy], + CalendarRight::MayReadItems => &[Acl::Read, Acl::ReadItems], + CalendarRight::MayWriteAll => &[ + Acl::Modify, + Acl::AddItems, + Acl::ModifyItems, + Acl::RemoveItems, + ], + CalendarRight::MayWriteOwn => &[Acl::ModifyItemsOwn], + CalendarRight::MayUpdatePrivate => &[Acl::ModifyPrivateProperties], + CalendarRight::MayRSVP => &[Acl::ModifyRSVP], + CalendarRight::MayShare => &[Acl::Administer], + CalendarRight::MayDelete => &[Acl::Delete], + } + } + + fn all_rights() -> &'static [Self] { + &[ + CalendarRight::MayReadFreeBusy, + CalendarRight::MayReadItems, + CalendarRight::MayWriteAll, + CalendarRight::MayWriteOwn, + CalendarRight::MayUpdatePrivate, + CalendarRight::MayRSVP, + CalendarRight::MayShare, + CalendarRight::MayDelete, + ] + } +} + +impl From for CalendarProperty { + fn from(right: CalendarRight) -> Self { + CalendarProperty::Rights(right) + } +} + +impl JmapObjectId for CalendarProperty { + fn as_id(&self) -> Option { + if let CalendarProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let CalendarProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(new_id) = new_id { + *self = CalendarProperty::IdValue(new_id); + return true; + } + false + } +} diff --git a/crates/jmap-proto/src/object/calendar_event.rs b/crates/jmap-proto/src/object/calendar_event.rs new file mode 100644 index 00000000..5fea1f86 --- /dev/null +++ b/crates/jmap-proto/src/object/calendar_event.rs @@ -0,0 +1,363 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::{MaybeInvalid, deserialize::DeserializeArguments}, + types::date::UTCDate, +}; +use calcard::{ + common::timezone::Tz, + jscalendar::{JSCalendarProperty, JSCalendarValue}, +}; +use jmap_tools::{JsonPointerItem, Key}; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, Default)] +pub struct CalendarEvent; + +impl JmapObject for CalendarEvent { + type Property = JSCalendarProperty; + + type Element = JSCalendarValue; + + type Id = Id; + + type Filter = CalendarEventFilter; + + type Comparator = CalendarEventComparator; + + type GetArguments = CalendarEventGetArguments; + + type SetArguments<'de> = CalendarEventSetArguments; + + type QueryArguments = CalendarEventQueryArguments; + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = JSCalendarProperty::Id; +} + +impl JmapObjectId for JSCalendarValue { + fn as_id(&self) -> Option { + if let JSCalendarValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let JSCalendarValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + match self { + JSCalendarValue::IdReference(r) => Some(r), + _ => None, + } + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + *self = JSCalendarValue::Id(id); + true + } else { + false + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CalendarEventFilter { + InCalendar(MaybeInvalid), + After(UTCDate), + Before(UTCDate), + Text(String), + Title(Option), + Description(Option), + Location(Option), + Owner(Option), + Attendee(Option), + Uid(String), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CalendarEventComparator { + Start, + Uid, + RecurrenceId, + Created, + Updated, + _T(String), +} + +#[derive(Debug, Clone, Default)] +pub struct CalendarEventGetArguments { + pub recurrence_overrides_before: Option, + pub recurrence_overrides_after: Option, + pub reduce_participants: Option, + pub time_zone: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct CalendarEventSetArguments { + pub send_scheduling_messages: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct CalendarEventQueryArguments { + pub expand_recurrences: Option, + pub time_zone: Option, +} + +impl<'de> DeserializeArguments<'de> for CalendarEventFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"inCalendar" => { + *self = CalendarEventFilter::InCalendar(map.next_value()?); + }, + b"after" => { + *self = CalendarEventFilter::After(map.next_value()?); + }, + b"before" => { + *self = CalendarEventFilter::Before(map.next_value()?); + }, + b"text" => { + *self = CalendarEventFilter::Text(map.next_value::>()?.to_lowercase()); + }, + b"title" => { + *self = CalendarEventFilter::Title(map.next_value::>>()?.map(|s| s.to_lowercase())); + }, + b"description" => { + *self = CalendarEventFilter::Description(map.next_value::>>()?.map(|s| s.to_lowercase())); + }, + b"location" => { + *self = CalendarEventFilter::Location(map.next_value::>>()?.map(|s| s.to_lowercase())); + }, + b"owner" => { + *self = CalendarEventFilter::Owner(map.next_value::>>()?.map(|s| s.to_lowercase())); + }, + b"attendee" => { + *self = CalendarEventFilter::Attendee(map.next_value::>>()?.map(|s| s.to_lowercase())); + }, + b"uid" => { + *self = CalendarEventFilter::Uid(map.next_value()?); + }, + _ => { + *self = CalendarEventFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for CalendarEventComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"start" => { + *self = CalendarEventComparator::Start; + }, + b"uid" => { + *self = CalendarEventComparator::Uid; + }, + b"recurrenceId" => { + *self = CalendarEventComparator::RecurrenceId; + }, + b"created" => { + *self = CalendarEventComparator::Created; + }, + b"updated" => { + *self = CalendarEventComparator::Updated; + }, + _ => { + *self = CalendarEventComparator::_T(value.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for CalendarEventGetArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"recurrenceOverridesBefore" => { + self.recurrence_overrides_before = map.next_value()?; + }, + b"recurrenceOverridesAfter" => { + self.recurrence_overrides_after = map.next_value()?; + }, + b"reduceParticipants" => { + self.reduce_participants = map.next_value()?; + }, + b"timeZone" => { + self.time_zone = map.next_value::>()?.and_then(|s| Tz::from_str(s).ok()); + }, + _ => { + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for CalendarEventSetArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"sendSchedulingMessages" => { + self.send_scheduling_messages = map.next_value()?; + }, + _ => { + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for CalendarEventQueryArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"expandRecurrences" => { + self.expand_recurrences = map.next_value()?; + }, + b"timeZone" => { + self.time_zone = map.next_value::>()?.and_then(|s| Tz::from_str(s).ok()); + }, + _ => { + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl CalendarEventFilter { + pub fn into_string(self) -> Cow<'static, str> { + match self { + CalendarEventFilter::InCalendar(_) => "inCalendar", + CalendarEventFilter::After(_) => "after", + CalendarEventFilter::Before(_) => "before", + CalendarEventFilter::Text(_) => "text", + CalendarEventFilter::Title(_) => "title", + CalendarEventFilter::Description(_) => "description", + CalendarEventFilter::Location(_) => "location", + CalendarEventFilter::Owner(_) => "owner", + CalendarEventFilter::Attendee(_) => "attendee", + CalendarEventFilter::Uid(_) => "uid", + CalendarEventFilter::_T(s) => return Cow::Owned(s), + } + .into() + } +} + +impl CalendarEventComparator { + pub fn into_string(self) -> Cow<'static, str> { + match self { + CalendarEventComparator::Start => "start", + CalendarEventComparator::Uid => "uid", + CalendarEventComparator::RecurrenceId => "recurrenceId", + CalendarEventComparator::Created => "created", + CalendarEventComparator::Updated => "updated", + CalendarEventComparator::_T(s) => return Cow::Owned(s), + } + .into() + } +} + +impl Default for CalendarEventFilter { + fn default() -> Self { + CalendarEventFilter::_T(String::new()) + } +} + +impl Default for CalendarEventComparator { + fn default() -> Self { + CalendarEventComparator::_T(String::new()) + } +} + +impl JmapObjectId for JSCalendarProperty { + fn as_id(&self) -> Option { + if let JSCalendarProperty::IdValue(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let JSCalendarProperty::IdValue(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + match self { + JSCalendarProperty::IdReference(r) => Some(r), + JSCalendarProperty::Pointer(value) => { + let value = value.as_slice(); + match (value.first(), value.get(1)) { + ( + Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::CalendarIds))), + Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdReference( + r, + )))), + ) => Some(r), + _ => None, + } + } + _ => None, + } + } + + fn try_set_id(&mut self, new_id: AnyId) -> bool { + if let AnyId::Id(id) = new_id { + if let JSCalendarProperty::Pointer(value) = self { + let value = value.as_mut_slice(); + if let Some(value) = value.get_mut(1) { + *value = JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdValue(id))); + return true; + } + } else { + *self = JSCalendarProperty::IdValue(id); + return true; + } + } + false + } +} diff --git a/crates/jmap-proto/src/object/calendar_event_notification.rs b/crates/jmap-proto/src/object/calendar_event_notification.rs new file mode 100644 index 00000000..c42ddbe4 --- /dev/null +++ b/crates/jmap-proto/src/object/calendar_event_notification.rs @@ -0,0 +1,397 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::{MaybeInvalid, deserialize::DeserializeArguments}, + types::{date::UTCDate, state::State}, +}; +use calcard::jscalendar::JSCalendar; +use jmap_tools::{Element, Key, Property}; +use serde::Serialize; +use std::{borrow::Cow, str::FromStr}; +use types::id::Id; + +#[derive(Debug, Clone, Default)] +pub struct CalendarEventNotification; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CalendarEventNotificationObject { + pub id: Id, + + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub changed_by: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "type")] + pub notification_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub calendar_event_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_draft: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub event: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub event_patch: Option>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PersonObject { + pub name: String, + pub email: Option, + pub principal_id: Option, + pub calendar_address: Option, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct CalendarEventNotificationGetResponse { + #[serde(rename = "accountId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + + pub list: Vec, + + #[serde(rename = "notFound")] + pub not_found: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CalendarEventNotificationProperty { + Id, + Created, + ChangedBy, + Comment, + Type, + CalendarEventId, + IsDraft, + Event, + EventPatch, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CalendarEventNotificationValue { + Id(Id), + Date(UTCDate), + Type(CalendarEventNotificationType), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CalendarEventNotificationType { + Created, + Updated, + Destroyed, +} + +impl Property for CalendarEventNotificationProperty { + fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option { + CalendarEventNotificationProperty::parse(value) + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + CalendarEventNotificationProperty::Id => "id", + CalendarEventNotificationProperty::Created => "created", + CalendarEventNotificationProperty::ChangedBy => "changedBy", + CalendarEventNotificationProperty::Comment => "comment", + CalendarEventNotificationProperty::Type => "type", + CalendarEventNotificationProperty::CalendarEventId => "calendarEventId", + CalendarEventNotificationProperty::IsDraft => "isDraft", + CalendarEventNotificationProperty::Event => "event", + CalendarEventNotificationProperty::EventPatch => "eventPatch", + } + .into() + } +} + +impl Element for CalendarEventNotificationValue { + type Property = CalendarEventNotificationProperty; + + fn try_parse