/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::{calendar_event::CalendarSyntheticId, changes::state::JmapCacheState}; use calcard::{ common::{PartialDateTime, timezone::Tz}, icalendar::{ ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarParameter, ICalendarParameterName, ICalendarParameterValue, ICalendarParticipationRole, ICalendarProperty, ICalendarValue, }, jscalendar::{ JSCalendarDateTime, JSCalendarProperty, JSCalendarValue, import::ConversionOptions, }, }; use chrono::DateTime; use common::{Server, auth::AccessToken}; 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::{JmapObjectId, calendar_event}, request::IntoValid, }; use jmap_tools::{Key, Map, Value}; use std::{borrow::Cow, str::FromStr}; use store::{ ValueKey, ahash::{AHashMap, AHashSet}, roaring::RoaringBitmap, write::{AlignedBytes, Archive}, }; 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> { let return_all_properties = request.properties.is_none(); let properties = request.unwrap_properties(&[]); let account_id = request.account_id.document_id(); let personal_id = access_token.personal_id(account_id, Collection::Calendar); let cache = self .fetch_dav_resources( access_token.account_id(), account_id, SyncCollection::Calendar, ) .await?; let calendar_event_ids = if access_token.is_member(account_id) { cache.document_ids(false).collect::() } else { cache.shared_items(access_token, [Acl::ReadItems], true) }; let (mut ids, has_synthetic_ids) = if let Some(rr) = request.ids.take() { let rr = rr.unwrap(); if rr.len() > self.core.jmap.get_max_objects { return Err(trc::JmapEvent::RequestTooLarge.into_err()); } let mut ids = Vec::with_capacity(rr.len()); let mut has_synthetic_ids = false; for id in rr.into_valid() { has_synthetic_ids |= id.is_synthetic(); ids.push(id); } (ids, has_synthetic_ids) } else { ( calendar_event_ids .iter() .take(self.core.jmap.get_max_objects) .map(Into::into) .collect::>(), false, ) }; 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 mut return_converted_props = !return_all_properties; let mut return_is_origin = false; let mut return_utc_dates = false; 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 => { return_is_origin = true; jmap_properties.push(property); } _ => { if matches!(property, JSCalendarProperty::ICalendar) { return_converted_props = true; } jscal_properties.push(property); } } } (jmap_properties, jscal_properties) } else { return_is_origin = true; ( vec![ JSCalendarProperty::Id, JSCalendarProperty::CalendarIds, JSCalendarProperty::IsDraft, JSCalendarProperty::IsOrigin, ], vec![], ) }; let current_account_info = self .account_info(access_token.account_id()) .await .caused_by(trc::location!())?; let return_is_origin = if return_is_origin { if account_id == access_token.account_id() { Some(Cow::Borrowed(¤t_account_info)) } else { Some( self.account_info(account_id) .await .map(Cow::Owned) .caused_by(trc::location!())?, ) } } else { None }; // Sort by baseId let mut original_order: Option> = None; if has_synthetic_ids { original_order = Some(ids.iter().enumerate().map(|(i, id)| (*id, i)).collect()); 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); while let Some(id) = ids.next() { // Obtain the calendar_event object let document_id = id.document_id(); if !calendar_event_ids.contains(document_id) { response.push_not_found(id); continue; } let Some(_calendar_event) = self .store() .get_value::>(ValueKey::archive( account_id, Collection::CalendarEvent, document_id, )) .await? else { response.push_not_found(id); continue; }; let mut calendar_event = _calendar_event .deserialize::() .caused_by(trc::location!())?; // Extract recurrence keys from synthetic ids let mut recurrence_keys = AHashSet::new(); let mut include_base_event = false; if let Some(recurrence_key) = id.recurrence_key() { recurrence_keys.insert(recurrence_key); } else { include_base_event = true; } while let Some(next_id) = ids.peek() { if next_id.document_id() == document_id { if let Some(recurrence_key) = next_id.recurrence_key() { recurrence_keys.insert(recurrence_key); } else { include_base_event = true; } ids.next(); } else { break; } } // 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| { current_account_info .addresses() .iter() .any(|a| a.eq_ignore_ascii_case(addr)) }) } _ => true, }); } } } // Expand synthetic ids let mut results = Vec::with_capacity(recurrence_keys.len() + 1); if !recurrence_keys.is_empty() { let ical = &calendar_event.data.event; if let Some(expansions) = calendar_event .data .expand_from_ids(&mut recurrence_keys, default_tz) { for expansion in expansions { let Some(recurrence_key) = expansion.recurrence_key() else { continue; }; let component = &ical.components[expansion.comp_id as usize]; let source_component = component; 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 tz = None; 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 => { if let Some(new_tz) = entry .tz_id() .and_then(|id| Tz::from_str(id).ok()) .filter(|tz| *tz != Tz::UTC) { tz = Some(new_tz); } false } ICalendarProperty::Due | ICalendarProperty::Completed | ICalendarProperty::Created => is_recurrent, ICalendarProperty::Duration => { has_duration = true; true } _ => true, }) .cloned() .collect::>(), }; let tz = tz.unwrap_or(default_tz); let tz_name = tz.name().unwrap_or_default().to_string(); let start_timestamp = DateTime::from_timestamp(expansion.start, 0) .map(|dt| dt.with_timezone(&tz)) .map(|dt| dt.naive_local()) .map(|dt| dt.and_utc().timestamp()) .unwrap_or(expansion.start); let end_timestamp = DateTime::from_timestamp(expansion.end, 0) .map(|dt| dt.with_timezone(&tz)) .map(|dt| dt.naive_local()) .map(|dt| dt.and_utc().timestamp()) .unwrap_or(expansion.end); component.entries.push(ICalendarEntry { name: ICalendarProperty::Dtstart, params: vec![ICalendarParameter::tzid(tz_name.clone())], values: vec![ICalendarValue::PartialDateTime(Box::new( PartialDateTime::from_naive_timestamp(start_timestamp), ))], }); if is_recurrent_or_override { component.entries.push( source_component .property(&ICalendarProperty::RecurrenceId) .filter(|entry| { entry .parameters(&ICalendarParameterName::Range) .next() .is_none() || calendar_event .data .expand_single(expansion.comp_id, default_tz) .is_some_and(|first| { first.start_naive == expansion.start_naive }) }) .cloned() .unwrap_or_else(|| ICalendarEntry { name: ICalendarProperty::RecurrenceId, params: vec![ICalendarParameter::tzid(tz_name.clone())], values: vec![ICalendarValue::PartialDateTime(Box::new( PartialDateTime::from_naive_timestamp(start_timestamp), ))], }), ); } if !has_duration { component.entries.push(ICalendarEntry { name: ICalendarProperty::Dtend, params: vec![ICalendarParameter::tzid(tz_name)], values: vec![ICalendarValue::PartialDateTime(Box::new( PartialDateTime::from_naive_timestamp(end_timestamp), ))], }); } 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(recurrence_key, document_id), expanded_ical, expansion, )); } } for recurrence_key in recurrence_keys { response.push_not_found(::new( recurrence_key, document_id, )); } } 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_origin.as_ref().is_some_and(|account| { 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| { account .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 { let is_synthetic = id.is_synthetic(); let is_null_for_synthetic = |property: &JSCalendarProperty| { is_synthetic && matches!( property, JSCalendarProperty::RecurrenceRule | JSCalendarProperty::RecurrenceOverrides ) }; let mut result = Map::from_iter(jscal.into_expanded_object().filter(|(k, _)| { k.as_property().is_some_and(|p| { jscal_properties.contains(p) && !is_null_for_synthetic(p) }) })); for property in jscal_properties .iter() .filter(|property| is_null_for_synthetic(property)) { result.insert_unchecked(property.clone(), Value::Null); } result }; 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::BaseEventId, if id.is_synthetic() { Value::Element(JSCalendarValue::Id(id.document_id().into())) } else { Value::Null }, ); } 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(personal_id) .is_some_and(|v| v.flags & PREF_USE_DEFAULT_ALERTS != 0), ), ); } _ => {} } } response.list.push(result.into()); } } // Restore original order if let Some(original_order) = original_order { response.list.sort_by_key(|obj| { obj.as_object() .unwrap() .get(&Key::Property(JSCalendarProperty::::Id)) .and_then(|v| v.as_element()) .and_then(|v: &JSCalendarValue| v.as_id()) .and_then(|id| original_order.get(&id)) .cloned() .unwrap_or(usize::MAX) }); } Ok(response) } }