Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+248
View File
@@ -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))
}
}
+407
View File
@@ -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),
) = (&param.name, &param.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
}
})
}
+130
View File
@@ -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()))
}
}
}
+156
View File
@@ -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))
}
}
}
+149
View File
@@ -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,
}
}
}
+565
View File
@@ -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",
);
}
}
}
}
+657
View File
@@ -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)
}
+438
View File
@@ -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()))
}
}
+536
View File
@@ -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"),
))
}
}