Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
657 lines
28 KiB
Rust
657 lines
28 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* 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<calendar_event::CalendarEvent>,
|
|
access_token: &AccessToken,
|
|
) -> impl Future<Output = trc::Result<GetResponse<calendar_event::CalendarEvent>>> + Send;
|
|
}
|
|
|
|
impl CalendarEventGet for Server {
|
|
async fn calendar_event_get(
|
|
&self,
|
|
mut request: GetRequest<calendar_event::CalendarEvent>,
|
|
access_token: &AccessToken,
|
|
) -> trc::Result<GetResponse<calendar_event::CalendarEvent>> {
|
|
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::<RoaringBitmap>()
|
|
} 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::<Vec<_>>(),
|
|
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<AHashMap<Id, usize>> = 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::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::CalendarEvent,
|
|
document_id,
|
|
))
|
|
.await?
|
|
else {
|
|
response.push_not_found(id);
|
|
continue;
|
|
};
|
|
let mut calendar_event = _calendar_event
|
|
.deserialize::<CalendarEvent>()
|
|
.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::<Vec<_>>(),
|
|
};
|
|
|
|
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((
|
|
<Id as CalendarSyntheticId>::new(recurrence_key, document_id),
|
|
expanded_ical,
|
|
expansion,
|
|
));
|
|
}
|
|
}
|
|
|
|
for recurrence_key in recurrence_keys {
|
|
response.push_not_found(<Id as CalendarSyntheticId>::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::<AHashSet<_>>();
|
|
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::<Id, BlobId>(
|
|
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<Id>| {
|
|
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>::Id))
|
|
.and_then(|v| v.as_element())
|
|
.and_then(|v: &JSCalendarValue<Id, BlobId>| v.as_id())
|
|
.and_then(|id| original_order.get(&id))
|
|
.cloned()
|
|
.unwrap_or(usize::MAX)
|
|
});
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
}
|