mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-12-09 21:05:59 +08:00
950 lines
36 KiB
Rust
950 lines
36 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::calendar_event::{CalendarSyntheticId, assert_is_unique_uid};
|
|
use calcard::{
|
|
common::timezone::Tz,
|
|
icalendar::{
|
|
ICalendarAction, ICalendarComponent, ICalendarComponentType, ICalendarDuration,
|
|
ICalendarEntry, ICalendarParameter, ICalendarParameterValue, ICalendarProperty,
|
|
ICalendarRelated, ICalendarValue,
|
|
},
|
|
jscalendar::{JSCalendar, JSCalendarDateTime, JSCalendarProperty, JSCalendarValue},
|
|
};
|
|
use chrono::DateTime;
|
|
use common::{DavName, DavResources, Server, auth::AccessToken};
|
|
use directory::Permission;
|
|
use groupware::{
|
|
DestroyArchive,
|
|
cache::GroupwareCache,
|
|
calendar::{
|
|
ALERT_EMAIL, ALERT_RELATIVE_TO_END, ArchivedDefaultAlert, Calendar, CalendarEvent,
|
|
CalendarEventData, EVENT_DRAFT, EVENT_HIDE_ATTENDEES, EVENT_INVITE_OTHERS,
|
|
EVENT_INVITE_SELF,
|
|
},
|
|
scheduling::{ItipMessages, event_create::itip_create, event_update::itip_update},
|
|
};
|
|
use http_proto::HttpSessionData;
|
|
use jmap_proto::{
|
|
error::set::SetError,
|
|
method::set::{SetRequest, SetResponse},
|
|
object::calendar_event,
|
|
request::IntoValid,
|
|
types::state::State,
|
|
};
|
|
use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Map, Value};
|
|
use std::{borrow::Cow, str::FromStr};
|
|
use store::{
|
|
ahash::AHashSet,
|
|
roaring::RoaringBitmap,
|
|
write::{BatchBuilder, now, serialize::rkyv_deserialize},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
acl::Acl,
|
|
blob::BlobId,
|
|
collection::{Collection, SyncCollection},
|
|
id::Id,
|
|
};
|
|
|
|
pub trait CalendarEventSet: Sync + Send {
|
|
fn calendar_event_set(
|
|
&self,
|
|
request: SetRequest<'_, calendar_event::CalendarEvent>,
|
|
access_token: &AccessToken,
|
|
session: &HttpSessionData,
|
|
) -> impl Future<Output = trc::Result<SetResponse<calendar_event::CalendarEvent>>> + Send;
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn create_calendar_event(
|
|
&self,
|
|
cache: &DavResources,
|
|
batch: &mut BatchBuilder,
|
|
access_token: &AccessToken,
|
|
account_id: u32,
|
|
send_scheduling_messages: bool,
|
|
can_add_calendars: &Option<RoaringBitmap>,
|
|
js_calendar_event: JSCalendar<'_, Id, BlobId>,
|
|
updates: Value<'_, JSCalendarProperty<Id>, JSCalendarValue<Id, BlobId>>,
|
|
) -> impl Future<Output = trc::Result<Result<CalendarCreateResult, SetError<JSCalendarProperty<Id>>>>>;
|
|
}
|
|
|
|
pub struct CalendarCreateResult {
|
|
pub document_id: u32,
|
|
pub nudge_queue: bool,
|
|
}
|
|
|
|
impl CalendarEventSet for Server {
|
|
async fn calendar_event_set(
|
|
&self,
|
|
mut request: SetRequest<'_, calendar_event::CalendarEvent>,
|
|
access_token: &AccessToken,
|
|
_session: &HttpSessionData,
|
|
) -> trc::Result<SetResponse<calendar_event::CalendarEvent>> {
|
|
let account_id = request.account_id.document_id();
|
|
let cache = self
|
|
.fetch_dav_resources(access_token, account_id, SyncCollection::Calendar)
|
|
.await?;
|
|
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
|
let will_destroy = request.unwrap_destroy().into_valid().collect::<Vec<_>>();
|
|
|
|
// Obtain calendarIds
|
|
let (can_add_calendars, can_delete_calendars, can_modify_calendars) =
|
|
if access_token.is_shared(account_id) {
|
|
(
|
|
cache
|
|
.shared_containers(access_token, [Acl::AddItems], true)
|
|
.into(),
|
|
cache
|
|
.shared_containers(access_token, [Acl::RemoveItems], true)
|
|
.into(),
|
|
cache
|
|
.shared_containers(access_token, [Acl::ModifyItems], true)
|
|
.into(),
|
|
)
|
|
} else {
|
|
(None, None, None)
|
|
};
|
|
|
|
// Process creates
|
|
let mut batch = BatchBuilder::new();
|
|
let send_scheduling_messages = request.arguments.send_scheduling_messages.unwrap_or(false);
|
|
let mut nudge_queue = false;
|
|
'create: for (id, object) in request.unwrap_create() {
|
|
match self
|
|
.create_calendar_event(
|
|
&cache,
|
|
&mut batch,
|
|
access_token,
|
|
account_id,
|
|
send_scheduling_messages,
|
|
&can_add_calendars,
|
|
JSCalendar::default(),
|
|
object,
|
|
)
|
|
.await?
|
|
{
|
|
Ok(result) => {
|
|
response.created(id, result.document_id);
|
|
nudge_queue |= result.nudge_queue;
|
|
}
|
|
Err(err) => {
|
|
response.not_created.append(id, err);
|
|
continue 'create;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process updates
|
|
'update: for (id, object) in request.unwrap_update().into_valid() {
|
|
// Make sure id won't be destroyed
|
|
if will_destroy.contains(&id) {
|
|
response.not_updated.append(id, SetError::will_destroy());
|
|
continue 'update;
|
|
} else if id.is_synthetic() {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Id)
|
|
.with_description("Updating synthetic ids is not yet supported."),
|
|
);
|
|
continue 'update;
|
|
}
|
|
|
|
// Obtain calendar_event card
|
|
let document_id = id.document_id();
|
|
let calendar_event_ = if let Some(calendar_event_) = self
|
|
.get_archive(account_id, Collection::CalendarEvent, document_id)
|
|
.await?
|
|
{
|
|
calendar_event_
|
|
} else {
|
|
response.not_updated.append(id, SetError::not_found());
|
|
continue 'update;
|
|
};
|
|
let calendar_event = calendar_event_
|
|
.to_unarchived::<CalendarEvent>()
|
|
.caused_by(trc::location!())?;
|
|
let mut new_calendar_event = calendar_event
|
|
.deserialize::<CalendarEvent>()
|
|
.caused_by(trc::location!())?;
|
|
let mut js_calendar_group =
|
|
std::mem::take(&mut new_calendar_event.data.event).into_jscalendar::<Id, BlobId>();
|
|
|
|
// Process changes
|
|
if let Err(err) = update_calendar_event(
|
|
access_token,
|
|
object,
|
|
&mut new_calendar_event,
|
|
&mut js_calendar_group,
|
|
) {
|
|
response.not_updated.append(id, err);
|
|
continue 'update;
|
|
}
|
|
|
|
// Convert JSCalendar to iCalendar
|
|
let Some(ical) = js_calendar_group.into_icalendar() else {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_description("Failed to convert calendar event to iCalendar."),
|
|
);
|
|
continue 'update;
|
|
};
|
|
new_calendar_event.data.event = ical;
|
|
|
|
// Validate UID
|
|
match (
|
|
new_calendar_event.data.event.uids().next(),
|
|
calendar_event.inner.data.event.uids().next(),
|
|
) {
|
|
(Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
|
|
(None, None) | (None, Some(_)) => {}
|
|
_ => {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Uid)
|
|
.with_description("You cannot change the UID of a calendar event."),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
|
|
// Validate new calendarIds
|
|
for calendar_id in new_calendar_event.added_calendar_ids(calendar_event.inner) {
|
|
if !cache.has_container_id(&calendar_id) {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::CalendarIds)
|
|
.with_description(format!(
|
|
"calendarId {} does not exist.",
|
|
Id::from(calendar_id)
|
|
)),
|
|
);
|
|
continue 'update;
|
|
} else if can_add_calendars
|
|
.as_ref()
|
|
.is_some_and(|ids| !ids.contains(calendar_id))
|
|
{
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::forbidden().with_description(format!(
|
|
"You are not allowed to add calendar events to calendar {}.",
|
|
Id::from(calendar_id)
|
|
)),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
|
|
// Validate deleted calendarIds
|
|
if let Some(can_delete_calendars) = &can_delete_calendars {
|
|
for calendar_id in new_calendar_event.removed_calendar_ids(calendar_event.inner) {
|
|
if !can_delete_calendars.contains(calendar_id) {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::forbidden().with_description(format!(
|
|
"You are not allowed to remove calendar events from calendar {}.",
|
|
Id::from(calendar_id)
|
|
)),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate changed calendarIds
|
|
if let Some(can_modify_calendars) = &can_modify_calendars {
|
|
for calendar_id in new_calendar_event.unchanged_calendar_ids(calendar_event.inner) {
|
|
if !can_modify_calendars.contains(calendar_id) {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::forbidden().with_description(format!(
|
|
"You are not allowed to modify calendar {}.",
|
|
Id::from(calendar_id)
|
|
)),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check size and quota
|
|
new_calendar_event.size = new_calendar_event.data.event.size() as u32;
|
|
if new_calendar_event.size as usize > self.core.groupware.max_ical_size {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties().with_description(format!(
|
|
"Event size {} exceeds the maximum allowed size of {} bytes.",
|
|
new_calendar_event.size, self.core.groupware.max_ical_size
|
|
)),
|
|
);
|
|
continue 'update;
|
|
}
|
|
|
|
// Obtain previous alarm
|
|
let now = now() as i64;
|
|
let prev_email_alarm = calendar_event.inner.data.next_alarm(now, Tz::Floating);
|
|
|
|
// Build event
|
|
let mut next_email_alarm = None;
|
|
new_calendar_event.data = CalendarEventData::new(
|
|
new_calendar_event.data.event,
|
|
Tz::Floating,
|
|
self.core.groupware.max_ical_instances,
|
|
&mut next_email_alarm,
|
|
);
|
|
|
|
// Scheduling
|
|
let mut itip_messages = None;
|
|
if send_scheduling_messages
|
|
&& self.core.groupware.itip_enabled
|
|
&& !access_token.emails.is_empty()
|
|
&& access_token.has_permission(Permission::CalendarSchedulingSend)
|
|
&& new_calendar_event.data.event_range_end() > now
|
|
{
|
|
let result = if new_calendar_event.schedule_tag.is_some() {
|
|
let old_ical = rkyv_deserialize(&calendar_event.inner.data.event)
|
|
.caused_by(trc::location!())?;
|
|
|
|
itip_update(
|
|
&mut new_calendar_event.data.event,
|
|
&old_ical,
|
|
access_token.emails.as_slice(),
|
|
)
|
|
} else {
|
|
itip_create(
|
|
&mut new_calendar_event.data.event,
|
|
access_token.emails.as_slice(),
|
|
)
|
|
};
|
|
|
|
match result {
|
|
Ok(messages) => {
|
|
let mut is_organizer = false;
|
|
if messages
|
|
.iter()
|
|
.map(|r| {
|
|
is_organizer = r.from_organizer;
|
|
r.to.len()
|
|
})
|
|
.sum::<usize>()
|
|
< self.core.groupware.itip_outbound_max_recipients
|
|
{
|
|
// Only update schedule tag if the user is the organizer
|
|
if is_organizer {
|
|
if let Some(schedule_tag) = &mut new_calendar_event.schedule_tag {
|
|
*schedule_tag += 1;
|
|
} else {
|
|
new_calendar_event.schedule_tag = Some(1);
|
|
}
|
|
}
|
|
|
|
itip_messages = Some(ItipMessages::new(messages));
|
|
} else {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Participants)
|
|
.with_description(concat!(
|
|
"The number of scheduling message recipients ",
|
|
"exceeds the maximum allowed."
|
|
)),
|
|
);
|
|
continue 'update;
|
|
}
|
|
}
|
|
Err(err) => {
|
|
if err.is_jmap_error() {
|
|
response.not_updated.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Participants)
|
|
.with_description(err.to_string()),
|
|
);
|
|
continue 'update;
|
|
}
|
|
|
|
// Event changed, but there are no iTIP messages to send
|
|
if let Some(schedule_tag) = &mut new_calendar_event.schedule_tag {
|
|
*schedule_tag += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
nudge_queue |= next_email_alarm.is_some() || itip_messages.is_some();
|
|
|
|
// Validate quota
|
|
let extra_bytes = (new_calendar_event.size as u64)
|
|
.saturating_sub(u32::from(calendar_event.inner.size) as u64);
|
|
if extra_bytes > 0 {
|
|
match self
|
|
.has_available_quota(
|
|
&self.get_resource_token(access_token, account_id).await?,
|
|
extra_bytes,
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {}
|
|
Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => {
|
|
response.not_updated.append(id, SetError::over_quota());
|
|
continue 'update;
|
|
}
|
|
Err(err) => return Err(err.caused_by(trc::location!())),
|
|
}
|
|
}
|
|
|
|
// Update record
|
|
new_calendar_event
|
|
.update(
|
|
access_token,
|
|
calendar_event,
|
|
account_id,
|
|
document_id,
|
|
&mut batch,
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
if prev_email_alarm != next_email_alarm {
|
|
if let Some(prev_alarm) = prev_email_alarm {
|
|
prev_alarm.delete_task(&mut batch);
|
|
}
|
|
if let Some(next_alarm) = next_email_alarm {
|
|
next_alarm.write_task(&mut batch);
|
|
}
|
|
}
|
|
if let Some(itip_messages) = itip_messages {
|
|
itip_messages
|
|
.queue(&mut batch)
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
response.updated.append(id, None);
|
|
}
|
|
|
|
// Process deletions
|
|
'destroy: for id in will_destroy {
|
|
let document_id = id.document_id();
|
|
|
|
if !cache.has_item_id(&document_id) {
|
|
response.not_destroyed.append(id, SetError::not_found());
|
|
continue;
|
|
} else if id.is_synthetic() {
|
|
response.not_destroyed.append(
|
|
id,
|
|
SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Id)
|
|
.with_description("Deleting synthetic ids is not yet supported."),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let Some(calendar_event_) = self
|
|
.get_archive(account_id, Collection::CalendarEvent, document_id)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
else {
|
|
response.not_destroyed.append(id, SetError::not_found());
|
|
continue;
|
|
};
|
|
|
|
let calendar_event = calendar_event_
|
|
.to_unarchived::<CalendarEvent>()
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Validate ACLs
|
|
if let Some(can_delete_calendars) = &can_delete_calendars {
|
|
for name in calendar_event.inner.names.iter() {
|
|
let parent_id = name.parent_id.to_native();
|
|
if !can_delete_calendars.contains(parent_id) {
|
|
response.not_destroyed.append(
|
|
id,
|
|
SetError::forbidden().with_description(format!(
|
|
"You are not allowed to remove events from calendar {}.",
|
|
Id::from(parent_id)
|
|
)),
|
|
);
|
|
continue 'destroy;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete event
|
|
DestroyArchive(calendar_event)
|
|
.delete_all(
|
|
access_token,
|
|
account_id,
|
|
document_id,
|
|
send_scheduling_messages,
|
|
&mut batch,
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
|
|
nudge_queue |= send_scheduling_messages;
|
|
|
|
response.destroyed.push(id);
|
|
}
|
|
|
|
// 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!())?;
|
|
|
|
if nudge_queue {
|
|
self.notify_task_queue();
|
|
}
|
|
|
|
response.new_state = State::Exact(change_id).into();
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
async fn create_calendar_event(
|
|
&self,
|
|
cache: &DavResources,
|
|
batch: &mut BatchBuilder,
|
|
access_token: &AccessToken,
|
|
account_id: u32,
|
|
send_scheduling_messages: bool,
|
|
can_add_calendars: &Option<RoaringBitmap>,
|
|
mut js_calendar_group: JSCalendar<'_, Id, BlobId>,
|
|
updates: Value<'_, JSCalendarProperty<Id>, JSCalendarValue<Id, BlobId>>,
|
|
) -> trc::Result<Result<CalendarCreateResult, SetError<JSCalendarProperty<Id>>>> {
|
|
// Process changes
|
|
let mut event = CalendarEvent::default();
|
|
let use_default_alerts = match update_calendar_event(
|
|
access_token,
|
|
updates,
|
|
&mut event,
|
|
&mut js_calendar_group,
|
|
) {
|
|
Ok(use_default_alerts) => use_default_alerts,
|
|
Err(err) => {
|
|
return Ok(Err(err));
|
|
}
|
|
};
|
|
|
|
// Convert JSCalendar to iCalendar
|
|
let Some(mut ical) = js_calendar_group.into_icalendar() else {
|
|
return Ok(Err(SetError::invalid_properties().with_description(
|
|
"Failed to convert calendar event to iCalendar.",
|
|
)));
|
|
};
|
|
|
|
// Verify that the calendar ids valid
|
|
let default_alert_comp_id = ical.components.len();
|
|
for name in &event.names {
|
|
if !cache.has_container_id(&name.parent_id) {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::CalendarIds)
|
|
.with_description(format!(
|
|
"calendarId {} does not exist.",
|
|
Id::from(name.parent_id)
|
|
))));
|
|
} else if can_add_calendars
|
|
.as_ref()
|
|
.is_some_and(|ids| !ids.contains(name.parent_id))
|
|
{
|
|
return Ok(Err(SetError::forbidden().with_description(format!(
|
|
"You are not allowed to add calendar events to calendar {}.",
|
|
Id::from(name.parent_id)
|
|
))));
|
|
} else if let Some(show_without_time) = use_default_alerts
|
|
&& let Some(_calendar) = self
|
|
.get_archive(account_id, Collection::Calendar, name.parent_id)
|
|
.await?
|
|
{
|
|
ical.components.extend(
|
|
_calendar
|
|
.unarchive::<Calendar>()
|
|
.caused_by(trc::location!())?
|
|
.default_alerts(access_token, !show_without_time)
|
|
.map(default_alert_to_ical),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add default alarms
|
|
if ical.components.len() > default_alert_comp_id {
|
|
let component_ids = default_alert_comp_id as u32..ical.components.len() as u32;
|
|
for component in &mut ical.components {
|
|
if component.component_type.is_event_or_todo()
|
|
&& !component.is_recurrence_override()
|
|
{
|
|
component.component_ids.extend(component_ids.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate UID
|
|
if let Err(err) = assert_is_unique_uid(self, account_id, ical.uids().next()).await? {
|
|
return Ok(Err(err));
|
|
}
|
|
|
|
// Check size and quota
|
|
let size = ical.size();
|
|
if size > self.core.groupware.max_ical_size {
|
|
return Ok(Err(SetError::invalid_properties().with_description(
|
|
format!(
|
|
"Event size {} exceeds the maximum allowed size of {} bytes.",
|
|
size, self.core.groupware.max_ical_size
|
|
),
|
|
)));
|
|
}
|
|
|
|
// Build event
|
|
let mut next_email_alarm = None;
|
|
event.data = CalendarEventData::new(
|
|
ical,
|
|
Tz::Floating,
|
|
self.core.groupware.max_ical_instances,
|
|
&mut next_email_alarm,
|
|
);
|
|
event.size = size as u32;
|
|
|
|
// Scheduling
|
|
let mut itip_messages = None;
|
|
if send_scheduling_messages
|
|
&& self.core.groupware.itip_enabled
|
|
&& !access_token.emails.is_empty()
|
|
&& access_token.has_permission(Permission::CalendarSchedulingSend)
|
|
&& event.data.event_range_end() > now() as i64
|
|
{
|
|
match itip_create(&mut event.data.event, access_token.emails.as_slice()) {
|
|
Ok(messages) => {
|
|
if messages.iter().map(|r| r.to.len()).sum::<usize>()
|
|
< self.core.groupware.itip_outbound_max_recipients
|
|
{
|
|
event.schedule_tag = Some(1);
|
|
itip_messages = Some(ItipMessages::new(messages));
|
|
} else {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Participants)
|
|
.with_description(concat!(
|
|
"The number of scheduling message recipients ",
|
|
"exceeds the maximum allowed."
|
|
))));
|
|
}
|
|
}
|
|
Err(err) => {
|
|
if err.is_jmap_error() {
|
|
return Ok(Err(SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Participants)
|
|
.with_description(err.to_string())));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let nudge_queue = next_email_alarm.is_some() || itip_messages.is_some();
|
|
|
|
// Validate quota
|
|
match self
|
|
.has_available_quota(
|
|
&self.get_resource_token(access_token, account_id).await?,
|
|
size as u64,
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {}
|
|
Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => {
|
|
return Ok(Err(SetError::over_quota()));
|
|
}
|
|
Err(err) => return Err(err.caused_by(trc::location!())),
|
|
}
|
|
|
|
// Insert record
|
|
let document_id = self
|
|
.store()
|
|
.assign_document_ids(account_id, Collection::CalendarEvent, 1)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
event
|
|
.insert(
|
|
access_token,
|
|
account_id,
|
|
document_id,
|
|
next_email_alarm,
|
|
batch,
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
|
|
if let Some(itip_messages) = itip_messages {
|
|
itip_messages.queue(batch).caused_by(trc::location!())?;
|
|
}
|
|
|
|
Ok(Ok(CalendarCreateResult {
|
|
document_id,
|
|
nudge_queue,
|
|
}))
|
|
}
|
|
}
|
|
|
|
fn update_calendar_event<'x>(
|
|
_access_token: &AccessToken,
|
|
updates: Value<'x, JSCalendarProperty<Id>, JSCalendarValue<Id, BlobId>>,
|
|
event: &mut CalendarEvent,
|
|
js_calendar_group: &mut JSCalendar<'x, Id, BlobId>,
|
|
) -> Result<Option<bool>, SetError<JSCalendarProperty<Id>>> {
|
|
// Extract event
|
|
let js_calendar_events = js_calendar_group
|
|
.0
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.get_mut(&Key::Property(JSCalendarProperty::Entries))
|
|
.unwrap()
|
|
.as_array_mut()
|
|
.unwrap();
|
|
|
|
let js_calendar_event = if let Some(js_calendar_event) = js_calendar_events.first_mut() {
|
|
js_calendar_event
|
|
} else {
|
|
js_calendar_events.push(Value::Object(Map::new()));
|
|
js_calendar_events.first_mut().unwrap()
|
|
};
|
|
|
|
let mut utc_start = None;
|
|
let mut utc_end = None;
|
|
let mut use_default_alerts = false;
|
|
let mut show_without_time = false;
|
|
let mut entries = js_calendar_event.as_object_mut().unwrap();
|
|
|
|
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) {
|
|
(JSCalendarProperty::IsDraft, Value::Bool(set)) => {
|
|
if set {
|
|
event.flags |= EVENT_DRAFT;
|
|
} else {
|
|
event.flags &= !EVENT_DRAFT;
|
|
}
|
|
}
|
|
(JSCalendarProperty::MayInviteSelf, Value::Bool(set)) => {
|
|
if set {
|
|
event.flags |= EVENT_INVITE_SELF;
|
|
} else {
|
|
event.flags &= !EVENT_INVITE_SELF;
|
|
}
|
|
}
|
|
(JSCalendarProperty::MayInviteOthers, Value::Bool(set)) => {
|
|
if set {
|
|
event.flags |= EVENT_INVITE_OTHERS;
|
|
} else {
|
|
event.flags &= !EVENT_INVITE_OTHERS;
|
|
}
|
|
}
|
|
(JSCalendarProperty::HideAttendees, Value::Bool(set)) => {
|
|
if set {
|
|
event.flags |= EVENT_HIDE_ATTENDEES;
|
|
} else {
|
|
event.flags &= !EVENT_HIDE_ATTENDEES;
|
|
}
|
|
}
|
|
(JSCalendarProperty::UseDefaultAlerts, Value::Bool(set)) => {
|
|
use_default_alerts = set;
|
|
}
|
|
(JSCalendarProperty::UtcStart, Value::Element(JSCalendarValue::DateTime(start))) => {
|
|
utc_start = Some(start.timestamp);
|
|
}
|
|
(JSCalendarProperty::UtcEnd, Value::Element(JSCalendarValue::DateTime(end))) => {
|
|
utc_end = Some(end.timestamp);
|
|
}
|
|
(JSCalendarProperty::CalendarIds, value) => {
|
|
patch_parent_ids(&mut event.names, None, value)?;
|
|
}
|
|
(JSCalendarProperty::Pointer(pointer), value) => {
|
|
if matches!(
|
|
pointer.first(),
|
|
Some(JsonPointerItem::Key(Key::Property(
|
|
JSCalendarProperty::CalendarIds
|
|
)))
|
|
) {
|
|
let mut pointer = pointer.iter();
|
|
pointer.next();
|
|
patch_parent_ids(&mut event.names, pointer.next(), value)?;
|
|
} else if !js_calendar_event.patch_jptr(pointer.iter(), value) {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::Pointer(pointer))
|
|
.with_description("Patch operation failed."));
|
|
}
|
|
entries = js_calendar_event.as_object_mut().unwrap();
|
|
}
|
|
(
|
|
property @ (JSCalendarProperty::Id
|
|
| JSCalendarProperty::BaseEventId
|
|
| JSCalendarProperty::IsOrigin
|
|
| JSCalendarProperty::Method),
|
|
_,
|
|
) => {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(property)
|
|
.with_description("This property is immutable."));
|
|
}
|
|
(
|
|
property @ (JSCalendarProperty::IsDraft
|
|
| JSCalendarProperty::MayInviteSelf
|
|
| JSCalendarProperty::MayInviteOthers
|
|
| JSCalendarProperty::HideAttendees
|
|
| JSCalendarProperty::UseDefaultAlerts
|
|
| JSCalendarProperty::UtcStart
|
|
| JSCalendarProperty::UtcEnd),
|
|
_,
|
|
) => {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(property)
|
|
.with_description("Invalid value."));
|
|
}
|
|
(property, value) => {
|
|
if let (JSCalendarProperty::ShowWithoutTime, Value::Bool(set)) = (&property, &value)
|
|
{
|
|
show_without_time = *set;
|
|
}
|
|
|
|
entries.insert(property, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate UTC start/end
|
|
if let (Some(mut start), Some(mut end)) = (utc_start, utc_end) {
|
|
if start >= end {
|
|
return Err(SetError::invalid_properties()
|
|
.with_properties([JSCalendarProperty::UtcStart, JSCalendarProperty::UtcEnd])
|
|
.with_description("utcStart must be before utcEnd."));
|
|
}
|
|
|
|
if let Some(timezone) = entries
|
|
.get(&Key::Property(JSCalendarProperty::TimeZone))
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|tz| Tz::from_str(tz.as_ref()).ok())
|
|
{
|
|
if let Some(dt_start) =
|
|
DateTime::from_timestamp(start, 0).map(|dt| dt.with_timezone(&timezone))
|
|
{
|
|
start = dt_start.naive_local().and_utc().timestamp();
|
|
}
|
|
if let Some(dt_end) =
|
|
DateTime::from_timestamp(end, 0).map(|dt| dt.with_timezone(&timezone))
|
|
{
|
|
end = dt_end.naive_local().and_utc().timestamp();
|
|
}
|
|
} else {
|
|
entries.insert(
|
|
Key::Property(JSCalendarProperty::TimeZone),
|
|
Value::Str(Cow::Borrowed("Etc/UTC")),
|
|
);
|
|
}
|
|
|
|
entries.insert(
|
|
Key::Property(JSCalendarProperty::Start),
|
|
Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new(
|
|
start, true,
|
|
))),
|
|
);
|
|
entries.insert(
|
|
Key::Property(JSCalendarProperty::Duration),
|
|
Value::Element(JSCalendarValue::Duration(ICalendarDuration::from_seconds(
|
|
end - start,
|
|
))),
|
|
);
|
|
} else if utc_start.is_some() || utc_end.is_some() {
|
|
return Err(SetError::invalid_properties()
|
|
.with_properties([JSCalendarProperty::UtcStart, JSCalendarProperty::UtcEnd])
|
|
.with_description("Both utcStart and utcEnd must be provided."));
|
|
}
|
|
|
|
// Make sure the calendar_event belongs to at least one calendar
|
|
if event.names.is_empty() {
|
|
return Err(SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::CalendarIds)
|
|
.with_description("Event has to belong to at least one calendar."));
|
|
}
|
|
|
|
Ok(use_default_alerts.then_some(show_without_time))
|
|
}
|
|
|
|
fn patch_parent_ids(
|
|
current: &mut Vec<DavName>,
|
|
patch: Option<&JsonPointerItem<JSCalendarProperty<Id>>>,
|
|
update: Value<'_, JSCalendarProperty<Id>, JSCalendarValue<Id, BlobId>>,
|
|
) -> Result<(), SetError<JSCalendarProperty<Id>>> {
|
|
match (patch, update) {
|
|
(
|
|
Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdValue(id)))),
|
|
Value::Bool(false) | Value::Null,
|
|
) => {
|
|
let id = id.document_id();
|
|
current.retain(|name| name.parent_id != id);
|
|
Ok(())
|
|
}
|
|
(
|
|
Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdValue(id)))),
|
|
Value::Bool(true),
|
|
) => {
|
|
let id = id.document_id();
|
|
if !current.iter().any(|name| name.parent_id == id) {
|
|
current.push(DavName::new_with_rand_name(id));
|
|
}
|
|
Ok(())
|
|
}
|
|
(None, Value::Object(object)) => {
|
|
let mut new_ids = object
|
|
.into_expanded_boolean_set()
|
|
.filter_map(|id| {
|
|
if let Key::Property(JSCalendarProperty::IdValue(id)) = id {
|
|
Some(id.document_id())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<AHashSet<_>>();
|
|
|
|
current.retain(|name| new_ids.remove(&name.parent_id));
|
|
|
|
for id in new_ids {
|
|
current.push(DavName::new_with_rand_name(id));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
_ => Err(SetError::invalid_properties()
|
|
.with_property(JSCalendarProperty::CalendarIds)
|
|
.with_description("Invalid patch operation for calendarIds.")),
|
|
}
|
|
}
|
|
|
|
fn default_alert_to_ical(alert: &ArchivedDefaultAlert) -> ICalendarComponent {
|
|
let flags = alert.flags.to_native();
|
|
ICalendarComponent {
|
|
component_type: ICalendarComponentType::VAlarm,
|
|
entries: vec![
|
|
ICalendarEntry::new(ICalendarProperty::Action).with_value(
|
|
if flags & ALERT_EMAIL != 0 {
|
|
ICalendarValue::Action(ICalendarAction::Email)
|
|
} else {
|
|
ICalendarValue::Action(ICalendarAction::Display)
|
|
},
|
|
),
|
|
ICalendarEntry::new(ICalendarProperty::Trigger)
|
|
.with_param_opt((flags & ALERT_RELATIVE_TO_END != 0).then_some(
|
|
ICalendarParameter::related(ICalendarParameterValue::Related(
|
|
ICalendarRelated::End,
|
|
)),
|
|
))
|
|
.with_value(ICalendarValue::Duration(alert.offset.to_native())),
|
|
],
|
|
component_ids: vec![],
|
|
}
|
|
}
|