Import upstream v0.16.22, stripped
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.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
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;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::{
|
||||
copy::{CopyRequest, CopyResponse},
|
||||
set::SetRequest,
|
||||
},
|
||||
object::calendar_event,
|
||||
request::{
|
||||
Call, IntoValid, MaybeInvalid, RequestMethod, SetRequestMethod,
|
||||
method::{MethodFunction, MethodName, MethodObject},
|
||||
reference::MaybeResultReference,
|
||||
},
|
||||
types::state::State,
|
||||
};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait JmapCalendarEventCopy: Sync + Send {
|
||||
fn calendar_event_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, calendar_event::CalendarEvent>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<CopyResponse<calendar_event::CalendarEvent>>> + Send;
|
||||
}
|
||||
|
||||
impl JmapCalendarEventCopy for Server {
|
||||
async fn calendar_event_copy<'x>(
|
||||
&self,
|
||||
request: CopyRequest<'x, calendar_event::CalendarEvent>,
|
||||
access_token: &AccessToken,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<CopyResponse<calendar_event::CalendarEvent>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let from_account_id = request.from_account_id.document_id();
|
||||
|
||||
if account_id == from_account_id {
|
||||
return Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("From accountId is equal to fromAccountId"));
|
||||
}
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let old_state = cache.assert_state(false, &request.if_in_state)?;
|
||||
let mut response = CopyResponse {
|
||||
from_account_id: request.from_account_id,
|
||||
account_id: request.account_id,
|
||||
new_state: old_state.clone(),
|
||||
old_state,
|
||||
created: VecMap::with_capacity(request.create.len()),
|
||||
not_created: VecMap::new(),
|
||||
};
|
||||
|
||||
let from_cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
from_account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let from_calendar_event_ids = if access_token.is_member(from_account_id) {
|
||||
from_cache.document_ids(false).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
from_cache.shared_items(access_token, [Acl::ReadItems], true)
|
||||
};
|
||||
|
||||
let can_add_calendars = if access_token.is_shared(account_id) {
|
||||
cache
|
||||
.shared_containers(access_token, [Acl::AddItems], true)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let on_success_delete = request.on_success_destroy_original.unwrap_or(false);
|
||||
let mut destroy_ids = Vec::new();
|
||||
|
||||
// Obtain account info
|
||||
let account_info = self
|
||||
.account_info(access_token.account_id())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Prepare batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
'create: for (id, create) in request.create.into_valid() {
|
||||
let from_calendar_event_id = id.document_id();
|
||||
if !from_calendar_event_ids.contains(from_calendar_event_id) {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
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
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
from_account_id,
|
||||
Collection::CalendarEvent,
|
||||
from_calendar_event_id,
|
||||
))
|
||||
.await?
|
||||
else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Item {} not found in account {}.",
|
||||
id, response.from_account_id
|
||||
)),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let calendar_event = _calendar_event
|
||||
.deserialize::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
match self
|
||||
.create_calendar_event(
|
||||
&cache,
|
||||
&mut batch,
|
||||
access_token,
|
||||
account_id,
|
||||
&account_info,
|
||||
false,
|
||||
&can_add_calendars,
|
||||
calendar_event.data.event.into_jscalendar(),
|
||||
create,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(document_id) => {
|
||||
response.created(id, document_id);
|
||||
|
||||
// Add to destroy list
|
||||
if on_success_delete {
|
||||
destroy_ids.push(MaybeInvalid::Value(id));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
let change_id = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
response.new_state = State::Exact(change_id);
|
||||
}
|
||||
|
||||
// Destroy ids
|
||||
if on_success_delete && !destroy_ids.is_empty() {
|
||||
*next_call = Call {
|
||||
id: String::new(),
|
||||
name: MethodName::new(MethodObject::CalendarEvent, MethodFunction::Set),
|
||||
method: RequestMethod::Set(SetRequestMethod::CalendarEvent(Box::new(SetRequest {
|
||||
account_id: request.from_account_id,
|
||||
if_in_state: request.destroy_from_if_in_state,
|
||||
create: None,
|
||||
update: None,
|
||||
destroy: MaybeResultReference::Value(destroy_ids).into(),
|
||||
arguments: Default::default(),
|
||||
}))),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use calcard::jscalendar::JSCalendarProperty;
|
||||
use common::Server;
|
||||
use groupware::calendar::expand::RecurrenceKey;
|
||||
use jmap_proto::error::set::SetError;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::CalendarEventField, 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)
|
||||
|
||||
- Principal/getAvailability
|
||||
- If there are overlapping BusyPeriod time ranges with different "busyStatus" properties
|
||||
the server MUST choose the value in the following order: confirmed > unavailable > tentative.
|
||||
- Return event properties
|
||||
|
||||
*/
|
||||
|
||||
pub trait CalendarSyntheticId {
|
||||
fn new(key: RecurrenceKey, document_id: u32) -> Self;
|
||||
|
||||
fn is_synthetic(&self) -> bool;
|
||||
|
||||
fn recurrence_key(&self) -> Option<RecurrenceKey>;
|
||||
}
|
||||
|
||||
impl CalendarSyntheticId for Id {
|
||||
fn new(key: RecurrenceKey, document_id: u32) -> Id {
|
||||
Id::from_parts(key.prefix(), document_id)
|
||||
}
|
||||
|
||||
fn recurrence_key(&self) -> Option<RecurrenceKey> {
|
||||
RecurrenceKey::from_prefix(self.prefix_id())
|
||||
}
|
||||
|
||||
fn is_synthetic(&self) -> bool {
|
||||
self.prefix_id() != 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn assert_is_unique_uid(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
uid: Option<&str>,
|
||||
) -> trc::Result<Result<(), SetError<JSCalendarProperty<Id>>>> {
|
||||
if let Some(uid) = uid
|
||||
&& server
|
||||
.document_exists(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
CalendarEventField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
Ok(Err(SetError::invalid_properties()
|
||||
.with_property(JSCalendarProperty::Uid)
|
||||
.with_description(format!(
|
||||
"An event with UID {uid} already exists.",
|
||||
))))
|
||||
} else {
|
||||
Ok(Ok(()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::blob::download::BlobDownload;
|
||||
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, MaybeInvalid},
|
||||
};
|
||||
use jmap_tools::{Key, Value};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait CalendarEventParse: Sync + Send {
|
||||
fn calendar_event_parse(
|
||||
&self,
|
||||
request: ParseRequest<CalendarEvent>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<ParseResponse<CalendarEvent>>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarEventParse for Server {
|
||||
async fn calendar_event_parse(
|
||||
&self,
|
||||
request: ParseRequest<CalendarEvent>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<ParseResponse<CalendarEvent>> {
|
||||
if request.blob_ids.len() > self.core.jmap.calendar_parse_max_items {
|
||||
return Err(trc::JmapEvent::RequestTooLarge.into_err());
|
||||
}
|
||||
let return_all_properties = request.properties.is_none();
|
||||
let properties = request
|
||||
.properties
|
||||
.map(|v| v.into_valid().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut response = ParseResponse {
|
||||
account_id: request.account_id,
|
||||
parsed: VecMap::with_capacity(request.blob_ids.len()),
|
||||
not_parsable: vec![],
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for blob_id in request.blob_ids.into_valid() {
|
||||
// Fetch raw message to parse
|
||||
let raw_vcard = match self.blob_download(&blob_id, access_token).await? {
|
||||
Some(raw_vcard) => raw_vcard,
|
||||
None => {
|
||||
response.not_found.push(MaybeInvalid::Value(blob_id));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let Ok(vcard) = ICalendar::parse(std::str::from_utf8(&raw_vcard).unwrap_or_default())
|
||||
else {
|
||||
response.not_parsable.push(blob_id);
|
||||
continue;
|
||||
};
|
||||
let mut js_calendar_entries = vcard
|
||||
.into_jscalendar_with_opt::<Id, BlobId>(ConversionOptions::default())
|
||||
.into_inner()
|
||||
.into_object()
|
||||
.unwrap()
|
||||
.remove(&Key::Property(JSCalendarProperty::Entries))
|
||||
.unwrap()
|
||||
.into_array()
|
||||
.unwrap();
|
||||
|
||||
if !return_all_properties {
|
||||
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, Value::Array(js_calendar_entries));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use calcard::{common::timezone::Tz, jscalendar::JSCalendarDateTime};
|
||||
use chrono::offset::TimeZone;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{CalendarEvent, expand::RecurrenceKey},
|
||||
};
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::{
|
||||
calendar,
|
||||
calendar_event::{self, CalendarEventComparator, CalendarEventFilter},
|
||||
},
|
||||
request::MaybeInvalid,
|
||||
types::state::State,
|
||||
};
|
||||
use nlp::language::Language;
|
||||
use std::{cmp::Ordering, sync::Arc};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
search::{CalendarSearchField, SearchComparator, SearchFilter, SearchQuery},
|
||||
write::{AlignedBytes, Archive, SearchIndex},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
TimeRange,
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub trait CalendarEventQuery: Sync + Send {
|
||||
fn calendar_event_query(
|
||||
&self,
|
||||
request: QueryRequest<calendar_event::CalendarEvent>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
|
||||
fn calendar_query(
|
||||
&self,
|
||||
request: QueryRequest<calendar::Calendar>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarEventQuery for Server {
|
||||
async fn calendar_event_query(
|
||||
&self,
|
||||
mut request: QueryRequest<calendar_event::CalendarEvent>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await?;
|
||||
let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC);
|
||||
let mut filter: Option<TimeRange> = None;
|
||||
|
||||
// Extract from/to arguments
|
||||
for cond in &request.filter {
|
||||
if let Filter::Property(CalendarEventFilter::After(date)) = cond {
|
||||
if let Some(after) = local_timestamp(date, default_tz) {
|
||||
filter.get_or_insert_default().start = after;
|
||||
}
|
||||
} else if let Filter::Property(CalendarEventFilter::Before(date)) = cond
|
||||
&& let Some(before) = local_timestamp(date, default_tz)
|
||||
{
|
||||
filter.get_or_insert_default().end = before;
|
||||
}
|
||||
}
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
CalendarEventFilter::InCalendar(MaybeInvalid::Value(id)) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.children_ids(id.document_id()),
|
||||
)))
|
||||
}
|
||||
CalendarEventFilter::Uid(uid) => {
|
||||
filters.push(SearchFilter::eq(CalendarSearchField::Uid, uid));
|
||||
}
|
||||
CalendarEventFilter::Text(value) => {
|
||||
let (text, language) =
|
||||
Language::detect(value, self.core.email.default_language);
|
||||
filters.push(SearchFilter::Or);
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Title,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Description,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Location,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Owner,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Attendee,
|
||||
text,
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
CalendarEventFilter::Title(title) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
CalendarSearchField::Title,
|
||||
title,
|
||||
self.core.email.default_language,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Description(description) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
CalendarSearchField::Description,
|
||||
description,
|
||||
self.core.email.default_language,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Location(location) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
CalendarSearchField::Location,
|
||||
location,
|
||||
self.core.email.default_language,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Owner(owner) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Owner,
|
||||
owner,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Attendee(attendee) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Attendee,
|
||||
attendee,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::After(after) => {
|
||||
/*
|
||||
The end of the event, or any recurrence of the event, in the time zone given
|
||||
as the "timeZone" argument, must be after this date to match the condition.
|
||||
*/
|
||||
if let Some(after) = local_timestamp(&after, default_tz) {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
r.event_time_range()
|
||||
.and_then(|(_, end)| (after < end).then_some(r.document_id))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
CalendarEventFilter::Before(before) => {
|
||||
/*
|
||||
The start of the event, or any recurrence of the event, in the time zone given
|
||||
as the "timeZone" argument, must be before this date to match the condition.
|
||||
*/
|
||||
|
||||
if let Some(before) = local_timestamp(&before, default_tz) {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.resources.iter().filter_map(|r| {
|
||||
r.event_time_range().and_then(|(start, _)| {
|
||||
(before > start).then_some(r.document_id)
|
||||
})
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false);
|
||||
let comparators = if !expand_recurrences {
|
||||
request
|
||||
.sort
|
||||
.take()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|comparator| match comparator.property {
|
||||
CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => {
|
||||
Ok(SearchComparator::field(
|
||||
CalendarSearchField::Start,
|
||||
comparator.is_ascending,
|
||||
))
|
||||
}
|
||||
CalendarEventComparator::Uid => Ok(SearchComparator::field(
|
||||
CalendarSearchField::Uid,
|
||||
comparator.is_ascending,
|
||||
)),
|
||||
CalendarEventComparator::Created | CalendarEventComparator::Updated => {
|
||||
Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(comparator.property.into_string().into_owned()))
|
||||
}
|
||||
CalendarEventComparator::_T(other) => Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(other.to_string())),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let results = self
|
||||
.search_store()
|
||||
.query_account(
|
||||
SearchQuery::new(SearchIndex::Calendar)
|
||||
.with_filters(filters)
|
||||
.with_comparators(comparators)
|
||||
.with_account_id(account_id)
|
||||
.with_mask(if access_token.is_shared(account_id) {
|
||||
cache.shared_items(access_token, [Acl::ReadItems], true)
|
||||
} else {
|
||||
cache.document_ids(false).collect()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Extract comparators
|
||||
let comparators = request
|
||||
.sort
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_default();
|
||||
|
||||
if expand_recurrences && !results.is_empty() {
|
||||
let Some(time_range) = filter.filter(|f| f.start != i64::MIN && f.end != i64::MAX)
|
||||
else {
|
||||
return Err(trc::JmapEvent::InvalidArguments.into_err().details(
|
||||
"Both 'after' and 'before' filters are required when expanding recurrences",
|
||||
));
|
||||
};
|
||||
let max_instances = self.core.groupware.max_ical_instances;
|
||||
let mut expanded_results = Vec::with_capacity(results.len() as usize);
|
||||
let has_uid_comparator = comparators
|
||||
.iter()
|
||||
.any(|c| matches!(c.property, CalendarEventComparator::Uid));
|
||||
|
||||
for document_id in results {
|
||||
let Some(_calendar_event) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let calendar_event = _calendar_event
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Expand recurrences
|
||||
let uid = if has_uid_comparator {
|
||||
Arc::new(
|
||||
calendar_event
|
||||
.data
|
||||
.event
|
||||
.uids()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
Arc::new(String::new())
|
||||
};
|
||||
for expansion in calendar_event
|
||||
.data
|
||||
.expand(default_tz, time_range)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let Some(recurrence_key) = expansion.recurrence_key() else {
|
||||
continue;
|
||||
};
|
||||
if expanded_results.len() < max_instances {
|
||||
expanded_results.push(SearchResult {
|
||||
created: calendar_event.created.to_native().to_be_bytes(),
|
||||
updated: calendar_event.modified.to_native().to_be_bytes(),
|
||||
start: expansion.start.to_be_bytes(),
|
||||
uid: uid.clone(),
|
||||
document_id,
|
||||
recurrence_key,
|
||||
});
|
||||
} else {
|
||||
return Err(trc::JmapEvent::InvalidArguments.into_err().details(
|
||||
"The number of expanded recurrences exceeds the server limit",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
expanded_results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
// Sort results
|
||||
if !expanded_results.is_empty() {
|
||||
expanded_results.sort_by(|a, b| {
|
||||
for comparator in comparators {
|
||||
let ordering = if comparator.is_ascending {
|
||||
a.get_property(&comparator.property)
|
||||
.cmp(b.get_property(&comparator.property))
|
||||
} else {
|
||||
b.get_property(&comparator.property)
|
||||
.cmp(a.get_property(&comparator.property))
|
||||
};
|
||||
|
||||
if ordering != Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
});
|
||||
|
||||
// Add results
|
||||
for result in expanded_results {
|
||||
if !response.add(result.recurrence_key.prefix(), result.document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
response.build()
|
||||
} else {
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
for document_id in results {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
async fn calendar_query(
|
||||
&self,
|
||||
request: QueryRequest<calendar::Calendar>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let results = cache.document_ids(true).collect::<Vec<_>>();
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len() as usize,
|
||||
self.core.jmap.query_max_results,
|
||||
State::Initial,
|
||||
&request,
|
||||
);
|
||||
|
||||
for document_id in results {
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option<i64> {
|
||||
tz.from_local_datetime(&dt.to_naive_date_time()?)
|
||||
.single()
|
||||
.map(|dt| dt.timestamp())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SearchResult {
|
||||
recurrence_key: RecurrenceKey,
|
||||
document_id: u32,
|
||||
start: [u8; std::mem::size_of::<i64>()],
|
||||
created: [u8; std::mem::size_of::<i64>()],
|
||||
updated: [u8; std::mem::size_of::<i64>()],
|
||||
uid: Arc<String>,
|
||||
}
|
||||
|
||||
impl SearchResult {
|
||||
fn get_property(&self, comparator: &CalendarEventComparator) -> &[u8] {
|
||||
match comparator {
|
||||
CalendarEventComparator::Uid => self.uid.as_bytes(),
|
||||
CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => {
|
||||
self.start.as_ref()
|
||||
}
|
||||
CalendarEventComparator::Created => self.created.as_ref(),
|
||||
CalendarEventComparator::Updated => self.updated.as_ref(),
|
||||
CalendarEventComparator::_T(_) => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user