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:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
||||
use dav_proto::RequestHeaders;
|
||||
use groupware::{
|
||||
DestroyArchive,
|
||||
cache::GroupwareCache,
|
||||
calendar::{Calendar, CalendarEvent},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use registry::schema::enums::Permission;
|
||||
use store::write::{BatchBuilder, ValueClass};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::PrincipalField,
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarDeleteRequestHandler: Sync + Send {
|
||||
fn handle_calendar_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarDeleteRequestHandler for Server {
|
||||
async fn handle_calendar_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Check resource type
|
||||
let delete_resource = resources
|
||||
.by_path(delete_path)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = delete_resource.document_id();
|
||||
let account_info = self
|
||||
.scheduling_account_info(access_token.account_id(), account_id)
|
||||
.await?;
|
||||
let send_itip = self.core.groupware.itip_enabled
|
||||
&& !headers.no_schedule_reply
|
||||
&& !account_info.addresses().is_empty()
|
||||
&& access_token.has_permission(Permission::CalendarSchedulingSend);
|
||||
|
||||
// Fetch entry
|
||||
let mut batch = BatchBuilder::new();
|
||||
if delete_resource.is_container() {
|
||||
// Deleting the default calendar is not allowed
|
||||
#[cfg(not(any(feature = "dev_mode", feature = "test_mode")))]
|
||||
if self
|
||||
.core
|
||||
.groupware
|
||||
.default_calendar_name
|
||||
.as_ref()
|
||||
.is_some_and(|name| name == delete_path)
|
||||
{
|
||||
return Err(DavError::Condition(crate::DavErrorCondition::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
dav_proto::schema::response::CalCondition::DefaultCalendarNeeded,
|
||||
)));
|
||||
}
|
||||
|
||||
let calendar_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Calendar,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let calendar = calendar_
|
||||
.to_unarchived::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !calendar
|
||||
.inner
|
||||
.acls
|
||||
.effective_acl(access_token)
|
||||
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::Calendar,
|
||||
document_id: document_id.into(),
|
||||
etag: calendar.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Delete calendar and events
|
||||
DestroyArchive(calendar)
|
||||
.delete_with_events(
|
||||
self,
|
||||
&account_info,
|
||||
account_id,
|
||||
document_id,
|
||||
resources
|
||||
.subtree(delete_path)
|
||||
.filter(|r| !r.is_container())
|
||||
.map(|r| r.document_id())
|
||||
.collect::<Vec<_>>(),
|
||||
resources.format_resource(delete_resource).into(),
|
||||
send_itip,
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Reset default calendar id
|
||||
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!())?;
|
||||
if default_calendar_id.is_some_and(|id| id == document_id) {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.clear(PrincipalField::DefaultCalendarId);
|
||||
}
|
||||
} else {
|
||||
// Validate ACL
|
||||
let calendar_id = delete_resource.parent_id().unwrap();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(access_token, calendar_id, Acl::RemoveItems)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEvent,
|
||||
document_id: document_id.into(),
|
||||
etag: event_.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate schedule tag
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
if headers.if_schedule_tag.is_some()
|
||||
&& event.inner.schedule_tag.as_ref().map(|t| t.to_native())
|
||||
!= headers.if_schedule_tag
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
|
||||
// Delete event
|
||||
DestroyArchive(event)
|
||||
.delete(
|
||||
&account_info,
|
||||
account_id,
|
||||
document_id,
|
||||
calendar_id,
|
||||
resources.format_resource(delete_resource).into(),
|
||||
send_itip,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::query::CalendarQueryHandler;
|
||||
use crate::{DavError, calendar::query::is_resource_in_time_range, common::uri::DavUriResource};
|
||||
use calcard::{
|
||||
common::{PartialDateTime, timezone::Tz},
|
||||
icalendar::{
|
||||
ArchivedICalendarComponentType, ArchivedICalendarEntry, ArchivedICalendarParameterName,
|
||||
ArchivedICalendarParameterValue, ArchivedICalendarProperty, ArchivedICalendarStatus,
|
||||
ArchivedICalendarValue, ICalendar, ICalendarComponent, ICalendarComponentType,
|
||||
ICalendarEntry, ICalendarFreeBusyType, ICalendarParameter, ICalendarPeriod,
|
||||
ICalendarProperty, ICalendarTransparency, ICalendarValue,
|
||||
},
|
||||
};
|
||||
use common::{DavResourcePath, DavResources, PROD_ID, Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::request::FreeBusyQuery};
|
||||
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::str::FromStr;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{
|
||||
ahash::AHashMap,
|
||||
write::{now, serialize::rkyv_deserialize},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
TimeRange,
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarFreebusyRequestHandler: Sync + Send {
|
||||
fn handle_calendar_freebusy_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: FreeBusyQuery,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn build_freebusy_object(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
request: FreeBusyQuery,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
resource: DavResourcePath<'_>,
|
||||
) -> impl Future<Output = crate::Result<ICalendar>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarFreebusyRequestHandler for Server {
|
||||
async fn handle_calendar_freebusy_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: FreeBusyQuery,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if !resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
self.build_freebusy_object(access_token, request, &resources, account_id, resource)
|
||||
.await
|
||||
.map(|ical| {
|
||||
HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_text_body(ical.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_freebusy_object(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
request: FreeBusyQuery,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
resource: DavResourcePath<'_>,
|
||||
) -> crate::Result<ICalendar> {
|
||||
// Obtain shared ids
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
resources
|
||||
.shared_items(
|
||||
access_token,
|
||||
[Acl::ReadItems, Acl::SchedulingReadFreeBusy],
|
||||
false,
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build FreeBusy component
|
||||
let default_tz = resource
|
||||
.resource
|
||||
.calendar_preferences(account_id)
|
||||
.map(|p| p.tz)
|
||||
.unwrap_or(Tz::UTC);
|
||||
let mut entries = Vec::with_capacity(6);
|
||||
if let Some(range) = request.range {
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(range.start),
|
||||
))],
|
||||
});
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Dtend,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(range.end),
|
||||
))],
|
||||
});
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Dtstamp,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(now() as i64),
|
||||
))],
|
||||
});
|
||||
|
||||
let document_ids = resources
|
||||
.children(resource.document_id())
|
||||
.filter(|resource| {
|
||||
shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(resource.document_id()))
|
||||
&& is_resource_in_time_range(resource.resource, &range)
|
||||
})
|
||||
.map(|resource| resource.document_id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut fb_entries: AHashMap<ICalendarFreeBusyType, Vec<(i64, i64)>> =
|
||||
AHashMap::with_capacity(document_ids.len());
|
||||
let max_instances = self.core.groupware.max_ical_instances;
|
||||
let mut total_instances: usize = 0;
|
||||
|
||||
for document_id in document_ids {
|
||||
let Some(archive) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let event = archive
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
/*
|
||||
Only VEVENT components without a TRANSP property or with the TRANSP
|
||||
property set to OPAQUE, and VFREEBUSY components SHOULD be considered
|
||||
in generating the free busy time information.
|
||||
*/
|
||||
let mut components = event
|
||||
.data
|
||||
.event
|
||||
.components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, comp)| {
|
||||
(matches!(comp.component_type, ArchivedICalendarComponentType::VEvent)
|
||||
&& comp
|
||||
.transparency()
|
||||
.is_none_or(|t| t == &ICalendarTransparency::Opaque))
|
||||
|| matches!(
|
||||
comp.component_type,
|
||||
ArchivedICalendarComponentType::VFreebusy
|
||||
)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if components.peek().is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let events =
|
||||
CalendarQueryHandler::new(event, Some(range), default_tz).into_expanded_times();
|
||||
|
||||
if events.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
total_instances = total_instances.saturating_add(events.len());
|
||||
if total_instances > max_instances {
|
||||
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
||||
}
|
||||
|
||||
for (component_id, component) in components {
|
||||
let component_id = component_id as u32;
|
||||
match component.component_type {
|
||||
ArchivedICalendarComponentType::VEvent => {
|
||||
let fbtype = match component.status() {
|
||||
Some(ArchivedICalendarStatus::Cancelled) => continue,
|
||||
Some(ArchivedICalendarStatus::Tentative) => {
|
||||
ICalendarFreeBusyType::BusyTentative
|
||||
}
|
||||
_ => ICalendarFreeBusyType::Busy,
|
||||
};
|
||||
|
||||
let mut events_in_range = Vec::new();
|
||||
for event in &events {
|
||||
if event.comp_id == component_id
|
||||
&& range.is_in_range(false, event.start, event.end)
|
||||
{
|
||||
events_in_range.push((event.start, event.end));
|
||||
}
|
||||
}
|
||||
|
||||
if !events_in_range.is_empty() {
|
||||
fb_entries
|
||||
.entry(fbtype)
|
||||
.or_default()
|
||||
.extend(events_in_range);
|
||||
}
|
||||
}
|
||||
ArchivedICalendarComponentType::VFreebusy => {
|
||||
for entry in component.entries.iter() {
|
||||
if matches!(entry.name, ArchivedICalendarProperty::Freebusy) {
|
||||
let mut fb_in_range =
|
||||
freebusy_in_range_utc(entry, &range, default_tz).peekable();
|
||||
if fb_in_range.peek().is_some() {
|
||||
let fb_type = entry
|
||||
.params
|
||||
.iter()
|
||||
.find_map(|param| {
|
||||
if let (
|
||||
ArchivedICalendarParameterName::Fbtype,
|
||||
ArchivedICalendarParameterValue::Fbtype(param),
|
||||
) = (¶m.name, ¶m.value)
|
||||
{
|
||||
rkyv_deserialize(param).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(ICalendarFreeBusyType::Busy);
|
||||
|
||||
fb_entries.entry(fb_type).or_default().extend(fb_in_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (fbtype, events_in_range) in fb_entries {
|
||||
entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Freebusy,
|
||||
params: vec![ICalendarParameter::fbtype(fbtype)],
|
||||
values: merge_intervals(events_in_range),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build ICalendar
|
||||
Ok(ICalendar {
|
||||
components: vec![
|
||||
ICalendarComponent {
|
||||
component_type: ICalendarComponentType::VCalendar,
|
||||
entries: vec![
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Version,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text("2.0".to_string())],
|
||||
},
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Prodid,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text(PROD_ID.to_string())],
|
||||
},
|
||||
],
|
||||
component_ids: vec![1],
|
||||
},
|
||||
ICalendarComponent {
|
||||
component_type: ICalendarComponentType::VFreebusy,
|
||||
entries,
|
||||
component_ids: vec![],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_intervals(mut intervals: Vec<(i64, i64)>) -> Vec<ICalendarValue> {
|
||||
if intervals.len() > 1 {
|
||||
intervals.sort_unstable_by_key(|a| a.0);
|
||||
|
||||
let mut unique_intervals = Vec::new();
|
||||
let mut start_time = intervals[0].0;
|
||||
let mut end_time = intervals[0].1;
|
||||
|
||||
for &(curr_start, curr_end) in intervals.iter().skip(1) {
|
||||
if curr_start <= end_time {
|
||||
end_time = end_time.max(curr_end);
|
||||
} else {
|
||||
unique_intervals.push(build_ical_value(start_time, end_time));
|
||||
start_time = curr_start;
|
||||
end_time = curr_end;
|
||||
}
|
||||
}
|
||||
|
||||
unique_intervals.push(build_ical_value(start_time, end_time));
|
||||
unique_intervals
|
||||
} else {
|
||||
intervals
|
||||
.into_iter()
|
||||
.map(|(start, end)| build_ical_value(start, end))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ical_value(from: i64, to: i64) -> ICalendarValue {
|
||||
ICalendarValue::Period(ICalendarPeriod::Range {
|
||||
start: PartialDateTime::from_utc_timestamp(from),
|
||||
end: PartialDateTime::from_utc_timestamp(to),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn freebusy_in_range(
|
||||
entry: &ArchivedICalendarEntry,
|
||||
range: &TimeRange,
|
||||
default_tz: Tz,
|
||||
) -> impl Iterator<Item = ICalendarValue> {
|
||||
let tz = entry
|
||||
.tz_id()
|
||||
.and_then(|tz_id| Tz::from_str(tz_id).ok())
|
||||
.unwrap_or(default_tz);
|
||||
|
||||
entry.values.iter().filter_map(move |value| {
|
||||
if let ArchivedICalendarValue::Period(period) = &value {
|
||||
period.time_range(tz).and_then(|(start, end)| {
|
||||
let start = start.timestamp();
|
||||
let end = end.timestamp();
|
||||
if range.is_in_range(false, start, end) {
|
||||
rkyv_deserialize(value).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn freebusy_in_range_utc(
|
||||
entry: &ArchivedICalendarEntry,
|
||||
range: &TimeRange,
|
||||
default_tz: Tz,
|
||||
) -> impl Iterator<Item = (i64, i64)> {
|
||||
let tz = entry
|
||||
.tz_id()
|
||||
.and_then(|tz_id| Tz::from_str(tz_id).ok())
|
||||
.unwrap_or(default_tz);
|
||||
|
||||
entry.values.iter().filter_map(move |value| {
|
||||
if let ArchivedICalendarValue::Period(period) = &value {
|
||||
period.time_range(tz).and_then(|(start, end)| {
|
||||
let start = start.timestamp();
|
||||
let end = end.timestamp();
|
||||
if range.is_in_range(false, start, end) {
|
||||
Some((start, end))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
|
||||
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarGetRequestHandler: Sync + Send {
|
||||
fn handle_calendar_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarGetRequestHandler for Server {
|
||||
async fn handle_calendar_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
resource.parent_id().unwrap(),
|
||||
Acl::ReadItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch event
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let event = event_
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
let etag = event_.etag();
|
||||
let schedule_tag = event.schedule_tag.as_ref().map(|tag| tag.to_native());
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEvent,
|
||||
document_id: resource.document_id().into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_etag(etag)
|
||||
.with_schedule_tag_opt(schedule_tag)
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string());
|
||||
|
||||
let ical = event.data.event.to_string();
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(ical))
|
||||
} else {
|
||||
Ok(response.with_content_length(ical.len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::proppatch::CalendarPropPatchRequestHandler;
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{Namespace, request::MkCol, response::MkColResponse},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{Calendar, CalendarPreferences},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
|
||||
pub(crate) trait CalendarMkColRequestHandler: Sync + Send {
|
||||
fn handle_calendar_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarMkColRequestHandler for Server {
|
||||
async fn handle_calendar_mkcol_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: Option<MkCol>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let name = resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
} else if name.contains('/')
|
||||
|| self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.by_path(name)
|
||||
.is_some()
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: name,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::MKCOL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build file container
|
||||
let mut calendar = Calendar {
|
||||
name: name.to_string(),
|
||||
preferences: vec![CalendarPreferences {
|
||||
account_id,
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Apply MKCOL properties
|
||||
let mut return_prop_stat = None;
|
||||
let mut is_mkcalendar = false;
|
||||
if let Some(mkcol) = request {
|
||||
let mut prop_stat = PropStatBuilder::default();
|
||||
is_mkcalendar = mkcol.is_mkcalendar;
|
||||
if !self.apply_calendar_properties(
|
||||
access_token.personal_id(account_id, Collection::Calendar),
|
||||
&mut calendar,
|
||||
false,
|
||||
mkcol.props,
|
||||
&mut prop_stat,
|
||||
) {
|
||||
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.with_mkcalendar(is_mkcalendar)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if headers.ret != Return::Minimal {
|
||||
return_prop_stat = Some(prop_stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
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!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.with_mkcalendar(is_mkcalendar)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod copy_move;
|
||||
pub mod delete;
|
||||
pub mod freebusy;
|
||||
pub mod get;
|
||||
pub mod mkcol;
|
||||
pub mod proppatch;
|
||||
pub mod query;
|
||||
pub mod scheduling;
|
||||
pub mod update;
|
||||
|
||||
use crate::{DavError, DavErrorCondition};
|
||||
use common::{DavResources, Server};
|
||||
use dav_proto::schema::{
|
||||
property::{CalDavProperty, CalendarData, DavProperty, WebDavProperty},
|
||||
response::CalCondition,
|
||||
};
|
||||
use groupware::scheduling::ItipError;
|
||||
use hyper::StatusCode;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::CalendarEventField};
|
||||
|
||||
pub(crate) static CALENDAR_CONTAINER_PROPS: [DavProperty; 31] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
DavProperty::WebDav(WebDavProperty::GetLastModified),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::Acl),
|
||||
DavProperty::WebDav(WebDavProperty::AclRestrictions),
|
||||
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes),
|
||||
DavProperty::WebDav(WebDavProperty::QuotaUsedBytes),
|
||||
DavProperty::CalDav(CalDavProperty::CalendarDescription),
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCalendarData),
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCollationSet),
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
|
||||
DavProperty::CalDav(CalDavProperty::CalendarTimezone),
|
||||
DavProperty::CalDav(CalDavProperty::MaxResourceSize),
|
||||
DavProperty::CalDav(CalDavProperty::MinDateTime),
|
||||
DavProperty::CalDav(CalDavProperty::MaxDateTime),
|
||||
DavProperty::CalDav(CalDavProperty::MaxInstances),
|
||||
DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance),
|
||||
DavProperty::CalDav(CalDavProperty::TimezoneServiceSet),
|
||||
DavProperty::CalDav(CalDavProperty::TimezoneId),
|
||||
];
|
||||
|
||||
pub(crate) static CALENDAR_ITEM_PROPS: [DavProperty; 20] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
DavProperty::WebDav(WebDavProperty::GetLastModified),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::Acl),
|
||||
DavProperty::WebDav(WebDavProperty::AclRestrictions),
|
||||
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLength),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType),
|
||||
DavProperty::CalDav(CalDavProperty::CalendarData(CalendarData {
|
||||
properties: vec![],
|
||||
expand: None,
|
||||
limit_recurrence: None,
|
||||
limit_freebusy: None,
|
||||
})),
|
||||
];
|
||||
|
||||
pub(crate) async fn assert_is_unique_uid(
|
||||
server: &Server,
|
||||
resources: &DavResources,
|
||||
account_id: u32,
|
||||
calendar_id: u32,
|
||||
uid: Option<&str>,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(uid) = uid {
|
||||
let hits = server
|
||||
.document_ids_matching(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
CalendarEventField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !hits.is_empty() {
|
||||
for path in resources.children(calendar_id) {
|
||||
if hits.contains(path.document_id()) {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::NoUidConflict(resources.format_resource(path).into()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) trait ItipPrecondition {
|
||||
fn failed_precondition(&self) -> Option<CalCondition>;
|
||||
}
|
||||
|
||||
impl ItipPrecondition for ItipError {
|
||||
fn failed_precondition(&self) -> Option<CalCondition> {
|
||||
match self {
|
||||
ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents),
|
||||
ItipError::OrganizerIsLocalAddress
|
||||
| ItipError::SenderIsNotParticipant(_)
|
||||
| ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer),
|
||||
ItipError::CannotModifyProperty(_)
|
||||
| ItipError::CannotModifyInstance
|
||||
| ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange),
|
||||
ItipError::MissingUid
|
||||
| ItipError::MultipleUid
|
||||
| ItipError::MultipleObjectTypes
|
||||
| ItipError::MultipleObjectInstances
|
||||
| ItipError::MissingMethod
|
||||
| ItipError::InvalidComponentType
|
||||
| ItipError::OutOfSequence
|
||||
| ItipError::UnknownParticipant(_)
|
||||
| ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::common::timezone::Tz;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{
|
||||
Namespace,
|
||||
property::{CalDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
|
||||
request::{DavPropertyValue, PropertyUpdate},
|
||||
response::{BaseCondition, CalCondition, MultiStatus, Response},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{Calendar, CalendarEvent, SupportedComponent, Timezone},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::str::FromStr;
|
||||
use store::write::BatchBuilder;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
pub(crate) trait CalendarPropPatchRequestHandler: Sync + Send {
|
||||
fn handle_calendar_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: PropertyUpdate,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn apply_calendar_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
calendar: &mut Calendar,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
|
||||
fn apply_event_properties(
|
||||
&self,
|
||||
event: &mut CalendarEvent,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl CalendarPropPatchRequestHandler for Server {
|
||||
async fn handle_calendar_proppatch_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
mut request: PropertyUpdate,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let uri = headers.uri;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resource_
|
||||
.resource
|
||||
.and_then(|r| resources.by_path(r))
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let document_id = resource.document_id();
|
||||
let collection = if resource.is_container() {
|
||||
Collection::Calendar
|
||||
} else {
|
||||
Collection::CalendarEvent
|
||||
};
|
||||
|
||||
if !request.has_changes() {
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
|
||||
// Verify ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
let (acl, document_id) = if resource.is_container() {
|
||||
(Acl::Modify, resource.document_id())
|
||||
} else {
|
||||
(Acl::ModifyItems, resource.parent_id().unwrap())
|
||||
};
|
||||
|
||||
if !resources.has_access_to_container(access_token, document_id, acl) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch archive
|
||||
let archive = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection,
|
||||
document_id: document_id.into(),
|
||||
etag: archive.etag().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PROPPATCH,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_success;
|
||||
let mut batch = BatchBuilder::new();
|
||||
let mut items = PropStatBuilder::default();
|
||||
|
||||
let etag = if resource.is_container() {
|
||||
// Deserialize
|
||||
let calendar = archive
|
||||
.to_unarchived::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_calendar = archive
|
||||
.deserialize::<Calendar>()
|
||||
.caused_by(trc::location!())?;
|
||||
let personal_id = access_token.personal_id(account_id, Collection::Calendar);
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_calendar_properties(
|
||||
personal_id,
|
||||
&mut new_calendar,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_calendar_properties(
|
||||
personal_id,
|
||||
&mut new_calendar,
|
||||
true,
|
||||
request.set,
|
||||
&mut items,
|
||||
);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_calendar_properties(
|
||||
personal_id,
|
||||
&mut new_calendar,
|
||||
request.remove,
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_calendar
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
calendar,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
calendar.etag().into()
|
||||
}
|
||||
} else {
|
||||
// Deserialize
|
||||
let event = archive
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_event = archive
|
||||
.deserialize::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Remove properties
|
||||
if !request.set_first && !request.remove.is_empty() {
|
||||
remove_event_properties(
|
||||
&mut new_event,
|
||||
std::mem::take(&mut request.remove),
|
||||
&mut items,
|
||||
);
|
||||
}
|
||||
|
||||
// Set properties
|
||||
is_success = self.apply_event_properties(&mut new_event, true, request.set, &mut items);
|
||||
|
||||
// Remove properties
|
||||
if is_success && !request.remove.is_empty() {
|
||||
remove_event_properties(&mut new_event, request.remove, &mut items);
|
||||
}
|
||||
|
||||
if is_success {
|
||||
new_event
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
event,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag()
|
||||
} else {
|
||||
event.etag().into()
|
||||
}
|
||||
};
|
||||
|
||||
if is_success {
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
if headers.ret != Return::Minimal || !is_success {
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(
|
||||
MultiStatus::new(vec![Response::new_propstat(uri, items.build())])
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_calendar_properties(
|
||||
&self,
|
||||
personal_id: u32,
|
||||
calendar: &mut Calendar,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
calendar.preferences_mut(personal_id).name = name;
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CalDav(CalDavProperty::CalendarDescription),
|
||||
DavValue::String(name),
|
||||
) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
calendar.preferences_mut(personal_id).description = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CalDav(CalDavProperty::CalendarTimezone),
|
||||
DavValue::ICalendar(ical),
|
||||
) => {
|
||||
if ical.size() > self.core.groupware.max_ical_size {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
} else if !ical.is_timezone() {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::ValidCalendarData,
|
||||
"Invalid calendar timezone",
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
calendar.preferences_mut(personal_id).time_zone = Timezone::Custom(ical);
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(DavProperty::CalDav(CalDavProperty::TimezoneId), DavValue::String(tz_id)) => {
|
||||
if let Ok(tz) = Tz::from_str(&tz_id) {
|
||||
calendar.preferences_mut(personal_id).time_zone =
|
||||
Timezone::IANA(tz.as_id());
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::ValidTimezone,
|
||||
"Invalid timezone ID",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
calendar.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavValue::ResourceTypes(types),
|
||||
) => {
|
||||
if !types
|
||||
.0
|
||||
.iter()
|
||||
.all(|rt| matches!(rt, ResourceType::Collection | ResourceType::Calendar))
|
||||
{
|
||||
items.insert_precondition_failed(
|
||||
property.property,
|
||||
StatusCode::FORBIDDEN,
|
||||
BaseCondition::ValidResourceType,
|
||||
);
|
||||
has_errors = true;
|
||||
} else {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
}
|
||||
(
|
||||
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
|
||||
DavValue::Components(components),
|
||||
) => {
|
||||
if !is_update {
|
||||
calendar.supported_components = Bitmap::<SupportedComponent>::from_iter(
|
||||
components
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|v| SupportedComponent::from(v.0)),
|
||||
)
|
||||
.into_inner();
|
||||
if calendar.supported_components != 0 {
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarComponent,
|
||||
"At least one supported component must be specified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
} else {
|
||||
items.insert_precondition_failed_with_description(
|
||||
property.property,
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarComponent,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
calendar.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if calendar.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
calendar.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
|
||||
fn apply_event_properties(
|
||||
&self,
|
||||
event: &mut CalendarEvent,
|
||||
is_update: bool,
|
||||
properties: Vec<DavPropertyValue>,
|
||||
items: &mut PropStatBuilder,
|
||||
) -> bool {
|
||||
let mut has_errors = false;
|
||||
|
||||
for property in properties {
|
||||
match (&property.property, property.value) {
|
||||
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
|
||||
if name.len() <= self.core.groupware.live_property_size {
|
||||
event.display_name = Some(name);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
event.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
{
|
||||
if is_update {
|
||||
event.dead_properties.remove_element(dead);
|
||||
}
|
||||
|
||||
if event.dead_properties.size() + values.size() + dead.size()
|
||||
< self.core.groupware.dead_property_size.unwrap()
|
||||
{
|
||||
event.dead_properties.add_element(dead.clone(), values.0);
|
||||
items.insert_ok(property.property);
|
||||
} else {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
"Property value is too long",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
(_, DavValue::Null) => {
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property.property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be modified",
|
||||
);
|
||||
has_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!has_errors
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_event_properties(
|
||||
event: &mut CalendarEvent,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName) => {
|
||||
event.display_name = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
event.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_calendar_properties(
|
||||
personal_id: u32,
|
||||
calendar: &mut Calendar,
|
||||
properties: Vec<DavProperty>,
|
||||
items: &mut PropStatBuilder,
|
||||
) {
|
||||
for property in properties {
|
||||
match &property {
|
||||
DavProperty::CalDav(CalDavProperty::CalendarDescription) => {
|
||||
calendar.preferences_mut(personal_id).description = None;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::CalDav(CalDavProperty::CalendarTimezone)
|
||||
| DavProperty::CalDav(CalDavProperty::TimezoneId) => {
|
||||
calendar.preferences_mut(personal_id).time_zone = Timezone::Default;
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
DavProperty::DeadProperty(dead) => {
|
||||
calendar.dead_properties.remove_element(dead);
|
||||
items.insert_with_status(property, StatusCode::NO_CONTENT);
|
||||
}
|
||||
_ => {
|
||||
items.insert_error_with_description(
|
||||
property,
|
||||
StatusCode::CONFLICT,
|
||||
"Property cannot be deleted",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::freebusy::freebusy_in_range;
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{
|
||||
CalendarFilter, DavQuery,
|
||||
propfind::{PropFindItem, PropFindRequestHandler},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::{
|
||||
common::{PartialDateTime, timezone::Tz},
|
||||
icalendar::{
|
||||
ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarEntry,
|
||||
ArchivedICalendarParameter, ArchivedICalendarProperty, ArchivedICalendarValue,
|
||||
ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty,
|
||||
ICalendarValue,
|
||||
},
|
||||
};
|
||||
use common::{DavResource, Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::{CalDavProperty, CalendarData, DavProperty},
|
||||
request::{CalendarQuery, Filter, FilterOp, PropFind, Timezone},
|
||||
response::MultiStatus,
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{ArchivedCalendarEvent, expand::CalendarEventExpansion},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::{fmt::Write, slice::Iter, str::FromStr};
|
||||
use store::{
|
||||
ahash::{AHashMap, AHashSet},
|
||||
write::serialize::rkyv_deserialize,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{TimeRange, acl::Acl, collection::SyncCollection};
|
||||
|
||||
pub(crate) trait CalendarQueryRequestHandler: Sync + Send {
|
||||
fn handle_calendar_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: CalendarQuery,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarQueryRequestHandler for Server {
|
||||
async fn handle_calendar_query_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
request: CalendarQuery,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let Some(resource) = resources.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
) else {
|
||||
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
|
||||
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
|
||||
};
|
||||
if !resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Obtain shared ids
|
||||
let shared_ids = if !access_token.is_member(account_id) {
|
||||
resources
|
||||
.shared_items(access_token, [Acl::ReadItems], false)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Pre-filter by date range
|
||||
let filter_range = extract_filter_range(&request);
|
||||
|
||||
// Obtain document ids in folder
|
||||
let mut items = Vec::with_capacity(16);
|
||||
for resource in resources.children(resource.document_id()) {
|
||||
if shared_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(resource.document_id()))
|
||||
&& filter_range
|
||||
.as_ref()
|
||||
.is_none_or(|range| is_resource_in_time_range(resource.resource, range))
|
||||
{
|
||||
items.push(PropFindItem::new(
|
||||
resources.format_resource(resource),
|
||||
account_id,
|
||||
resource,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the time range from the request
|
||||
let max_time_range = extract_data_range(&request.properties, filter_range);
|
||||
|
||||
self.handle_dav_query(
|
||||
access_token,
|
||||
DavQuery::calendar_query(request, max_time_range, items, headers),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_resource_in_time_range(resource: &DavResource, filter: &TimeRange) -> bool {
|
||||
// Check whether the resource has a time range and if it overlaps with the filter
|
||||
if let Some((start, end)) = resource.event_time_range() {
|
||||
((filter.start < end) || (filter.start <= start))
|
||||
&& (filter.end > start || filter.end >= end)
|
||||
} else {
|
||||
// If the resource does not have a time range, it is not in the range
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_filter_range(query: &CalendarQuery) -> Option<TimeRange> {
|
||||
let mut range = TimeRange {
|
||||
start: i64::MAX,
|
||||
end: i64::MIN,
|
||||
};
|
||||
|
||||
for filter in &query.filters {
|
||||
let op = match filter {
|
||||
Filter::Component { op, .. } => op,
|
||||
Filter::Property { op, .. } => op,
|
||||
Filter::Parameter { op, .. } => op,
|
||||
_ => continue,
|
||||
};
|
||||
if let FilterOp::TimeRange(date_range) = op {
|
||||
if date_range.start < range.start {
|
||||
range.start = date_range.start;
|
||||
}
|
||||
if date_range.end > range.end {
|
||||
range.end = date_range.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if range.start != i64::MAX {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_data_range(propfind: &PropFind, filter_range: Option<TimeRange>) -> Option<TimeRange> {
|
||||
let props = match propfind {
|
||||
PropFind::AllProp(props) | PropFind::Prop(props) => props,
|
||||
PropFind::PropName => &[][..],
|
||||
};
|
||||
|
||||
for prop in props {
|
||||
if let DavProperty::CalDav(CalDavProperty::CalendarData(data)) = prop {
|
||||
let mut range = filter_range.unwrap_or(TimeRange {
|
||||
start: i64::MAX,
|
||||
end: i64::MIN,
|
||||
});
|
||||
|
||||
for data_range in [&data.expand, &data.limit_recurrence, &data.limit_freebusy]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if data_range.start < range.start {
|
||||
range.start = data_range.start;
|
||||
}
|
||||
if data_range.end > range.end {
|
||||
range.end = data_range.end;
|
||||
}
|
||||
}
|
||||
|
||||
return if range.start != i64::MAX {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
filter_range
|
||||
}
|
||||
|
||||
pub fn try_parse_tz(tz: &Timezone) -> Option<Tz> {
|
||||
match tz {
|
||||
Timezone::Name(value) | Timezone::Id(value) => Tz::from_str(value).ok(),
|
||||
Timezone::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CalendarQueryHandler {
|
||||
default_tz: Tz,
|
||||
expanded_times: Vec<CalendarEventExpansion>,
|
||||
}
|
||||
|
||||
impl CalendarQueryHandler {
|
||||
pub fn new(
|
||||
event: &ArchivedCalendarEvent,
|
||||
max_time_range: Option<TimeRange>,
|
||||
default_tz: Tz,
|
||||
) -> Self {
|
||||
Self {
|
||||
default_tz,
|
||||
expanded_times: max_time_range
|
||||
.map(|max_time_range| {
|
||||
event
|
||||
.data
|
||||
.expand(default_tz, max_time_range)
|
||||
.unwrap_or_else(|| {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::RuleExpansionError),
|
||||
Reason = "chrono error",
|
||||
Details = event.data.event.to_string(),
|
||||
);
|
||||
vec![]
|
||||
})
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter(&mut self, event: &ArchivedCalendarEvent, filters: &CalendarFilter) -> bool {
|
||||
let ical = &event.data.event;
|
||||
let mut is_all = true;
|
||||
let mut matches_one = false;
|
||||
|
||||
for filter in filters {
|
||||
match filter {
|
||||
Filter::AnyOf => {
|
||||
is_all = false;
|
||||
}
|
||||
Filter::AllOf => {
|
||||
is_all = true;
|
||||
}
|
||||
Filter::Property { prop, op, comp } => {
|
||||
let mut properties = find_components(ical, comp)
|
||||
.flat_map(|(_, comp)| find_properties(comp, prop))
|
||||
.peekable();
|
||||
|
||||
let result = if properties.peek().is_some() {
|
||||
properties.any(|entry| {
|
||||
match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
let mut matched_any = false;
|
||||
|
||||
for value in entry.values.iter() {
|
||||
if let Some(text) = value.as_text()
|
||||
&& text_match.matches(text)
|
||||
{
|
||||
matched_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
matched_any
|
||||
}
|
||||
FilterOp::TimeRange(range) => {
|
||||
if let Some(ArchivedICalendarValue::PartialDateTime(date)) =
|
||||
entry.values.first()
|
||||
{
|
||||
let tz = entry
|
||||
.tz_id()
|
||||
.and_then(|tz_id| Tz::from_str(tz_id).ok())
|
||||
.unwrap_or(self.default_tz);
|
||||
|
||||
if let Some(date) = date
|
||||
.to_date_time()
|
||||
.and_then(|date| date.to_date_time_with_tz(tz))
|
||||
{
|
||||
let timestamp = date.timestamp();
|
||||
// RFC4791#9.9: start <= DTSTART AND end > DTSTART
|
||||
range.start <= timestamp && range.end > timestamp
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Parameter {
|
||||
prop,
|
||||
param,
|
||||
op,
|
||||
comp,
|
||||
} => {
|
||||
let mut parameters = find_components(ical, comp)
|
||||
.flat_map(|(_, comp)| {
|
||||
find_properties(comp, prop)
|
||||
.filter_map(|entry| find_parameter(entry, param))
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let result = if parameters.peek().is_some() {
|
||||
parameters.any(|entry| match op {
|
||||
FilterOp::Exists => true,
|
||||
FilterOp::Undefined => false,
|
||||
FilterOp::TextMatch(text_match) => {
|
||||
if let Some(text) = entry.value.as_text() {
|
||||
text_match.matches(text)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
FilterOp::TimeRange(_) => false,
|
||||
})
|
||||
} else {
|
||||
matches!(op, FilterOp::Undefined)
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Filter::Component { comp, op } => {
|
||||
let result = match op {
|
||||
FilterOp::Exists => find_components(ical, comp).next().is_some(),
|
||||
FilterOp::Undefined => find_components(ical, comp).next().is_none(),
|
||||
FilterOp::TimeRange(range) => {
|
||||
if !matches!(comp.last(), Some(ICalendarComponentType::VAlarm)) {
|
||||
let matching_comp_ids = find_components(ical, comp)
|
||||
.map(|(id, comp)| (id as u32, &comp.component_type))
|
||||
.collect::<AHashMap<_, _>>();
|
||||
|
||||
!matching_comp_ids.is_empty()
|
||||
&& self.expanded_times.iter().any(|event| {
|
||||
matching_comp_ids.get(&event.comp_id).is_some_and(|ct| {
|
||||
range.is_in_range(
|
||||
ct == &&ICalendarComponentType::VTodo,
|
||||
event.start,
|
||||
event.end,
|
||||
)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let matching_comp_ids = event
|
||||
.data
|
||||
.alarms
|
||||
.iter()
|
||||
.map(|alarm| alarm.parent_id.to_native() as u32)
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
!matching_comp_ids.is_empty()
|
||||
&& self.expanded_times.iter().any(|time| {
|
||||
matching_comp_ids.contains(&time.comp_id)
|
||||
&& event.data.alarms.iter().any(|alarm| {
|
||||
alarm.parent_id.to_native() as u32 == time.comp_id
|
||||
&& alarm
|
||||
.delta
|
||||
.to_timestamp(
|
||||
time.start,
|
||||
time.end,
|
||||
self.default_tz,
|
||||
)
|
||||
.is_some_and(|timestamp| {
|
||||
range.is_in_range(
|
||||
false, timestamp, timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
FilterOp::TextMatch(_) => false,
|
||||
};
|
||||
|
||||
if result {
|
||||
matches_one = true;
|
||||
} else if is_all {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is_all || matches_one
|
||||
}
|
||||
|
||||
pub fn serialize_ical(
|
||||
&mut self,
|
||||
event: &ArchivedCalendarEvent,
|
||||
data: &CalendarData,
|
||||
instances_limit: &mut usize,
|
||||
) -> Option<String> {
|
||||
let mut out = String::with_capacity(event.size.to_native() as usize);
|
||||
let _v = [0.into()];
|
||||
let mut component_iter: Iter<'_, rkyv::rend::u32_le> = _v.iter();
|
||||
let mut component_stack: Vec<(&ArchivedICalendarComponent, Iter<'_, rkyv::rend::u32_le>)> =
|
||||
Vec::with_capacity(4);
|
||||
|
||||
if data.expand.is_some() {
|
||||
self.expanded_times.sort_unstable_by_key(|a| a.start);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(component_id) = component_iter.next() {
|
||||
let component_id = component_id.to_native();
|
||||
let component = event
|
||||
.data
|
||||
.event
|
||||
.components
|
||||
.get(component_id as usize)
|
||||
.unwrap();
|
||||
|
||||
// Limit recurrence override
|
||||
if let Some(limit_recurrence) = &data.limit_recurrence
|
||||
&& component.is_recurrence_override()
|
||||
&& !self.expanded_times.iter().any(|event| {
|
||||
event.comp_id == component_id
|
||||
&& limit_recurrence.is_in_range(
|
||||
component.component_type == ICalendarComponentType::VTodo,
|
||||
event.start,
|
||||
event.end,
|
||||
)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit freebusy
|
||||
if let Some(limit_recurrence) = &data.limit_freebusy
|
||||
&& component.component_type == ICalendarComponentType::VFreebusy
|
||||
&& !self.expanded_times.iter().any(|event| {
|
||||
event.comp_id == component_id
|
||||
&& limit_recurrence.is_in_range(false, event.start, event.end)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter entries
|
||||
let mut entries = component
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if data.properties.is_empty()
|
||||
|| component.component_type == ICalendarComponentType::VCalendar
|
||||
{
|
||||
Some((entry, true))
|
||||
} else {
|
||||
data.properties
|
||||
.iter()
|
||||
.find(|prop| {
|
||||
prop.component.as_ref().is_none_or(|comp| {
|
||||
comp == &component.component_type
|
||||
|| component_stack.iter().any(|(parent_comp, _)| {
|
||||
comp == &parent_comp.component_type
|
||||
})
|
||||
}) && prop.name.as_ref().is_none_or(|name| name == &entry.name)
|
||||
})
|
||||
.map(|prop| (entry, !prop.no_value))
|
||||
}
|
||||
})
|
||||
.peekable();
|
||||
|
||||
// Expand recurrences
|
||||
let component_name = component.component_type.as_str();
|
||||
if let Some(expand) = &data
|
||||
.expand
|
||||
.filter(|_| component.component_type.has_time_ranges())
|
||||
{
|
||||
let is_recurrent = component.is_recurrent();
|
||||
let is_recurrent_or_override =
|
||||
is_recurrent || component.is_recurrence_override();
|
||||
let is_todo = component.component_type == ICalendarComponentType::VTodo;
|
||||
let mut has_duration = false;
|
||||
let entries = entries
|
||||
.filter(|(entry, _)| match &entry.name {
|
||||
ArchivedICalendarProperty::Dtstart
|
||||
| ArchivedICalendarProperty::Dtend
|
||||
| ArchivedICalendarProperty::Exdate
|
||||
| ArchivedICalendarProperty::Exrule
|
||||
| ArchivedICalendarProperty::Rdate
|
||||
| ArchivedICalendarProperty::Rrule
|
||||
| ArchivedICalendarProperty::RecurrenceId => false,
|
||||
ArchivedICalendarProperty::Due
|
||||
| ArchivedICalendarProperty::Completed
|
||||
| ArchivedICalendarProperty::Created => is_recurrent,
|
||||
ArchivedICalendarProperty::Duration => {
|
||||
has_duration = true;
|
||||
true
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for event in &self.expanded_times {
|
||||
if event.comp_id == component_id
|
||||
&& (!is_recurrent_or_override
|
||||
|| expand.is_in_range(is_todo, event.start, event.end))
|
||||
{
|
||||
if *instances_limit > 0 {
|
||||
*instances_limit -= 1;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
let _ = write!(&mut out, "BEGIN:{component_name}\r\n");
|
||||
|
||||
// Write DTSTART, DTEND and RECURRENCE-ID
|
||||
let mut entry = ICalendarEntry {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(event.start),
|
||||
))],
|
||||
};
|
||||
let _ = entry.write_to(&mut out);
|
||||
if is_recurrent_or_override {
|
||||
entry.name = ICalendarProperty::RecurrenceId;
|
||||
let _ = entry.write_to(&mut out);
|
||||
}
|
||||
if !has_duration {
|
||||
entry.name = ICalendarProperty::Dtend;
|
||||
entry.values = vec![ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(event.end),
|
||||
))];
|
||||
let _ = entry.write_to(&mut out);
|
||||
}
|
||||
|
||||
// Write other component entries
|
||||
for (entry, with_value) in &entries {
|
||||
let _ = entry.write_to(&mut out, *with_value);
|
||||
}
|
||||
let _ = write!(&mut out, "END:{component_name}\r\n");
|
||||
}
|
||||
}
|
||||
} else if entries.peek().is_some()
|
||||
|| (component.component_type == ICalendarComponentType::VCalendar
|
||||
&& !component.component_ids.is_empty())
|
||||
{
|
||||
let _ = write!(&mut out, "BEGIN:{component_name}\r\n");
|
||||
|
||||
match data.limit_freebusy {
|
||||
Some(range)
|
||||
if component.component_type == ICalendarComponentType::VFreebusy =>
|
||||
{
|
||||
// Filter freebusy
|
||||
for (entry, with_value) in entries {
|
||||
if matches!(entry.name, ArchivedICalendarProperty::Freebusy) {
|
||||
let mut fb_in_range =
|
||||
freebusy_in_range(entry, &range, self.default_tz)
|
||||
.peekable();
|
||||
if fb_in_range.peek().is_none() {
|
||||
continue;
|
||||
} else {
|
||||
let _ = ICalendarEntry {
|
||||
name: ICalendarProperty::Freebusy,
|
||||
params: rkyv_deserialize(&entry.params)
|
||||
.ok()
|
||||
.unwrap_or_default(),
|
||||
values: fb_in_range.collect(),
|
||||
}
|
||||
.write_to(&mut out);
|
||||
}
|
||||
} else {
|
||||
let _ = entry.write_to(&mut out, with_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for (entry, with_value) in entries {
|
||||
let _ = entry.write_to(&mut out, with_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !component.component_ids.is_empty() {
|
||||
component_stack.push((component, component_iter));
|
||||
component_iter = component.component_ids.iter();
|
||||
} else if component.component_ids.is_empty() {
|
||||
let _ = write!(&mut out, "END:{component_name}\r\n");
|
||||
}
|
||||
}
|
||||
} else if let Some((component, iter)) = component_stack.pop() {
|
||||
let _ = write!(&mut out, "END:{}\r\n", component.component_type.as_str());
|
||||
component_iter = iter;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(out)
|
||||
}
|
||||
|
||||
pub fn into_expanded_times(self) -> Vec<CalendarEventExpansion> {
|
||||
self.expanded_times
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_components<'x>(
|
||||
ical: &'x ArchivedICalendar,
|
||||
comp: &[ICalendarComponentType],
|
||||
) -> impl Iterator<Item = (usize, &'x ArchivedICalendarComponent)> {
|
||||
// TODO: Properly expand the component type path
|
||||
let comp = comp.last().unwrap_or(&ICalendarComponentType::VCalendar);
|
||||
ical.components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(_, entry)| {
|
||||
comp == &ICalendarComponentType::VCalendar || &entry.component_type == comp
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_properties<'x>(
|
||||
comp: &'x ArchivedICalendarComponent,
|
||||
prop: &ICalendarProperty,
|
||||
) -> impl Iterator<Item = &'x ArchivedICalendarEntry> {
|
||||
comp.entries.iter().filter(move |entry| &entry.name == prop)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn find_parameter<'x>(
|
||||
entry: &'x ArchivedICalendarEntry,
|
||||
name: &ICalendarParameterName,
|
||||
) -> Option<&'x ArchivedICalendarParameter> {
|
||||
entry.params.iter().find(|param| param.name == *name)
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod,
|
||||
calendar::freebusy::CalendarFreebusyRequestHandler,
|
||||
common::{
|
||||
ETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
};
|
||||
use calcard::{
|
||||
Entry, Parser,
|
||||
icalendar::{
|
||||
ICalendarComponentType, ICalendarEntry, ICalendarMethod, ICalendarProperty, ICalendarValue,
|
||||
Uri,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders,
|
||||
schema::{
|
||||
property::Rfc1123DateTime,
|
||||
request::FreeBusyQuery,
|
||||
response::{CalCondition, Href, ScheduleResponse, ScheduleResponseItem},
|
||||
},
|
||||
};
|
||||
use groupware::{
|
||||
DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification, strip_mailto_scheme,
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use store::{ahash::AHashMap, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::collection::{Collection, SyncCollection};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub(crate) trait CalendarEventNotificationHandler: Sync + Send {
|
||||
fn handle_scheduling_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn handle_scheduling_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
|
||||
fn handle_scheduling_post_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationHandler for Server {
|
||||
async fn handle_scheduling_get_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
is_head: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource_ = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource_.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::CalendarEventNotification,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource = resources
|
||||
.by_path(
|
||||
resource_
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
|
||||
)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Fetch event
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEventNotification,
|
||||
resource.document_id(),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let event = event_
|
||||
.unarchive::<CalendarEventNotification>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
let etag = event_.etag();
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification,
|
||||
document_id: resource.document_id().into(),
|
||||
etag: etag.clone().into(),
|
||||
path: resource_.resource.unwrap(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::GET,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = HttpResponse::new(StatusCode::OK)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_etag(etag)
|
||||
.with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string());
|
||||
|
||||
let ical = event.event.to_string();
|
||||
|
||||
if !is_head {
|
||||
Ok(response.with_binary_body(ical))
|
||||
} else {
|
||||
Ok(response.with_content_length(ical.len()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_scheduling_delete_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let delete_path = resource
|
||||
.resource
|
||||
.filter(|r| !r.is_empty())
|
||||
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::CalendarEventNotification,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Check resource type
|
||||
let resource = resources
|
||||
.by_path(delete_path)
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id) {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
let document_id = resource.document_id();
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEventNotification,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification,
|
||||
document_id: document_id.into(),
|
||||
etag: event_.etag().into(),
|
||||
path: delete_path,
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::DELETE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEventNotification>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Delete event
|
||||
let mut batch = BatchBuilder::new();
|
||||
DestroyArchive(event)
|
||||
.delete(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
|
||||
}
|
||||
|
||||
async fn handle_scheduling_post_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
if resource.resource.is_none_or(|r| r != "outbox") {
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Parse iTIP message
|
||||
if bytes.len() > self.core.groupware.max_ical_size {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxResourceSize(self.core.groupware.max_ical_size as u32),
|
||||
)));
|
||||
}
|
||||
let itip_raw = std::str::from_utf8(&bytes).map_err(|_| {
|
||||
DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Invalid UTF-8 in iCalendar data"),
|
||||
)
|
||||
})?;
|
||||
let itip = match Parser::new(itip_raw).entry() {
|
||||
Entry::ICalendar(ical) if ical.components.len() > 1 => ical,
|
||||
_ => {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Failed to parse iCalendar data"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Parse request
|
||||
let mut from_date = None;
|
||||
let mut to_date = None;
|
||||
let mut organizer = None;
|
||||
let mut attendees = AHashMap::new();
|
||||
let mut uid = None;
|
||||
let tz_resolver = itip.build_tz_resolver();
|
||||
let mut found_freebusy = false;
|
||||
|
||||
for component in &itip.components {
|
||||
if component.component_type != ICalendarComponentType::VFreebusy {
|
||||
continue;
|
||||
} else if !found_freebusy {
|
||||
found_freebusy = true;
|
||||
} else {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Multiple VFREEBUSY components found"),
|
||||
));
|
||||
}
|
||||
|
||||
for entry in &component.entries {
|
||||
let tz_id = entry.tz_id();
|
||||
match (&entry.name, entry.values.first()) {
|
||||
(ICalendarProperty::Dtstart, Some(ICalendarValue::PartialDateTime(dt))) => {
|
||||
from_date = dt.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id));
|
||||
}
|
||||
(ICalendarProperty::Dtend, Some(ICalendarValue::PartialDateTime(dt))) => {
|
||||
to_date = dt.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id));
|
||||
}
|
||||
(ICalendarProperty::Uid, Some(ICalendarValue::Text(_))) => {
|
||||
uid = Some(entry);
|
||||
}
|
||||
(
|
||||
ICalendarProperty::Organizer,
|
||||
Some(ICalendarValue::Text(_) | ICalendarValue::Uri(Uri::Location(_))),
|
||||
) => {
|
||||
organizer = Some(entry);
|
||||
}
|
||||
(
|
||||
ICalendarProperty::Attendee,
|
||||
Some(
|
||||
ICalendarValue::Text(value) | ICalendarValue::Uri(Uri::Location(value)),
|
||||
),
|
||||
) => {
|
||||
if let Some(email) = sanitize_email(strip_mailto_scheme(value.as_str())) {
|
||||
attendees.insert(email, entry);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(from_date), Some(to_date)) = (from_date, to_date) else {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Missing DTSTART or DTEND in VFREEBUSY component"),
|
||||
));
|
||||
};
|
||||
let Some(organizer) = organizer else {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Missing ORGANIZER in VFREEBUSY component"),
|
||||
));
|
||||
};
|
||||
if attendees.is_empty() {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
CalCondition::ValidSchedulingMessage,
|
||||
)
|
||||
.with_details("Missing ATTENDEE in VFREEBUSY component"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut response = ScheduleResponse::default();
|
||||
|
||||
for (email, attendee) in attendees {
|
||||
if let Some(account_id) = self
|
||||
.account_id_from_email(&email, true)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if let Some(resource) = self
|
||||
.core
|
||||
.groupware
|
||||
.default_calendar_name
|
||||
.as_ref()
|
||||
.and_then(|name| resources.by_path(name))
|
||||
{
|
||||
let mut free_busy = self
|
||||
.build_freebusy_object(
|
||||
access_token,
|
||||
FreeBusyQuery::new(from_date.timestamp(), to_date.timestamp()),
|
||||
&resources,
|
||||
account_id,
|
||||
resource,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add iTIP method
|
||||
free_busy.components[0].entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::Method,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Method(ICalendarMethod::Reply)],
|
||||
});
|
||||
|
||||
// Add properties
|
||||
let component = &mut free_busy.components[1];
|
||||
component.entries.push(organizer.clone());
|
||||
component.entries.push(attendee.clone());
|
||||
if let Some(uid) = uid {
|
||||
component.entries.push(uid.clone());
|
||||
}
|
||||
|
||||
response.items.0.push(ScheduleResponseItem {
|
||||
recipient: Href(format!("mailto:{email}")),
|
||||
request_status: "2.0;Success".into(),
|
||||
calendar_data: Some(free_busy.to_string()),
|
||||
});
|
||||
} else {
|
||||
response.items.0.push(ScheduleResponseItem {
|
||||
recipient: Href(format!("mailto:{email}")),
|
||||
request_status: "3.7;Default calendar not found".into(),
|
||||
calendar_data: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
response.items.0.push(ScheduleResponseItem {
|
||||
recipient: Href(format!("mailto:{email}")),
|
||||
request_status: "3.7;Invalid calendar user or insufficient permissions".into(),
|
||||
calendar_data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK).with_xml_body(response.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::assert_is_unique_uid;
|
||||
use crate::{
|
||||
DavError, DavErrorCondition, DavMethod,
|
||||
calendar::ItipPrecondition,
|
||||
common::{
|
||||
ETag, ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
file::DavFileResource,
|
||||
fix_percent_encoding,
|
||||
};
|
||||
use calcard::{
|
||||
Entry, Parser,
|
||||
common::timezone::Tz,
|
||||
icalendar::{ICalendar, ICalendarComponentType},
|
||||
};
|
||||
use common::{DavName, Server, auth::AccessToken};
|
||||
use dav_proto::{
|
||||
RequestHeaders, Return,
|
||||
schema::{property::Rfc1123DateTime, response::CalCondition},
|
||||
};
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
calendar::{CalendarEvent, CalendarEventData, itip::ItipSendStatus},
|
||||
scheduling::{
|
||||
ItipMessages, event_create::itip_create, event_update::itip_update,
|
||||
itip::itip_set_unreachable_status,
|
||||
},
|
||||
};
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use std::collections::HashSet;
|
||||
use store::write::{BatchBuilder, now};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
};
|
||||
|
||||
pub(crate) trait CalendarUpdateRequestHandler: Sync + Send {
|
||||
fn handle_calendar_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
is_patch: bool,
|
||||
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
||||
}
|
||||
|
||||
impl CalendarUpdateRequestHandler for Server {
|
||||
async fn handle_calendar_update_request(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
headers: &RequestHeaders<'_>,
|
||||
bytes: Vec<u8>,
|
||||
_is_patch: bool,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Validate URI
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await?
|
||||
.into_owned_uri()?;
|
||||
let account_id = resource.account_id;
|
||||
let resources = self
|
||||
.fetch_dav_resources(
|
||||
access_token.account_id(),
|
||||
account_id,
|
||||
SyncCollection::Calendar,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_name = fix_percent_encoding(
|
||||
resource
|
||||
.resource
|
||||
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
|
||||
);
|
||||
|
||||
if bytes.len() > self.core.groupware.max_ical_size {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxResourceSize(self.core.groupware.max_ical_size as u32),
|
||||
)));
|
||||
}
|
||||
let ical_raw = std::str::from_utf8(&bytes).map_err(|_| {
|
||||
DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarData,
|
||||
)
|
||||
.with_details("Invalid UTF-8 in iCalendar data"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let ical = match Parser::new(ical_raw).entry() {
|
||||
Entry::ICalendar(ical) => ical,
|
||||
_ => {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::SupportedCalendarData,
|
||||
)
|
||||
.with_details("Failed to parse iCalendar data"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let account_info = self
|
||||
.scheduling_account_info(access_token.account_id(), account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
|
||||
if resource.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
let parent_id = resource.parent_id().unwrap();
|
||||
let document_id = resource.document_id();
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Update
|
||||
let event_ = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
|
||||
let event = event_
|
||||
.to_unarchived::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Validate headers
|
||||
match self
|
||||
.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: Collection::CalendarEvent,
|
||||
document_id: Some(document_id),
|
||||
etag: event.etag().into(),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
|
||||
if headers.ret == Return::Representation =>
|
||||
{
|
||||
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
|
||||
.with_content_type("text/calendar; charset=utf-8")
|
||||
.with_etag(event.etag())
|
||||
.with_last_modified(
|
||||
Rfc1123DateTime::new(i64::from(event.inner.modified)).to_string(),
|
||||
)
|
||||
.with_header("Preference-Applied", "return=representation")
|
||||
.with_binary_body(event.inner.data.event.to_string()));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
if ical == event.inner.data.event {
|
||||
// No changes, return existing event
|
||||
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
|
||||
}
|
||||
|
||||
// Validate iCal
|
||||
if event.inner.data.event.uids().next().unwrap_or_default() != validate_ical(&ical)? {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::NoUidConflict(resources.format_resource(resource).into()),
|
||||
)));
|
||||
}
|
||||
|
||||
// Validate schedule tag
|
||||
if headers.if_schedule_tag.is_some()
|
||||
&& event.inner.schedule_tag.as_ref().map(|t| t.to_native())
|
||||
!= headers.if_schedule_tag
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
||||
}
|
||||
|
||||
// Obtain previous alarm
|
||||
let now = now() as i64;
|
||||
let prev_email_alarm = event.inner.data.next_alarm(now, Tz::Floating);
|
||||
|
||||
// Build event
|
||||
let mut next_email_alarm = None;
|
||||
let mut new_event = event
|
||||
.deserialize::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
let old_ical = new_event.data.event;
|
||||
new_event.size = bytes.len() as u32;
|
||||
new_event.data = CalendarEventData::new(
|
||||
ical,
|
||||
Tz::Floating,
|
||||
self.core.groupware.max_ical_instances,
|
||||
&mut next_email_alarm,
|
||||
);
|
||||
|
||||
// Scheduling
|
||||
let mut itip_messages = None;
|
||||
let itip_status = ItipSendStatus::resolve(
|
||||
self,
|
||||
access_token,
|
||||
&account_info,
|
||||
new_event.data.event_range_end(),
|
||||
);
|
||||
if itip_status.is_send() {
|
||||
let result = if new_event.schedule_tag.is_some() {
|
||||
itip_update(
|
||||
&mut new_event.data.event,
|
||||
&old_ical,
|
||||
account_info.addresses(),
|
||||
)
|
||||
} else {
|
||||
itip_create(&mut new_event.data.event, account_info.addresses())
|
||||
};
|
||||
|
||||
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_event.schedule_tag {
|
||||
*schedule_tag += 1;
|
||||
} else {
|
||||
new_event.schedule_tag = Some(1);
|
||||
}
|
||||
}
|
||||
|
||||
itip_messages = Some(ItipMessages::new(messages));
|
||||
} else {
|
||||
return Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxAttendeesPerInstance,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(failed_precondition) = err.failed_precondition() {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
failed_precondition,
|
||||
)
|
||||
.with_details(err.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
Reason = err.to_string(),
|
||||
);
|
||||
|
||||
// Event changed, but there are no iTIP messages to send
|
||||
if let Some(schedule_tag) = &mut new_event.schedule_tag {
|
||||
*schedule_tag += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itip_set_unreachable_status(&mut new_event.data.event, account_info.addresses());
|
||||
} else if let Some(reason) = itip_status.reason() {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
Reason = reason,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
let extra_bytes =
|
||||
(bytes.len() as u64).saturating_sub(u32::from(event.inner.size) as u64);
|
||||
if extra_bytes > 0 {
|
||||
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let schedule_tag = new_event.schedule_tag;
|
||||
let etag = new_event
|
||||
.update(
|
||||
access_token.account_tenant_ids(),
|
||||
event,
|
||||
account_id,
|
||||
document_id,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
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!())?;
|
||||
}
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT)
|
||||
.with_etag_opt(etag)
|
||||
.with_schedule_tag_opt(schedule_tag))
|
||||
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
|
||||
if !parent.is_container() {
|
||||
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
// Validate ACL
|
||||
if !access_token.is_member(account_id)
|
||||
&& !resources.has_access_to_container(
|
||||
access_token,
|
||||
parent.document_id(),
|
||||
Acl::AddItems,
|
||||
)
|
||||
{
|
||||
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
// Validate headers
|
||||
self.validate_headers(
|
||||
access_token,
|
||||
headers,
|
||||
vec![ResourceState {
|
||||
account_id,
|
||||
collection: resource.collection,
|
||||
document_id: Some(u32::MAX),
|
||||
path: resource_name.as_ref(),
|
||||
..Default::default()
|
||||
}],
|
||||
Default::default(),
|
||||
DavMethod::PUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate ical object
|
||||
assert_is_unique_uid(
|
||||
self,
|
||||
&resources,
|
||||
account_id,
|
||||
parent.document_id(),
|
||||
validate_ical(&ical)?.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Build event
|
||||
let mut next_email_alarm = None;
|
||||
let mut event = CalendarEvent {
|
||||
names: vec![DavName {
|
||||
name: name.to_string(),
|
||||
parent_id: parent.document_id(),
|
||||
}],
|
||||
data: CalendarEventData::new(
|
||||
ical,
|
||||
Tz::Floating,
|
||||
self.core.groupware.max_ical_instances,
|
||||
&mut next_email_alarm,
|
||||
),
|
||||
size: bytes.len() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Scheduling
|
||||
let mut itip_messages = None;
|
||||
let itip_status = ItipSendStatus::resolve(
|
||||
self,
|
||||
access_token,
|
||||
&account_info,
|
||||
event.data.event_range_end(),
|
||||
);
|
||||
if itip_status.is_send() {
|
||||
match itip_create(&mut event.data.event, account_info.addresses()) {
|
||||
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 Err(DavError::Condition(DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::MaxAttendeesPerInstance,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(failed_precondition) = err.failed_precondition() {
|
||||
return Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
failed_precondition,
|
||||
)
|
||||
.with_details(err.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
Reason = err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
itip_set_unreachable_status(&mut event.data.event, account_info.addresses());
|
||||
} else if let Some(reason) = itip_status.reason() {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::ItipMessageError),
|
||||
AccountId = account_id,
|
||||
Reason = reason,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
if !bytes.is_empty() {
|
||||
self.has_available_quota(
|
||||
self.account(account_id).await?.as_ref(),
|
||||
bytes.len() as u64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::CalendarEvent, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let schedule_tag = event.schedule_tag;
|
||||
let etag = event
|
||||
.insert(
|
||||
access_token.account_tenant_ids(),
|
||||
account_id,
|
||||
document_id,
|
||||
next_email_alarm,
|
||||
&mut batch,
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.etag();
|
||||
if let Some(itip_messages) = itip_messages {
|
||||
itip_messages
|
||||
.queue(&mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
self.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_etag_opt(etag)
|
||||
.with_schedule_tag_opt(schedule_tag))
|
||||
} else {
|
||||
Err(DavError::Code(StatusCode::CONFLICT))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_ical(ical: &ICalendar) -> crate::Result<&str> {
|
||||
// Validate UIDs
|
||||
let mut uids = HashSet::with_capacity(1);
|
||||
|
||||
// Validate component types
|
||||
let mut types: [u8; 5] = [0; 5];
|
||||
for comp in &ical.components {
|
||||
*(match comp.component_type {
|
||||
ICalendarComponentType::VEvent => &mut types[0],
|
||||
ICalendarComponentType::VTodo => &mut types[1],
|
||||
ICalendarComponentType::VJournal => &mut types[2],
|
||||
ICalendarComponentType::VFreebusy => &mut types[3],
|
||||
ICalendarComponentType::VAvailability => &mut types[4],
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}) += 1;
|
||||
|
||||
if let Some(uid) = comp.uid() {
|
||||
uids.insert(uid);
|
||||
}
|
||||
}
|
||||
|
||||
if uids.len() == 1 && types.iter().filter(|&&v| v == 0).count() == 4 {
|
||||
Ok(uids.iter().next().unwrap())
|
||||
} else {
|
||||
Err(DavError::Condition(
|
||||
DavErrorCondition::new(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
CalCondition::ValidCalendarObjectResource,
|
||||
)
|
||||
.with_details("iCalendar must contain exactly one UID and same component types"),
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user