diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index b8f3a1bb..57183b0a 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -386,14 +386,6 @@ impl Tokenizer<'_> { impl TimeRange { pub fn is_in_range(&self, match_overlap: bool, start: i64, end: i64) -> bool { - /*let c = println!( - "is_in_range ({match_overlap}): {} to {}, resource from {} to {}, result: {}", - chrono::DateTime::from_timestamp(self.start, 0).unwrap(), - chrono::DateTime::from_timestamp(self.end, 0).unwrap(), - chrono::DateTime::from_timestamp(start, 0).unwrap(), - chrono::DateTime::from_timestamp(end, 0).unwrap(), - result - );*/ if !match_overlap { // RFC4791#9.9: (start < DTEND AND end > DTSTART) self.start < end && self.end > start diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs index 7b587130..7b07cc7c 100644 --- a/crates/dav/src/calendar/query.rs +++ b/crates/dav/src/calendar/query.rs @@ -19,7 +19,7 @@ use calcard::{ ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarEntry, ArchivedICalendarParameter, ArchivedICalendarProperty, ArchivedICalendarValue, ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty, - ICalendarValue, dates::CalendarEvent, + ICalendarValue, }, }; use common::{DavResource, Server, auth::AccessToken}; @@ -31,7 +31,10 @@ use dav_proto::{ response::MultiStatus, }, }; -use groupware::{cache::GroupwareCache, calendar::ArchivedCalendarEvent}; +use groupware::{ + cache::GroupwareCache, + calendar::{ArchivedCalendarEvent, expand::CalendarEventExpansion}, +}; use http_proto::HttpResponse; use hyper::StatusCode; use std::{fmt::Write, slice::Iter, str::FromStr}; @@ -219,7 +222,7 @@ pub fn try_parse_tz(tz: &Timezone) -> Option { pub(crate) struct CalendarQueryHandler { default_tz: Tz, - expanded_times: Vec>, + expanded_times: Vec, } impl CalendarQueryHandler { @@ -620,7 +623,7 @@ impl CalendarQueryHandler { Some(out) } - pub fn into_expanded_times(self) -> Vec> { + pub fn into_expanded_times(self) -> Vec { self.expanded_times } } diff --git a/crates/groupware/src/calendar/expand.rs b/crates/groupware/src/calendar/expand.rs index 5c7d2c5c..06281279 100644 --- a/crates/groupware/src/calendar/expand.rs +++ b/crates/groupware/src/calendar/expand.rs @@ -4,18 +4,28 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use calcard::{common::timezone::Tz, icalendar::dates::CalendarEvent}; +use super::ArchivedCalendarEventData; +use crate::calendar::CalendarEventData; +use ahash::AHashSet; +use calcard::common::timezone::Tz; use chrono::{DateTime, TimeZone}; use dav_proto::schema::property::TimeRange; use store::write::bitpack::BitpackIterator; use utils::codec::leb128::Leb128Reader; -use super::ArchivedCalendarEventData; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CalendarEventExpansion { + pub comp_id: u32, + pub expansion_id: u32, + pub start: i64, + pub end: i64, +} impl ArchivedCalendarEventData { - pub fn expand(&self, default_tz: Tz, limit: TimeRange) -> Option>> { + pub fn expand(&self, default_tz: Tz, limit: TimeRange) -> Option> { let mut expansion = Vec::with_capacity(self.time_ranges.len()); let base_offset = self.base_offset.to_native(); + let mut base_expansion_id = 0; 'outer: for range in self.time_ranges.iter() { let instances = range.instances.as_ref(); @@ -37,6 +47,8 @@ impl ArchivedCalendarEventData { // Recurring event let unpacker = BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count); + let mut expansion_id = base_expansion_id; + base_expansion_id += offset_or_count; for start_offset in unpacker { let start_date_naive = start_offset as i64 + base_offset; let end_date_naive = start_date_naive + duration; @@ -56,14 +68,17 @@ impl ArchivedCalendarEventData { if ((start < limit.end) || (start <= limit.start)) && (end > limit.start || end >= limit.end) { - expansion.push(CalendarEvent { + expansion.push(CalendarEventExpansion { comp_id, + expansion_id, start, end, }); } else if start > limit.end { continue 'outer; } + + expansion_id += 1; } } else { // Single event @@ -85,15 +100,201 @@ impl ArchivedCalendarEventData { if ((start < limit.end) || (start <= limit.start)) && (end > limit.start || end >= limit.end) { - expansion.push(CalendarEvent { + expansion.push(CalendarEventExpansion { comp_id, + expansion_id: base_expansion_id, start, end, }); } + + base_expansion_id += 1; } } Some(expansion) } } + +impl CalendarEventData { + pub fn expand_from_ids( + &self, + expansion_ids: &mut AHashSet, + default_tz: Tz, + ) -> Option> { + let mut expansion = Vec::with_capacity(expansion_ids.len()); + let base_offset = self.base_offset; + let mut base_expansion_id = 0; + + 'outer: for range in self.time_ranges.iter() { + let instances = range.instances.as_ref(); + let (offset_or_count, bytes_read) = instances.read_leb128::()?; + let mut start_tz = Tz::from_id(range.start_tz)?; + let mut end_tz = Tz::from_id(range.end_tz)?; + + if start_tz.is_floating() && !default_tz.is_floating() { + start_tz = default_tz; + } + if end_tz.is_floating() && !default_tz.is_floating() { + end_tz = default_tz; + } + + if instances.len() > bytes_read { + let match_range = base_expansion_id..base_expansion_id + offset_or_count; + let mut match_count = expansion_ids + .iter() + .filter(|id| match_range.contains(id)) + .count(); + let mut expansion_id = base_expansion_id; + base_expansion_id += offset_or_count; + + if match_count > 0 { + let unpacker = BitpackIterator::from_bytes_and_offset( + instances, + bytes_read, + offset_or_count, + ); + for start_offset in unpacker { + if expansion_ids.remove(&expansion_id) { + let start_date_naive = start_offset as i64 + base_offset; + let end_date_naive = start_date_naive + range.duration as i64; + let start = start_tz + .from_local_datetime( + &DateTime::from_timestamp(start_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + let end = end_tz + .from_local_datetime( + &DateTime::from_timestamp(end_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + + expansion.push(CalendarEventExpansion { + comp_id: range.id as u32, + expansion_id, + start, + end, + }); + + match_count -= 1; + if match_count == 0 { + if expansion_ids.is_empty() { + break 'outer; + } else { + continue 'outer; + } + } + } + expansion_id += 1; + } + } + } else { + if expansion_ids.remove(&base_expansion_id) { + // Single event + let start_date_naive = offset_or_count as i64 + base_offset; + let end_date_naive = start_date_naive + range.duration as i64; + let start = start_tz + .from_local_datetime( + &DateTime::from_timestamp(start_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + let end = end_tz + .from_local_datetime( + &DateTime::from_timestamp(end_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + + expansion.push(CalendarEventExpansion { + comp_id: range.id as u32, + expansion_id: base_expansion_id, + start, + end, + }); + + if expansion_ids.is_empty() { + break 'outer; + } + } + + base_expansion_id += 1; + } + } + + if !expansion_ids.is_empty() { + expansion.extend( + expansion_ids + .drain() + .map(|expansion_id| CalendarEventExpansion { + comp_id: u32::MAX, + expansion_id, + start: i64::MAX, + end: i64::MAX, + }), + ); + } + + Some(expansion) + } + + pub fn expand_single(&self, comp_id: u32, default_tz: Tz) -> Option { + let range = self.time_ranges.iter().find(|r| r.id as u32 == comp_id)?; + let instances = range.instances.as_ref(); + let (offset_or_count, bytes_read) = instances.read_leb128::()?; + let mut start_tz = Tz::from_id(range.start_tz)?; + let mut end_tz = Tz::from_id(range.end_tz)?; + + if start_tz.is_floating() && !default_tz.is_floating() { + start_tz = default_tz; + } + if end_tz.is_floating() && !default_tz.is_floating() { + end_tz = default_tz; + } + let start_offset = if instances.len() > bytes_read { + // Recurring event + let mut unpacker = + BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count); + unpacker.next()? + } else { + // Single event + offset_or_count + }; + let start_date_naive = start_offset as i64 + self.base_offset; + let end_date_naive = start_date_naive + range.duration as i64; + let start = start_tz + .from_local_datetime(&DateTime::from_timestamp(start_date_naive, 0)?.naive_local()) + .single()? + .timestamp(); + let end = end_tz + .from_local_datetime(&DateTime::from_timestamp(end_date_naive, 0)?.naive_local()) + .single()? + .timestamp(); + + Some(CalendarEventExpansion { + comp_id, + expansion_id: u32::MAX, + start, + end, + }) + } +} + +impl Default for CalendarEventExpansion { + fn default() -> Self { + Self { + comp_id: u32::MAX, + expansion_id: u32::MAX, + start: i64::MAX, + end: i64::MAX, + } + } +} + +impl CalendarEventExpansion { + pub fn is_valid(&self) -> bool { + self.comp_id != u32::MAX && self.start != i64::MAX && self.end != i64::MAX + } +} diff --git a/crates/groupware/src/calendar/itip.rs b/crates/groupware/src/calendar/itip.rs index 0992c961..62fe2920 100644 --- a/crates/groupware/src/calendar/itip.rs +++ b/crates/groupware/src/calendar/itip.rs @@ -224,7 +224,11 @@ impl ItipIngest for Server { // Build event for schedule inbox let itip_document_id = self .store() - .assign_document_ids(account_id, Collection::CalendarEventNotification, 1) + .assign_document_ids( + account_id, + Collection::CalendarEventNotification, + 1, + ) .await .caused_by(trc::location!())?; let itip_message = CalendarEventNotification { @@ -412,14 +416,8 @@ impl ItipIngest for Server { 'outer: for entry in &mut component.entries { if entry.name == ICalendarProperty::Attendee && entry - .values - .first() - .and_then(|v| v.as_text()) - .is_some_and(|v| { - v.strip_prefix("mailto:") - .unwrap_or(v) - .eq_ignore_ascii_case(&rsvp.attendee) - }) + .calendar_address() + .is_some_and(|v| v.eq_ignore_ascii_case(&rsvp.attendee)) { let mut add_partstat = true; for param in &mut entry.params { diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 60fc0818..bde3d0dd 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, ICalendarDuration}; +use calcard::icalendar::{ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry}; use common::{DavName, auth::AccessToken}; use dav_proto::schema::request::DeadProperty; use types::acl::AclGrant; @@ -29,10 +29,9 @@ pub struct Calendar { } pub const CALENDAR_SUBSCRIBED: u16 = 1; -pub const CALENDAR_DEFAULT: u16 = 1 << 1; -pub const CALENDAR_VISIBLE: u16 = 1 << 2; -pub const CALENDAR_AVAILABILITY_ALL: u16 = 1 << 3; -pub const CALENDAR_AVAILABILITY_ATTENDING: u16 = 1 << 4; +pub const CALENDAR_INVISIBLE: u16 = 1 << 1; +pub const CALENDAR_AVAILABILITY_NONE: u16 = 1 << 2; +pub const CALENDAR_AVAILABILITY_ATTENDING: u16 = 1 << 3; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, @@ -52,7 +51,6 @@ pub struct CalendarPreferences { rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct DefaultAlert { - pub account_id: u32, pub id: String, pub offset: ICalendarDuration, pub flags: u16, @@ -71,6 +69,8 @@ pub const EVENT_HIDE_ATTENDEES: u16 = 1 << 2; pub const EVENT_DRAFT: u16 = 1 << 3; pub const EVENT_ORIGIN: u16 = 1 << 4; +pub const PREF_USE_DEFAULT_ALERTS: u16 = 1; + #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] @@ -78,7 +78,7 @@ pub struct CalendarEvent { pub names: Vec, pub display_name: Option, pub data: CalendarEventData, - pub user_properties: Vec, + pub preferences: Vec, pub flags: u16, pub dead_properties: DeadProperty, pub size: u32, @@ -143,9 +143,11 @@ pub struct ComponentTimeRange { #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] -pub struct UserProperties { +pub struct EventPreferences { pub account_id: u32, - pub properties: ICalendar, + pub flags: u16, + pub properties: Vec, + pub alerts: Vec, } #[derive( @@ -192,6 +194,17 @@ impl Calendar { } impl ArchivedCalendar { + pub fn default_alerts( + &self, + access_token: &AccessToken, + with_time: bool, + ) -> impl Iterator { + self.preferences(access_token) + .default_alerts + .iter() + .filter(move |a| (a.flags & ALERT_WITH_TIME != 0) == with_time) + } + pub fn preferences(&self, access_token: &AccessToken) -> &ArchivedCalendarPreferences { if self.preferences.len() == 1 { &self.preferences[0] @@ -205,3 +218,71 @@ impl ArchivedCalendar { } } } + +impl CalendarEvent { + pub fn preferences(&self, access_token: &AccessToken) -> Option<&EventPreferences> { + self.preferences + .iter() + .find(|p| p.account_id == access_token.primary_id()) + } + + pub fn preferences_mut(&mut self, access_token: &AccessToken) -> &mut EventPreferences { + let account_id = access_token.primary_id(); + let idx = if let Some(idx) = self + .preferences + .iter() + .position(|p| p.account_id == account_id) + { + idx + } else { + self.preferences.push(EventPreferences { + account_id, + flags: PREF_USE_DEFAULT_ALERTS, + properties: Vec::new(), + alerts: Vec::new(), + }); + self.preferences.len() - 1 + }; + + &mut self.preferences[idx] + } + + pub fn added_calendar_ids( + &self, + prev_data: &ArchivedCalendarEvent, + ) -> impl Iterator { + self.names + .iter() + .filter(|m| prev_data.names.iter().all(|pm| pm.parent_id != m.parent_id)) + .map(|m| m.parent_id) + } + + pub fn removed_calendar_ids( + &self, + prev_data: &ArchivedCalendarEvent, + ) -> impl Iterator { + prev_data + .names + .iter() + .filter(|m| self.names.iter().all(|pm| pm.parent_id != m.parent_id)) + .map(|m| m.parent_id.to_native()) + } + + pub fn unchanged_calendar_ids( + &self, + prev_data: &ArchivedCalendarEvent, + ) -> impl Iterator { + self.names + .iter() + .filter(|m| prev_data.names.iter().any(|pm| pm.parent_id == m.parent_id)) + .map(|m| m.parent_id) + } +} + +impl ArchivedCalendarEvent { + pub fn preferences(&self, access_token: &AccessToken) -> Option<&ArchivedEventPreferences> { + self.preferences + .iter() + .find(|p| p.account_id == access_token.primary_id()) + } +} diff --git a/crates/groupware/src/calendar/storage.rs b/crates/groupware/src/calendar/storage.rs index ebc79520..81742571 100644 --- a/crates/groupware/src/calendar/storage.rs +++ b/crates/groupware/src/calendar/storage.rs @@ -351,24 +351,23 @@ impl DestroyArchive> { send_itip: bool, batch: &mut BatchBuilder, ) -> trc::Result<()> { - let event = self.0; - if let Some(delete_idx) = event + if let Some(delete_idx) = self + .0 .inner .names .iter() .position(|name| name.parent_id == calendar_id) { - batch - .with_account_id(account_id) - .with_collection(Collection::CalendarEvent); - - if event.inner.names.len() > 1 { + if self.0.inner.names.len() > 1 { // Unlink calendar id from event + let event = self.0; let mut new_event = event .deserialize::() .caused_by(trc::location!())?; new_event.names.swap_remove(delete_idx); batch + .with_account_id(account_id) + .with_collection(Collection::CalendarEvent) .update_document(document_id) .custom( ObjectIndexBuilder::new() @@ -378,40 +377,7 @@ impl DestroyArchive> { ) .caused_by(trc::location!())?; } else { - // Delete event - batch.delete_document(document_id); - - // Remove next alarm if it exists - let now = now() as i64; - if let Some(next_alarm) = event.inner.data.next_alarm(now, Tz::Floating) { - next_alarm.delete_task(batch); - } - - // Scheduling - if send_itip - && event.inner.schedule_tag.is_some() - && event.inner.data.event_range_end() > now - { - let event = event - .deserialize::() - .caused_by(trc::location!())?; - - if let Ok(messages) = - itip_cancel(&event.data.event, access_token.emails.as_slice(), true) - { - ItipMessages::new(vec![messages]) - .queue(batch) - .caused_by(trc::location!())?; - } - } - - batch - .custom( - ObjectIndexBuilder::<_, ()>::new() - .with_tenant_id(access_token) - .with_current(event), - ) - .caused_by(trc::location!())?; + self.delete_all(access_token, account_id, document_id, send_itip, batch)?; } if let Some(delete_path) = delete_path { @@ -423,6 +389,57 @@ impl DestroyArchive> { Ok(()) } + + #[allow(clippy::too_many_arguments)] + pub fn delete_all( + self, + access_token: &AccessToken, + account_id: u32, + document_id: u32, + send_itip: bool, + batch: &mut BatchBuilder, + ) -> trc::Result<()> { + let event = self.0; + // Delete event + batch + .with_account_id(account_id) + .with_collection(Collection::CalendarEvent) + .delete_document(document_id); + + // Remove next alarm if it exists + let now = now() as i64; + if let Some(next_alarm) = event.inner.data.next_alarm(now, Tz::Floating) { + next_alarm.delete_task(batch); + } + + // Scheduling + if send_itip + && event.inner.schedule_tag.is_some() + && event.inner.data.event_range_end() > now + { + let event = event + .deserialize::() + .caused_by(trc::location!())?; + + if let Ok(messages) = + itip_cancel(&event.data.event, access_token.emails.as_slice(), true) + { + ItipMessages::new(vec![messages]) + .queue(batch) + .caused_by(trc::location!())?; + } + } + + batch + .custom( + ObjectIndexBuilder::<_, ()>::new() + .with_tenant_id(access_token) + .with_current(event), + ) + .caused_by(trc::location!())?; + + Ok(()) + } } impl DestroyArchive> { diff --git a/crates/groupware/src/scheduling/mod.rs b/crates/groupware/src/scheduling/mod.rs index 3c52fb24..ba126298 100644 --- a/crates/groupware/src/scheduling/mod.rs +++ b/crates/groupware/src/scheduling/mod.rs @@ -362,6 +362,28 @@ impl ItipError { _ => None, } } + + pub fn is_jmap_error(&self) -> bool { + matches!( + self, + ItipError::MultipleOrganizer + | ItipError::OrganizerIsLocalAddress + | ItipError::SenderIsNotParticipant(_) + | ItipError::OrganizerMismatch + | ItipError::CannotModifyProperty(_) + | ItipError::CannotModifyInstance + | ItipError::CannotModifyAddress + //| ItipError::MissingUid + | ItipError::MultipleUid + | ItipError::MultipleObjectTypes + | ItipError::MultipleObjectInstances + | ItipError::MissingMethod + | ItipError::InvalidComponentType + | ItipError::OutOfSequence + | ItipError::UnknownParticipant(_) + | ItipError::UnsupportedMethod(_) + ) + } } impl Display for ItipError { diff --git a/crates/jmap-proto/src/error/set.rs b/crates/jmap-proto/src/error/set.rs index fe9942b0..9c1893bc 100644 --- a/crates/jmap-proto/src/error/set.rs +++ b/crates/jmap-proto/src/error/set.rs @@ -87,6 +87,8 @@ pub enum SetErrorType { AddressBookHasContents, #[serde(rename = "nodeHasChildren")] NodeHasChildren, + #[serde(rename = "calendarHasEvent")] + CalendarHasEvent, } impl SetErrorType { @@ -119,6 +121,7 @@ impl SetErrorType { SetErrorType::ScriptIsActive => "scriptIsActive", SetErrorType::AddressBookHasContents => "addressBookHasContents", SetErrorType::NodeHasChildren => "nodeHasChildren", + SetErrorType::CalendarHasEvent => "calendarHasEvent", } } } @@ -200,6 +203,10 @@ impl SetError { pub fn node_has_children() -> Self { Self::new(SetErrorType::NodeHasChildren).with_description("File node has children.") } + + pub fn calendar_has_event() -> Self { + Self::new(SetErrorType::CalendarHasEvent).with_description("Calendar is not empty.") + } } impl From for InvalidProperty { diff --git a/crates/jmap-proto/src/method/availability.rs b/crates/jmap-proto/src/method/availability.rs index c487258c..4a78375e 100644 --- a/crates/jmap-proto/src/method/availability.rs +++ b/crates/jmap-proto/src/method/availability.rs @@ -13,7 +13,7 @@ use crate::{ }; use calcard::jscalendar::{JSCalendar, JSCalendarProperty}; use serde::{Deserialize, Deserializer, Serialize}; -use types::id::Id; +use types::{blob::BlobId, id::Id}; #[derive(Debug, Clone, Default)] pub struct GetAvailabilityRequest { @@ -37,7 +37,7 @@ pub struct BusyPeriod { pub utc_start: UTCDate, pub utc_end: UTCDate, pub busy_status: Option, - pub event: Option>, + pub event: Option>, } #[derive(Debug, Serialize, Clone)] diff --git a/crates/jmap-proto/src/object/calendar_event.rs b/crates/jmap-proto/src/object/calendar_event.rs index 5fea1f86..37cfc21c 100644 --- a/crates/jmap-proto/src/object/calendar_event.rs +++ b/crates/jmap-proto/src/object/calendar_event.rs @@ -15,7 +15,7 @@ use calcard::{ }; use jmap_tools::{JsonPointerItem, Key}; use std::{borrow::Cow, str::FromStr}; -use types::id::Id; +use types::{blob::BlobId, id::Id}; #[derive(Debug, Clone, Default)] pub struct CalendarEvent; @@ -23,7 +23,7 @@ pub struct CalendarEvent; impl JmapObject for CalendarEvent { type Property = JSCalendarProperty; - type Element = JSCalendarValue; + type Element = JSCalendarValue; type Id = Id; @@ -44,7 +44,7 @@ impl JmapObject for CalendarEvent { const ID_PROPERTY: Self::Property = JSCalendarProperty::Id; } -impl JmapObjectId for JSCalendarValue { +impl JmapObjectId for JSCalendarValue { fn as_id(&self) -> Option { if let JSCalendarValue::Id(id) = self { Some(*id) @@ -54,10 +54,10 @@ impl JmapObjectId for JSCalendarValue { } fn as_any_id(&self) -> Option { - if let JSCalendarValue::Id(id) = self { - Some(AnyId::Id(*id)) - } else { - None + match self { + JSCalendarValue::Id(id) => Some(AnyId::Id(*id)), + JSCalendarValue::BlobId(blob_id) => Some(AnyId::BlobId(blob_id.clone())), + _ => None, } } diff --git a/crates/jmap-proto/src/object/calendar_event_notification.rs b/crates/jmap-proto/src/object/calendar_event_notification.rs index c42ddbe4..c0db7982 100644 --- a/crates/jmap-proto/src/object/calendar_event_notification.rs +++ b/crates/jmap-proto/src/object/calendar_event_notification.rs @@ -13,7 +13,7 @@ use calcard::jscalendar::JSCalendar; use jmap_tools::{Element, Key, Property}; use serde::Serialize; use std::{borrow::Cow, str::FromStr}; -use types::id::Id; +use types::{blob::BlobId, id::Id}; #[derive(Debug, Clone, Default)] pub struct CalendarEventNotification; @@ -43,10 +43,10 @@ pub struct CalendarEventNotificationObject { pub is_draft: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub event: Option>, + pub event: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub event_patch: Option>, + pub event_patch: Option>, } #[derive(Debug, Serialize, Clone)] diff --git a/crates/jmap/src/calendar/get.rs b/crates/jmap/src/calendar/get.rs index 45a26bab..481f8fb8 100644 --- a/crates/jmap/src/calendar/get.rs +++ b/crates/jmap/src/calendar/get.rs @@ -5,13 +5,20 @@ */ use crate::{api::acl::JmapRights, changes::state::JmapCacheState}; +use calcard::jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType}; use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; -use groupware::{cache::GroupwareCache, calendar::Calendar}; +use groupware::{ + cache::GroupwareCache, + calendar::{ + ALERT_EMAIL, ALERT_RELATIVE_TO_END, ArchivedDefaultAlert, CALENDAR_AVAILABILITY_ATTENDING, + CALENDAR_AVAILABILITY_NONE, CALENDAR_INVISIBLE, CALENDAR_SUBSCRIBED, Calendar, + }, +}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, - object::calendar::{self, CalendarProperty, CalendarValue}, + object::calendar::{self, CalendarProperty, CalendarValue, IncludeInAvailability}, }; -use jmap_tools::{Map, Value}; +use jmap_tools::{Key, Map, Value}; use store::roaring::RoaringBitmap; use trc::AddContext; use types::{ @@ -38,6 +45,8 @@ impl CalendarGet for Server { CalendarProperty::Id, CalendarProperty::Name, CalendarProperty::Description, + CalendarProperty::Color, + CalendarProperty::TimeZone, CalendarProperty::SortOrder, CalendarProperty::IsDefault, CalendarProperty::IsSubscribed, @@ -116,18 +125,83 @@ impl CalendarGet for Server { calendar.preferences(access_token).sort_order.to_native(), ); } - /*CalendarProperty::IsDefault => { - result.insert_unchecked(CalendarProperty::IsDefault, calendar.is_default); + CalendarProperty::IsDefault => { + let todo = "implement me"; + //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()), + Value::Bool( + calendar.preferences(access_token).flags & CALENDAR_SUBSCRIBED != 0, + ), ); - }*/ + } + CalendarProperty::Color => { + result.insert_unchecked( + CalendarProperty::Color, + calendar + .preferences(access_token) + .color + .as_ref() + .map(|c| c.to_string()), + ); + } + CalendarProperty::IsVisible => { + result.insert_unchecked( + CalendarProperty::IsVisible, + Value::Bool( + calendar.preferences(access_token).flags & CALENDAR_INVISIBLE == 0, + ), + ); + } + CalendarProperty::IncludeInAvailability => { + let flags = calendar.preferences(access_token).flags; + + result.insert_unchecked( + CalendarProperty::IncludeInAvailability, + Value::Element(CalendarValue::IncludeInAvailability( + if flags & CALENDAR_AVAILABILITY_ATTENDING != 0 { + IncludeInAvailability::Attending + } else if flags & CALENDAR_AVAILABILITY_NONE != 0 { + IncludeInAvailability::None + } else { + IncludeInAvailability::All + }, + )), + ); + } + CalendarProperty::DefaultAlertsWithTime => { + result.insert_unchecked( + CalendarProperty::DefaultAlertsWithTime, + Value::Object(Map::from_iter( + calendar + .default_alerts(access_token, true) + .map(default_alarm_to_value), + )), + ); + } + CalendarProperty::DefaultAlertsWithoutTime => { + result.insert_unchecked( + CalendarProperty::DefaultAlertsWithoutTime, + Value::Object(Map::from_iter( + calendar + .default_alerts(access_token, false) + .map(default_alarm_to_value), + )), + ); + } + CalendarProperty::TimeZone => { + result.insert_unchecked( + CalendarProperty::TimeZone, + calendar + .preferences(access_token) + .time_zone + .tz() + .map(|tz| Value::Element(CalendarValue::Timezone(tz))) + .unwrap_or(Value::Null), + ); + } CalendarProperty::ShareWith => { result.insert_unchecked( CalendarProperty::ShareWith, @@ -161,3 +235,51 @@ impl CalendarGet for Server { Ok(response) } } + +fn default_alarm_to_value( + alarm: &ArchivedDefaultAlert, +) -> ( + Key<'static, CalendarProperty>, + Value<'static, CalendarProperty, CalendarValue>, +) { + ( + Key::Owned(alarm.id.to_string()), + Value::Object(Map::from(vec![ + ( + Key::Property(CalendarProperty::Type), + Value::Element(CalendarValue::Type(JSCalendarType::Alert)), + ), + ( + Key::Property(CalendarProperty::Action), + Value::Element(CalendarValue::Action(if alarm.flags & ALERT_EMAIL != 0 { + JSCalendarAlertAction::Email + } else { + JSCalendarAlertAction::Display + })), + ), + ( + Key::Property(CalendarProperty::Trigger), + Value::Object(Map::from(vec![ + ( + Key::Property(CalendarProperty::Type), + Value::Element(CalendarValue::Type(JSCalendarType::OffsetTrigger)), + ), + ( + Key::Property(CalendarProperty::Offset), + Value::Element(CalendarValue::Duration(alarm.offset.to_native())), + ), + ( + Key::Property(CalendarProperty::RelativeTo), + Value::Element(CalendarValue::RelativeTo( + if alarm.flags & ALERT_RELATIVE_TO_END != 0 { + JSCalendarRelativeTo::End + } else { + JSCalendarRelativeTo::Start + }, + )), + ), + ])), + ), + ])), + ) +} diff --git a/crates/jmap/src/calendar/set.rs b/crates/jmap/src/calendar/set.rs index 61d7000a..c8250558 100644 --- a/crates/jmap/src/calendar/set.rs +++ b/crates/jmap/src/calendar/set.rs @@ -5,13 +5,22 @@ */ use crate::api::acl::{JmapAcl, JmapRights}; +use calcard::jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType}; use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; -use groupware::{DestroyArchive, cache::GroupwareCache}; +use groupware::{ + DestroyArchive, + cache::GroupwareCache, + calendar::{ + ALERT_EMAIL, ALERT_RELATIVE_TO_END, ALERT_WITH_TIME, CALENDAR_AVAILABILITY_ATTENDING, + CALENDAR_AVAILABILITY_NONE, CALENDAR_INVISIBLE, CALENDAR_SUBSCRIBED, Calendar, + CalendarPreferences, DefaultAlert, Timezone, + }, +}; use http_proto::HttpSessionData; use jmap_proto::{ error::set::SetError, method::set::{SetRequest, SetResponse}, - object::calendar::{self, CalendarProperty, CalendarValue}, + object::calendar::{self, CalendarProperty, CalendarValue, IncludeInAvailability}, request::IntoValid, types::state::State, }; @@ -40,8 +49,7 @@ impl CalendarSet for Server { access_token: &AccessToken, _session: &HttpSessionData, ) -> trc::Result> { - todo!() - /*let account_id = request.account_id.document_id(); + let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await?; @@ -71,7 +79,7 @@ impl CalendarSet for Server { .collect::(), preferences: vec![CalendarPreferences { account_id, - name: "Address Book".to_string(), + name: "".to_string(), ..Default::default() }], ..Default::default() @@ -181,10 +189,7 @@ impl CalendarSet for Server { } // Process deletions - let on_destroy_remove_contents = request - .arguments - .on_destroy_remove_contents - .unwrap_or(false); + let on_destroy_remove_events = request.arguments.on_destroy_remove_events.unwrap_or(false); for id in will_destroy { let document_id = id.document_id(); @@ -224,22 +229,23 @@ impl CalendarSet for Server { // Obtain children ids let children_ids = cache.children_ids(document_id).collect::>(); - if !children_ids.is_empty() && !on_destroy_remove_contents { + if !children_ids.is_empty() && !on_destroy_remove_events { response .not_destroyed - .append(id, SetError::calendar_has_contents()); + .append(id, SetError::calendar_has_event()); continue; } // Delete record DestroyArchive(calendar) - .delete_with_cards( + .delete_with_events( self, access_token, account_id, document_id, children_ids, None, + false, &mut batch, ) .await @@ -259,11 +265,11 @@ impl CalendarSet for Server { response.new_state = State::Exact(change_id).into(); } - Ok(response)*/ + Ok(response) } } -/*fn update_calendar( +fn update_calendar( updates: Value<'_, CalendarProperty, CalendarValue>, calendar: &mut Calendar, access_token: &AccessToken, @@ -287,44 +293,117 @@ impl CalendarSet for Server { (CalendarProperty::Description, Value::Null) => { calendar.preferences_mut(access_token).description = None; } + (CalendarProperty::Color, Value::Str(value)) if value.len() < 16 => { + calendar.preferences_mut(access_token).color = value.into_owned().into(); + } + (CalendarProperty::Color, Value::Null) => { + calendar.preferences_mut(access_token).color = None; + } + (CalendarProperty::TimeZone, Value::Element(CalendarValue::Timezone(tz))) => { + calendar.preferences_mut(access_token).time_zone = Timezone::IANA(tz.as_id()); + } + (CalendarProperty::TimeZone, Value::Null) => { + calendar.preferences_mut(access_token).time_zone = Timezone::Default; + } (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); - } + calendar.preferences_mut(access_token).flags |= CALENDAR_SUBSCRIBED; } else { - calendar.subscribers.retain(|id| *id != account_id); + calendar.preferences_mut(access_token).flags &= !CALENDAR_SUBSCRIBED; + } + } + (CalendarProperty::IsVisible, Value::Bool(visible)) => { + if visible { + calendar.preferences_mut(access_token).flags &= !CALENDAR_INVISIBLE; + } else { + calendar.preferences_mut(access_token).flags |= CALENDAR_INVISIBLE; + } + } + ( + CalendarProperty::IncludeInAvailability, + Value::Element(CalendarValue::IncludeInAvailability(availability)), + ) => { + let flags = &mut calendar.preferences_mut(access_token).flags; + + match availability { + IncludeInAvailability::All => { + *flags &= !(CALENDAR_AVAILABILITY_NONE | CALENDAR_AVAILABILITY_ATTENDING); + } + IncludeInAvailability::Attending => { + *flags &= !CALENDAR_AVAILABILITY_NONE; + *flags |= CALENDAR_AVAILABILITY_ATTENDING; + } + IncludeInAvailability::None => { + *flags &= !CALENDAR_AVAILABILITY_ATTENDING; + *flags |= CALENDAR_AVAILABILITY_NONE; + } + } + } + ( + property @ (CalendarProperty::DefaultAlertsWithTime + | CalendarProperty::DefaultAlertsWithoutTime), + Value::Object(value), + ) => { + let with_time = matches!(property, CalendarProperty::DefaultAlertsWithTime); + let alerts = &mut calendar.preferences_mut(access_token).default_alerts; + + alerts.retain(|alert| (alert.flags & ALERT_WITH_TIME != 0) != with_time); + + for (key, value) in value.into_vec() { + alerts.push(value_to_default_alert(key.to_string().into_owned(), value)?); } } (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(); + (CalendarProperty::Pointer(pointer), value) => { + let mut ptr_iter = pointer.iter(); + ptr_iter.next(); - calendar.acls = JmapRights::acl_patch::( - std::mem::take(&mut calendar.acls), - pointer, - value, - )?; - has_acl_changes = true; + match ptr_iter.next() { + Some(JsonPointerItem::Key(Key::Property(CalendarProperty::ShareWith))) => { + calendar.acls = JmapRights::acl_patch::( + std::mem::take(&mut calendar.acls), + ptr_iter, + value, + )?; + has_acl_changes = true; + } + Some(JsonPointerItem::Key(Key::Property( + property @ (CalendarProperty::DefaultAlertsWithTime + | CalendarProperty::DefaultAlertsWithoutTime), + ))) => match (ptr_iter.next(), value) { + (Some(JsonPointerItem::Key(key)), value) if ptr_iter.next().is_none() => { + let id = key.to_string().into_owned(); + let with_time = + matches!(property, CalendarProperty::DefaultAlertsWithTime); + let alerts = &mut calendar.preferences_mut(access_token).default_alerts; + alerts.retain(|alert| { + (alert.flags & ALERT_WITH_TIME != 0) != with_time || alert.id != id + }); + + alerts.push(value_to_default_alert(id, value)?); + } + _ => { + return Err(SetError::invalid_properties() + .with_property(CalendarProperty::Pointer(pointer)) + .with_description("Field could not be patched.")); + } + }, + _ => { + return Err(SetError::invalid_properties() + .with_property(CalendarProperty::Pointer(pointer)) + .with_description("Field could not be patched.")); + } + } } (property, _) => { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_property(property) .with_description("Field could not be set.")); } } @@ -339,4 +418,76 @@ impl CalendarSet for Server { Ok(has_acl_changes) } -*/ + +fn value_to_default_alert( + id: String, + value: Value<'_, CalendarProperty, CalendarValue>, +) -> Result> { + let mut alert = DefaultAlert { + id, + ..Default::default() + }; + let mut has_offset = false; + + for (key, value) in value.into_expanded_object() { + let Key::Property(key) = key else { + continue; + }; + + match (key, value) { + (CalendarProperty::Type, Value::Element(CalendarValue::Type(value))) => { + if value != JSCalendarType::Alert { + return Err(SetError::invalid_properties() + .with_property(CalendarProperty::Trigger) + .with_description("Invalid alert object type.")); + } + } + ( + CalendarProperty::Action, + Value::Element(CalendarValue::Action(JSCalendarAlertAction::Email)), + ) => { + alert.flags |= ALERT_EMAIL; + } + (CalendarProperty::Trigger, Value::Object(value)) => { + for (key, value) in value.into_vec() { + let Key::Property(key) = key else { + continue; + }; + + match (key, value) { + ( + CalendarProperty::RelativeTo, + Value::Element(CalendarValue::RelativeTo(JSCalendarRelativeTo::End)), + ) => { + alert.flags |= ALERT_RELATIVE_TO_END; + } + ( + CalendarProperty::Offset, + Value::Element(CalendarValue::Duration(value)), + ) => { + alert.offset = value; + has_offset = true; + } + (CalendarProperty::Offset, Value::Element(CalendarValue::Type(value))) => { + if value != JSCalendarType::OffsetTrigger { + return Err(SetError::invalid_properties() + .with_property(CalendarProperty::Trigger) + .with_description("Invalid alert trigger type.")); + } + } + _ => {} + } + } + } + _ => {} + } + } + + if has_offset { + Ok(alert) + } else { + Err(SetError::invalid_properties() + .with_property(CalendarProperty::Trigger) + .with_description("Missing alert offset.")) + } +} diff --git a/crates/jmap/src/calendar_event/copy.rs b/crates/jmap/src/calendar_event/copy.rs index 83239930..36ade53f 100644 --- a/crates/jmap/src/calendar_event/copy.rs +++ b/crates/jmap/src/calendar_event/copy.rs @@ -4,7 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{calendar_event::set::CalendarEventSet, changes::state::JmapCacheState}; +use crate::{ + calendar_event::{CalendarSyntheticId, set::CalendarEventSet}, + changes::state::JmapCacheState, +}; +use calcard::jscalendar::JSCalendarProperty; use common::{Server, auth::AccessToken}; use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; use http_proto::HttpSessionData; @@ -48,9 +52,7 @@ impl JmapCalendarEventCopy for Server { next_call: &mut Option>>, _session: &HttpSessionData, ) -> trc::Result> { - todo!() - - /*let account_id = request.account_id.document_id(); + let account_id = request.account_id.document_id(); let from_account_id = request.from_account_id.document_id(); if account_id == from_account_id { @@ -82,7 +84,7 @@ impl JmapCalendarEventCopy for Server { from_cache.shared_items(access_token, [Acl::ReadItems], true) }; - let can_add_address_books = if access_token.is_shared(account_id) { + let can_add_calendars = if access_token.is_shared(account_id) { cache .shared_containers(access_token, [Acl::AddItems], true) .into() @@ -94,6 +96,7 @@ impl JmapCalendarEventCopy for Server { // Obtain quota let mut batch = BatchBuilder::new(); + let mut nudge_queue = false; 'create: for (id, create) in request.create.into_valid() { let from_calendar_event_id = id.document_id(); @@ -107,6 +110,18 @@ impl JmapCalendarEventCopy for Server { ); continue; } + if id.is_synthetic() { + response.not_created.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::Id) + .with_description(format!( + "Item {} is a synthetic id and cannot be copied.", + id + )), + ); + continue; + } let Some(_calendar_event) = self .get_archive( @@ -136,14 +151,16 @@ impl JmapCalendarEventCopy for Server { &mut batch, access_token, account_id, - &can_add_address_books, - calendar_event.card.into_jscalendar(), + false, + &can_add_calendars, + calendar_event.data.event.into_jscalendar(), create, ) .await? { - Ok(document_id) => { - response.created(id, document_id); + Ok(result) => { + response.created(id, result.document_id); + nudge_queue |= result.nudge_queue; // Add to destroy list if on_success_delete { @@ -165,6 +182,10 @@ impl JmapCalendarEventCopy for Server { .and_then(|ids| ids.last_change_id(account_id)) .caused_by(trc::location!())?; + if nudge_queue { + self.notify_task_queue(); + } + response.new_state = State::Exact(change_id); } @@ -185,6 +206,6 @@ impl JmapCalendarEventCopy for Server { .into(); } - Ok(response)*/ + Ok(response) } } diff --git a/crates/jmap/src/calendar_event/get.rs b/crates/jmap/src/calendar_event/get.rs index aa6f2872..7e0244a5 100644 --- a/crates/jmap/src/calendar_event/get.rs +++ b/crates/jmap/src/calendar_event/get.rs @@ -4,17 +4,34 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::changes::state::JmapCacheState; -use calcard::jscalendar::{JSCalendarProperty, JSCalendarValue}; +use crate::{calendar_event::CalendarSyntheticId, changes::state::JmapCacheState}; +use calcard::{ + common::{PartialDateTime, timezone::Tz}, + icalendar::{ + ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, + ICalendarParameterName, ICalendarParameterValue, ICalendarParticipationRole, + ICalendarProperty, ICalendarValue, + }, + jscalendar::{ + JSCalendarDateTime, JSCalendarProperty, JSCalendarValue, import::ConversionOptions, + }, +}; use common::{Server, auth::AccessToken}; -use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; +use groupware::{ + cache::GroupwareCache, + calendar::{ + CalendarEvent, EVENT_DRAFT, EVENT_HIDE_ATTENDEES, EVENT_INVITE_OTHERS, EVENT_INVITE_SELF, + PREF_USE_DEFAULT_ALERTS, expand::CalendarEventExpansion, + }, +}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::calendar_event, - request::IntoValid, + request::reference::MaybeResultReference, }; use jmap_tools::{Map, Value}; -use store::roaring::RoaringBitmap; +use std::sync::Arc; +use store::{ahash::AHashSet, roaring::RoaringBitmap}; use trc::AddContext; use types::{ acl::Acl, @@ -37,12 +54,17 @@ impl CalendarEventGet for Server { 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 ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; + let return_all_properties = request + .properties + .as_ref() + .is_none_or(|v| matches!(v, MaybeResultReference::Value(v) if v.is_empty())); + let properties = request.unwrap_properties(&[ + JSCalendarProperty::Id, + JSCalendarProperty::CalendarIds, + JSCalendarProperty::IsDraft, + JSCalendarProperty::IsOrigin, + ]); let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) @@ -52,7 +74,7 @@ impl CalendarEventGet for Server { } else { cache.shared_containers(access_token, [Acl::ReadItems], true) }; - let ids = if let Some(ids) = ids { + let mut ids = if let Some(ids) = ids { ids } else { calendar_event_ids @@ -67,11 +89,86 @@ impl CalendarEventGet for Server { 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); + let mut return_converted_props = !return_all_properties; + let mut return_is_orgin = OriginAddresses::None; + let mut return_utc_dates = false; - for id in ids { + let (jmap_properties, jscal_properties) = if !return_all_properties { + let mut jmap_properties = Vec::with_capacity(4); + let mut jscal_properties = Vec::with_capacity(properties.len()); + + for property in properties { + match property { + JSCalendarProperty::Id + | JSCalendarProperty::BaseEventId + | JSCalendarProperty::CalendarIds + | JSCalendarProperty::IsDraft + | JSCalendarProperty::UseDefaultAlerts + | JSCalendarProperty::MayInviteSelf + | JSCalendarProperty::MayInviteOthers + | JSCalendarProperty::HideAttendees => { + jmap_properties.push(property); + } + JSCalendarProperty::UtcStart | JSCalendarProperty::UtcEnd => { + return_utc_dates = true; + jmap_properties.push(property); + } + JSCalendarProperty::IsOrigin => { + if return_is_orgin.is_none() { + if access_token.primary_id() == account_id { + return_is_orgin = OriginAddresses::Ref(access_token); + } else { + return_is_orgin = OriginAddresses::Owned( + self.get_access_token(account_id).await?, + ); + } + jmap_properties.push(JSCalendarProperty::IsOrigin); + } + } + _ => { + if matches!(property, JSCalendarProperty::ICalComponent) { + return_converted_props = true; + } + + jscal_properties.push(property); + } + } + } + (jmap_properties, jscal_properties) + } else { + (properties, vec![]) + }; + + // Sort by baseId + ids.sort_unstable_by_key(|id| id.document_id()); + let mut ids = ids.into_iter().peekable(); + + // Process arguments + let override_range = if request.arguments.recurrence_overrides_after.is_some() + || request.arguments.recurrence_overrides_before.is_some() + { + let after = request + .arguments + .recurrence_overrides_after + .map(|v| v.timestamp()) + .unwrap_or(i64::MIN); + let before = request + .arguments + .recurrence_overrides_before + .map(|v| v.timestamp()) + .unwrap_or(i64::MAX); + if after < before { + Some(after..before) + } else { + None + } + } else { + None + }; + let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC); + let reduce_participants = request.arguments.reduce_participants.unwrap_or(false); + + 'outer: while let Some(id) = ids.next() { // Obtain the calendar_event object let document_id = id.document_id(); if !calendar_event_ids.contains(document_id) { @@ -79,56 +176,373 @@ impl CalendarEventGet for Server { continue; } - let _calendar_event = if let Some(calendar_event) = self + let Some(_calendar_event) = self .get_archive(account_id, Collection::CalendarEvent, document_id) .await? - { - calendar_event - } else { + else { response.not_found.push(id); continue; }; - - let calendar_event = _calendar_event + let mut 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() + // Extract expansion ids from synthetic ids + let mut expansion_ids = AHashSet::new(); + let mut include_base_event = false; + if let Some(expansion_id) = id.expansion_id() { + expansion_ids.insert(expansion_id); } 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)), - ); + include_base_event = true; } - - 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); + while let Some(next_id) = ids.peek() { + if next_id.document_id() == document_id { + if let Some(expansion_id) = next_id.expansion_id() { + expansion_ids.insert(expansion_id); + } else { + include_base_event = true; + } + ids.next(); + } else { + break; } - result.insert_unchecked(JSCalendarProperty::CalendarIds, Value::Object(obj)); } - response.list.push(result.into()); + // Reduce participants + if reduce_participants { + for component in &mut calendar_event.data.event.components { + if component.component_type.is_scheduling_object() { + component.entries.retain(|entry| match &entry.name { + ICalendarProperty::Attendee => { + entry.parameters(&ICalendarParameterName::Role).any(|role| { + matches!( + role, + ICalendarParameterValue::Role( + ICalendarParticipationRole::Owner, + ), + ) + }) || entry.calendar_address().is_some_and(|addr| { + access_token + .emails + .iter() + .any(|a| a.eq_ignore_ascii_case(addr)) + }) + } + _ => true, + }); + } + } + } + + // Expand synthetic ids + let mut results = Vec::with_capacity(expansion_ids.len() + 1); + if !expansion_ids.is_empty() { + let ical = &calendar_event.data.event; + if let Some(expansions) = calendar_event + .data + .expand_from_ids(&mut expansion_ids, default_tz) + { + for expansion in expansions { + if !expansion.is_valid() { + response.not_found.push(::new( + expansion.expansion_id, + document_id, + )); + continue 'outer; + } + let component = &ical.components[expansion.comp_id as usize]; + let is_recurrent = component.is_recurrent(); + let is_recurrent_or_override = + is_recurrent || component.is_recurrence_override(); + let mut has_duration = false; + let component_ids = &component.component_ids; + let mut component = ICalendarComponent { + component_type: component.component_type.clone(), + component_ids: Vec::new(), + entries: component + .entries + .iter() + .filter(|entry| match &entry.name { + ICalendarProperty::Dtstart + | ICalendarProperty::Dtend + | ICalendarProperty::Exdate + | ICalendarProperty::Exrule + | ICalendarProperty::Rdate + | ICalendarProperty::Rrule + | ICalendarProperty::RecurrenceId => false, + ICalendarProperty::Due + | ICalendarProperty::Completed + | ICalendarProperty::Created => is_recurrent, + ICalendarProperty::Duration => { + has_duration = true; + true + } + _ => true, + }) + .cloned() + .collect::>(), + }; + component.entries.push(ICalendarEntry { + name: ICalendarProperty::Dtstart, + params: vec![], + values: vec![ICalendarValue::PartialDateTime(Box::new( + PartialDateTime::from_utc_timestamp(expansion.start), + ))], + }); + if is_recurrent_or_override { + component.entries.push(ICalendarEntry { + name: ICalendarProperty::RecurrenceId, + params: vec![], + values: vec![ICalendarValue::PartialDateTime(Box::new( + PartialDateTime::from_utc_timestamp(expansion.start), + ))], + }); + } + if !has_duration { + component.entries.push(ICalendarEntry { + name: ICalendarProperty::Dtend, + params: vec![], + values: vec![ICalendarValue::PartialDateTime(Box::new( + PartialDateTime::from_utc_timestamp(expansion.end), + ))], + }); + } + + let mut expanded_ical = ICalendar { + components: vec![ + ICalendarComponent { + component_type: ICalendarComponentType::VCalendar, + entries: vec![], + component_ids: vec![1], + }, + component, + ], + }; + + if !component_ids.is_empty() { + for component_id in component_ids { + let mut sub_component = + ical.components[*component_id as usize].clone(); + sub_component.component_ids.clear(); + let component_id = expanded_ical.components.len() as u32; + expanded_ical.components.push(sub_component); + expanded_ical.components[1].component_ids.push(component_id); + } + } + + results.push(( + ::new(expansion.expansion_id, document_id), + expanded_ical, + expansion, + )); + } + } else { + response + .not_found + .extend(expansion_ids.into_iter().map(|expansion_id| { + ::new(expansion_id, document_id) + })); + continue; + } + } + + if include_base_event { + let mut event = std::mem::take(&mut calendar_event.data.event); + + // Obtain UTC start/end if requested + let expansion = if return_utc_dates + && let Some(expansion) = event + .components + .iter() + .position(|c| { + c.component_type.is_scheduling_object() && !c.is_recurrence_override() + }) + .and_then(|comp_id| { + calendar_event + .data + .expand_single(comp_id as u32, default_tz) + }) { + expansion + } else { + CalendarEventExpansion::default() + }; + + // Remove recurrence ids + if let Some(range) = &override_range { + let remove_ids = event + .components + .iter() + .enumerate() + .filter_map(|(comp_id, c)| { + if c.is_recurrence_override() + && let Some(timestamp) = c + .property(&ICalendarProperty::RecurrenceId) + .and_then(|p| p.values.first()) + .and_then(|v| v.as_partial_date_time()) + .and_then(|v| v.to_date_time()) + .and_then(|v| v.to_date_time_with_tz(default_tz)) + .map(|v| v.timestamp()) + && !range.contains(×tamp) + { + Some(comp_id as u32) + } else { + None + } + }) + .collect::>(); + if !remove_ids.is_empty() { + for component in &mut event.components { + component + .component_ids + .retain(|id| !remove_ids.contains(id)); + } + } + } + + results.push((Id::from(document_id), event, expansion)); + } + + for (id, ical, expansion) in results { + let is_origin = return_is_orgin.addresses().is_some_and(|addresses| { + ical.components + .iter() + .find(|c| c.component_type.is_scheduling_object()) + .and_then(|c| c.property(&ICalendarProperty::Organizer)) + .and_then(|v| v.calendar_address()) + .is_none_or(|v| addresses.iter().any(|a| a.eq_ignore_ascii_case(v))) + }); + + let jscal = ical + .into_jscalendar_with_opt::( + ConversionOptions::default() + .include_ical_components(return_converted_props) + .return_first(true), + ) + .into_inner(); + let mut result = if return_all_properties { + jscal.into_object().unwrap() + } else { + Map::from_iter(jscal.into_expanded_object().filter(|(k, _)| { + k.as_property() + .is_some_and(|p| jscal_properties.contains(p)) + })) + }; + + for property in &jmap_properties { + match property { + JSCalendarProperty::Id => { + result.insert_unchecked( + JSCalendarProperty::Id, + Value::Element(JSCalendarValue::Id(id)), + ); + } + JSCalendarProperty::BaseEventId => { + result.insert_unchecked( + JSCalendarProperty::Id, + Value::Element(JSCalendarValue::Id(id.document_id().into())), + ); + } + JSCalendarProperty::CalendarIds => { + 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), + ); + } + JSCalendarProperty::IsDraft => { + result.insert_unchecked( + JSCalendarProperty::IsDraft, + Value::Bool(calendar_event.flags & EVENT_DRAFT != 0), + ); + } + JSCalendarProperty::IsOrigin => { + result.insert_unchecked( + JSCalendarProperty::IsOrigin, + Value::Bool(is_origin), + ); + } + JSCalendarProperty::MayInviteSelf => { + result.insert_unchecked( + JSCalendarProperty::MayInviteSelf, + Value::Bool(calendar_event.flags & EVENT_INVITE_SELF != 0), + ); + } + JSCalendarProperty::MayInviteOthers => { + result.insert_unchecked( + JSCalendarProperty::MayInviteOthers, + Value::Bool(calendar_event.flags & EVENT_INVITE_OTHERS != 0), + ); + } + JSCalendarProperty::HideAttendees => { + result.insert_unchecked( + JSCalendarProperty::HideAttendees, + Value::Bool(calendar_event.flags & EVENT_HIDE_ATTENDEES != 0), + ); + } + + JSCalendarProperty::UtcStart => { + result.insert_unchecked( + JSCalendarProperty::UtcStart, + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + expansion.start, + false, + ))), + ); + } + JSCalendarProperty::UtcEnd => { + result.insert_unchecked( + JSCalendarProperty::UtcEnd, + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + expansion.end, + false, + ))), + ); + } + JSCalendarProperty::UseDefaultAlerts => { + result.insert_unchecked( + JSCalendarProperty::UseDefaultAlerts, + Value::Bool( + calendar_event + .preferences(access_token) + .is_none_or(|v| v.flags & PREF_USE_DEFAULT_ALERTS != 0), + ), + ); + } + + _ => {} + } + } + + response.list.push(result.into()); + } } - Ok(response)*/ + Ok(response) + } +} + +enum OriginAddresses<'x> { + Owned(Arc), + Ref(&'x AccessToken), + None, +} + +impl<'x> OriginAddresses<'x> { + fn addresses(&self) -> Option<&[String]> { + match self { + OriginAddresses::Owned(t) if !t.emails.is_empty() => Some(&t.emails), + OriginAddresses::Ref(t) if !t.emails.is_empty() => Some(&t.emails), + _ => None, + } + } + + fn is_none(&self) -> bool { + matches!(self, OriginAddresses::None) } } diff --git a/crates/jmap/src/calendar_event/mod.rs b/crates/jmap/src/calendar_event/mod.rs index 58b319ed..95037923 100644 --- a/crates/jmap/src/calendar_event/mod.rs +++ b/crates/jmap/src/calendar_event/mod.rs @@ -4,8 +4,94 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use calcard::jscalendar::JSCalendarProperty; +use common::{DavName, DavResources, Server}; +use jmap_proto::error::set::SetError; +use store::query::Filter; +use trc::AddContext; +use types::{collection::Collection, field::CalendarField, id::Id}; + pub mod copy; pub mod get; pub mod parse; pub mod query; pub mod set; + +/* + +TODO: Not yet implemented: + +- CalendarEvent + - Per-user properties (However, the database schema is ready to support this) + - mayInviteSelf, mayInviteOthers and hideAttendees (stored but not enforced) + +- CalendarEvent/set + - synthetic id update and removal + + +*/ + +pub trait CalendarSyntheticId { + fn new(expansion_id: u32, document_id: u32) -> Self; + + fn is_synthetic(&self) -> bool; + + fn expansion_id(&self) -> Option; +} + +impl CalendarSyntheticId for Id { + fn new(expansion_id: u32, document_id: u32) -> Id { + Id::from_parts(expansion_id + 1, document_id) + } + + fn expansion_id(&self) -> Option { + let prefix = self.prefix_id(); + if prefix > 0 { Some(prefix - 1) } else { None } + } + + fn is_synthetic(&self) -> bool { + self.prefix_id() > 0 + } +} + +pub(super) async fn assert_is_unique_uid( + server: &Server, + resources: &DavResources, + account_id: u32, + calendar_ids: &[DavName], + uid: Option<&str>, +) -> trc::Result>>> { + if let Some(uid) = uid { + let hits = server + .store() + .filter( + account_id, + Collection::CalendarEvent, + vec![Filter::eq(CalendarField::Uid, uid.as_bytes().to_vec())], + ) + .await + .caused_by(trc::location!())?; + if !hits.results.is_empty() { + for document_id in resources + .paths + .iter() + .filter(move |item| { + item.parent_id + .is_some_and(|id| calendar_ids.iter().any(|ab| ab.parent_id == id)) + }) + .map(|path| resources.resources[path.resource_idx].document_id) + { + if hits.results.contains(document_id) { + return Ok(Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::Uid) + .with_description(format!( + "Contact with UID {uid} already exists with id {}.", + Id::from(document_id) + )))); + } + } + } + } + + Ok(Ok(())) +} diff --git a/crates/jmap/src/calendar_event/parse.rs b/crates/jmap/src/calendar_event/parse.rs index 94600b7c..391f10f4 100644 --- a/crates/jmap/src/calendar_event/parse.rs +++ b/crates/jmap/src/calendar_event/parse.rs @@ -5,14 +5,18 @@ */ use crate::blob::download::BlobDownload; -use calcard::icalendar::ICalendar; +use calcard::{ + icalendar::ICalendar, + jscalendar::{JSCalendarProperty, import::ConversionOptions}, +}; use common::{Server, auth::AccessToken}; use jmap_proto::{ method::parse::{ParseRequest, ParseResponse}, object::calendar_event::CalendarEvent, request::IntoValid, }; -use types::id::Id; +use jmap_tools::{Key, Value}; +use types::{blob::BlobId, id::Id}; use utils::map::vec_map::VecMap; pub trait CalendarEventParse: Sync + Send { @@ -60,20 +64,29 @@ impl CalendarEventParse for Server { response.not_parsable.push(blob_id); continue; }; - let mut js_calendar_event = vcard.into_jscalendar::(); + let mut js_calendar_entries = vcard + .into_jscalendar_with_opt::(ConversionOptions::default()) + .into_inner() + .into_object() + .unwrap() + .remove(&Key::Property(JSCalendarProperty::Entries)) + .unwrap() + .into_array() + .unwrap(); 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))); + for entry in &mut js_calendar_entries { + entry + .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()); + .append(blob_id, Value::Array(js_calendar_entries)); } Ok(response) diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs index af54c75e..9f2a897f 100644 --- a/crates/jmap/src/calendar_event/set.rs +++ b/crates/jmap/src/calendar_event/set.rs @@ -4,9 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use calcard::jscalendar::{JSCalendar, JSCalendarProperty, JSCalendarValue}; +use std::borrow::Cow; + +use crate::calendar_event::{CalendarSyntheticId, assert_is_unique_uid}; +use calcard::{ + common::timezone::Tz, + icalendar::ICalendarDuration, + jscalendar::{JSCalendar, JSCalendarDateTime, JSCalendarProperty, JSCalendarValue}, +}; use common::{DavName, DavResources, Server, auth::AccessToken}; -use groupware::{DestroyArchive, cache::GroupwareCache}; +use directory::Permission; +use groupware::{ + DestroyArchive, + cache::GroupwareCache, + calendar::{ + CalendarEvent, CalendarEventData, EVENT_DRAFT, EVENT_HIDE_ATTENDEES, EVENT_INVITE_OTHERS, + EVENT_INVITE_SELF, + }, + scheduling::{ItipMessages, event_create::itip_create, event_update::itip_update}, +}; use http_proto::HttpSessionData; use jmap_proto::{ error::set::SetError, @@ -15,8 +31,12 @@ use jmap_proto::{ request::IntoValid, types::state::State, }; -use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Value}; -use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder}; +use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Map, Value}; +use store::{ + ahash::AHashSet, + roaring::RoaringBitmap, + write::{BatchBuilder, now, serialize::rkyv_deserialize}, +}; use trc::AddContext; use types::{ acl::Acl, @@ -40,10 +60,16 @@ pub trait CalendarEventSet: Sync + Send { 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>>>>; + send_scheduling_messages: bool, + can_add_calendars: &Option, + js_calendar_event: JSCalendar<'_, Id, BlobId>, + updates: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) -> impl Future>>>>; +} + +pub struct CalendarCreateResult { + pub document_id: u32, + pub nudge_queue: bool, } impl CalendarEventSet for Server { @@ -53,8 +79,7 @@ impl CalendarEventSet for Server { access_token: &AccessToken, _session: &HttpSessionData, ) -> trc::Result> { - todo!() - /*let account_id = request.account_id.document_id(); + let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await?; @@ -62,7 +87,7 @@ impl CalendarEventSet for Server { let will_destroy = request.unwrap_destroy().into_valid().collect::>(); // Obtain calendarIds - let (can_add_address_books, can_delete_address_books, can_modify_address_books) = + let (can_add_calendars, can_delete_calendars, can_modify_calendars) = if access_token.is_shared(account_id) { ( cache @@ -81,6 +106,8 @@ impl CalendarEventSet for Server { // Process creates let mut batch = BatchBuilder::new(); + let send_scheduling_messages = request.arguments.send_scheduling_messages.unwrap_or(false); + let mut nudge_queue = false; 'create: for (id, object) in request.unwrap_create() { match self .create_calendar_event( @@ -88,14 +115,16 @@ impl CalendarEventSet for Server { &mut batch, access_token, account_id, - &can_add_address_books, + send_scheduling_messages, + &can_add_calendars, JSCalendar::default(), object, ) .await? { - Ok(document_id) => { - response.created(id, document_id); + Ok(result) => { + response.created(id, result.document_id); + nudge_queue |= result.nudge_queue; } Err(err) => { response.not_created.append(id, err); @@ -110,6 +139,14 @@ impl CalendarEventSet for Server { if will_destroy.contains(&id) { response.not_updated.append(id, SetError::will_destroy()); continue 'update; + } else if id.is_synthetic() { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::Id) + .with_description("Updating synthetic ids is not yet supported."), + ); + continue 'update; } // Obtain calendar_event card @@ -129,35 +166,35 @@ impl CalendarEventSet for Server { let mut new_calendar_event = calendar_event .deserialize::() .caused_by(trc::location!())?; - let mut js_calendar_event = new_calendar_event.card.into_jscalendar(); + let mut js_calendar_group = + std::mem::take(&mut new_calendar_event.data.event).into_jscalendar::(); // Process changes if let Err(err) = update_calendar_event( + access_token, object, - &mut new_calendar_event.names, - &mut js_calendar_event, + &mut new_calendar_event, + &mut js_calendar_group, ) { 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 { + // Convert JSCalendar to iCalendar + let Some(ical) = js_calendar_group.into_icalendar() else { response.not_updated.append( id, SetError::invalid_properties() - .with_description("Failed to convert calendar_event to vCard."), + .with_description("Failed to convert calendar event to iCalendar."), ); continue 'update; - } + }; + new_calendar_event.data.event = ical; // Validate UID match ( - new_calendar_event.card.uid(), - calendar_event.inner.card.uid(), + new_calendar_event.data.event.uids().next(), + calendar_event.inner.data.event.uids().next(), ) { (Some(old_uid), Some(new_uid)) if old_uid == new_uid => {} (None, None) | (None, Some(_)) => {} @@ -166,34 +203,34 @@ impl CalendarEventSet for Server { id, SetError::invalid_properties() .with_property(JSCalendarProperty::Uid) - .with_description("You cannot change the UID of a calendar_event."), + .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) { + for calendar_id in new_calendar_event.added_calendar_ids(calendar_event.inner) { + if !cache.has_container_id(&calendar_id) { response.not_updated.append( id, SetError::invalid_properties() .with_property(JSCalendarProperty::CalendarIds) .with_description(format!( "calendarId {} does not exist.", - Id::from(addressbook_id) + Id::from(calendar_id) )), ); continue 'update; - } else if can_add_address_books + } else if can_add_calendars .as_ref() - .is_some_and(|ids| !ids.contains(addressbook_id)) + .is_some_and(|ids| !ids.contains(calendar_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) + "You are not allowed to add calendar events to calendar {}.", + Id::from(calendar_id) )), ); continue 'update; @@ -201,16 +238,14 @@ impl CalendarEventSet for Server { } // 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) { + if let Some(can_delete_calendars) = &can_delete_calendars { + for calendar_id in new_calendar_event.removed_calendar_ids(calendar_event.inner) { + if !can_delete_calendars.contains(calendar_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) + "You are not allowed to remove calendar events from calendar {}.", + Id::from(calendar_id) )), ); continue 'update; @@ -219,16 +254,14 @@ impl CalendarEventSet for Server { } // 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) { + if let Some(can_modify_calendars) = &can_modify_calendars { + for calendar_id in new_calendar_event.unchanged_calendar_ids(calendar_event.inner) { + if !can_modify_calendars.contains(calendar_id) { response.not_updated.append( id, SetError::forbidden().with_description(format!( "You are not allowed to modify calendar {}.", - Id::from(addressbook_id) + Id::from(calendar_id) )), ); continue 'update; @@ -237,16 +270,111 @@ impl CalendarEventSet for Server { } // Check size and quota - if new_calendar_event.size as usize > self.core.groupware.max_vcard_size { + new_calendar_event.size = new_calendar_event.data.event.size() as u32; + if new_calendar_event.size as usize > self.core.groupware.max_ical_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 + "Event size {} exceeds the maximum allowed size of {} bytes.", + new_calendar_event.size, self.core.groupware.max_ical_size )), ); continue 'update; } + + // Obtain previous alarm + let now = now() as i64; + let prev_email_alarm = calendar_event.inner.data.next_alarm(now, Tz::Floating); + + // Build event + let mut next_email_alarm = None; + new_calendar_event.data = CalendarEventData::new( + new_calendar_event.data.event, + Tz::Floating, + self.core.groupware.max_ical_instances, + &mut next_email_alarm, + ); + + // Scheduling + let mut itip_messages = None; + if send_scheduling_messages + && self.core.groupware.itip_enabled + && !access_token.emails.is_empty() + && access_token.has_permission(Permission::CalendarSchedulingSend) + && new_calendar_event.data.event_range_end() > now + { + let result = if new_calendar_event.schedule_tag.is_some() { + let old_ical = rkyv_deserialize(&calendar_event.inner.data.event) + .caused_by(trc::location!())?; + + itip_update( + &mut new_calendar_event.data.event, + &old_ical, + access_token.emails.as_slice(), + ) + } else { + itip_create( + &mut new_calendar_event.data.event, + access_token.emails.as_slice(), + ) + }; + + match result { + Ok(messages) => { + let mut is_organizer = false; + if messages + .iter() + .map(|r| { + is_organizer = r.from_organizer; + r.to.len() + }) + .sum::() + < self.core.groupware.itip_outbound_max_recipients + { + // Only update schedule tag if the user is the organizer + if is_organizer { + if let Some(schedule_tag) = &mut new_calendar_event.schedule_tag { + *schedule_tag += 1; + } else { + new_calendar_event.schedule_tag = Some(1); + } + } + + itip_messages = Some(ItipMessages::new(messages)); + } else { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::Participants) + .with_description(concat!( + "The number of scheduling message recipients ", + "exceeds the maximum allowed." + )), + ); + continue 'update; + } + } + Err(err) => { + if err.is_jmap_error() { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::Participants) + .with_description(err.to_string()), + ); + continue 'update; + } + + // Event changed, but there are no iTIP messages to send + if let Some(schedule_tag) = &mut new_calendar_event.schedule_tag { + *schedule_tag += 1; + } + } + } + } + nudge_queue |= next_email_alarm.is_some() || itip_messages.is_some(); + + // Validate quota let extra_bytes = (new_calendar_event.size as u64) .saturating_sub(u32::from(calendar_event.inner.size) as u64); if extra_bytes > 0 { @@ -276,6 +404,20 @@ impl CalendarEventSet for Server { &mut batch, ) .caused_by(trc::location!())?; + if prev_email_alarm != next_email_alarm { + if let Some(prev_alarm) = prev_email_alarm { + prev_alarm.delete_task(&mut batch); + } + if let Some(next_alarm) = next_email_alarm { + next_alarm.write_task(&mut batch); + } + } + if let Some(itip_messages) = itip_messages { + itip_messages + .queue(&mut batch) + .caused_by(trc::location!())?; + } + response.updated.append(id, None); } @@ -286,7 +428,15 @@ impl CalendarEventSet for Server { if !cache.has_container_id(&document_id) { response.not_destroyed.append(id, SetError::not_found()); continue; - }; + } else if id.is_synthetic() { + response.not_destroyed.append( + id, + SetError::invalid_properties() + .with_property(JSCalendarProperty::Id) + .with_description("Deleting synthetic ids is not yet supported."), + ); + continue; + } let Some(calendar_event_) = self .get_archive(account_id, Collection::CalendarEvent, document_id) @@ -302,14 +452,14 @@ impl CalendarEventSet for Server { .caused_by(trc::location!())?; // Validate ACLs - if let Some(can_delete_address_books) = &can_delete_address_books { + if let Some(can_delete_calendars) = &can_delete_calendars { for name in calendar_event.inner.names.iter() { let parent_id = name.parent_id.to_native(); - if !can_delete_address_books.contains(parent_id) { + if !can_delete_calendars.contains(parent_id) { response.not_destroyed.append( id, SetError::forbidden().with_description(format!( - "You are not allowed to remove calendar_events from calendar {}.", + "You are not allowed to remove events from calendar {}.", Id::from(parent_id) )), ); @@ -318,9 +468,15 @@ impl CalendarEventSet for Server { } } - // Delete record + // Delete event DestroyArchive(calendar_event) - .delete_all(access_token, account_id, document_id, &mut batch) + .delete_all( + access_token, + account_id, + document_id, + send_scheduling_messages, + &mut batch, + ) .caused_by(trc::location!())?; response.destroyed.push(id); @@ -334,10 +490,14 @@ impl CalendarEventSet for Server { .and_then(|ids| ids.last_change_id(account_id)) .caused_by(trc::location!())?; + if nudge_queue { + self.notify_task_queue(); + } + response.new_state = State::Exact(change_id).into(); } - Ok(response)*/ + Ok(response) } async fn create_calendar_event( @@ -346,20 +506,21 @@ impl CalendarEventSet for Server { 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!() - /* + send_scheduling_messages: bool, + can_add_calendars: &Option, + mut js_calendar_group: JSCalendar<'_, Id, BlobId>, + updates: Value<'_, JSCalendarProperty, JSCalendarValue>, + ) -> trc::Result>>> { // Process changes - let mut names = Vec::new(); - if let Err(err) = update_calendar_event(updates, &mut names, &mut js_calendar_event) { + let mut event = CalendarEvent::default(); + if let Err(err) = + update_calendar_event(access_token, updates, &mut event, &mut js_calendar_group) + { return Ok(Err(err)); } // Verify that the calendar ids valid - for name in &names { + for name in &event.names { if !cache.has_container_id(&name.parent_id) { return Ok(Err(SetError::invalid_properties() .with_property(JSCalendarProperty::CalendarIds) @@ -367,38 +528,88 @@ impl CalendarEventSet for Server { "calendarId {} does not exist.", Id::from(name.parent_id) )))); - } else if can_add_address_books + } else if can_add_calendars .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 {}.", + "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."))); + // Convert JSCalendar to iCalendar + let Some(ical) = js_calendar_group.into_icalendar() else { + return Ok(Err(SetError::invalid_properties().with_description( + "Failed to convert calendar event to iCalendar.", + ))); }; // Validate UID - if let Err(err) = assert_is_unique_uid(self, cache, account_id, &names, card.uid()).await? { + if let Err(err) = + assert_is_unique_uid(self, cache, account_id, &event.names, ical.uids().next()).await? + { return Ok(Err(err)); } // Check size and quota - let size = card.size(); - if size > self.core.groupware.max_vcard_size { + let size = ical.size(); + if size > self.core.groupware.max_ical_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 + "Event size {} exceeds the maximum allowed size of {} bytes.", + size, self.core.groupware.max_ical_size ), ))); } + + // Build event + let mut next_email_alarm = None; + event.data = CalendarEventData::new( + ical, + Tz::Floating, + self.core.groupware.max_ical_instances, + &mut next_email_alarm, + ); + event.size = size as u32; + + // Scheduling + let mut itip_messages = None; + if send_scheduling_messages + && self.core.groupware.itip_enabled + && !access_token.emails.is_empty() + && access_token.has_permission(Permission::CalendarSchedulingSend) + && event.data.event_range_end() > now() as i64 + { + match itip_create(&mut event.data.event, access_token.emails.as_slice()) { + Ok(messages) => { + if messages.iter().map(|r| r.to.len()).sum::() + < self.core.groupware.itip_outbound_max_recipients + { + event.schedule_tag = Some(1); + itip_messages = Some(ItipMessages::new(messages)); + } else { + return Ok(Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::Participants) + .with_description(concat!( + "The number of scheduling message recipients ", + "exceeds the maximum allowed." + )))); + } + } + Err(err) => { + if err.is_jmap_error() { + return Ok(Err(SetError::invalid_properties() + .with_property(JSCalendarProperty::Participants) + .with_description(err.to_string()))); + } + } + } + } + let nudge_queue = next_email_alarm.is_some() || itip_messages.is_some(); + + // Validate quota match self .has_available_quota( &self.get_resource_token(access_token, account_id).await?, @@ -419,24 +630,54 @@ impl CalendarEventSet for Server { .assign_document_ids(account_id, Collection::CalendarEvent, 1) .await .caused_by(trc::location!())?; - CalendarEvent { - names, - size: size as u32, - card, - ..Default::default() + event + .insert( + access_token, + account_id, + document_id, + next_email_alarm, + batch, + ) + .caused_by(trc::location!())?; + + if let Some(itip_messages) = itip_messages { + itip_messages.queue(batch).caused_by(trc::location!())?; } - .insert(access_token, account_id, document_id, batch) - .caused_by(trc::location!()) - .map(|_| Ok(document_id))*/ + + Ok(Ok(CalendarCreateResult { + document_id, + nudge_queue, + })) } } -/* fn update_calendar_event<'x>( - updates: Value<'x, JSCalendarProperty, JSCalendarValue>, - addressbooks: &mut Vec, - js_calendar_event: &mut JSCalendar<'x, Id>, + _access_token: &AccessToken, + updates: Value<'x, JSCalendarProperty, JSCalendarValue>, + event: &mut CalendarEvent, + js_calendar_group: &mut JSCalendar<'x, Id, BlobId>, ) -> Result<(), SetError>> { + // Extract event + let js_calendar_events = js_calendar_group + .0 + .as_object_mut() + .unwrap() + .get_mut(&Key::Property(JSCalendarProperty::Entries)) + .unwrap() + .as_array_mut() + .unwrap(); + + let js_calendar_event = if let Some(js_calendar_event) = js_calendar_events.first_mut() { + js_calendar_event + } else { + js_calendar_events.push(Value::Object(Map::new())); + js_calendar_events.first_mut().unwrap() + }; + + let mut utc_start = None; + let mut utc_end = None; + let mut entries = js_calendar_event.as_object_mut().unwrap(); + for (property, value) in updates.into_expanded_object() { let Key::Property(property) = property else { return Err(SetError::invalid_properties() @@ -445,8 +686,45 @@ fn update_calendar_event<'x>( }; match (property, value) { + (JSCalendarProperty::IsDraft, Value::Bool(set)) => { + if set { + event.flags |= EVENT_DRAFT; + } else { + event.flags &= !EVENT_DRAFT; + } + } + (JSCalendarProperty::MayInviteSelf, Value::Bool(set)) => { + if set { + event.flags |= EVENT_INVITE_SELF; + } else { + event.flags &= !EVENT_INVITE_SELF; + } + } + (JSCalendarProperty::MayInviteOthers, Value::Bool(set)) => { + if set { + event.flags |= EVENT_INVITE_OTHERS; + } else { + event.flags &= !EVENT_INVITE_OTHERS; + } + } + (JSCalendarProperty::HideAttendees, Value::Bool(set)) => { + if set { + event.flags |= EVENT_HIDE_ATTENDEES; + } else { + event.flags &= !EVENT_HIDE_ATTENDEES; + } + } + (JSCalendarProperty::UseDefaultAlerts, Value::Bool(_)) => { + // TODO not yet implemented + } + (JSCalendarProperty::UtcStart, Value::Element(JSCalendarValue::DateTime(start))) => { + utc_start = Some(start.timestamp); + } + (JSCalendarProperty::UtcEnd, Value::Element(JSCalendarValue::DateTime(end))) => { + utc_end = Some(end.timestamp); + } (JSCalendarProperty::CalendarIds, value) => { - patch_parent_ids(addressbooks, None, value)?; + patch_parent_ids(&mut event.names, None, value)?; } (JSCalendarProperty::Pointer(pointer), value) => { if matches!( @@ -457,28 +735,79 @@ fn update_calendar_event<'x>( ) { 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) { + patch_parent_ids(&mut event.names, pointer.next(), value)?; + } else if !js_calendar_event.patch_jptr(pointer.iter(), value) { return Err(SetError::invalid_properties() .with_property(JSCalendarProperty::Pointer(pointer)) .with_description("Patch operation failed.")); } + entries = js_calendar_event.as_object_mut().unwrap(); + } + ( + property @ (JSCalendarProperty::Id + | JSCalendarProperty::BaseEventId + | JSCalendarProperty::IsOrigin + | JSCalendarProperty::Method), + _, + ) => { + return Err(SetError::invalid_properties() + .with_property(property) + .with_description("This property is immutable.")); + } + ( + property @ (JSCalendarProperty::IsDraft + | JSCalendarProperty::MayInviteSelf + | JSCalendarProperty::MayInviteOthers + | JSCalendarProperty::HideAttendees + | JSCalendarProperty::UseDefaultAlerts + | JSCalendarProperty::UtcStart + | JSCalendarProperty::UtcEnd), + _, + ) => { + return Err(SetError::invalid_properties() + .with_property(property) + .with_description("Invalid value.")); } (property, value) => { - js_calendar_event - .0 - .as_object_mut() - .unwrap() - .insert(property, value); + entries.insert(property, value); } } } + // Validate UTC start/end + if let (Some(start), Some(end)) = (utc_start, utc_end) { + if start >= end { + return Err(SetError::invalid_properties() + .with_properties([JSCalendarProperty::UtcStart, JSCalendarProperty::UtcEnd]) + .with_description("utcStart must be before utcEnd.")); + } + entries.insert( + Key::Property(JSCalendarProperty::Start), + Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new( + start, true, + ))), + ); + entries.insert( + Key::Property(JSCalendarProperty::Duration), + Value::Element(JSCalendarValue::Duration(ICalendarDuration::from_seconds( + end - start, + ))), + ); + entries.insert( + Key::Property(JSCalendarProperty::TimeZone), + Value::Str(Cow::Borrowed("Etc/UTC")), + ); + } else if utc_start.is_some() || utc_end.is_some() { + return Err(SetError::invalid_properties() + .with_properties([JSCalendarProperty::UtcStart, JSCalendarProperty::UtcEnd]) + .with_description("Both utcStart and utcEnd must be provided.")); + } + // Make sure the calendar_event belongs to at least one calendar - if addressbooks.is_empty() { + if event.names.is_empty() { return Err(SetError::invalid_properties() .with_property(JSCalendarProperty::CalendarIds) - .with_description("Contact has to belong to at least one calendar.")); + .with_description("Event has to belong to at least one calendar.")); } Ok(()) @@ -487,7 +816,7 @@ fn update_calendar_event<'x>( fn patch_parent_ids( current: &mut Vec, patch: Option<&JsonPointerItem>>, - update: Value<'_, JSCalendarProperty, JSCalendarValue>, + update: Value<'_, JSCalendarProperty, JSCalendarValue>, ) -> Result<(), SetError>> { match (patch, update) { ( @@ -533,5 +862,3 @@ fn patch_parent_ids( .with_description("Invalid patch operation for calendarIds.")), } } - -*/ diff --git a/crates/jmap/src/contact/get.rs b/crates/jmap/src/contact/get.rs index 0b615763..7e05dccc 100644 --- a/crates/jmap/src/contact/get.rs +++ b/crates/jmap/src/contact/get.rs @@ -5,13 +5,13 @@ */ use crate::changes::state::JmapCacheState; -use calcard::jscontact::{JSContactProperty, JSContactValue}; +use calcard::jscontact::{JSContactProperty, JSContactValue, import::ConversionOptions}; use common::{Server, auth::AccessToken}; use groupware::{cache::GroupwareCache, contact::ContactCard}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::contact, - request::IntoValid, + request::reference::MaybeResultReference, }; use jmap_tools::{Map, Value}; use store::roaring::RoaringBitmap; @@ -38,7 +38,10 @@ impl ContactCardGet for Server { access_token: &AccessToken, ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; - let return_all_properties = request.properties.is_none(); + let return_all_properties = request + .properties + .as_ref() + .is_none_or(|v| matches!(v, MaybeResultReference::Value(v) if v.is_empty())); let properties = request.unwrap_properties(&[JSContactProperty::Id, JSContactProperty::AddressBookIds]); let account_id = request.account_id.document_id(); @@ -65,9 +68,26 @@ impl ContactCardGet for Server { list: Vec::with_capacity(ids.len()), not_found: vec![], }; - let return_id = return_all_properties || properties.contains(&JSContactProperty::Id); - let return_address_book_ids = - return_all_properties || properties.contains(&JSContactProperty::AddressBookIds); + let mut return_id = return_all_properties; + let mut return_address_book_ids = return_all_properties; + let mut return_converted_props = !return_all_properties; + + if !return_all_properties { + for property in &properties { + match property { + JSContactProperty::Id => { + return_id = true; + } + JSContactProperty::AddressBookIds => { + return_address_book_ids = true; + } + JSContactProperty::VCard => { + return_converted_props = true; + } + _ => {} + } + } + } for id in ids { // Obtain the contact object @@ -91,19 +111,17 @@ impl ContactCardGet for Server { .deserialize::() .caused_by(trc::location!())?; + let jscontact = contact + .card + .into_jscontact_with_options::( + ConversionOptions::default().include_vcard_parameters(return_converted_props), + ) + .into_inner(); let mut result = if return_all_properties { - contact - .card - .into_jscontact::() - .into_inner() - .into_object() - .unwrap() + jscontact.into_object().unwrap() } else { Map::from_iter( - contact - .card - .into_jscontact::() - .into_inner() + jscontact .into_expanded_object() .filter(|(k, _)| k.as_property().is_some_and(|p| properties.contains(p))), ) diff --git a/crates/jmap/src/contact/set.rs b/crates/jmap/src/contact/set.rs index 439f9277..e52d7113 100644 --- a/crates/jmap/src/contact/set.rs +++ b/crates/jmap/src/contact/set.rs @@ -426,6 +426,8 @@ fn update_contact_card<'x>( addressbooks: &mut Vec, js_contact: &mut JSContact<'x, Id, BlobId>, ) -> Result<(), SetError>> { + let mut entries = js_contact.0.as_object_mut().unwrap(); + for (property, value) in updates.into_expanded_object() { let Key::Property(property) = property else { return Err(SetError::invalid_properties() @@ -452,13 +454,10 @@ fn update_contact_card<'x>( .with_property(JSContactProperty::Pointer(pointer)) .with_description("Patch operation failed.")); } + entries = js_contact.0.as_object_mut().unwrap(); } (property, value) => { - js_contact - .0 - .as_object_mut() - .unwrap() - .insert(property, value); + entries.insert(property, value); } } } diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 738aee68..c75dccbe 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -38,6 +38,7 @@ impl PrincipalQuery for Server { results: RoaringBitmap::new(), }; let mut is_set = true; + let todo = "implement other search criteria"; for cond in std::mem::take(&mut request.filter) { match cond { diff --git a/crates/types/src/id.rs b/crates/types/src/id.rs index 568841b9..df095905 100644 --- a/crates/types/src/id.rs +++ b/crates/types/src/id.rs @@ -100,26 +100,32 @@ impl Id { } } + #[inline(always)] pub fn from_parts(prefix_id: DocumentId, doc_id: DocumentId) -> Id { Id(((prefix_id as u64) << 32) | doc_id as u64) } + #[inline(always)] pub fn id(&self) -> u64 { self.0 } + #[inline(always)] pub fn document_id(&self) -> DocumentId { (self.0 & 0xFFFFFFFF) as DocumentId } + #[inline(always)] pub fn prefix_id(&self) -> DocumentId { (self.0 >> 32) as DocumentId } + #[inline(always)] pub fn is_singleton(&self) -> bool { self.0 == 20080258862541 } + #[inline(always)] pub fn is_valid(&self) -> bool { self.0 != u64::MAX }