diff --git a/Cargo.lock b/Cargo.lock index dd155cdd..0a1dc80b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,6 +1011,9 @@ dependencies = [ name = "calcard" version = "0.1.0" dependencies = [ + "ahash 0.8.11", + "chrono", + "chrono-tz", "hashify", "mail-builder", "mail-parser", @@ -1137,6 +1140,27 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.2.5" @@ -1258,6 +1282,7 @@ dependencies = [ "base64 0.22.1", "bincode", "biscuit", + "calcard", "chrono", "compact_str", "decancer", @@ -1711,6 +1736,7 @@ name = "dav" version = "0.11.7" dependencies = [ "calcard", + "chrono", "common", "compact_str", "dav-proto", @@ -1732,6 +1758,7 @@ name = "dav-proto" version = "0.11.7" dependencies = [ "calcard", + "chrono", "hashify", "hyper 1.6.0", "mail-parser", @@ -2787,6 +2814,7 @@ name = "groupware" version = "0.11.7" dependencies = [ "calcard", + "chrono", "common", "compact_str", "dav-proto", @@ -4906,6 +4934,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "password-hash" version = "0.5.0" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 878def77..df088180 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stalwart-cli" -description = "Stalwart Mail Server CLI" +description = "Stalwart Server CLI" authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only OR LicenseRef-SEL" repository = "https://github.com/stalwartlabs/cli" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 1c0e26de..4f02282a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -19,6 +19,7 @@ mail-auth = { version = "0.6" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } smtp-proto = { version = "0.1", features = ["rkyv"] } dns-update = { version = "0.1" } +calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } ahash = { version = "0.8.2", features = ["serde"] } parking_lot = "0.12.1" regex = "1.7.0" diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 60ad883c..d6727722 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -19,6 +19,7 @@ use std::{ use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; use auth::{AccessToken, oauth::config::OAuthConfig, roles::RolePermissions}; +use calcard::common::timezone::Tz; use config::{ dav::DavConfig, imap::ImapConfig, @@ -268,6 +269,9 @@ pub enum DavResourceMetadata { is_container: bool, }, Calendar { + tz: Tz, + }, + CalendarEvent { start: i64, duration: u32, }, @@ -542,13 +546,20 @@ impl DavResources { impl DavResource { pub fn event_time_range(&self) -> Option<(i64, i64)> { match &self.data { - DavResourceMetadata::Calendar { start, duration } => { + DavResourceMetadata::CalendarEvent { start, duration } => { Some((*start, *start + *duration as i64)) } _ => None, } } + pub fn timezone(&self) -> Option { + match &self.data { + DavResourceMetadata::Calendar { tz } => Some(*tz), + _ => None, + } + } + pub fn is_container(&self) -> bool { match &self.data { DavResourceMetadata::File { is_container, .. } => *is_container, diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 2c9e640c..4fe6b06e 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -15,3 +15,4 @@ rkyv = { version = "0.8.10", features = ["little_endian"] } calcard = { path = "/Users/me/code/calcard", features = ["serde", "rkyv"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" +chrono = { version = "0.4.40", features = ["serde"] } \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-006.json b/crates/dav-proto/resources/requests/propfind-006.json index 92fbb577..c1b1d1b4 100644 --- a/crates/dav-proto/resources/requests/propfind-006.json +++ b/crates/dav-proto/resources/requests/propfind-006.json @@ -2,7 +2,7 @@ "type": "Prop", "data": [ { - "type": "CalDav", + "type": "Principal", "data": { "type": "CalendarHomeSet" } diff --git a/crates/dav-proto/resources/requests/propfind-007.json b/crates/dav-proto/resources/requests/propfind-007.json index 87a1bc2a..8abfd1d7 100644 --- a/crates/dav-proto/resources/requests/propfind-007.json +++ b/crates/dav-proto/resources/requests/propfind-007.json @@ -26,13 +26,6 @@ "attrs": null } }, - { - "type": "DeadProperty", - "data": { - "name": "cs:source", - "attrs": null - } - }, { "type": "CalDav", "data": { diff --git a/crates/dav-proto/resources/requests/propfind-008.json b/crates/dav-proto/resources/requests/propfind-008.json index 4b6757bd..425a66af 100644 --- a/crates/dav-proto/resources/requests/propfind-008.json +++ b/crates/dav-proto/resources/requests/propfind-008.json @@ -14,10 +14,9 @@ } }, { - "type": "DeadProperty", + "type": "WebDav", "data": { - "name": "cs:getctag", - "attrs": null + "type": "GetCTag" } }, { diff --git a/crates/dav-proto/resources/requests/report-024.json b/crates/dav-proto/resources/requests/report-024.json new file mode 100644 index 00000000..5c82d5af --- /dev/null +++ b/crates/dav-proto/resources/requests/report-024.json @@ -0,0 +1,45 @@ +{ + "type": "ExpandProperty", + "properties": [ + { + "property": { + "type": "DeadProperty", + "data": { + "name": "version-history", + "attrs": "name=\"version-history\"" + } + }, + "depth": 0 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "version-set", + "attrs": "name=\"version-set\"" + } + }, + "depth": 1 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "creator-displayname", + "attrs": "name=\"creator-displayname\"" + } + }, + "depth": 2 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "activity-set", + "attrs": "name=\"activity-set\"" + } + }, + "depth": 2 + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-024.xml b/crates/dav-proto/resources/requests/report-024.xml new file mode 100644 index 00000000..72b15729 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-024.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-025.json b/crates/dav-proto/resources/requests/report-025.json new file mode 100644 index 00000000..c2519035 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-025.json @@ -0,0 +1,83 @@ +{ + "type": "ExpandProperty", + "properties": [ + { + "property": { + "type": "DeadProperty", + "data": { + "name": "calendar-proxy-read-for", + "attrs": "name=\"calendar-proxy-read-for\" namespace=\"\nhttp://calendarserver.org/ns/\"" + } + }, + "depth": 0 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "email-address-set", + "attrs": "name=\"email-address-set\"\nnamespace=\"http://calendarserver.org/ns/\"" + } + }, + "depth": 1 + }, + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "depth": 1 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "calendar-user-address-set", + "attrs": "name=\"calendar-user-address-set\"\nnamespace=\"urn:ietf:params:xml:ns:caldav\"" + } + }, + "depth": 1 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "calendar-proxy-write-for", + "attrs": "name=\"calendar-proxy-write-for\"\nnamespace=\"http://calendarserver.org/ns/\"" + } + }, + "depth": 0 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "email-address-set", + "attrs": "name=\"email-address-set\" namespace=\"http://calendarserver.org/ns/\"" + } + }, + "depth": 1 + }, + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "depth": 1 + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "calendar-user-address-set", + "attrs": "name=\"calendar-user-address-set\"\nnamespace=\"urn:ietf:params:xml:ns:caldav\"" + } + }, + "depth": 1 + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-025.xml b/crates/dav-proto/resources/requests/report-025.xml new file mode 100644 index 00000000..fa75338a --- /dev/null +++ b/crates/dav-proto/resources/requests/report-025.xml @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/008.xml b/crates/dav-proto/resources/responses/008.xml index b966e279..fa1ac79a 100644 --- a/crates/dav-proto/resources/responses/008.xml +++ b/crates/dav-proto/resources/responses/008.xml @@ -1,11 +1,11 @@ - + http://cal.example.com/bernard/work/abcd2.ics "fffff-abcd2" - +]]> HTTP/1.1 200 OK @@ -25,7 +25,7 @@ END:VCALENDAR "fffff-abcd3" - +]]> HTTP/1.1 200 OK diff --git a/crates/dav-proto/resources/responses/009.xml b/crates/dav-proto/resources/responses/009.xml index 7af87667..b9b8621c 100644 --- a/crates/dav-proto/resources/responses/009.xml +++ b/crates/dav-proto/resources/responses/009.xml @@ -1,10 +1,10 @@ - + - + HTTP/1.1 200 OK diff --git a/crates/dav-proto/resources/responses/010.xml b/crates/dav-proto/resources/responses/010.xml index 40cecd2f..52f36ba3 100644 --- a/crates/dav-proto/resources/responses/010.xml +++ b/crates/dav-proto/resources/responses/010.xml @@ -1,18 +1,18 @@ - + /home/bernard/addressbook/v102.vcf "23ba4d-ff11fb" - +]]> HTTP/1.1 200 OK diff --git a/crates/dav-proto/resources/responses/011.xml b/crates/dav-proto/resources/responses/011.xml index 43e15699..1d2b4fbe 100644 --- a/crates/dav-proto/resources/responses/011.xml +++ b/crates/dav-proto/resources/responses/011.xml @@ -1,5 +1,5 @@ - + /home/bernard/addressbook/ HTTP/1.1 507 Insufficient Storage diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index 06110094..13b1b2e6 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -15,7 +15,7 @@ use mail_parser::DateTime; use crate::schema::{ property::{ CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName, - Comp, DateRange, DavProperty, DavValue, PrincipalProperty, ResourceType, WebDavProperty, + Comp, DavProperty, DavValue, PrincipalProperty, ResourceType, TimeRange, WebDavProperty, }, request::{DavPropertyValue, DeadProperty, VCardPropertyWithGroup}, response::List, @@ -171,7 +171,7 @@ impl Tokenizer<'_> { }, raw, } => { - data.expand = Some(DateRange::from_raw(&raw)?); + data.expand = TimeRange::from_raw(&raw)?; self.expect_element_end()?; } Token::ElementStart { @@ -182,7 +182,7 @@ impl Tokenizer<'_> { }, raw, } => { - data.limit_recurrence = Some(DateRange::from_raw(&raw)?); + data.limit_recurrence = TimeRange::from_raw(&raw)?; self.expect_element_end()?; } Token::ElementStart { @@ -193,7 +193,7 @@ impl Tokenizer<'_> { }, raw, } => { - data.limit_freebusy = Some(DateRange::from_raw(&raw)?); + data.limit_freebusy = TimeRange::from_raw(&raw)?; self.expect_element_end()?; } Token::ElementEnd => { @@ -381,9 +381,23 @@ impl Tokenizer<'_> { } } -impl DateRange { - pub fn from_raw(raw: &RawElement<'_>) -> super::Result { - let mut range = DateRange { start: 0, end: 0 }; +impl TimeRange { + pub fn is_in_range(&self, match_overlap: bool, start: i64, end: i64) -> bool { + if !match_overlap { + // RFC4791#9.9: (start < DTEND AND end > DTSTART) + self.start < end && self.end > start + } else { + // RFC4791#9.9: ((start < DUE) OR (start <= DTSTART)) AND ((end > DTSTART) OR (end >= DUE)) + let range = self.start..=self.end; + range.contains(&start) || range.contains(&end) + } + } + + pub fn from_raw(raw: &RawElement<'_>) -> super::Result> { + let mut range = TimeRange { + start: i64::MIN, + end: i64::MAX, + }; for attribute in raw.attributes::() { match attribute? { @@ -397,7 +411,15 @@ impl DateRange { } } - Ok(range) + if range.end < range.start { + range.end = i64::MAX; + } + + if range.start != i64::MIN || range.end != i64::MAX { + Ok(Some(range)) + } else { + Ok(None) + } } } @@ -572,7 +594,7 @@ impl AttributeValue for ICalendarDateTime { Self: Sized, { let mut dt = PartialDateTime::default(); - dt.parse_timestamp(&mut s.as_bytes().iter().peekable()); + dt.parse_timestamp(&mut s.as_bytes().iter().peekable(), true); dt.to_timestamp().map(ICalendarDateTime) } } diff --git a/crates/dav-proto/src/requests/report.rs b/crates/dav-proto/src/requests/report.rs index e565fad2..bc5122cf 100644 --- a/crates/dav-proto/src/requests/report.rs +++ b/crates/dav-proto/src/requests/report.rs @@ -12,11 +12,12 @@ use calcard::{ use crate::{ parser::{tokenizer::Tokenizer, DavParser, RawElement, Token, XmlValueParser}, schema::{ - property::DateRange, + property::{DavProperty, TimeRange}, request::{ - AclPrincipalPropSet, AddressbookQuery, CalendarQuery, Filter, FilterOp, FreeBusyQuery, - MultiGet, PrincipalMatch, PrincipalPropertySearch, PropFind, Report, SyncCollection, - TextMatch, Timezone, VCardPropertyWithGroup, + AclPrincipalPropSet, AddressbookQuery, CalendarQuery, DeadElementTag, ExpandProperty, + ExpandPropertyItem, Filter, FilterOp, FreeBusyQuery, MultiGet, PrincipalMatch, + PrincipalPropertySearch, PropFind, Report, SyncCollection, TextMatch, Timezone, + VCardPropertyWithGroup, }, Attribute, Collation, Element, MatchType, NamedElement, Namespace, }, @@ -67,6 +68,10 @@ impl DavParser for Report { } => stream .expect_element_end() .map(|_| Report::PrincipalSearchPropertySet), + NamedElement { + ns: Namespace::Dav, + element: Element::ExpandProperty, + } => ExpandProperty::parse(stream).map(Report::ExpandProperty), other => Err(other.into_unexpected()), } } @@ -193,14 +198,16 @@ impl DavParser for CalendarQuery { ns: Namespace::CalDav, element: Element::TimeRange, } => { - let range = DateRange::from_raw(&raw)?; + let range = TimeRange::from_raw(&raw)?; stream.expect_element_end()?; - if let Some(filter) = Filter::from_parts( - components.iter().map(|(c, _)| *c).collect(), - property.clone(), - parameter.clone(), - FilterOp::TimeRange(range), - ) { + if let Some(filter) = range.and_then(|range| { + Filter::from_parts( + components.iter().map(|(c, _)| *c).collect(), + property.clone(), + parameter.clone(), + FilterOp::TimeRange(range), + ) + }) { cq.filters.push(filter); } } @@ -381,7 +388,7 @@ impl DavParser for FreeBusyQuery { element: Element::TimeRange, }, raw, - } => DateRange::from_raw(&raw).map(|range| FreeBusyQuery { range }), + } => TimeRange::from_raw(&raw).map(|range| FreeBusyQuery { range }), other => Err(other.into_unexpected()), } } @@ -495,6 +502,67 @@ impl DavParser for SyncCollection { } } +impl DavParser for ExpandProperty { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut ep = ExpandProperty { properties: vec![] }; + let mut depth = 1; + + loop { + match stream.token()? { + Token::ElementStart { name, raw } => match name { + NamedElement { + ns, + element: Element::Property, + } => { + for attribute in raw.attributes::() { + if let Attribute::Name(name) = attribute? { + if let Some(property) = Element::try_parse(name.as_bytes()) + .copied() + .and_then(|element| { + DavProperty::from_element(NamedElement { ns, element }) + }) + { + ep.properties.push(ExpandPropertyItem { + property, + depth: depth - 1, + }); + } else { + let attrs = raw.0.attributes_raw().trim_ascii(); + ep.properties.push(ExpandPropertyItem { + property: DavProperty::DeadProperty(DeadElementTag { + name, + attrs: (!attrs.is_empty()).then(|| { + String::from_utf8_lossy(attrs).into_owned() + }), + }), + depth: depth - 1, + }); + } + break; + } + } + depth += 1; + } + name => return Err(name.into_unexpected()), + }, + Token::ElementEnd => { + depth -= 1; + + if depth == 0 { + break; + } + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + element => return Err(element.into_unexpected()), + } + } + + Ok(ep) + } +} + impl TextMatch { fn parse(raw: RawElement<'_>) -> crate::parser::Result { let mut tm = TextMatch { diff --git a/crates/dav-proto/src/responses/acl.rs b/crates/dav-proto/src/responses/acl.rs index 4f35837c..8f34aa73 100644 --- a/crates/dav-proto/src/responses/acl.rs +++ b/crates/dav-proto/src/responses/acl.rs @@ -93,6 +93,7 @@ impl Display for Principal { write!(f, "")?; match self { Principal::Href(href) => href.fmt(f), + Principal::Response(response) => response.fmt(f), Principal::All => "".fmt(f), Principal::Authenticated => "".fmt(f), Principal::Unauthenticated => "".fmt(f), diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs index 426b22f7..d3a4eb06 100644 --- a/crates/dav-proto/src/responses/property.rs +++ b/crates/dav-proto/src/responses/property.rs @@ -143,6 +143,7 @@ impl Display for DavValue { ) ) } + DavValue::Response(v) => v.fmt(f), DavValue::Null => Ok(()), } } diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index a3c5799d..70cc69af 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -13,7 +13,7 @@ use crate::{Depth, Timeout}; use super::{ request::{DavPropertyValue, DeadElementTag, DeadProperty}, - response::{Ace, AclRestrictions, Href, List, SupportedPrivilege}, + response::{Ace, AclRestrictions, Href, List, Response, SupportedPrivilege}, Collation, Namespace, }; @@ -117,14 +117,14 @@ pub enum PrincipalProperty { #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct CalendarData { pub properties: Vec, - pub expand: Option, - pub limit_recurrence: Option, - pub limit_freebusy: Option, + pub expand: Option, + pub limit_recurrence: Option, + pub limit_freebusy: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -pub struct DateRange { +pub struct TimeRange { pub start: i64, pub end: i64, } @@ -163,6 +163,7 @@ pub enum DavValue { Href(List), Acl(List), AclRestrictions(AclRestrictions), + Response(Response), DeadProperty(DeadProperty), SupportedAddressData, SupportedCalendarData, diff --git a/crates/dav-proto/src/schema/request.rs b/crates/dav-proto/src/schema/request.rs index 3fbe1264..55ea9af6 100644 --- a/crates/dav-proto/src/schema/request.rs +++ b/crates/dav-proto/src/schema/request.rs @@ -10,7 +10,7 @@ use calcard::{ }; use super::{ - property::{DateRange, DavProperty, DavValue, LockScope, LockType}, + property::{DavProperty, DavValue, LockScope, LockType, TimeRange}, response::Ace, Collation, MatchType, }; @@ -74,7 +74,16 @@ pub enum Report { #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -pub struct ExpandProperty {} +pub struct ExpandProperty { + pub properties: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct ExpandPropertyItem { + pub property: DavProperty, + pub depth: u32, +} #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] @@ -112,7 +121,7 @@ pub enum Timezone { #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct FreeBusyQuery { - pub range: DateRange, + pub range: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -160,7 +169,7 @@ pub enum Filter { pub enum FilterOp { Exists, Undefined, - TimeRange(DateRange), + TimeRange(TimeRange), TextMatch(TextMatch), } diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs index 84450ea8..c4a82c4b 100644 --- a/crates/dav-proto/src/schema/response.rs +++ b/crates/dav-proto/src/schema/response.rs @@ -25,6 +25,8 @@ pub struct MultiStatus { pub sync_token: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct Response { pub href: Href, pub typ: ResponseType, @@ -33,17 +35,24 @@ pub struct Response { pub location: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum ResponseType { PropStat(List), Status { href: List, status: Status }, } +#[derive(Debug, Clone, PartialEq, Eq)] #[repr(transparent)] pub struct Status(pub StatusCode); +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] pub struct Location(pub Href); +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] pub struct ResponseDescription(pub String); @@ -66,6 +75,8 @@ pub struct MkColResponse { pub mkcalendar: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct PropStat { pub prop: Prop, pub status: Status, @@ -73,6 +84,8 @@ pub struct PropStat { pub response_description: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] pub struct Prop(pub List); @@ -111,6 +124,7 @@ pub enum GrantDeny { #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum Principal { Href(Href), + Response(Response), All, #[default] Authenticated, @@ -158,12 +172,16 @@ pub struct ErrorResponse { pub error: Condition, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum Condition { Base(BaseCondition), Cal(CalCondition), Card(CardCondition), } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum BaseCondition { NoConflictingLock(List), LockTokenSubmitted(List), @@ -192,11 +210,15 @@ pub enum BaseCondition { ValidSyncToken, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct Resource { pub href: Href, pub privilege: Privilege, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum CalCondition { CalendarCollectionLocationOk, ValidCalendarData, @@ -217,6 +239,8 @@ pub enum CalCondition { MaxAttendeesPerInstance, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub enum CardCondition { SupportedAddressData, SupportedAddressDataConversion, @@ -263,3 +287,37 @@ impl MultiStatus { self.response.0.is_empty() } } + +#[cfg(test)] +mod serde_impl { + use super::Status; + use hyper::StatusCode; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + impl Serialize for Status { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serialize the status code as a u16 + serializer.serialize_u16(self.0.as_u16()) + } + } + + impl<'de> Deserialize<'de> for Status { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize as u16 + let status_value = u16::deserialize(deserializer)?; + + // Convert u16 to StatusCode + let status_code = StatusCode::try_from(status_value).map_err(|_| { + serde::de::Error::custom(format!("Invalid status code: {}", status_value)) + })?; + + Ok(Status(status_code)) + } + } +} diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 0102d622..9cb00a59 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -20,6 +20,7 @@ hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } percent-encoding = "2.3.1" rkyv = { version = "0.8.10", features = ["little_endian"] } compact_str = "0.9.0" +chrono = "0.4.40" [dev-dependencies] diff --git a/crates/dav/src/calendar/copy_move.rs b/crates/dav/src/calendar/copy_move.rs index f8182924..cd17be15 100644 --- a/crates/dav/src/calendar/copy_move.rs +++ b/crates/dav/src/calendar/copy_move.rs @@ -62,7 +62,7 @@ impl CalendarCopyMoveRequestHandler for Server { access_token, from_account_id, Collection::Calendar, - if from_resource.is_container { + if from_resource.is_container() { from_resource.document_id } else { from_resource.parent_id.unwrap() @@ -112,16 +112,16 @@ impl CalendarCopyMoveRequestHandler for Server { .map(|(_, name)| name) .unwrap_or(destination_resource_name); - match (from_resource.is_container, to_resource.is_container) { + match (from_resource.is_container(), to_resource.is_container()) { (true, true) => { let from_children_ids = from_resources .subtree(from_resource_name) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(); let to_document_ids = to_resources .subtree(destination_resource_name) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(); @@ -240,7 +240,7 @@ impl CalendarCopyMoveRequestHandler for Server { if let Some(parent_resource) = parent_resource { // Creating items under a event is not allowed // Copying/moving containers under a container is not allowed - if !parent_resource.is_container || from_resource.is_container { + if !parent_resource.is_container() || from_resource.is_container() { return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } @@ -322,7 +322,7 @@ impl CalendarCopyMoveRequestHandler for Server { } } else { // Copying/moving events to the root is not allowed - if !from_resource.is_container { + if !from_resource.is_container() { return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } @@ -354,7 +354,7 @@ impl CalendarCopyMoveRequestHandler for Server { // Copy/move container let from_children_ids = from_resources .subtree(from_resource_name) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(); if is_move { @@ -474,7 +474,7 @@ async fn copy_event( .as_ref(), to_account_id, to_calendar_id, - event.inner.event.uids().next().unwrap_or_default(), + event.inner.data.event.uids().next().unwrap_or_default(), ) .await?; @@ -602,7 +602,7 @@ async fn move_event( .as_ref(), to_account_id, to_calendar_id, - event.inner.event.uids().next().unwrap_or_default(), + event.inner.data.event.uids().next().unwrap_or_default(), ) .await?; diff --git a/crates/dav/src/calendar/delete.rs b/crates/dav/src/calendar/delete.rs index e02c4afe..cc61f828 100644 --- a/crates/dav/src/calendar/delete.rs +++ b/crates/dav/src/calendar/delete.rs @@ -64,7 +64,7 @@ impl CalendarDeleteRequestHandler for Server { // Fetch entry let mut batch = BatchBuilder::new(); - if delete_resource.is_container { + if delete_resource.is_container() { let calendar_ = self .get_archive(account_id, Collection::Calendar, document_id) .await @@ -112,7 +112,7 @@ impl CalendarDeleteRequestHandler for Server { document_id, resources .subtree(delete_path) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(), &mut batch, diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs new file mode 100644 index 00000000..7a100291 --- /dev/null +++ b/crates/dav/src/calendar/freebusy.rs @@ -0,0 +1,287 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::str::FromStr; + +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, ArchivedICalendarParameter, + ArchivedICalendarProperty, ArchivedICalendarStatus, ArchivedICalendarValue, ICalendar, + ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarFreeBusyType, + ICalendarParameter, ICalendarPeriod, ICalendarProperty, ICalendarTransparency, + ICalendarValue, + }, +}; +use common::{PROD_ID, Server, auth::AccessToken}; +use dav_proto::{ + RequestHeaders, + schema::{property::TimeRange, request::FreeBusyQuery}, +}; +use groupware::{calendar::CalendarEvent, hierarchy::DavHierarchy}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{acl::Acl, collection::Collection}; +use store::write::serialize::rkyv_deserialize; +use trc::AddContext; + +pub(crate) trait CalendarFreebusyRequestHandler: Sync + Send { + fn handle_calendar_freebusy_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: FreeBusyQuery, + ) -> impl Future> + Send; +} + +impl CalendarFreebusyRequestHandler for Server { + async fn handle_calendar_freebusy_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: FreeBusyQuery, + ) -> crate::Result { + // 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, Collection::Calendar) + .await + .caused_by(trc::location!())?; + let resource = resources + .paths + .by_name( + 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)); + } + let default_tz = resource.timezone().unwrap_or(Tz::UTC); + + // Obtain shared ids + let shared_ids = if !access_token.is_member(account_id) { + self.shared_containers( + access_token, + account_id, + Collection::Calendar, + [Acl::ReadItems, Acl::ReadFreeBusy], + false, + ) + .await + .caused_by(trc::location!())? + .into() + } else { + None + }; + + // Build FreeBusy component + let mut entries = Vec::new(); + if let Some(range) = request.range { + 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, &range) + }) + .map(|resource| resource.document_id) + .collect::>(); + + for document_id in document_ids { + let archive = if let Some(archive) = self + .get_archive(account_id, Collection::CalendarEvent, document_id) + .await + .caused_by(trc::location!())? + { + archive + } else { + continue; + }; + let event = archive + .unarchive::() + .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; + } + + for (component_id, component) in components { + let component_id = component_id as u16; + match component.component_type { + ArchivedICalendarComponentType::VEvent => { + let fbtype = match component.status() { + Some(ArchivedICalendarStatus::Cancelled) => continue, + Some(ArchivedICalendarStatus::Tentative) => { + ICalendarFreeBusyType::BusyTentative + } + Some(ArchivedICalendarStatus::Other(v)) => { + ICalendarFreeBusyType::Other(v.as_str().to_string()) + } + _ => 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(ICalendarValue::Period( + ICalendarPeriod::Range { + start: PartialDateTime::from_utc_timestamp(event.start), + end: PartialDateTime::from_utc_timestamp(event.end), + }, + )); + } + } + + if !events_in_range.is_empty() { + entries.push(ICalendarEntry { + name: ICalendarProperty::Freebusy, + params: vec![ICalendarParameter::Fbtype(fbtype)], + values: 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(entry, &range, true, default_tz) + .peekable(); + if fb_in_range.peek().is_some() { + entries.push(ICalendarEntry { + name: ICalendarProperty::Freebusy, + params: entry + .params + .iter() + .filter(|param| { + matches!( + param, + ArchivedICalendarParameter::Fbtype(_) + ) + }) + .filter_map(|v| rkyv_deserialize(v).ok()) + .collect(), + values: fb_in_range.collect(), + }); + } + } + } + } + _ => {} + } + } + } + } + + // Build ICalendar + let ical = 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![], + }, + ], + } + .to_string(); + + Ok(HttpResponse::new(StatusCode::OK) + .with_content_type("text/calendar; charset=utf-8") + .with_text_body(ical)) + } +} + +pub(crate) fn freebusy_in_range( + entry: &ArchivedICalendarEntry, + range: &TimeRange, + to_utc: bool, + default_tz: Tz, +) -> impl Iterator { + 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) { + if to_utc { + ICalendarValue::Period(ICalendarPeriod::Range { + start: PartialDateTime::from_utc_timestamp(start), + end: PartialDateTime::from_utc_timestamp(end), + }) + .into() + } else { + rkyv_deserialize(value).ok() + } + } else { + None + } + }) + } else { + None + } + }) +} diff --git a/crates/dav/src/calendar/get.rs b/crates/dav/src/calendar/get.rs index 0fc06c93..b07d9d77 100644 --- a/crates/dav/src/calendar/get.rs +++ b/crates/dav/src/calendar/get.rs @@ -55,7 +55,7 @@ impl CalendarGetRequestHandler for Server { .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, ) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - if resource.is_container { + if resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } @@ -108,7 +108,7 @@ impl CalendarGetRequestHandler for Server { .with_etag(etag) .with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string()); - let ical = event.event.to_string(); + let ical = event.data.event.to_string(); if !is_head { Ok(response.with_binary_body(ical)) diff --git a/crates/dav/src/calendar/mod.rs b/crates/dav/src/calendar/mod.rs index b66a975f..112a281e 100644 --- a/crates/dav/src/calendar/mod.rs +++ b/crates/dav/src/calendar/mod.rs @@ -6,6 +6,7 @@ pub mod copy_move; pub mod delete; +pub mod freebusy; pub mod get; pub mod mkcol; pub mod proppatch; @@ -104,7 +105,7 @@ pub(crate) async fn assert_is_unique_uid( .caused_by(trc::location!())?; if !hits.results.is_empty() { for path in resources.paths.iter() { - if !path.is_container + if !path.is_container() && hits.results.contains(path.document_id) && path.parent_id.unwrap() == calendar_id { diff --git a/crates/dav/src/calendar/proppatch.rs b/crates/dav/src/calendar/proppatch.rs index d8838556..4308ef5e 100644 --- a/crates/dav/src/calendar/proppatch.rs +++ b/crates/dav/src/calendar/proppatch.rs @@ -81,7 +81,7 @@ impl CalendarPropPatchRequestHandler for Server { .and_then(|r| resources.paths.by_name(r)) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let document_id = resource.document_id; - let collection = if resource.is_container { + let collection = if resource.is_container() { Collection::Calendar } else { Collection::CalendarEvent @@ -93,7 +93,7 @@ impl CalendarPropPatchRequestHandler for Server { // Verify ACL if !access_token.is_member(account_id) { - let (acl, document_id) = if resource.is_container { + let (acl, document_id) = if resource.is_container() { (Acl::Read, resource.document_id) } else { (Acl::ReadItems, resource.parent_id.unwrap()) @@ -142,7 +142,7 @@ impl CalendarPropPatchRequestHandler for Server { let mut batch = BatchBuilder::new(); let mut items = Vec::with_capacity(request.remove.len() + request.set.len()); - let etag = if resource.is_container { + let etag = if resource.is_container() { // Deserialize let calendar = archive .to_unarchived::() diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs index ae7f888c..70a9a841 100644 --- a/crates/dav/src/calendar/query.rs +++ b/crates/dav/src/calendar/query.rs @@ -4,26 +4,45 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use calcard::{ + common::{PartialDateTime, timezone::Tz}, + icalendar::{ + ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarEntry, + ArchivedICalendarParameter, ArchivedICalendarProperty, ArchivedICalendarValue, + ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty, + ICalendarValue, dates::CalendarEvent, + }, +}; +use common::{DavResource, Server, auth::AccessToken}; use dav_proto::{ RequestHeaders, - schema::request::{CalendarQuery, FreeBusyQuery}, + schema::{ + property::{CalDavProperty, CalendarData, DavProperty, TimeRange}, + request::{CalendarQuery, Filter, FilterOp, PropFind, Timezone}, + }, }; -use groupware::hierarchy::DavHierarchy; +use groupware::{calendar::ArchivedCalendarEvent, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; +use std::{fmt::Write, slice::Iter, str::FromStr}; +use store::{ + ahash::{AHashMap, AHashSet}, + write::serialize::rkyv_deserialize, +}; use trc::AddContext; use crate::{ DavError, common::{ - DavQuery, + CalendarFilter, DavQuery, propfind::{PropFindItem, PropFindRequestHandler}, uri::DavUriResource, }, }; +use super::freebusy::freebusy_in_range; + pub(crate) trait CalendarQueryRequestHandler: Sync + Send { fn handle_calendar_query_request( &self, @@ -31,13 +50,6 @@ pub(crate) trait CalendarQueryRequestHandler: Sync + Send { headers: RequestHeaders<'_>, request: CalendarQuery, ) -> impl Future> + Send; - - fn handle_calendar_freebusy_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: FreeBusyQuery, - ) -> impl Future> + Send; } impl CalendarQueryRequestHandler for Server { @@ -65,7 +77,7 @@ impl CalendarQueryRequestHandler for Server { .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, ) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - if !resource.is_container { + if !resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } @@ -75,7 +87,8 @@ impl CalendarQueryRequestHandler for Server { access_token, account_id, Collection::Calendar, - Acl::ReadItems, + [Acl::ReadItems], + false, ) .await .caused_by(trc::location!())? @@ -84,12 +97,18 @@ impl CalendarQueryRequestHandler for Server { 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, range)) { items.push(PropFindItem::new( resources.format_resource(resource), @@ -99,117 +118,522 @@ impl CalendarQueryRequestHandler for Server { } } + // 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, items, headers), + DavQuery::calendar_query(request, max_time_range, items, headers), ) .await } +} - async fn handle_calendar_freebusy_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: FreeBusyQuery, - ) -> crate::Result { - todo!() +pub(crate) fn is_resource_in_time_range(resource: &DavResource, range: &TimeRange) -> bool { + if let Some((start, end)) = resource.event_time_range() { + // Check if either the start or end of the resource is within the range + let range = range.start..=range.end; + range.contains(&start) || range.contains(&end) + } else { + // If the resource does not have a time range, it is not in the range + false } } -/* -pub(crate) fn vcard_query(card: &ArchivedVCard, filters: &AddressbookFilter) -> bool { - let mut is_all = true; - let mut matches_one = false; +fn extract_filter_range(query: &CalendarQuery) -> Option { + let mut range = TimeRange { + start: i64::MAX, + end: i64::MIN, + }; - for filter in filters { - match filter { - Filter::AnyOf => { - is_all = false; + 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; } - Filter::AllOf => { - is_all = true; + if date_range.end > range.end { + range.end = date_range.end; } - Filter::Property { prop, op, .. } => { - let result = if let Some(entry) = find_property(card, prop) { - 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() { - if text_match.matches(&text.to_lowercase()) { - matched_any = true; - break; - } - } - } + if range.start != i64::MAX { + Some(range) + } else { + None + } +} - matched_any - } - FilterOp::TimeRange(_) => false, - } - } else { - matches!(op, FilterOp::Undefined) - }; +fn extract_data_range(propfind: &PropFind, filter_range: Option) -> Option { + let props = match propfind { + PropFind::PropName => todo!(), + PropFind::AllProp(props) | PropFind::Prop(props) => props, + }; - if result { - matches_one = true; - } else if is_all { - return false; + 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; } } - Filter::Parameter { - prop, param, op, .. - } => { - let result = if let Some(entry) = - find_property(card, prop).and_then(|entry| find_parameter(entry, param)) - { - match op { - FilterOp::Exists => true, - FilterOp::Undefined => false, - FilterOp::TextMatch(text_match) => { - if let Some(text) = entry.as_text() { - text_match.matches(&text.to_lowercase()) + + return if range.start != i64::MAX { + Some(range) + } else { + None + }; + } + } + + filter_range +} + +pub fn try_parse_tz(tz: &Timezone) -> Option { + match tz { + Timezone::Name(value) | Timezone::Id(value) => Tz::from_str(value).ok(), + Timezone::None => None, + } +} + +pub(crate) struct CalendarQueryHandler { + default_tz: Tz, + filtered_components: AHashSet, + expanded_times: Vec>, +} + +impl CalendarQueryHandler { + pub fn new( + event: &ArchivedCalendarEvent, + max_time_range: Option, + default_tz: Tz, + ) -> Self { + Self { + default_tz, + filtered_components: AHashSet::new(), + expanded_times: max_time_range + .map(|max_time_range| { + event + .data + .expand(default_tz, max_time_range) + .unwrap_or_else(|| { + let todo = "log error"; + 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 result = false; + + for (_, comp) in find_components(ical, comp) { + if let Some(entry) = find_property(comp, prop) { + result = 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() { + if text_match.matches(&text.to_lowercase()) { + 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 + } + } + }; + + if result { + break; + } + } + } + + if result || matches!(op, FilterOp::Undefined) { + matches_one = true; + } else if is_all { + return false; + } + } + Filter::Parameter { + prop, + param, + op, + comp, + } => { + let mut result = false; + + for (_, comp) in find_components(ical, comp) { + if let Some(entry) = + find_property(comp, prop).and_then(|entry| find_parameter(entry, param)) + { + result = match op { + FilterOp::Exists => true, + FilterOp::Undefined => false, + FilterOp::TextMatch(text_match) => { + if let Some(text) = entry.as_text() { + text_match.matches(&text.to_lowercase()) + } else { + false + } + } + FilterOp::TimeRange(_) => false, + }; + if result { + break; + } + } + } + + if result || matches!(op, FilterOp::Undefined) { + 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) => { + let matching_comp_ids = find_components(ical, comp) + .map(|(id, comp)| (id as u16, &comp.component_type)) + .collect::>(); + if !matching_comp_ids.is_empty() { + let filtered_components = self + .expanded_times + .iter() + .filter(|event| { + matching_comp_ids.get(&event.comp_id).is_some_and(|ct| { + range.is_in_range( + ct == &&ICalendarComponentType::VTodo, + event.start, + event.end, + ) + }) + }) + .map(|event| event.comp_id) + .collect::>(); + if self.filtered_components.is_empty() { + self.filtered_components = filtered_components; + } else { + self.filtered_components + .retain(|id| filtered_components.contains(id)); + } + !self.filtered_components.is_empty() } else { false } } - FilterOp::TimeRange(_) => false, - } - } else { - matches!(op, FilterOp::Undefined) - }; + FilterOp::TextMatch(_) => false, + }; - if result { - matches_one = true; - } else if is_all { - return false; + if result { + matches_one = true; + } else if is_all { + return false; + } } } - Filter::Component { .. } => {} } + + is_all || matches_one } - is_all || matches_one + pub fn serialize_ical(&mut self, event: &ArchivedCalendarEvent, data: &CalendarData) -> String { + let mut out = String::with_capacity(event.size.to_native() as usize); + let _v = [0.into()]; + let mut component_iter: Iter<'_, rkyv::rend::u16_le> = _v.iter(); + let mut component_stack = Vec::with_capacity(4); + + if data.expand.is_some() { + self.expanded_times + .sort_unstable_by(|a, b| a.start.cmp(&b.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(); + + // Skip filtered components + if !self.filtered_components.is_empty() + && component.component_type.has_time_ranges() + && !self.filtered_components.contains(&component_id) + { + continue; + } + + // Limit recurrence override + if let Some(limit_recurrence) = &data.limit_recurrence { + if 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 { + if 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) + && 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 { + let is_recurrent = component.is_recurrent(); + let is_recurrence_override = component.is_recurrence_override(); + if is_recurrent || 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::>(); + for event in &self.expanded_times { + if event.comp_id == component_id + && expand.is_in_range(is_todo, event.start, event.end) + { + 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_recurrence_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"); + } + } + continue; + } + } + + // Skip filtered components + if entries.peek().is_none() { + continue; + } + + let _ = write!(&mut out, "BEGIN:{component_name}\r\n"); + + if data.limit_freebusy.is_none() + || component.component_type != ICalendarComponentType::VFreebusy + { + for (entry, with_value) in entries { + let _ = entry.write_to(&mut out, with_value); + } + } else { + // Filter freebusy + let range = data.limit_freebusy.unwrap(); + for (entry, with_value) in entries { + if matches!(entry.name, ArchivedICalendarProperty::Freebusy) { + let mut fb_in_range = + freebusy_in_range(entry, &range, false, 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); + } + } + } + + if !component.component_ids.is_empty() { + component_stack.push((component, component_iter)); + component_iter = component.component_ids.iter(); + } else { + 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; + } + } + + out + } + + pub fn into_expanded_times(self) -> Vec> { + self.expanded_times + } +} + +#[inline(always)] +fn find_components<'x>( + ical: &'x ArchivedICalendar, + comp: &[ICalendarComponentType], +) -> impl Iterator { + // TODO: Properly expand the component type path + let comp = comp + .last() + .copied() + .unwrap_or(ICalendarComponentType::VCalendar); + ical.components + .iter() + .enumerate() + .filter(move |(_, entry)| { + comp == ICalendarComponentType::VCalendar || entry.component_type == comp + }) } #[inline(always)] fn find_property<'x>( - card: &'x ArchivedVCard, - prop: &VCardPropertyWithGroup, -) -> Option<&'x ArchivedVCardEntry> { - card.entries - .iter() - .find(|entry| entry.name == prop.name && entry.group == prop.group) + comp: &'x ArchivedICalendarComponent, + prop: &ICalendarProperty, +) -> Option<&'x ArchivedICalendarEntry> { + comp.entries.iter().find(|entry| &entry.name == prop) } #[inline(always)] fn find_parameter<'x>( - entry: &'x ArchivedVCardEntry, - name: &VCardParameterName, -) -> Option<&'x ArchivedVCardParameter> { + entry: &'x ArchivedICalendarEntry, + name: &ICalendarParameterName, +) -> Option<&'x ArchivedICalendarParameter> { entry.params.iter().find(|param| param.matches_name(name)) } -*/ diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index 8a0a8333..1b1a7eef 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -15,7 +15,11 @@ use dav_proto::{ RequestHeaders, Return, schema::{property::Rfc1123DateTime, response::CalCondition}, }; -use groupware::{DavName, calendar::CalendarEvent, hierarchy::DavHierarchy}; +use groupware::{ + DavName, + calendar::{CalendarEvent, CalendarEventData}, + hierarchy::DavHierarchy, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; @@ -90,7 +94,7 @@ impl CalendarUpdateRequestHandler for Server { }; if let Some(resource) = resources.paths.by_name(resource_name) { - if resource.is_container { + if resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } @@ -151,7 +155,7 @@ impl CalendarUpdateRequestHandler for Server { Rfc1123DateTime::new(i64::from(event.inner.modified)).to_string(), ) .with_header("Preference-Applied", "return=representation") - .with_binary_body(event.inner.event.to_string())); + .with_binary_body(event.inner.data.event.to_string())); } Err(e) => return Err(e), } @@ -168,7 +172,7 @@ impl CalendarUpdateRequestHandler for Server { } // Validate iCal - if event.inner.event.uids().next().unwrap_or_default() != validate_ical(&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()), @@ -180,7 +184,7 @@ impl CalendarUpdateRequestHandler for Server { .deserialize::() .caused_by(trc::location!())?; new_event.size = bytes.len() as u32; - new_event.event = ical; + new_event.data = CalendarEventData::new(ical, self.core.dav.max_ical_instances); // Prepare write batch let mut batch = BatchBuilder::new(); @@ -192,7 +196,7 @@ impl CalendarUpdateRequestHandler for Server { Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) } else if let Some((Some(parent), name)) = resources.map_parent(resource_name) { - if !parent.is_container { + if !parent.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } @@ -253,7 +257,7 @@ impl CalendarUpdateRequestHandler for Server { name: name.to_string(), parent_id: parent.document_id, }], - event: ical, + data: CalendarEventData::new(ical, self.core.dav.max_ical_instances), size: bytes.len() as u32, ..Default::default() }; diff --git a/crates/dav/src/card/copy_move.rs b/crates/dav/src/card/copy_move.rs index d153361f..b7ecb033 100644 --- a/crates/dav/src/card/copy_move.rs +++ b/crates/dav/src/card/copy_move.rs @@ -62,7 +62,7 @@ impl CardCopyMoveRequestHandler for Server { access_token, from_account_id, Collection::AddressBook, - if from_resource.is_container { + if from_resource.is_container() { from_resource.document_id } else { from_resource.parent_id.unwrap() @@ -112,16 +112,16 @@ impl CardCopyMoveRequestHandler for Server { .map(|(_, name)| name) .unwrap_or(destination_resource_name); - match (from_resource.is_container, to_resource.is_container) { + match (from_resource.is_container(), to_resource.is_container()) { (true, true) => { let from_children_ids = from_resources .subtree(from_resource_name) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(); let to_document_ids = to_resources .subtree(destination_resource_name) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(); @@ -240,7 +240,7 @@ impl CardCopyMoveRequestHandler for Server { if let Some(parent_resource) = parent_resource { // Creating items under a card is not allowed // Copying/moving containers under a container is not allowed - if !parent_resource.is_container || from_resource.is_container { + if !parent_resource.is_container() || from_resource.is_container() { return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } @@ -322,7 +322,7 @@ impl CardCopyMoveRequestHandler for Server { } } else { // Copying/moving cards to the root is not allowed - if !from_resource.is_container { + if !from_resource.is_container() { return Err(DavError::Code(StatusCode::BAD_GATEWAY)); } @@ -354,7 +354,7 @@ impl CardCopyMoveRequestHandler for Server { // Copy/move container let from_children_ids = from_resources .subtree(from_resource_name) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(); if is_move { diff --git a/crates/dav/src/card/delete.rs b/crates/dav/src/card/delete.rs index 688b687c..c62ebb47 100644 --- a/crates/dav/src/card/delete.rs +++ b/crates/dav/src/card/delete.rs @@ -64,7 +64,7 @@ impl CardDeleteRequestHandler for Server { // Fetch entry let mut batch = BatchBuilder::new(); - if delete_resource.is_container { + if delete_resource.is_container() { let book_ = self .get_archive(account_id, Collection::AddressBook, document_id) .await @@ -112,7 +112,7 @@ impl CardDeleteRequestHandler for Server { document_id, resources .subtree(delete_path) - .filter(|r| !r.is_container) + .filter(|r| !r.is_container()) .map(|r| r.document_id) .collect::>(), &mut batch, diff --git a/crates/dav/src/card/get.rs b/crates/dav/src/card/get.rs index b5aaa875..75946ad2 100644 --- a/crates/dav/src/card/get.rs +++ b/crates/dav/src/card/get.rs @@ -55,7 +55,7 @@ impl CardGetRequestHandler for Server { .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, ) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - if resource.is_container { + if resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } diff --git a/crates/dav/src/card/mod.rs b/crates/dav/src/card/mod.rs index 7dbd0447..c58956bb 100644 --- a/crates/dav/src/card/mod.rs +++ b/crates/dav/src/card/mod.rs @@ -93,7 +93,7 @@ pub(crate) async fn assert_is_unique_uid( .caused_by(trc::location!())?; if !hits.results.is_empty() { for path in resources.paths.iter() { - if !path.is_container + if !path.is_container() && hits.results.contains(path.document_id) && path.parent_id.unwrap() == addressbook_id { diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs index e7a4bc3c..09579beb 100644 --- a/crates/dav/src/card/proppatch.rs +++ b/crates/dav/src/card/proppatch.rs @@ -81,7 +81,7 @@ impl CardPropPatchRequestHandler for Server { .and_then(|r| resources.paths.by_name(r)) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let document_id = resource.document_id; - let collection = if resource.is_container { + let collection = if resource.is_container() { Collection::AddressBook } else { Collection::ContactCard @@ -93,7 +93,7 @@ impl CardPropPatchRequestHandler for Server { // Verify ACL if !access_token.is_member(account_id) { - let (acl, document_id) = if resource.is_container { + let (acl, document_id) = if resource.is_container() { (Acl::Read, resource.document_id) } else { (Acl::ReadItems, resource.parent_id.unwrap()) @@ -142,7 +142,7 @@ impl CardPropPatchRequestHandler for Server { let mut batch = BatchBuilder::new(); let mut items = Vec::with_capacity(request.remove.len() + request.set.len()); - let etag = if resource.is_container { + let etag = if resource.is_container() { // Deserialize let book = archive .to_unarchived::() diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs index 71a0ef41..6bf52c50 100644 --- a/crates/dav/src/card/query.rs +++ b/crates/dav/src/card/query.rs @@ -4,20 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use calcard::vcard::{ - ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, -}; -use common::{Server, auth::AccessToken}; -use dav_proto::{ - RequestHeaders, - schema::request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup}, -}; -use groupware::hierarchy::DavHierarchy; -use http_proto::HttpResponse; -use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; -use trc::AddContext; - use crate::{ DavError, common::{ @@ -26,6 +12,23 @@ use crate::{ uri::DavUriResource, }, }; +use calcard::vcard::{ + ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, +}; +use common::{Server, auth::AccessToken}; +use dav_proto::{ + RequestHeaders, + schema::{ + property::CardDavPropertyName, + request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup}, + }, +}; +use groupware::hierarchy::DavHierarchy; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{acl::Acl, collection::Collection}; +use std::fmt::Write; +use trc::AddContext; pub(crate) trait CardQueryRequestHandler: Sync + Send { fn handle_card_query_request( @@ -61,7 +64,7 @@ impl CardQueryRequestHandler for Server { .ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?, ) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - if !resource.is_container { + if !resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } @@ -71,7 +74,8 @@ impl CardQueryRequestHandler for Server { access_token, account_id, Collection::AddressBook, - Acl::ReadItems, + [Acl::ReadItems], + false, ) .await .caused_by(trc::location!())? @@ -198,3 +202,25 @@ fn find_parameter<'x>( ) -> Option<&'x ArchivedVCardParameter> { entry.params.iter().find(|param| param.matches_name(name)) } + +pub(crate) fn serialize_vcard_with_props( + card: &ArchivedVCard, + props: &[CardDavPropertyName], +) -> String { + if !props.is_empty() { + let mut vcard = String::with_capacity(128); + let _ = write!(&mut vcard, "BEGIN:VCARD\r\n"); + for item in props { + for entry in card.entries.iter() { + if entry.name == item.name && entry.group == item.group { + let _ = entry.write_to(&mut vcard, !item.no_value); + break; + } + } + } + let _ = write!(&mut vcard, "END:VCARD\r\n"); + vcard + } else { + card.to_string() + } +} diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs index 70abb112..c021896a 100644 --- a/crates/dav/src/card/update.rs +++ b/crates/dav/src/card/update.rs @@ -85,7 +85,7 @@ impl CardUpdateRequestHandler for Server { }; if let Some(resource) = resources.paths.by_name(resource_name) { - if resource.is_container { + if resource.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } @@ -191,7 +191,7 @@ impl CardUpdateRequestHandler for Server { Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) } else if let Some((Some(parent), name)) = resources.map_parent(resource_name) { - if !parent.is_container { + if !parent.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 0dba62e8..e5120108 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -83,7 +83,8 @@ pub(crate) trait DavAclHandler: Sync + Send { access_token: &AccessToken, account_id: u32, grants: &ArchivedVec, - ) -> impl Future>> + Send; + expand: Option<&PropFind>, + ) -> impl Future>> + Send; } impl DavAclHandler for Server { @@ -115,7 +116,7 @@ impl DavAclHandler for Server { .resource .and_then(|r| resources.paths.by_name(r)) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; - if !resource.is_container && !matches!(collection, Collection::FileNode) { + if !resource.is_container() && !matches!(collection, Collection::FileNode) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -473,7 +474,8 @@ impl DavAclHandler for Server { access_token: &AccessToken, account_id: u32, grants: &ArchivedVec, - ) -> trc::Result> { + expand: Option<&PropFind>, + ) -> crate::Result> { let mut aces = Vec::with_capacity(grants.len()); if access_token.is_member(account_id) || grants.effective_acl(access_token).contains(Acl::Administer) @@ -500,14 +502,24 @@ impl DavAclHandler for Server { privileges.push(Privilege::ReadFreeBusy); } - let grant_account_name = self - .store() - .get_principal_name(grant_account_id) - .await - .caused_by(trc::location!())? - .unwrap_or_else(|| format!("_{grant_account_id}")); + let principal = if let Some(expand) = expand { + self.expand_principal(access_token, grant_account_id, expand) + .await? + .map(Principal::Response) + .unwrap_or_else(|| { + Principal::Href(Href(format!( + "{}/_{grant_account_id}/", + DavResourceName::Principal.base_path(), + ))) + }) + } else { + let grant_account_name = self + .store() + .get_principal_name(grant_account_id) + .await + .caused_by(trc::location!())? + .unwrap_or_else(|| format!("_{grant_account_id}")); - aces.push(Ace::new( Principal::Href(Href(format!( "{}/{}/", DavResourceName::Principal.base_path(), @@ -515,9 +527,10 @@ impl DavAclHandler for Server { &grant_account_name, NON_ALPHANUMERIC ), - ))), - GrantDeny::grant(privileges), - )); + ))) + }; + + aces.push(Ace::new(principal, GrantDeny::grant(privileges))); } } diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index f661202d..d6a4553c 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -12,10 +12,10 @@ use dav_proto::{ Depth, RequestHeaders, Return, schema::{ Namespace, - property::{ReportSet, ResourceType}, + property::{DavProperty, ReportSet, ResourceType, TimeRange}, request::{ - AddressbookQuery, ArchivedDeadProperty, CalendarQuery, Filter, MultiGet, PropFind, - SyncCollection, Timezone, VCardPropertyWithGroup, + AddressbookQuery, ArchivedDeadProperty, CalendarQuery, ExpandProperty, Filter, + MultiGet, PropFind, SyncCollection, Timezone, VCardPropertyWithGroup, }, }, }; @@ -47,6 +47,7 @@ pub(crate) struct DavQuery<'x> { pub limit: Option, pub ret: Return, pub depth_no_root: bool, + pub expand: bool, } #[derive(Default, Debug)] @@ -74,6 +75,7 @@ pub(crate) enum DavQueryFilter { Addressbook(AddressbookFilter), Calendar { filter: CalendarFilter, + max_time_range: Option, timezone: Timezone, }, } @@ -184,6 +186,7 @@ impl<'x> DavQuery<'x> { pub fn calendar_query( query: CalendarQuery, + max_time_range: Option, items: Vec, headers: RequestHeaders<'x>, ) -> Self { @@ -192,6 +195,7 @@ impl<'x> DavQuery<'x> { filter: DavQueryFilter::Calendar { filter: query.filters, timezone: query.timezone, + max_time_range, }, parent_collection: Collection::Calendar, items, @@ -222,6 +226,38 @@ impl<'x> DavQuery<'x> { limit: changes.limit, ret: headers.ret, depth_no_root: headers.depth_no_root, + expand: false, + } + } + + pub fn expand( + resource: OwnedUri<'x>, + expand: ExpandProperty, + headers: RequestHeaders<'x>, + ) -> Self { + Self { + resource: DavQueryResource::Uri(resource), + propfind: PropFind::Prop( + expand + .properties + .into_iter() + .filter_map(|item| { + if !matches!(item.property, DavProperty::DeadProperty(_)) { + Some(item.property) + } else { + None + } + }) + .collect(), + ), + depth: match headers.depth { + Depth::Zero => 0, + _ => 1, + }, + ret: headers.ret, + depth_no_root: headers.depth_no_root, + expand: true, + ..Default::default() } } diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index a12d0d5e..ed0b5bfe 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -6,12 +6,19 @@ use crate::{ DavError, DavErrorCondition, - calendar::{CALENDAR_CONTAINER_PROPS, CALENDAR_ITEM_PROPS}, - card::{CARD_CONTAINER_PROPS, CARD_ITEM_PROPS, query::vcard_query}, + calendar::{ + CALENDAR_CONTAINER_PROPS, CALENDAR_ITEM_PROPS, + query::{CalendarQueryHandler, try_parse_tz}, + }, + card::{ + CARD_CONTAINER_PROPS, CARD_ITEM_PROPS, + query::{serialize_vcard_with_props, vcard_query}, + }, common::{DavQueryResource, uri::DavUriResource}, file::{FILE_CONTAINER_PROPS, FILE_ITEM_PROPS}, principal::{CurrentUserPrincipal, propfind::PrincipalPropFind}, }; +use calcard::common::timezone::Tz; use common::{ DavResource, DavResources, Server, auth::{AccessToken, AsTenantId}, @@ -34,12 +41,13 @@ use dav_proto::{ }, }; use directory::{Type, backend::internal::manage::ManageDirectory}; -use groupware::{DavResourceName, calendar::ArchivedTimezone, hierarchy::DavHierarchy}; +use groupware::{ + DavCalendarResource, DavResourceName, calendar::ArchivedTimezone, hierarchy::DavHierarchy, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection}; use percent_encoding::NON_ALPHANUMERIC; -use std::fmt::Write; use std::sync::Arc; use store::{ ahash::AHashMap, @@ -101,6 +109,7 @@ pub(crate) struct PropFindItem { pub name: String, pub account_id: u32, pub document_id: u32, + pub parent_id: Option, pub is_container: bool, } @@ -363,7 +372,8 @@ impl PropFindRequestHandler for Server { access_token, account_id, collection_container, - Acl::ReadItems, + [Acl::ReadItems], + false, ) .await .caused_by(trc::location!())? @@ -508,7 +518,8 @@ impl PropFindRequestHandler for Server { access_token, account_id, collection_container, - Acl::ReadItems, + [Acl::ReadItems], + false, ) .await .caused_by(trc::location!())? @@ -534,7 +545,7 @@ impl PropFindRequestHandler for Server { .resource .and_then(|name| resources.paths.by_name(name)) { - if !resource.is_container { + if !resource.is_container() { if document_ids .as_ref() .as_ref() @@ -587,7 +598,7 @@ impl PropFindRequestHandler for Server { ))); } - let mut is_all_prop = false; + let mut skip_not_found = query.expand; let properties = match &query.propfind { PropFind::PropName => { let (container_props, children_props) = match collection_container { @@ -630,7 +641,7 @@ impl PropFindRequestHandler for Server { ); } PropFind::AllProp(items) => { - is_all_prop = true; + skip_not_found = true; let mut result = Vec::with_capacity(items.len() + DavProperty::ALL_PROPS.len()); result.extend(DavProperty::ALL_PROPS); result.extend(items.iter().filter(|field| !field.is_all_prop()).cloned()); @@ -662,17 +673,39 @@ impl PropFindRequestHandler for Server { .caused_by(trc::location!())?; // Filter + let mut calendar_filter = None; if let Some(query_filter) = &query_filter { match (query_filter, &archive) { - (DavQueryFilter::Addressbook(filters), ArchivedResource::ContactCard(card)) => { - if !vcard_query(&card.inner.card, filters) { + (DavQueryFilter::Addressbook(filter), ArchivedResource::ContactCard(card)) => { + if !vcard_query(&card.inner.card, filter) { continue; } } ( - DavQueryFilter::Calendar { filter, timezone }, + DavQueryFilter::Calendar { + filter, + timezone, + max_time_range, + }, ArchivedResource::CalendarEvent(event), - ) => {} + ) => { + let mut query_handler = CalendarQueryHandler::new( + event.inner, + *max_time_range, + try_parse_tz(timezone) + .or_else(|| { + item.parent_id.and_then(|calendar_id| { + self.cached_dav_resources(account_id, Collection::Calendar) + .and_then(|r| r.calendar_default_tz(calendar_id)) + }) + }) + .unwrap_or(Tz::UTC), + ); + if !query_handler.filter(event.inner, filter) { + continue; + } + calendar_filter = Some(query_handler); + } _ => (), } } @@ -696,12 +729,12 @@ impl PropFindRequestHandler for Server { property.clone(), DavValue::String(name.to_string()), )); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } WebDavProperty::GetContentLanguage => { - if !is_all_prop { + if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -711,7 +744,7 @@ impl PropFindRequestHandler for Server { property.clone(), DavValue::Uint64(value as u64), )); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -721,7 +754,7 @@ impl PropFindRequestHandler for Server { property.clone(), DavValue::String(value.to_string()), )); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -783,7 +816,7 @@ impl PropFindRequestHandler for Server { WebDavProperty::SupportedReportSet => { if let Some(report_set) = archive.supported_report_set() { fields.push(DavPropertyValue::new(property.clone(), report_set)); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -796,10 +829,24 @@ impl PropFindRequestHandler for Server { )); } WebDavProperty::CurrentUserPrincipal => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![access_token.current_user_principal()], - )); + if !query.expand { + fields.push(DavPropertyValue::new( + property.clone(), + vec![access_token.current_user_principal()], + )); + } else { + fields.push(DavPropertyValue::new( + property.clone(), + self.expand_principal( + access_token, + access_token.primary_id(), + &query.propfind, + ) + .await? + .map(DavValue::Response) + .unwrap_or(DavValue::Null), + )); + } } WebDavProperty::QuotaAvailableBytes => { if item.is_container { @@ -810,7 +857,7 @@ impl PropFindRequestHandler for Server { .caused_by(trc::location!())? .available, )); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -823,19 +870,33 @@ impl PropFindRequestHandler for Server { .caused_by(trc::location!())? .used, )); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } WebDavProperty::Owner => { - fields.push(DavPropertyValue::new( - property.clone(), - vec![ - data.owner(self, access_token, account_id) - .await - .caused_by(trc::location!())?, - ], - )); + if !query.expand { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ + data.owner(self, access_token, account_id) + .await + .caused_by(trc::location!())?, + ], + )); + } else { + fields.push(DavPropertyValue::new( + property.clone(), + self.expand_principal( + access_token, + account_id, + &query.propfind, + ) + .await? + .map(DavValue::Response) + .unwrap_or(DavValue::Null), + )); + } } WebDavProperty::Group => { fields.push(DavPropertyValue::empty(property.clone())); @@ -913,19 +974,23 @@ impl PropFindRequestHandler for Server { collection_container == Collection::Calendar, ), )); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } WebDavProperty::Acl => { if let Some(acls) = archive.acls() { let aces = self - .resolve_ace(access_token, account_id, acls) - .await - .caused_by(trc::location!())?; + .resolve_ace( + access_token, + account_id, + acls, + query.expand.then_some(&query.propfind), + ) + .await?; fields.push(DavPropertyValue::new(property.clone(), aces)); - } else if !is_all_prop { + } else if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -1003,29 +1068,16 @@ impl PropFindRequestHandler for Server { CardDavProperty::AddressData(items), ArchivedResource::ContactCard(card), ) => { - let vcard = if !items.is_empty() { - let mut vcard = String::with_capacity(128); - let _ = write!(&mut vcard, "BEGIN:VCARD\r\n"); - for item in items { - for entry in card.inner.card.entries.iter() { - if entry.name == item.name && entry.group == item.group { - let _ = entry.write_to(&mut vcard, !item.no_value); - break; - } - } - } - let _ = write!(&mut vcard, "END:VCARD\r\n"); - vcard - } else { - card.inner.card.to_string() - }; fields.push(DavPropertyValue::new( property.clone(), - DavValue::CData(vcard), + DavValue::CData(serialize_vcard_with_props( + &card.inner.card, + items, + )), )); } _ => { - if !is_all_prop { + if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -1113,7 +1165,7 @@ impl PropFindRequestHandler for Server { (CalDavProperty::MinDateTime, ArchivedResource::Calendar(_)) => { fields.push(DavPropertyValue::new( property.clone(), - DavValue::Timestamp(-2201212800), + DavValue::Timestamp(i64::MIN), )); } (CalDavProperty::MaxDateTime, ArchivedResource::Calendar(_)) => { @@ -1138,19 +1190,34 @@ impl PropFindRequestHandler for Server { )); } ( - CalDavProperty::CalendarData(calendar_data), - ArchivedResource::CalendarEvent(calendar), - ) => {} + CalDavProperty::CalendarData(data), + ArchivedResource::CalendarEvent(event), + ) => { + let ical = if calendar_filter.is_some() || !data.properties.is_empty() { + calendar_filter + .get_or_insert_with(|| { + CalendarQueryHandler::new(event.inner, None, Tz::UTC) + }) + .serialize_ical(event.inner, data) + } else { + event.inner.data.event.to_string() + }; + + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::CData(ical), + )); + } _ => { - if !is_all_prop { + if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } }, property => { - if !is_all_prop { + if !skip_not_found { fields_not_found.push(DavPropertyValue::empty(property.clone())); } } @@ -1158,7 +1225,7 @@ impl PropFindRequestHandler for Server { } // Add dead properties - if is_all_prop && !dead_properties.0.is_empty() { + if skip_not_found && !dead_properties.0.is_empty() { dead_properties.to_dav_values(&mut fields); } @@ -1214,7 +1281,8 @@ impl PropFindItem { name, account_id, document_id: resource.document_id, - is_container: resource.is_container, + parent_id: resource.parent_id, + is_container: resource.is_container(), } } } diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 7c88b8b4..1e6718e9 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -118,7 +118,7 @@ impl DavUriResource for Server { .by_name(resource) { Ok(Some(DocumentUri { - collection: if resource.is_container || uri.collection == Collection::FileNode { + collection: if resource.is_container() || uri.collection == Collection::FileNode { uri.collection } else if uri.collection == Collection::Calendar { Collection::CalendarEvent diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index 55d0dc2d..7b05c08e 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -68,10 +68,11 @@ impl FileCopyMoveRequestHandler for Server { from_account_id, Collection::FileNode, if is_move { - Bitmap::::from_iter([Acl::Read, Acl::Modify]) + [Acl::Read, Acl::Modify].as_slice().iter().copied() } else { - Bitmap::::from_iter([Acl::Read]) + [Acl::Read].as_slice().iter().copied() }, + false, ) .await .caused_by(trc::location!())?; @@ -212,7 +213,7 @@ impl FileCopyMoveRequestHandler for Server { .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let space_needed = from_files .subtree(&res.name) - .map(|a| a.size as u64) + .map(|a| a.size() as u64) .sum::(); self.has_available_quota( &self.get_resource_token(access_token, to_account_id).await?, @@ -232,7 +233,7 @@ impl FileCopyMoveRequestHandler for Server { .subtree(destination_resource_name) .collect::>(); if !ids.is_empty() { - ids.sort_unstable_by(|a, b| b.hierarchy_sequence.cmp(&a.hierarchy_sequence)); + ids.sort_unstable_by(|a, b| b.hierarchy_sequence().cmp(&a.hierarchy_sequence())); let mut sorted_ids = Vec::with_capacity(ids.len()); sorted_ids.extend(ids.into_iter().map(|a| a.document_id)); DestroyArchive(sorted_ids) @@ -408,12 +409,12 @@ async fn copy_container( let mut copy_files = if infinity_copy { from_files .subtree(&res.name) - .map(|r| (r.document_id, r.hierarchy_sequence)) + .map(|r| (r.document_id, r.hierarchy_sequence())) .collect::>() } else { from_files .subtree_with_depth(&res.name, 1) - .map(|r| (r.document_id, r.hierarchy_sequence)) + .map(|r| (r.document_id, r.hierarchy_sequence())) .collect::>() }; @@ -777,7 +778,7 @@ impl FromDavResource for Destination { account_id: u32::MAX, document_id: Some(item.document_id), parent_id: item.parent_id, - is_container: item.is_container, + is_container: item.is_container(), new_name: None, } } diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 80cb4aaf..e2004f07 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -56,7 +56,7 @@ impl FileDeleteRequestHandler for Server { } // Sort ids descending from the deepest to the root - ids.sort_unstable_by(|a, b| b.hierarchy_sequence.cmp(&a.hierarchy_sequence)); + ids.sort_unstable_by(|a, b| b.hierarchy_sequence().cmp(&a.hierarchy_sequence())); let document_id = ids.last().map(|a| a.document_id).unwrap(); let mut sorted_ids = Vec::with_capacity(ids.len()); sorted_ids.extend(ids.into_iter().map(|a| a.document_id)); @@ -64,7 +64,13 @@ impl FileDeleteRequestHandler for Server { // Validate ACLs if !access_token.is_member(account_id) { let permissions = self - .shared_containers(access_token, account_id, Collection::FileNode, Acl::Delete) + .shared_containers( + access_token, + account_id, + Collection::FileNode, + [Acl::Delete], + false, + ) .await .caused_by(trc::location!())?; if permissions.len() != sorted_ids.len() as u64 diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index ece847fa..c447f110 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -148,7 +148,7 @@ impl FromDavResource for FileItemId { FileItemId { document_id: item.document_id, parent_id: item.parent_id, - is_container: item.is_container, + is_container: item.is_container(), } } } diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 26e9c2f8..f793b04e 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -38,6 +38,13 @@ pub(crate) trait PrincipalPropFind: Sync + Send { response: &mut MultiStatus, ) -> impl Future> + Send; + fn expand_principal( + &self, + access_token: &AccessToken, + account_id: u32, + propfind: &PropFind, + ) -> impl Future>> + Send; + fn owner_href( &self, access_token: &AccessToken, @@ -303,6 +310,25 @@ impl PrincipalPropFind for Server { Ok(()) } + async fn expand_principal( + &self, + access_token: &AccessToken, + account_id: u32, + propfind: &PropFind, + ) -> crate::Result> { + let mut status = MultiStatus::new(vec![]); + self.prepare_principal_propfind_response( + access_token, + Collection::Principal, + [account_id].into_iter(), + propfind, + &mut status, + ) + .await?; + + Ok(status.response.0.into_iter().next()) + } + async fn owner_href(&self, access_token: &AccessToken, account_id: u32) -> trc::Result { if access_token.primary_id() == account_id { Ok(access_token.current_user_principal()) diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index 32bf52f3..806ed9fe 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -29,9 +29,9 @@ use crate::{ DavError, DavMethod, DavResourceName, calendar::{ copy_move::CalendarCopyMoveRequestHandler, delete::CalendarDeleteRequestHandler, - get::CalendarGetRequestHandler, mkcol::CalendarMkColRequestHandler, - proppatch::CalendarPropPatchRequestHandler, query::CalendarQueryRequestHandler, - update::CalendarUpdateRequestHandler, + freebusy::CalendarFreebusyRequestHandler, get::CalendarGetRequestHandler, + mkcol::CalendarMkColRequestHandler, proppatch::CalendarPropPatchRequestHandler, + query::CalendarQueryRequestHandler, update::CalendarUpdateRequestHandler, }, card::{ copy_move::CardCopyMoveRequestHandler, delete::CardDeleteRequestHandler, @@ -216,7 +216,24 @@ impl DavRequestDispatcher for Server { self.handle_calendar_freebusy_request(&access_token, headers, report) .await } - Report::ExpandProperty(report) => todo!(), + Report::ExpandProperty(report) => { + let uri = self + .validate_uri(&access_token, headers.uri) + .await + .and_then(|d| d.into_owned_uri())?; + match resource { + DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => { + self.handle_dav_query( + &access_token, + DavQuery::expand(uri, report, headers), + ) + .await + } + DavResourceName::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } + } + } }, DavMethod::PROPPATCH => { let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?; diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index eb8677b1..deafb2ff 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -17,6 +17,7 @@ hashify = "0.2" rkyv = { version = "0.8.10", features = ["little_endian"] } percent-encoding = "2.3.1" compact_str = "0.9.0" +chrono = "0.4.40" [features] test_mode = [] diff --git a/crates/groupware/src/calendar/dates.rs b/crates/groupware/src/calendar/dates.rs new file mode 100644 index 00000000..19790d9a --- /dev/null +++ b/crates/groupware/src/calendar/dates.rs @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::str::FromStr; + +use super::{ArchivedCalendarEventData, ArchivedTimezone, CalendarEventData, Timezone}; +use crate::calendar::ComponentTimeRange; +use calcard::{ + common::timezone::Tz, + icalendar::{ + ICalendar, + dates::{CalendarEvent, TimeOrDelta}, + }, +}; +use chrono::{DateTime, TimeZone}; +use dav_proto::schema::property::TimeRange; +use store::{ + ahash::AHashMap, + write::{bitpack::BitpackIterator, key::KeySerializer}, +}; +use utils::codec::leb128::Leb128Reader; + +impl CalendarEventData { + pub fn new(event: ICalendar, max_expansions: usize) -> Self { + let mut ranges = TimeRanges::default(); + + let expanded = event.expand_dates(Tz::Floating, max_expansions); + let mut groups: AHashMap<(u16, u16, u16, i32), Vec> = AHashMap::with_capacity(16); + + for event in expanded.events { + let start_naive = event.start.naive_local(); + let start_tz = event.start.timezone().as_id(); + let start_timestamp_utc = event.start.timestamp(); + let start_timestamp_naive = start_naive.and_utc().timestamp(); + let (end_timestamp_utc, end_timestamp_naive, end_tz) = match event.end { + TimeOrDelta::Time(time) => { + let end_naive = time.naive_local(); + let end_timestamp_utc = time.timestamp(); + let end_timestamp_naive = end_naive.and_utc().timestamp(); + ( + end_timestamp_utc, + end_timestamp_naive, + time.timezone().as_id(), + ) + } + TimeOrDelta::Delta(delta) => { + let delta = delta.num_seconds(); + ( + start_timestamp_utc + delta, + start_timestamp_naive + delta, + start_tz, + ) + } + }; + ranges.update(start_timestamp_utc, start_timestamp_naive); + ranges.update(end_timestamp_utc, end_timestamp_naive); + groups + .entry(( + start_tz, + end_tz, + event.comp_id, + (end_timestamp_naive - start_timestamp_naive) as i32, + )) + .or_default() + .push(start_timestamp_naive); + } + + let mut events = Vec::with_capacity(groups.len()); + for ((start_tz, end_tz, id, duration), mut instances) in groups { + let instances = if instances.len() > 1 { + instances.sort_unstable(); + // Bitpack instances + let mut instance_offsets = Vec::with_capacity(instances.len()); + for instance in instances { + instance_offsets.push((ranges.base_offset - instance) as u32); + } + KeySerializer::new(instance_offsets.len() * std::mem::size_of::()) + .bitpack_sorted(&instance_offsets) + .finalize() + } else { + KeySerializer::new(std::mem::size_of::()) + .write_leb128((ranges.base_offset - instances.first().unwrap()) as u32) + .finalize() + }; + + events.push(ComponentTimeRange { + id, + start_tz, + end_tz, + duration, + instances: instances.into_boxed_slice(), + }); + } + + for error in expanded.errors { + let todo = "log me"; + } + + CalendarEventData { + event, + time_ranges: events.into_boxed_slice(), + base_offset: ranges.base_offset, + base_time_utc: (ranges.min_time_utc - ranges.base_offset) as u32, + duration: (ranges.max_time_utc - ranges.min_time_utc) as u32, + } + } + + pub fn event_range(&self) -> Option<(i64, u32)> { + if self.base_offset != 0 { + Some((self.base_offset + self.base_time_utc as i64, self.duration)) + } else { + None + } + } +} + +impl ArchivedCalendarEventData { + pub fn expand(&self, default_tz: Tz, limit: TimeRange) -> Option>> { + let mut expansion = Vec::with_capacity(self.time_ranges.len()); + let base_offset = self.base_offset.to_native(); + let expansion_limit = limit.start..=limit.end; + + for range in self.time_ranges.iter() { + let instances = range.instances.as_ref(); + let (offset_or_count, bytes_read) = instances.read_leb128::()?; + + let comp_id = range.id.to_native(); + let duration = range.duration.to_native() as i64; + let mut start_tz = Tz::from_id(range.start_tz.to_native())?; + let mut end_tz = Tz::from_id(range.end_tz.to_native())?; + + if start_tz.is_floating() && !default_tz.is_floating() { + start_tz = default_tz; + } + if end_tz.is_floating() && !default_tz.is_floating() { + end_tz = default_tz; + } + + if instances.len() > bytes_read { + // Recurring event + let unpacker = + BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count); + for start_offset in unpacker { + let start_date_naive = start_offset as i64 + base_offset; + let end_date_naive = start_date_naive + duration; + let start = start_tz + .from_local_datetime( + &DateTime::from_timestamp(start_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + let end = end_tz + .from_local_datetime( + &DateTime::from_timestamp(end_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + + if expansion_limit.contains(&start) || expansion_limit.contains(&end) { + expansion.push(CalendarEvent { + comp_id, + start, + end, + }); + } else if end > limit.end { + break; + } + } + } else { + // Single event + let start_date_naive = offset_or_count as i64 + base_offset; + let end_date_naive = start_date_naive + duration; + let start = start_tz + .from_local_datetime( + &DateTime::from_timestamp(start_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + let end = end_tz + .from_local_datetime( + &DateTime::from_timestamp(end_date_naive, 0)?.naive_local(), + ) + .single()? + .timestamp(); + + if expansion_limit.contains(&start) || expansion_limit.contains(&end) { + expansion.push(CalendarEvent { + comp_id, + start, + end, + }); + } + } + } + + Some(expansion) + } +} + +#[derive(Default)] +struct TimeRanges { + max_time_utc: i64, + min_time_utc: i64, + base_offset: i64, +} + +impl TimeRanges { + pub fn update(&mut self, utc_timestamp: i64, naive_timestamp: i64) { + if utc_timestamp > self.max_time_utc { + self.max_time_utc = utc_timestamp; + } + if utc_timestamp < self.min_time_utc || self.max_time_utc == 0 { + self.min_time_utc = utc_timestamp; + } + let offset = std::cmp::min(utc_timestamp, naive_timestamp); + if offset < self.base_offset || self.base_offset == 0 { + self.base_offset = offset; + } + } +} + +impl ArchivedCalendarEventData { + pub fn event_range(&self) -> Option<(i64, u32)> { + if self.base_offset != 0 { + Some(( + self.base_offset.to_native() + self.base_time_utc.to_native() as i64, + self.duration.to_native(), + )) + } else { + None + } + } +} + +impl Timezone { + pub fn tz(&self) -> Option { + match self { + Timezone::IANA(iana) => Tz::from_str(iana).ok(), + Timezone::Custom(icalendar) => icalendar + .timezones() + .filter_map(|t| t.timezone().map(|x| x.1)) + .next(), + Timezone::Default => None, + } + } +} + +impl ArchivedTimezone { + pub fn tz(&self) -> Option { + match self { + ArchivedTimezone::IANA(iana) => Tz::from_str(iana).ok(), + ArchivedTimezone::Custom(icalendar) => icalendar + .timezones() + .filter_map(|t| t.timezone().map(|x| x.1)) + .next(), + ArchivedTimezone::Default => None, + } + } +} diff --git a/crates/groupware/src/calendar/index.rs b/crates/groupware/src/calendar/index.rs index 3b3c301d..3f290a97 100644 --- a/crates/groupware/src/calendar/index.rs +++ b/crates/groupware/src/calendar/index.rs @@ -8,9 +8,9 @@ use common::storage::index::{ IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, }; use jmap_proto::types::{collection::Collection, value::AclGrant}; -use store::SerializeInfallible; +use store::{SerializeInfallible, write::key::KeySerializer}; -use crate::{IDX_NAME, IDX_UID}; +use crate::{IDX_NAME, IDX_TIME, IDX_UID}; use super::{ ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert, @@ -26,6 +26,15 @@ impl IndexableObject for Calendar { field: IDX_NAME, value: self.name.as_str().into(), }, + IndexValue::Index { + field: IDX_TIME, + value: self + .preferences + .first() + .and_then(|p| p.time_zone.tz()) + .map(|tz| tz.as_id().serialize()) + .into(), + }, IndexValue::Acl { value: (&self.acls).into(), }, @@ -48,6 +57,15 @@ impl IndexableObject for &ArchivedCalendar { field: IDX_NAME, value: self.name.as_str().into(), }, + IndexValue::Index { + field: IDX_TIME, + value: self + .preferences + .first() + .and_then(|p| p.time_zone.tz()) + .map(|tz| tz.as_id().serialize()) + .into(), + }, IndexValue::Acl { value: self .acls @@ -83,7 +101,20 @@ impl IndexableObject for CalendarEvent { }, IndexValue::Index { field: IDX_UID, - value: self.event.uids().next().into(), + value: self.data.event.uids().next().into(), + }, + IndexValue::Index { + field: IDX_TIME, + value: self + .data + .event_range() + .map(|(start, duration)| { + KeySerializer::new(std::mem::size_of::() + std::mem::size_of::()) + .write(start as u64) + .write(duration) + .finalize() + }) + .into(), }, IndexValue::Quota { used: self.dead_properties.size() as u32 @@ -114,7 +145,20 @@ impl IndexableObject for &ArchivedCalendarEvent { }, IndexValue::Index { field: IDX_UID, - value: self.event.uids().next().into(), + value: self.data.event.uids().next().into(), + }, + IndexValue::Index { + field: IDX_TIME, + value: self + .data + .event_range() + .map(|(start, duration)| { + KeySerializer::new(std::mem::size_of::() + std::mem::size_of::()) + .write(start as u64) + .write(duration) + .finalize() + }) + .into(), }, IndexValue::Quota { used: self.dead_properties.size() as u32 diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index 0e9a2725..f1bf7cd5 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +pub mod dates; pub mod index; pub mod storage; @@ -67,7 +68,7 @@ pub const EVENT_ORIGIN: u16 = 1 << 4; pub struct CalendarEvent { pub names: Vec, pub display_name: Option, - pub event: ICalendar, + pub data: CalendarEventData, pub user_properties: Vec, pub flags: u16, pub dead_properties: DeadProperty, @@ -76,6 +77,28 @@ pub struct CalendarEvent { pub modified: i64, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +pub struct CalendarEventData { + pub event: ICalendar, + pub time_ranges: Box<[ComponentTimeRange]>, + pub base_offset: i64, + pub base_time_utc: u32, + pub duration: u32, +} + +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +pub struct ComponentTimeRange { + pub id: u16, + pub start_tz: u16, + pub end_tz: u16, + pub duration: i32, + pub instances: Box<[u8]>, +} + #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] diff --git a/crates/groupware/src/hierarchy.rs b/crates/groupware/src/hierarchy.rs index 612598fc..c29c64e1 100644 --- a/crates/groupware/src/hierarchy.rs +++ b/crates/groupware/src/hierarchy.rs @@ -4,27 +4,28 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::sync::Arc; - -use common::{DavResource, DavResourceId, DavResources, Server, auth::AccessToken}; +use crate::{ + DavName, DavResourceName, IDX_NAME, IDX_TIME, + calendar::{Calendar, CalendarPreferences}, + contact::AddressBook, + file::FileNode, +}; +use calcard::common::timezone::Tz; +use common::{ + DavResource, DavResourceId, DavResourceMetadata, DavResources, Server, auth::AccessToken, +}; use directory::backend::internal::manage::ManageDirectory; use jmap_proto::types::collection::Collection; use percent_encoding::NON_ALPHANUMERIC; +use std::sync::Arc; use store::{ - Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN, + Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN, U64_LEN, ahash::AHashMap, write::{BatchBuilder, key::DeserializeBigEndian}, }; use trc::AddContext; use utils::bimap::IdBimap; -use crate::{ - DavName, DavResourceName, IDX_NAME, - calendar::{Calendar, CalendarPreferences}, - contact::AddressBook, - file::FileNode, -}; - pub trait DavHierarchy: Sync + Send { fn fetch_dav_resources( &self, @@ -44,6 +45,12 @@ pub trait DavHierarchy: Sync + Send { access_token: &AccessToken, account_id: u32, ) -> impl Future> + Send; + + fn cached_dav_resources( + &self, + account_id: u32, + collection: Collection, + ) -> Option>; } impl DavHierarchy for Server { @@ -154,18 +161,36 @@ impl DavHierarchy for Server { Ok(()) } + + fn cached_dav_resources( + &self, + account_id: u32, + collection: Collection, + ) -> Option> { + self.inner + .cache + .dav + .get(&DavResourceId { + account_id, + collection: collection.into(), + }) + .clone() + } } async fn build_hierarchy( server: &Server, account_id: u32, - collection: Collection, + collection_: Collection, ) -> trc::Result { - let base_path = DavResourceName::from(collection).base_path(); - let collection = u8::from(collection); + let base_path = DavResourceName::from(collection_).base_path(); + let collection = u8::from(collection_); let mut containers: AHashMap = AHashMap::with_capacity(16); let mut resources: AHashMap> = AHashMap::with_capacity(16); + let mut time_ranges: AHashMap = AHashMap::new(); + let mut time_zones: AHashMap = AHashMap::new(); + server .store() .iterate( @@ -181,7 +206,7 @@ async fn build_hierarchy( account_id, collection: collection + 1, document_id: u32::MAX, - field: IDX_NAME, + field: IDX_TIME, key: u32::MAX.serialize(), }, ) @@ -196,19 +221,39 @@ async fn build_hierarchy( .get(U32_LEN) .copied() .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; + let field = key + .get(U32_LEN + 1) + .copied() + .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; if key_collection == collection { - containers.insert( - document_id, - std::str::from_utf8(value) - .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))? - .to_string(), - ); - } else { + if field == IDX_NAME { + containers.insert( + document_id, + std::str::from_utf8(value) + .map_err(|_| { + trc::Error::corrupted_key(key, None, trc::location!()) + })? + .to_string(), + ); + } else if field == IDX_TIME { + let tz = Tz::from_id(key.deserialize_be_u16(IndexKeyPrefix::len())?) + .ok_or_else(|| { + trc::Error::corrupted_key(key, None, trc::location!()) + })?; + + time_zones.insert(document_id, tz); + } + } else if field == IDX_NAME { resources.entry(document_id).or_default().push( DavName::deserialize(value) .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?, ); + } else if field == IDX_TIME { + let start_time = key.deserialize_be_u64(IndexKeyPrefix::len())?; + let duration = key.deserialize_be_u32(IndexKeyPrefix::len() + U64_LEN)?; + + time_ranges.insert(document_id, (start_time as i64, duration)); } Ok(true) @@ -246,9 +291,13 @@ async fn build_hierarchy( document_id, parent_id: dav_name.parent_id.into(), name, - size: 0, - is_container: false, - hierarchy_sequence: 1, + data: time_ranges + .get(&document_id) + .map(|(start, duration)| DavResourceMetadata::CalendarEvent { + start: *start, + duration: *duration, + }) + .unwrap_or(DavResourceMetadata::None), }); } } @@ -261,9 +310,10 @@ async fn build_hierarchy( document_id, parent_id: None, name, - size: 0, - is_container: true, - hierarchy_sequence: 0, + data: time_zones + .get(&document_id) + .map(|tz| DavResourceMetadata::Calendar { tz: *tz }) + .unwrap_or(DavResourceMetadata::None), }); } @@ -300,9 +350,11 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result for DavResourceName { } } } + +pub trait DavCalendarResource { + fn calendar_default_tz(&self, calendar_id: u32) -> Option; + fn event_default_tz(&self, event_id: u32) -> Option; +} + +impl DavCalendarResource for DavResources { + fn calendar_default_tz(&self, calendar_id: u32) -> Option { + self.paths + .iter() + .find(|c| c.is_container() && c.document_id == calendar_id) + .and_then(|c| c.timezone()) + } + + fn event_default_tz(&self, event_id: u32) -> Option { + self.paths + .iter() + .find(|c| !c.is_container() && c.document_id == event_id) + .and_then(|c| c.parent_id) + .and_then(|parent_id| self.calendar_default_tz(parent_id)) + } +} diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index c30b9b18..4ee570ae 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mail-server" -description = "Stalwart Mail Server" +description = "Stalwart Server" authors = [ "Stalwart Labs Ltd. "] repository = "https://github.com/stalwartlabs/jmap-server" homepage = "https://stalw.art" diff --git a/crates/store/src/fts/postings.rs b/crates/store/src/fts/postings.rs index 0c8a94f8..6a1b8a14 100644 --- a/crates/store/src/fts/postings.rs +++ b/crates/store/src/fts/postings.rs @@ -7,10 +7,12 @@ use std::cmp::Ordering; use ahash::AHashSet; -use bitpacking::{BitPacker, BitPacker1x, BitPacker4x, BitPacker8x}; use utils::codec::leb128::Leb128Reader; -use crate::{SerializeInfallible, write::key::KeySerializer}; +use crate::{ + SerializeInfallible, + write::{bitpack::BitpackIterator, key::KeySerializer}, +}; #[derive(Default)] pub(super) struct Postings { @@ -80,7 +82,7 @@ impl> SerializedPostings { impl<'x, T: AsRef<[u8]>> IntoIterator for &'x SerializedPostings { type Item = u32; - type IntoIter = PostingsIterator<'x>; + type IntoIter = BitpackIterator<'x>; fn into_iter(self) -> Self::IntoIter { let bytes = self.bytes.as_ref(); @@ -89,9 +91,9 @@ impl<'x, T: AsRef<[u8]>> IntoIterator for &'x SerializedPostings { if *byte == 0xFF { if let Some((items_left, bytes_read)) = bytes .get(bytes_offset + 1..) - .and_then(|bytes| bytes.read_leb128::()) + .and_then(|bytes| bytes.read_leb128::()) { - return PostingsIterator { + return BitpackIterator { bytes, bytes_offset: bytes_offset + bytes_read + 1, items_left, @@ -103,59 +105,7 @@ impl<'x, T: AsRef<[u8]>> IntoIterator for &'x SerializedPostings { } } - PostingsIterator::default() - } -} - -#[derive(Default)] -pub(super) struct PostingsIterator<'x> { - bytes: &'x [u8], - bytes_offset: usize, - chunk: Vec, - chunk_offset: usize, - pub items_left: usize, -} - -impl Iterator for PostingsIterator<'_> { - type Item = u32; - - fn next(&mut self) -> Option { - if let Some(item) = self.chunk.get(self.chunk_offset) { - self.chunk_offset += 1; - return Some(*item); - } - let block_len = match self.items_left { - 0 => return None, - 1..=31 => { - self.items_left -= 1; - let (item, bytes_read) = self.bytes.get(self.bytes_offset..)?.read_leb128()?; - self.bytes_offset += bytes_read; - return Some(item); - } - 32..=127 => BitPacker1x::BLOCK_LEN, - 128..=255 => BitPacker4x::BLOCK_LEN, - _ => BitPacker8x::BLOCK_LEN, - }; - - let bitpacker = TermIndexPacker::with_block_len(block_len); - let num_bits = *self.bytes.get(self.bytes_offset)?; - let bytes_read = ((num_bits as usize) * block_len / 8) + 1; - let initial_value = self.chunk.last().copied(); - - self.chunk = vec![0u32; block_len]; - self.chunk_offset = 1; - - bitpacker.decompress_strictly_sorted( - initial_value, - self.bytes - .get(self.bytes_offset + 1..self.bytes_offset + bytes_read)?, - &mut self.chunk[..], - num_bits, - ); - - self.bytes_offset += bytes_read; - self.items_left -= block_len; - self.chunk.first().copied() + BitpackIterator::default() } } @@ -171,244 +121,9 @@ impl SerializeInfallible for Postings { // Compress postings if !self.postings.is_empty() { - let mut bitpacker = TermIndexPacker::new(); - let mut compressed = vec![0u8; 4 * BitPacker8x::BLOCK_LEN]; - - let mut pos = 0; - let len = self.postings.len(); - let mut initial_value = None; - - serializer = serializer.write_leb128(len); - - while pos < len { - let block_len = match len - pos { - 0..=31 => { - for val in &self.postings[pos..] { - serializer = serializer.write_leb128(*val); - } - break; - } - 32..=127 => BitPacker1x::BLOCK_LEN, - 128..=255 => BitPacker4x::BLOCK_LEN, - _ => BitPacker8x::BLOCK_LEN, - }; - - let chunk = &self.postings[pos..pos + block_len]; - bitpacker.block_len(block_len); - let num_bits: u8 = bitpacker.num_bits_strictly_sorted(initial_value, chunk); - let compressed_len = bitpacker.compress_strictly_sorted( - initial_value, - chunk, - &mut compressed[..], - num_bits, - ); - serializer = serializer - .write(num_bits) - .write(&compressed[..compressed_len]); - initial_value = chunk[chunk.len() - 1].into(); - - pos += block_len; - } - } - - serializer.finalize() - } -} - -#[derive(Clone, Copy)] -pub(crate) struct TermIndexPacker { - bitpacker_1: BitPacker1x, - bitpacker_4: BitPacker4x, - bitpacker_8: BitPacker8x, - block_len: usize, -} - -impl TermIndexPacker { - pub fn with_block_len(block_len: usize) -> Self { - TermIndexPacker { - bitpacker_1: BitPacker1x::new(), - bitpacker_4: BitPacker4x::new(), - bitpacker_8: BitPacker8x::new(), - block_len, - } - } - - pub fn block_len(&mut self, num: usize) { - self.block_len = num; - } -} - -impl BitPacker for TermIndexPacker { - const BLOCK_LEN: usize = 0; - - fn new() -> Self { - TermIndexPacker { - bitpacker_1: BitPacker1x::new(), - bitpacker_4: BitPacker4x::new(), - bitpacker_8: BitPacker8x::new(), - block_len: 1, - } - } - - fn compress(&self, decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize { - match self.block_len { - BitPacker8x::BLOCK_LEN => self - .bitpacker_8 - .compress(decompressed, compressed, num_bits), - BitPacker4x::BLOCK_LEN => self - .bitpacker_4 - .compress(decompressed, compressed, num_bits), - _ => self - .bitpacker_1 - .compress(decompressed, compressed, num_bits), - } - } - - fn compress_sorted( - &self, - initial: u32, - decompressed: &[u32], - compressed: &mut [u8], - num_bits: u8, - ) -> usize { - match self.block_len { - BitPacker8x::BLOCK_LEN => { - self.bitpacker_8 - .compress_sorted(initial, decompressed, compressed, num_bits) - } - BitPacker4x::BLOCK_LEN => { - self.bitpacker_4 - .compress_sorted(initial, decompressed, compressed, num_bits) - } - _ => self - .bitpacker_1 - .compress_sorted(initial, decompressed, compressed, num_bits), - } - } - - fn decompress(&self, compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize { - match self.block_len { - BitPacker8x::BLOCK_LEN => { - self.bitpacker_8 - .decompress(compressed, decompressed, num_bits) - } - BitPacker4x::BLOCK_LEN => { - self.bitpacker_4 - .decompress(compressed, decompressed, num_bits) - } - _ => self - .bitpacker_1 - .decompress(compressed, decompressed, num_bits), - } - } - - fn decompress_sorted( - &self, - initial: u32, - compressed: &[u8], - decompressed: &mut [u32], - num_bits: u8, - ) -> usize { - match self.block_len { - BitPacker8x::BLOCK_LEN => { - self.bitpacker_8 - .decompress_sorted(initial, compressed, decompressed, num_bits) - } - BitPacker4x::BLOCK_LEN => { - self.bitpacker_4 - .decompress_sorted(initial, compressed, decompressed, num_bits) - } - _ => self - .bitpacker_1 - .decompress_sorted(initial, compressed, decompressed, num_bits), - } - } - - fn num_bits(&self, decompressed: &[u32]) -> u8 { - match self.block_len { - BitPacker8x::BLOCK_LEN => self.bitpacker_8.num_bits(decompressed), - BitPacker4x::BLOCK_LEN => self.bitpacker_4.num_bits(decompressed), - _ => self.bitpacker_1.num_bits(decompressed), - } - } - - fn num_bits_sorted(&self, initial: u32, decompressed: &[u32]) -> u8 { - match self.block_len { - BitPacker8x::BLOCK_LEN => self.bitpacker_8.num_bits_sorted(initial, decompressed), - BitPacker4x::BLOCK_LEN => self.bitpacker_4.num_bits_sorted(initial, decompressed), - _ => self.bitpacker_1.num_bits_sorted(initial, decompressed), - } - } - - fn compress_strictly_sorted( - &self, - initial: Option, - decompressed: &[u32], - compressed: &mut [u8], - num_bits: u8, - ) -> usize { - match self.block_len { - BitPacker8x::BLOCK_LEN => self.bitpacker_8.compress_strictly_sorted( - initial, - decompressed, - compressed, - num_bits, - ), - BitPacker4x::BLOCK_LEN => self.bitpacker_4.compress_strictly_sorted( - initial, - decompressed, - compressed, - num_bits, - ), - _ => self.bitpacker_1.compress_strictly_sorted( - initial, - decompressed, - compressed, - num_bits, - ), - } - } - - fn decompress_strictly_sorted( - &self, - initial: Option, - compressed: &[u8], - decompressed: &mut [u32], - num_bits: u8, - ) -> usize { - match self.block_len { - BitPacker8x::BLOCK_LEN => self.bitpacker_8.decompress_strictly_sorted( - initial, - compressed, - decompressed, - num_bits, - ), - BitPacker4x::BLOCK_LEN => self.bitpacker_4.decompress_strictly_sorted( - initial, - compressed, - decompressed, - num_bits, - ), - _ => self.bitpacker_1.decompress_strictly_sorted( - initial, - compressed, - decompressed, - num_bits, - ), - } - } - - fn num_bits_strictly_sorted(&self, initial: Option, decompressed: &[u32]) -> u8 { - match self.block_len { - BitPacker8x::BLOCK_LEN => self - .bitpacker_8 - .num_bits_strictly_sorted(initial, decompressed), - BitPacker4x::BLOCK_LEN => self - .bitpacker_4 - .num_bits_strictly_sorted(initial, decompressed), - _ => self - .bitpacker_1 - .num_bits_strictly_sorted(initial, decompressed), + serializer.bitpack_sorted(&self.postings).finalize() + } else { + serializer.finalize() } } } @@ -416,60 +131,8 @@ impl BitPacker for TermIndexPacker { #[cfg(test)] mod tests { - use ahash::AHashMap; - use super::*; - - #[test] - fn postings_roundtrip() { - for num_positions in [ - 1, - 10, - BitPacker1x::BLOCK_LEN, - BitPacker4x::BLOCK_LEN, - BitPacker8x::BLOCK_LEN, - BitPacker8x::BLOCK_LEN + BitPacker4x::BLOCK_LEN + BitPacker1x::BLOCK_LEN, - BitPacker8x::BLOCK_LEN + BitPacker4x::BLOCK_LEN + BitPacker1x::BLOCK_LEN + 1, - (BitPacker8x::BLOCK_LEN * 3) - + (BitPacker4x::BLOCK_LEN * 3) - + (BitPacker1x::BLOCK_LEN * 3) - + 1, - ] { - println!("Testing block {num_positions}...",); - let mut postings = Postings::default(); - for i in 0..num_positions { - postings.postings.push((i * i) as u32); - } - for fields in 0..std::cmp::min(10, num_positions) as u8 { - postings.fields.insert(fields); - } - - let deserialized = SerializedPostings::new(postings.serialize()); - let mut iter = (&deserialized).into_iter(); - - assert_eq!( - iter.items_left, num_positions, - "failed for num_positions: {}", - num_positions - ); - - for i in 0..num_positions { - assert_eq!( - iter.next(), - Some((i * i) as u32), - "failed for position: {}", - i - ); - } - assert_eq!(iter.next(), None, "expected end of iterator"); - - for field in 0..std::cmp::min(10, num_positions) as u8 { - assert!(deserialized.has_field(field), "failed for field: {}", field); - } - - assert_eq!(deserialized.positions().len(), num_positions); - } - } + use ahash::AHashMap; #[test] fn postings_match_positions() { diff --git a/crates/store/src/write/bitpack.rs b/crates/store/src/write/bitpack.rs new file mode 100644 index 00000000..ca16b6a9 --- /dev/null +++ b/crates/store/src/write/bitpack.rs @@ -0,0 +1,382 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use bitpacking::{BitPacker, BitPacker1x, BitPacker4x, BitPacker8x}; +use utils::codec::leb128::Leb128Reader; + +use super::key::KeySerializer; + +#[derive(Default)] +pub struct BitpackIterator<'x> { + pub(crate) bytes: &'x [u8], + pub(crate) bytes_offset: usize, + pub(crate) chunk: Vec, + pub(crate) chunk_offset: usize, + pub items_left: u32, +} + +#[derive(Clone, Copy)] +pub(crate) struct BitBlockPacker { + bitpacker_1: BitPacker1x, + bitpacker_4: BitPacker4x, + bitpacker_8: BitPacker8x, + block_len: usize, +} + +impl KeySerializer { + pub fn bitpack_sorted(self, items: &[u32]) -> Self { + let mut serializer = self; + let mut bitpacker = BitBlockPacker::new(); + let mut compressed = vec![0u8; 4 * BitPacker8x::BLOCK_LEN]; + + let mut pos = 0; + let len = items.len(); + let mut initial_value = None; + + serializer = serializer.write_leb128(len as u32); + + while pos < len { + let block_len = match len - pos { + 0..=31 => { + for val in &items[pos..] { + serializer = serializer.write_leb128(*val); + } + break; + } + 32..=127 => BitPacker1x::BLOCK_LEN, + 128..=255 => BitPacker4x::BLOCK_LEN, + _ => BitPacker8x::BLOCK_LEN, + }; + + let chunk = &items[pos..pos + block_len]; + bitpacker.block_len(block_len); + let num_bits: u8 = bitpacker.num_bits_strictly_sorted(initial_value, chunk); + let compressed_len = bitpacker.compress_strictly_sorted( + initial_value, + chunk, + &mut compressed[..], + num_bits, + ); + serializer = serializer + .write(num_bits) + .write(&compressed[..compressed_len]); + initial_value = chunk[chunk.len() - 1].into(); + + pos += block_len; + } + serializer + } +} + +impl<'x> BitpackIterator<'x> { + pub fn from_bytes_and_offset(bytes: &'x [u8], bytes_offset: usize, items_left: u32) -> Self { + BitpackIterator { + bytes, + bytes_offset, + items_left, + ..Default::default() + } + } + + pub fn new(bytes: &'x [u8]) -> Option { + bytes + .read_leb128::() + .map(|(items_left, bytes_offset)| BitpackIterator { + bytes, + bytes_offset, + items_left, + ..Default::default() + }) + } +} + +impl Iterator for BitpackIterator<'_> { + type Item = u32; + + fn next(&mut self) -> Option { + if let Some(item) = self.chunk.get(self.chunk_offset) { + self.chunk_offset += 1; + return Some(*item); + } + let block_len = match self.items_left { + 0 => return None, + 1..=31 => { + self.items_left -= 1; + let (item, bytes_read) = self.bytes.get(self.bytes_offset..)?.read_leb128()?; + self.bytes_offset += bytes_read; + return Some(item); + } + 32..=127 => BitPacker1x::BLOCK_LEN, + 128..=255 => BitPacker4x::BLOCK_LEN, + _ => BitPacker8x::BLOCK_LEN, + }; + + let bitpacker = BitBlockPacker::with_block_len(block_len); + let num_bits = *self.bytes.get(self.bytes_offset)?; + let bytes_read = ((num_bits as usize) * block_len / 8) + 1; + let initial_value = self.chunk.last().copied(); + + self.chunk = vec![0u32; block_len]; + self.chunk_offset = 1; + + bitpacker.decompress_strictly_sorted( + initial_value, + self.bytes + .get(self.bytes_offset + 1..self.bytes_offset + bytes_read)?, + &mut self.chunk[..], + num_bits, + ); + + self.bytes_offset += bytes_read; + self.items_left -= block_len as u32; + self.chunk.first().copied() + } +} + +impl BitBlockPacker { + pub fn with_block_len(block_len: usize) -> Self { + BitBlockPacker { + bitpacker_1: BitPacker1x::new(), + bitpacker_4: BitPacker4x::new(), + bitpacker_8: BitPacker8x::new(), + block_len, + } + } + + pub fn block_len(&mut self, num: usize) { + self.block_len = num; + } +} + +impl BitPacker for BitBlockPacker { + const BLOCK_LEN: usize = 0; + + fn new() -> Self { + BitBlockPacker { + bitpacker_1: BitPacker1x::new(), + bitpacker_4: BitPacker4x::new(), + bitpacker_8: BitPacker8x::new(), + block_len: 1, + } + } + + fn compress(&self, decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize { + match self.block_len { + BitPacker8x::BLOCK_LEN => self + .bitpacker_8 + .compress(decompressed, compressed, num_bits), + BitPacker4x::BLOCK_LEN => self + .bitpacker_4 + .compress(decompressed, compressed, num_bits), + _ => self + .bitpacker_1 + .compress(decompressed, compressed, num_bits), + } + } + + fn compress_sorted( + &self, + initial: u32, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + match self.block_len { + BitPacker8x::BLOCK_LEN => { + self.bitpacker_8 + .compress_sorted(initial, decompressed, compressed, num_bits) + } + BitPacker4x::BLOCK_LEN => { + self.bitpacker_4 + .compress_sorted(initial, decompressed, compressed, num_bits) + } + _ => self + .bitpacker_1 + .compress_sorted(initial, decompressed, compressed, num_bits), + } + } + + fn decompress(&self, compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize { + match self.block_len { + BitPacker8x::BLOCK_LEN => { + self.bitpacker_8 + .decompress(compressed, decompressed, num_bits) + } + BitPacker4x::BLOCK_LEN => { + self.bitpacker_4 + .decompress(compressed, decompressed, num_bits) + } + _ => self + .bitpacker_1 + .decompress(compressed, decompressed, num_bits), + } + } + + fn decompress_sorted( + &self, + initial: u32, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + match self.block_len { + BitPacker8x::BLOCK_LEN => { + self.bitpacker_8 + .decompress_sorted(initial, compressed, decompressed, num_bits) + } + BitPacker4x::BLOCK_LEN => { + self.bitpacker_4 + .decompress_sorted(initial, compressed, decompressed, num_bits) + } + _ => self + .bitpacker_1 + .decompress_sorted(initial, compressed, decompressed, num_bits), + } + } + + fn num_bits(&self, decompressed: &[u32]) -> u8 { + match self.block_len { + BitPacker8x::BLOCK_LEN => self.bitpacker_8.num_bits(decompressed), + BitPacker4x::BLOCK_LEN => self.bitpacker_4.num_bits(decompressed), + _ => self.bitpacker_1.num_bits(decompressed), + } + } + + fn num_bits_sorted(&self, initial: u32, decompressed: &[u32]) -> u8 { + match self.block_len { + BitPacker8x::BLOCK_LEN => self.bitpacker_8.num_bits_sorted(initial, decompressed), + BitPacker4x::BLOCK_LEN => self.bitpacker_4.num_bits_sorted(initial, decompressed), + _ => self.bitpacker_1.num_bits_sorted(initial, decompressed), + } + } + + fn compress_strictly_sorted( + &self, + initial: Option, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + match self.block_len { + BitPacker8x::BLOCK_LEN => self.bitpacker_8.compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + BitPacker4x::BLOCK_LEN => self.bitpacker_4.compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + _ => self.bitpacker_1.compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + } + } + + fn decompress_strictly_sorted( + &self, + initial: Option, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + match self.block_len { + BitPacker8x::BLOCK_LEN => self.bitpacker_8.decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + BitPacker4x::BLOCK_LEN => self.bitpacker_4.decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + _ => self.bitpacker_1.decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + } + } + + fn num_bits_strictly_sorted(&self, initial: Option, decompressed: &[u32]) -> u8 { + match self.block_len { + BitPacker8x::BLOCK_LEN => self + .bitpacker_8 + .num_bits_strictly_sorted(initial, decompressed), + BitPacker4x::BLOCK_LEN => self + .bitpacker_4 + .num_bits_strictly_sorted(initial, decompressed), + _ => self + .bitpacker_1 + .num_bits_strictly_sorted(initial, decompressed), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn bitpack_roundtrip() { + for num_positions in [ + 1, + 10, + BitPacker1x::BLOCK_LEN, + BitPacker4x::BLOCK_LEN, + BitPacker8x::BLOCK_LEN, + BitPacker8x::BLOCK_LEN + BitPacker4x::BLOCK_LEN + BitPacker1x::BLOCK_LEN, + BitPacker8x::BLOCK_LEN + BitPacker4x::BLOCK_LEN + BitPacker1x::BLOCK_LEN + 1, + (BitPacker8x::BLOCK_LEN * 3) + + (BitPacker4x::BLOCK_LEN * 3) + + (BitPacker1x::BLOCK_LEN * 3) + + 1, + (BitPacker8x::BLOCK_LEN * 32) + 1, + ] { + let serialized = KeySerializer::new(num_positions * std::mem::size_of::()) + .bitpack_sorted( + &(0..num_positions) + .map(|i| (i * i) as u32) + .collect::>(), + ) + .finalize(); + + println!( + "Testing block {num_positions} with {} size...", + serialized.len() + ); + + let mut iter = BitpackIterator::new(&serialized).unwrap(); + + assert_eq!( + iter.items_left, num_positions as u32, + "failed for num_positions: {}", + num_positions + ); + + for i in 0..num_positions { + assert_eq!( + iter.next(), + Some((i * i) as u32), + "failed for position: {}", + i + ); + } + assert_eq!(iter.next(), None, "expected end of iterator"); + } + } +} diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 1421e09b..9aa0b9d0 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -14,8 +14,8 @@ use crate::{ SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_EVENT, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUOTA, SUBSPACE_REPORT_IN, SUBSPACE_REPORT_OUT, SUBSPACE_SETTINGS, SUBSPACE_TASK_QUEUE, - SUBSPACE_TELEMETRY_INDEX, SUBSPACE_TELEMETRY_METRIC, SUBSPACE_TELEMETRY_SPAN, U32_LEN, U64_LEN, - ValueKey, WITH_SUBSPACE, + SUBSPACE_TELEMETRY_INDEX, SUBSPACE_TELEMETRY_METRIC, SUBSPACE_TELEMETRY_SPAN, U16_LEN, U32_LEN, + U64_LEN, ValueKey, WITH_SUBSPACE, }; use super::{ @@ -32,6 +32,7 @@ pub trait KeySerialize { } pub trait DeserializeBigEndian { + fn deserialize_be_u16(&self, index: usize) -> trc::Result; fn deserialize_be_u32(&self, index: usize) -> trc::Result; fn deserialize_be_u64(&self, index: usize) -> trc::Result; } @@ -101,6 +102,23 @@ impl KeySerialize for u64 { } impl DeserializeBigEndian for &[u8] { + fn deserialize_be_u16(&self, index: usize) -> trc::Result { + self.get(index..index + U16_LEN) + .ok_or_else(|| { + trc::StoreEvent::DataCorruption + .caused_by(trc::location!()) + .ctx(trc::Key::Value, *self) + }) + .and_then(|bytes| { + bytes.try_into().map_err(|_| { + trc::StoreEvent::DataCorruption + .caused_by(trc::location!()) + .ctx(trc::Key::Value, *self) + }) + }) + .map(u16::from_be_bytes) + } + fn deserialize_be_u32(&self, index: usize) -> trc::Result { self.get(index..index + U32_LEN) .ok_or_else(|| { diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 3971c01b..8af60c1b 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -26,6 +26,7 @@ use self::assert::AssertValue; pub mod assert; pub mod batch; +pub mod bitpack; pub mod blob; pub mod hash; pub mod key; diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index bce7f965..33f3e863 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -1142,12 +1142,11 @@ impl ServerEvent { pub fn description(&self) -> &'static str { match self { ServerEvent::Startup => { - concat!("Starting Stalwart Mail Server v", env!("CARGO_PKG_VERSION")) + concat!("Starting Stalwart Server v", env!("CARGO_PKG_VERSION")) + } + ServerEvent::Shutdown => { + concat!("Shutting down Stalwart Server v", env!("CARGO_PKG_VERSION")) } - ServerEvent::Shutdown => concat!( - "Shutting down Stalwart Mail Server v", - env!("CARGO_PKG_VERSION") - ), ServerEvent::StartupError => "Server startup error", ServerEvent::ThreadError => "Server thread error", ServerEvent::Licensing => "Server licensing event", @@ -1156,8 +1155,8 @@ impl ServerEvent { pub fn explain(&self) -> &'static str { match self { - ServerEvent::Startup => "Stalwart Mail Server has started", - ServerEvent::Shutdown => "Stalwart Mail Server is shutting down", + ServerEvent::Startup => "Stalwart Server has started", + ServerEvent::Shutdown => "Stalwart Server is shutting down", ServerEvent::StartupError => "An error occurred while starting the server", ServerEvent::ThreadError => "An error occurred with a server thread", ServerEvent::Licensing => "A licensing event occurred", diff --git a/crates/utils/src/config/parser.rs b/crates/utils/src/config/parser.rs index 30abd9ea..1912387f 100644 --- a/crates/utils/src/config/parser.rs +++ b/crates/utils/src/config/parser.rs @@ -15,7 +15,7 @@ use std::fmt::Write; const MAX_NEST_LEVEL: usize = 10; -// Simple TOML parser for Stalwart Mail Server configuration files. +// Simple TOML parser for Stalwart Server configuration files. impl Config { pub fn new(toml: impl AsRef) -> Result { let mut config = Config::default();