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,318 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::acl::JmapRights, calendar::Availability, changes::state::JmapCacheState};
|
||||
use calcard::jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{
|
||||
ALERT_EMAIL, ALERT_RELATIVE_TO_END, ArchivedDefaultAlert, CALENDAR_INVISIBLE,
|
||||
CALENDAR_SUBSCRIBED, Calendar,
|
||||
},
|
||||
};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::calendar::{self, CalendarProperty, CalendarValue, IncludeInAvailability},
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, ValueClass},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
};
|
||||
|
||||
pub trait CalendarGet: Sync + Send {
|
||||
fn calendar_get(
|
||||
&self,
|
||||
request: GetRequest<calendar::Calendar>,
|
||||
access_token: &AccessToken,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<calendar::Calendar>>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarGet for Server {
|
||||
async fn calendar_get(
|
||||
&self,
|
||||
mut request: GetRequest<calendar::Calendar>,
|
||||
access_token: &AccessToken,
|
||||
) -> trc::Result<GetResponse<calendar::Calendar>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
CalendarProperty::Id,
|
||||
CalendarProperty::Name,
|
||||
CalendarProperty::Description,
|
||||
CalendarProperty::Color,
|
||||
CalendarProperty::SortOrder,
|
||||
CalendarProperty::IsSubscribed,
|
||||
CalendarProperty::IsVisible,
|
||||
CalendarProperty::IsDefault,
|
||||
CalendarProperty::IncludeInAvailability,
|
||||
CalendarProperty::DefaultAlertsWithTime,
|
||||
CalendarProperty::DefaultAlertsWithoutTime,
|
||||
CalendarProperty::TimeZone,
|
||||
CalendarProperty::ShareWith,
|
||||
CalendarProperty::MyRights,
|
||||
]);
|
||||
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 is_owner = access_token.is_member(account_id);
|
||||
let calendar_ids = if is_owner {
|
||||
cache.document_ids(true).collect::<RoaringBitmap>()
|
||||
} else {
|
||||
cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true)
|
||||
};
|
||||
let default_calendar_id = self
|
||||
.store()
|
||||
.get_value::<u32>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::Principal.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.or_else(|| cache.document_ids(true).min());
|
||||
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
calendar_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: cache.get_state(true).into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the calendar object
|
||||
let document_id = id.document_id();
|
||||
if !calendar_ids.contains(document_id) {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
}
|
||||
let _calendar = if let Some(calendar) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Calendar,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
calendar
|
||||
} else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
let calendar = _calendar
|
||||
.unarchive::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
match property {
|
||||
CalendarProperty::Id => {
|
||||
result.insert_unchecked(CalendarProperty::Id, CalendarValue::Id(id));
|
||||
}
|
||||
CalendarProperty::Name => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::Name,
|
||||
calendar.preferences(personal_id).name.to_string(),
|
||||
);
|
||||
}
|
||||
CalendarProperty::Description => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::Description,
|
||||
calendar
|
||||
.preferences(personal_id)
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|v| v.to_string()),
|
||||
);
|
||||
}
|
||||
CalendarProperty::SortOrder => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::SortOrder,
|
||||
calendar.preferences(personal_id).sort_order.to_native(),
|
||||
);
|
||||
}
|
||||
CalendarProperty::IsDefault => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::IsDefault,
|
||||
default_calendar_id == Some(document_id),
|
||||
);
|
||||
}
|
||||
CalendarProperty::IsSubscribed => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::IsSubscribed,
|
||||
Value::Bool(
|
||||
calendar.preferences(personal_id).flags & CALENDAR_SUBSCRIBED != 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
CalendarProperty::Color => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::Color,
|
||||
calendar
|
||||
.preferences(personal_id)
|
||||
.color
|
||||
.as_ref()
|
||||
.map(|c| c.to_string()),
|
||||
);
|
||||
}
|
||||
CalendarProperty::IsVisible => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::IsVisible,
|
||||
Value::Bool(
|
||||
calendar.preferences(personal_id).flags & CALENDAR_INVISIBLE == 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
CalendarProperty::IncludeInAvailability => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::IncludeInAvailability,
|
||||
Value::Element(CalendarValue::IncludeInAvailability(
|
||||
IncludeInAvailability::from_flags(
|
||||
calendar.preferences(personal_id).flags.to_native(),
|
||||
)
|
||||
.unwrap_or(if is_owner {
|
||||
IncludeInAvailability::All
|
||||
} else {
|
||||
IncludeInAvailability::None
|
||||
}),
|
||||
)),
|
||||
);
|
||||
}
|
||||
CalendarProperty::DefaultAlertsWithTime => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::DefaultAlertsWithTime,
|
||||
Value::Object(Map::from_iter(
|
||||
calendar
|
||||
.default_alerts(personal_id, true)
|
||||
.map(default_alarm_to_value),
|
||||
)),
|
||||
);
|
||||
}
|
||||
CalendarProperty::DefaultAlertsWithoutTime => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::DefaultAlertsWithoutTime,
|
||||
Value::Object(Map::from_iter(
|
||||
calendar
|
||||
.default_alerts(personal_id, false)
|
||||
.map(default_alarm_to_value),
|
||||
)),
|
||||
);
|
||||
}
|
||||
CalendarProperty::TimeZone => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::TimeZone,
|
||||
calendar
|
||||
.preferences(personal_id)
|
||||
.time_zone
|
||||
.tz()
|
||||
.map(|tz| Value::Element(CalendarValue::Timezone(tz)))
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
CalendarProperty::ShareWith => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::ShareWith,
|
||||
JmapRights::share_with::<calendar::Calendar>(
|
||||
account_id,
|
||||
access_token,
|
||||
&calendar.acls.iter().map(AclGrant::from).collect::<Vec<_>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
CalendarProperty::MyRights => {
|
||||
result.insert_unchecked(
|
||||
CalendarProperty::MyRights,
|
||||
if access_token.is_shared(account_id) {
|
||||
JmapRights::rights::<calendar::Calendar>(
|
||||
calendar.acls.effective_acl(access_token),
|
||||
)
|
||||
} else {
|
||||
JmapRights::all_rights::<calendar::Calendar>()
|
||||
},
|
||||
);
|
||||
}
|
||||
property => {
|
||||
result.insert_unchecked(property.clone(), Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_alarm_to_value(
|
||||
alarm: &ArchivedDefaultAlert,
|
||||
) -> (
|
||||
Key<'static, CalendarProperty>,
|
||||
Value<'static, CalendarProperty, CalendarValue>,
|
||||
) {
|
||||
(
|
||||
Key::Owned(alarm.id.to_string()),
|
||||
Value::Object(Map::from(vec![
|
||||
(
|
||||
Key::Property(CalendarProperty::Type),
|
||||
Value::Element(CalendarValue::Type(JSCalendarType::Alert)),
|
||||
),
|
||||
(
|
||||
Key::Property(CalendarProperty::Action),
|
||||
Value::Element(CalendarValue::Action(if alarm.flags & ALERT_EMAIL != 0 {
|
||||
JSCalendarAlertAction::Email
|
||||
} else {
|
||||
JSCalendarAlertAction::Display
|
||||
})),
|
||||
),
|
||||
(
|
||||
Key::Property(CalendarProperty::Trigger),
|
||||
Value::Object(Map::from(vec![
|
||||
(
|
||||
Key::Property(CalendarProperty::Type),
|
||||
Value::Element(CalendarValue::Type(JSCalendarType::OffsetTrigger)),
|
||||
),
|
||||
(
|
||||
Key::Property(CalendarProperty::Offset),
|
||||
Value::Element(CalendarValue::Duration(alarm.offset.to_native())),
|
||||
),
|
||||
(
|
||||
Key::Property(CalendarProperty::RelativeTo),
|
||||
Value::Element(CalendarValue::RelativeTo(
|
||||
if alarm.flags & ALERT_RELATIVE_TO_END != 0 {
|
||||
JSCalendarRelativeTo::End
|
||||
} else {
|
||||
JSCalendarRelativeTo::Start
|
||||
},
|
||||
)),
|
||||
),
|
||||
])),
|
||||
),
|
||||
])),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use groupware::calendar::{
|
||||
CALENDAR_AVAILABILITY_ALL, CALENDAR_AVAILABILITY_ATTENDING, CALENDAR_AVAILABILITY_NONE,
|
||||
};
|
||||
use jmap_proto::object::calendar::IncludeInAvailability;
|
||||
|
||||
pub mod get;
|
||||
pub mod set;
|
||||
|
||||
pub(crate) trait Availability: Sized {
|
||||
fn from_flags(flags: u16) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl Availability for IncludeInAvailability {
|
||||
fn from_flags(flags: u16) -> Option<Self> {
|
||||
if flags & CALENDAR_AVAILABILITY_ALL != 0 {
|
||||
Some(IncludeInAvailability::All)
|
||||
} else if flags & CALENDAR_AVAILABILITY_ATTENDING != 0 {
|
||||
Some(IncludeInAvailability::Attending)
|
||||
} else if flags & CALENDAR_AVAILABILITY_NONE != 0 {
|
||||
Some(IncludeInAvailability::None)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::api::acl::{JmapAcl, JmapRights};
|
||||
use crate::changes::state::JmapCacheState;
|
||||
use calcard::jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use groupware::{
|
||||
DestroyArchive,
|
||||
cache::GroupwareCache,
|
||||
calendar::{
|
||||
ALERT_EMAIL, ALERT_RELATIVE_TO_END, ALERT_WITH_TIME, CALENDAR_AVAILABILITY_ALL,
|
||||
CALENDAR_AVAILABILITY_ATTENDING, CALENDAR_AVAILABILITY_NONE, CALENDAR_INVISIBLE,
|
||||
CALENDAR_SUBSCRIBED, Calendar, CalendarEvent, CalendarPreferences, DefaultAlert, Timezone,
|
||||
},
|
||||
};
|
||||
use http_proto::HttpSessionData;
|
||||
use jmap_proto::{
|
||||
error::set::SetError,
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::calendar::{self, CalendarProperty, CalendarValue, IncludeInAvailability},
|
||||
request::{MaybeInvalid, reference::MaybeIdReference},
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{JsonPointerItem, Key, Map, Value};
|
||||
use rand::{RngExt, distr::Alphanumeric};
|
||||
use store::{
|
||||
SerializeInfallible, ValueKey,
|
||||
ahash::AHashSet,
|
||||
write::{AlignedBytes, Archive, BatchBuilder, ValueClass},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
id::Id,
|
||||
};
|
||||
|
||||
pub trait CalendarSet: Sync + Send {
|
||||
fn calendar_set(
|
||||
&self,
|
||||
request: SetRequest<'_, calendar::Calendar>,
|
||||
access_token: &AccessToken,
|
||||
session: &HttpSessionData,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<calendar::Calendar>>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarSet for Server {
|
||||
async fn calendar_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, calendar::Calendar>,
|
||||
access_token: &AccessToken,
|
||||
_session: &HttpSessionData,
|
||||
) -> trc::Result<SetResponse<calendar::Calendar>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let cache = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
|
||||
.with_state(cache.assert_state(true, &request.if_in_state)?);
|
||||
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
||||
let is_shared = access_token.is_shared(account_id);
|
||||
let mut set_default = None;
|
||||
|
||||
// Process creates
|
||||
let mut batch = BatchBuilder::new();
|
||||
'create: for (id, object) in request.unwrap_create() {
|
||||
if is_shared {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("Cannot create calendars in a shared account."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
let mut calendar = Calendar {
|
||||
name: rand::rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(10)
|
||||
.map(char::from)
|
||||
.collect::<String>(),
|
||||
preferences: vec![CalendarPreferences {
|
||||
account_id,
|
||||
name: "".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Process changes
|
||||
if let Err(err) = update_calendar(None, object, &mut calendar, access_token, account_id)
|
||||
{
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Validate ACLs
|
||||
if !calendar.acls.is_empty() {
|
||||
if let Err(err) = self.acl_validate(&calendar.acls).await {
|
||||
response.not_created.append(id, err.into());
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
self.refresh_acls(&calendar.acls, None)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
// Insert record
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::Calendar, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
calendar
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(MaybeIdReference::Reference(id_ref)) =
|
||||
&request.arguments.on_success_set_is_default
|
||||
&& id_ref == &id
|
||||
{
|
||||
set_default = Some(document_id);
|
||||
}
|
||||
|
||||
response.created(id, document_id);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update() {
|
||||
let id = match id {
|
||||
MaybeInvalid::Value(id) => id,
|
||||
invalid => {
|
||||
response.not_updated.append(invalid, SetError::not_found());
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
response.not_updated.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Obtain calendar
|
||||
let document_id = id.document_id();
|
||||
let calendar_ = if let Some(calendar_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Calendar,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
calendar_
|
||||
} else {
|
||||
response.not_updated.append(id, SetError::not_found());
|
||||
continue 'update;
|
||||
};
|
||||
let calendar = calendar_
|
||||
.to_unarchived::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_calendar = calendar
|
||||
.deserialize::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Apply changes
|
||||
let has_acl_changes = match update_calendar(
|
||||
Some(id),
|
||||
object,
|
||||
&mut new_calendar,
|
||||
access_token,
|
||||
account_id,
|
||||
) {
|
||||
Ok(has_acl_changes_) => has_acl_changes_,
|
||||
Err(err) => {
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate ACL
|
||||
if is_shared {
|
||||
let acl = calendar.inner.acls.effective_acl(access_token);
|
||||
if !acl.contains(Acl::Modify) || (has_acl_changes && !acl.contains(Acl::Share)) {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("You are not allowed to modify this calendar."),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
if has_acl_changes {
|
||||
if let Err(err) = self.acl_validate(&new_calendar.acls).await {
|
||||
response.not_updated.append(id, err.into());
|
||||
continue 'update;
|
||||
}
|
||||
self.refresh_archived_acls(&new_calendar.acls, calendar.inner.acls.as_slice())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
// Update record
|
||||
new_calendar
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
calendar,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
let mut reset_default_calendar = false;
|
||||
if !will_destroy.is_empty() {
|
||||
let mut destroy_children = AHashSet::new();
|
||||
let mut destroy_parents = AHashSet::new();
|
||||
let default_calendar_id = self
|
||||
.store()
|
||||
.get_value::<u32>(ValueKey {
|
||||
account_id,
|
||||
collection: Collection::Principal.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let on_destroy_remove_events =
|
||||
request.arguments.on_destroy_remove_events.unwrap_or(false);
|
||||
for id in will_destroy {
|
||||
let document_id = id.document_id();
|
||||
|
||||
if !cache.has_container_id(&document_id) {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(calendar_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Calendar,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
continue;
|
||||
};
|
||||
|
||||
let calendar = calendar_
|
||||
.to_unarchived::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACLs
|
||||
if is_shared
|
||||
&& !calendar
|
||||
.inner
|
||||
.acls
|
||||
.effective_acl(access_token)
|
||||
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
|
||||
{
|
||||
response.not_destroyed.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
.with_description("You are not allowed to delete this calendar."),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Obtain children ids
|
||||
let children_ids = cache.children_ids(document_id).collect::<Vec<_>>();
|
||||
if !children_ids.is_empty() && !on_destroy_remove_events {
|
||||
response
|
||||
.not_destroyed
|
||||
.append(id, SetError::calendar_has_event());
|
||||
continue;
|
||||
}
|
||||
destroy_children.extend(children_ids.iter().copied());
|
||||
destroy_parents.insert(document_id);
|
||||
|
||||
// Delete record
|
||||
let delete_path = cache
|
||||
.container_resource_path_by_id(document_id)
|
||||
.map(|resource| cache.format_resource(resource));
|
||||
DestroyArchive(calendar)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
delete_path,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if default_calendar_id == Some(document_id) {
|
||||
reset_default_calendar = true;
|
||||
}
|
||||
|
||||
response.destroyed.push(id);
|
||||
}
|
||||
|
||||
// Delete children
|
||||
if !destroy_children.is_empty() {
|
||||
let account_info = self
|
||||
.account_info(access_token.account_id())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
for document_id in destroy_children {
|
||||
if let Some(event_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if event
|
||||
.inner
|
||||
.names
|
||||
.iter()
|
||||
.all(|n| destroy_parents.contains(&n.parent_id.to_native()))
|
||||
{
|
||||
// Event only belongs to calendars being deleted, delete it
|
||||
DestroyArchive(event).delete_all(
|
||||
&account_info,
|
||||
account_id,
|
||||
document_id,
|
||||
false,
|
||||
&mut batch,
|
||||
)?;
|
||||
} else {
|
||||
// Unlink calendar id from event
|
||||
let mut new_event = event
|
||||
.deserialize::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_event
|
||||
.names
|
||||
.retain(|n| !destroy_parents.contains(&n.parent_id));
|
||||
new_event.update(
|
||||
access_token.account_tenant_ids(),
|
||||
event,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set default calendar
|
||||
if let Some(MaybeIdReference::Id(id)) = &request.arguments.on_success_set_is_default {
|
||||
set_default = Some(id.document_id());
|
||||
}
|
||||
if let Some(default_calendar_id) = set_default {
|
||||
if response.not_created.is_empty()
|
||||
&& response.not_updated.is_empty()
|
||||
&& response.not_destroyed.is_empty()
|
||||
{
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.set(
|
||||
PrincipalField::DefaultCalendarId,
|
||||
default_calendar_id.serialize(),
|
||||
);
|
||||
}
|
||||
} else if reset_default_calendar {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.clear(PrincipalField::DefaultCalendarId);
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty()
|
||||
&& let Ok(change_id) = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.last_change_id(account_id)
|
||||
{
|
||||
self.notify_task_queue();
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_calendar(
|
||||
expected_id: Option<Id>,
|
||||
updates: Value<'_, CalendarProperty, CalendarValue>,
|
||||
calendar: &mut Calendar,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
) -> Result<bool, SetError<CalendarProperty>> {
|
||||
let personal_id = access_token.personal_id(account_id, Collection::Calendar);
|
||||
let mut has_acl_changes = false;
|
||||
|
||||
for (property, value) in updates.into_expanded_object() {
|
||||
let Key::Property(property) = property else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.to_owned())
|
||||
.with_description("Invalid property."));
|
||||
};
|
||||
|
||||
match (property, value) {
|
||||
(CalendarProperty::Name, Value::Str(value)) if (1..=255).contains(&value.len()) => {
|
||||
calendar.preferences_mut(personal_id).name = value.into_owned();
|
||||
}
|
||||
(CalendarProperty::Description, Value::Str(value)) if value.len() < 255 => {
|
||||
calendar.preferences_mut(personal_id).description = value.into_owned().into();
|
||||
}
|
||||
(CalendarProperty::Description, Value::Null) => {
|
||||
calendar.preferences_mut(personal_id).description = None;
|
||||
}
|
||||
(CalendarProperty::Color, Value::Str(value)) if value.len() < 16 => {
|
||||
calendar.preferences_mut(personal_id).color = value.into_owned().into();
|
||||
}
|
||||
(CalendarProperty::Color, Value::Null) => {
|
||||
calendar.preferences_mut(personal_id).color = None;
|
||||
}
|
||||
(CalendarProperty::TimeZone, Value::Element(CalendarValue::Timezone(tz))) => {
|
||||
calendar.preferences_mut(personal_id).time_zone = Timezone::IANA(tz.as_id());
|
||||
}
|
||||
(CalendarProperty::TimeZone, Value::Null) => {
|
||||
calendar.preferences_mut(personal_id).time_zone = Timezone::Default;
|
||||
}
|
||||
(CalendarProperty::SortOrder, Value::Number(value)) => {
|
||||
calendar.preferences_mut(personal_id).sort_order = value.cast_to_u64() as u32;
|
||||
}
|
||||
(CalendarProperty::IsSubscribed, Value::Bool(subscribe)) => {
|
||||
if subscribe {
|
||||
calendar.preferences_mut(personal_id).flags |= CALENDAR_SUBSCRIBED;
|
||||
} else {
|
||||
calendar.preferences_mut(personal_id).flags &= !CALENDAR_SUBSCRIBED;
|
||||
}
|
||||
}
|
||||
(CalendarProperty::IsVisible, Value::Bool(visible)) => {
|
||||
if visible {
|
||||
calendar.preferences_mut(personal_id).flags &= !CALENDAR_INVISIBLE;
|
||||
} else {
|
||||
calendar.preferences_mut(personal_id).flags |= CALENDAR_INVISIBLE;
|
||||
}
|
||||
}
|
||||
(
|
||||
CalendarProperty::IncludeInAvailability,
|
||||
Value::Element(CalendarValue::IncludeInAvailability(availability)),
|
||||
) => {
|
||||
let flags = &mut calendar.preferences_mut(personal_id).flags;
|
||||
|
||||
match availability {
|
||||
IncludeInAvailability::All => {
|
||||
*flags &= !(CALENDAR_AVAILABILITY_NONE | CALENDAR_AVAILABILITY_ATTENDING);
|
||||
*flags |= CALENDAR_AVAILABILITY_ALL;
|
||||
}
|
||||
IncludeInAvailability::Attending => {
|
||||
*flags &= !(CALENDAR_AVAILABILITY_NONE | CALENDAR_AVAILABILITY_ALL);
|
||||
*flags |= CALENDAR_AVAILABILITY_ATTENDING;
|
||||
}
|
||||
IncludeInAvailability::None => {
|
||||
*flags &= !(CALENDAR_AVAILABILITY_ATTENDING | CALENDAR_AVAILABILITY_ALL);
|
||||
*flags |= CALENDAR_AVAILABILITY_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
(
|
||||
property @ (CalendarProperty::DefaultAlertsWithTime
|
||||
| CalendarProperty::DefaultAlertsWithoutTime),
|
||||
Value::Object(value),
|
||||
) => {
|
||||
let with_time = matches!(property, CalendarProperty::DefaultAlertsWithTime);
|
||||
let alerts = &mut calendar.preferences_mut(personal_id).default_alerts;
|
||||
|
||||
alerts.retain(|alert| (alert.flags & ALERT_WITH_TIME != 0) != with_time);
|
||||
|
||||
for (key, value) in value.into_vec() {
|
||||
if let Value::Object(value) = value {
|
||||
alerts.push(value_to_default_alert(
|
||||
key.to_string().into_owned(),
|
||||
value,
|
||||
with_time,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
(CalendarProperty::ShareWith, value) => {
|
||||
calendar.acls = JmapRights::acl_set::<calendar::Calendar>(value)?;
|
||||
has_acl_changes = true;
|
||||
}
|
||||
(CalendarProperty::Pointer(pointer), value) => {
|
||||
let mut ptr_iter = pointer.iter();
|
||||
|
||||
match ptr_iter.next() {
|
||||
Some(JsonPointerItem::Key(Key::Property(CalendarProperty::ShareWith))) => {
|
||||
calendar.acls = JmapRights::acl_patch::<calendar::Calendar>(
|
||||
std::mem::take(&mut calendar.acls),
|
||||
ptr_iter,
|
||||
value,
|
||||
)?;
|
||||
has_acl_changes = true;
|
||||
}
|
||||
Some(JsonPointerItem::Key(Key::Property(
|
||||
property @ (CalendarProperty::DefaultAlertsWithTime
|
||||
| CalendarProperty::DefaultAlertsWithoutTime),
|
||||
))) => match (ptr_iter.next(), ptr_iter.next()) {
|
||||
(
|
||||
Some(key @ (JsonPointerItem::Key(_) | JsonPointerItem::Number(_))),
|
||||
None,
|
||||
) => {
|
||||
let id = match key {
|
||||
JsonPointerItem::Key(key) => key.to_string().into_owned(),
|
||||
JsonPointerItem::Number(n) => n.to_string(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let with_time =
|
||||
matches!(property, CalendarProperty::DefaultAlertsWithTime);
|
||||
let alerts = &mut calendar.preferences_mut(personal_id).default_alerts;
|
||||
alerts.retain(|alert| {
|
||||
(alert.flags & ALERT_WITH_TIME != 0) != with_time || alert.id != id
|
||||
});
|
||||
|
||||
if let Value::Object(value) = value {
|
||||
alerts.push(value_to_default_alert(id, value, with_time)?);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Pointer(pointer))
|
||||
.with_description("Field could not be patched."));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Pointer(pointer))
|
||||
.with_description("Field could not be patched."));
|
||||
}
|
||||
}
|
||||
}
|
||||
(CalendarProperty::Id, value) => {
|
||||
if !expected_id.is_some_and(|expected| crate::matches_id(&value, expected)) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Id)
|
||||
.with_description("The id property is immutable."));
|
||||
}
|
||||
}
|
||||
(property, _) => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property)
|
||||
.with_description("Field could not be set."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if calendar.preferences(personal_id).name.is_empty() {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Name)
|
||||
.with_description("Missing name."));
|
||||
}
|
||||
|
||||
Ok(has_acl_changes)
|
||||
}
|
||||
|
||||
fn value_to_default_alert(
|
||||
id: String,
|
||||
value: Map<'_, CalendarProperty, CalendarValue>,
|
||||
with_time: bool,
|
||||
) -> Result<DefaultAlert, SetError<CalendarProperty>> {
|
||||
let mut alert = DefaultAlert {
|
||||
id,
|
||||
..Default::default()
|
||||
};
|
||||
let mut has_offset = false;
|
||||
|
||||
for (key, value) in value.into_vec() {
|
||||
let Key::Property(key) = key else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match (key, value) {
|
||||
(CalendarProperty::Type, Value::Element(CalendarValue::Type(value)))
|
||||
if value != JSCalendarType::Alert =>
|
||||
{
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Trigger)
|
||||
.with_description("Invalid alert object type."));
|
||||
}
|
||||
(
|
||||
CalendarProperty::Action,
|
||||
Value::Element(CalendarValue::Action(JSCalendarAlertAction::Email)),
|
||||
) => {
|
||||
alert.flags |= ALERT_EMAIL;
|
||||
}
|
||||
(CalendarProperty::Trigger, Value::Object(value)) => {
|
||||
for (key, value) in value.into_vec() {
|
||||
let Key::Property(key) = key else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match (key, value) {
|
||||
(
|
||||
CalendarProperty::RelativeTo,
|
||||
Value::Element(CalendarValue::RelativeTo(JSCalendarRelativeTo::End)),
|
||||
) => {
|
||||
alert.flags |= ALERT_RELATIVE_TO_END;
|
||||
}
|
||||
(
|
||||
CalendarProperty::Offset,
|
||||
Value::Element(CalendarValue::Duration(value)),
|
||||
) => {
|
||||
alert.offset = value;
|
||||
has_offset = true;
|
||||
}
|
||||
(CalendarProperty::Offset, Value::Element(CalendarValue::Type(value)))
|
||||
if value != JSCalendarType::OffsetTrigger =>
|
||||
{
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Trigger)
|
||||
.with_description("Invalid alert trigger type."));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if has_offset {
|
||||
if with_time {
|
||||
alert.flags |= ALERT_WITH_TIME;
|
||||
}
|
||||
|
||||
Ok(alert)
|
||||
} else {
|
||||
Err(SetError::invalid_properties()
|
||||
.with_property(CalendarProperty::Trigger)
|
||||
.with_description("Missing alert offset."))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user