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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+226
View File
@@ -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)
}
}
+656
View File
@@ -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(&current_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(&timestamp)
{
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)
}
}
+81
View File
@@ -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(()))
}
}
+93
View File
@@ -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)
}
}
+444
View File
@@ -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