From ec4963f46bfb12a093759c1147cd324230369152 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Wed, 8 Oct 2025 19:52:23 +0200 Subject: [PATCH] JMAP for Calendars implementation (part 3) --- Cargo.lock | 2 +- crates/dav-proto/Cargo.toml | 2 + crates/dav-proto/src/parser/property.rs | 39 +- crates/dav-proto/src/requests/lockinfo.rs | 4 +- crates/dav-proto/src/requests/mod.rs | 108 +----- crates/dav-proto/src/requests/report.rs | 19 +- crates/dav-proto/src/responses/lock.rs | 13 +- crates/dav-proto/src/responses/mod.rs | 12 +- crates/dav-proto/src/responses/property.rs | 20 +- crates/dav-proto/src/schema/property.rs | 23 +- crates/dav-proto/src/schema/request.rs | 125 +------ crates/dav/src/calendar/freebusy.rs | 14 +- crates/dav/src/calendar/mod.rs | 29 ++ crates/dav/src/calendar/query.rs | 18 +- crates/dav/src/calendar/update.rs | 1 + crates/dav/src/common/lock.rs | 3 +- crates/dav/src/common/mod.rs | 29 +- crates/dav/src/common/propfind.rs | 12 +- crates/groupware/Cargo.toml | 1 - crates/groupware/src/calendar/expand.rs | 13 +- crates/groupware/src/calendar/index.rs | 172 ++++++++- crates/groupware/src/calendar/mod.rs | 3 +- crates/groupware/src/contact/index.rs | 3 + crates/groupware/src/contact/mod.rs | 3 +- crates/groupware/src/file/mod.rs | 3 +- crates/groupware/src/scheduling/mod.rs | 23 -- crates/imap/src/op/search.rs | 2 +- crates/jmap-proto/src/method/availability.rs | 2 +- crates/jmap-proto/src/method/query.rs | 23 +- .../jmap-proto/src/object/calendar_event.rs | 63 +++- crates/jmap-proto/src/request/parser.rs | 2 +- crates/jmap/src/api/request.rs | 1 + crates/jmap/src/calendar_event/get.rs | 4 +- crates/jmap/src/calendar_event/query.rs | 302 ++++++++++++---- crates/jmap/src/calendar_event/set.rs | 2 + crates/jmap/src/lib.rs | 6 +- crates/jmap/src/principal/availability.rs | 341 +++++++++++++++++- crates/jmap/src/thread/get.rs | 2 +- crates/migration/src/calendar.rs | 17 +- crates/store/src/query/mod.rs | 4 +- crates/store/src/query/sort.rs | 10 +- crates/types/src/dead_property.rs | 191 ++++++++++ crates/types/src/field.rs | 6 + crates/types/src/lib.rs | 20 + 44 files changed, 1232 insertions(+), 460 deletions(-) create mode 100644 crates/types/src/dead_property.rs diff --git a/Cargo.lock b/Cargo.lock index 41bb1be8..ec6945c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1797,6 +1797,7 @@ dependencies = [ "serde", "serde_json", "trc", + "types", ] [[package]] @@ -2852,7 +2853,6 @@ dependencies = [ "chrono", "common", "compact_str", - "dav-proto", "directory", "hashify", "percent-encoding", diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index f77a812d..2010cd7a 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] trc = { path = "../trc" } +types = { path = "../types" } hashify = "0.2.6" quick-xml = { version = "0.38" } calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } @@ -16,6 +17,7 @@ compact_str = "0.9.0" [dev-dependencies] calcard = { path = "/Users/me/code/calcard", features = ["serde", "rkyv"] } +types = { path = "../types", features = ["test_mode"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" chrono = { version = "0.4.40", features = ["serde"] } diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index 57183b0a..8994fff2 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -4,6 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{tokenizer::Tokenizer, DavParser, RawElement, Token, XmlValueParser}; +use crate::schema::{ + property::{ + CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName, + Comp, DavProperty, DavValue, PrincipalProperty, ResourceType, WebDavProperty, + }, + request::{DavPropertyValue, VCardPropertyWithGroup}, + response::List, + Attribute, AttributeValue, Element, NamedElement, Namespace, +}; use calcard::{ common::{IanaParse, PartialDateTime}, icalendar::{ICalendar, ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, @@ -11,18 +21,7 @@ use calcard::{ Entry, Parser, }; use mail_parser::DateTime; - -use crate::schema::{ - property::{ - CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName, - Comp, DavProperty, DavValue, PrincipalProperty, ResourceType, TimeRange, WebDavProperty, - }, - request::{DavPropertyValue, DeadProperty, VCardPropertyWithGroup}, - response::List, - Attribute, AttributeValue, Element, NamedElement, Namespace, -}; - -use super::{tokenizer::Tokenizer, DavParser, RawElement, Token, XmlValueParser}; +use types::{dead_property::DeadProperty, TimeRange}; impl Tokenizer<'_> { pub(crate) fn collect_properties( @@ -384,18 +383,12 @@ impl Tokenizer<'_> { } } -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)) - ((start < self.end) || (start <= self.start)) && (end > self.start || end >= self.end) - } - } +pub(crate) trait TimeRangeFromRaw { + fn from_raw(raw: &RawElement<'_>) -> super::Result>; +} - pub fn from_raw(raw: &RawElement<'_>) -> super::Result> { +impl TimeRangeFromRaw for TimeRange { + fn from_raw(raw: &RawElement<'_>) -> super::Result> { let mut range = TimeRange { start: i64::MIN, end: i64::MAX, diff --git a/crates/dav-proto/src/requests/lockinfo.rs b/crates/dav-proto/src/requests/lockinfo.rs index 79d29c03..34259f08 100644 --- a/crates/dav-proto/src/requests/lockinfo.rs +++ b/crates/dav-proto/src/requests/lockinfo.rs @@ -4,11 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use types::dead_property::DeadProperty; + use crate::{ parser::{tokenizer::Tokenizer, DavParser, Token}, schema::{ property::{LockScope, LockType}, - request::{DeadProperty, LockInfo}, + request::LockInfo, Element, NamedElement, Namespace, }, }; diff --git a/crates/dav-proto/src/requests/mod.rs b/crates/dav-proto/src/requests/mod.rs index 8fe0a3f9..c15af3e8 100644 --- a/crates/dav-proto/src/requests/mod.rs +++ b/crates/dav-proto/src/requests/mod.rs @@ -6,14 +6,9 @@ use crate::{ parser::{tokenizer::Tokenizer, DavParser, RawElement, Token}, - schema::{ - request::{ - ArchivedDeadElementTag, ArchivedDeadProperty, ArchivedDeadPropertyTag, DeadElementTag, - DeadProperty, DeadPropertyTag, - }, - Namespace, - }, + schema::Namespace, }; +use types::dead_property::{DeadElementTag, DeadProperty, DeadPropertyTag}; pub mod acl; pub mod lockinfo; @@ -58,8 +53,12 @@ impl DavParser for DeadProperty { } } -impl DeadProperty { - pub fn single_with_ns(namespace: Namespace, name: &str) -> Self { +pub trait NsDeadProperty { + fn single_with_ns(namespace: Namespace, name: &str) -> Self; +} + +impl NsDeadProperty for DeadProperty { + fn single_with_ns(namespace: Namespace, name: &str) -> Self { DeadProperty(vec![ DeadPropertyTag::ElementStart(DeadElementTag { name: format!("{}:{name}", namespace.prefix()), @@ -68,91 +67,6 @@ impl DeadProperty { DeadPropertyTag::ElementEnd, ]) } - - pub fn remove_element(&mut self, element: &DeadElementTag) { - let mut depth = 0; - let mut remove = false; - self.0.retain(|item| match item { - DeadPropertyTag::ElementStart(tag) => { - if depth == 0 && !remove && tag.name == element.name { - remove = true; - } - depth += 1; - - !remove - } - DeadPropertyTag::ElementEnd => { - depth -= 1; - if remove && depth == 0 { - remove = false; - false - } else { - !remove - } - } - _ => !remove, - }); - } - - pub fn add_element(&mut self, element: DeadElementTag, values: Vec) { - self.0.push(DeadPropertyTag::ElementStart(element)); - self.0.extend(values); - self.0.push(DeadPropertyTag::ElementEnd); - } - - pub fn size(&self) -> usize { - let mut size = 0; - for item in &self.0 { - match item { - DeadPropertyTag::ElementStart(tag) => { - size += tag.size(); - } - DeadPropertyTag::ElementEnd => { - size += 1; - } - DeadPropertyTag::Text(text) => { - size += text.len(); - } - } - } - size - } -} - -impl ArchivedDeadProperty { - pub fn size(&self) -> usize { - let mut size = 0; - for item in self.0.iter() { - match item { - ArchivedDeadPropertyTag::ElementStart(tag) => { - size += tag.size(); - } - ArchivedDeadPropertyTag::ElementEnd => { - size += 1; - } - ArchivedDeadPropertyTag::Text(text) => { - size += text.len(); - } - } - } - size - } -} - -impl DeadElementTag { - pub fn new(name: String, attrs: Option) -> Self { - DeadElementTag { name, attrs } - } - - pub fn size(&self) -> usize { - self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len()) - } -} - -impl ArchivedDeadElementTag { - pub fn size(&self) -> usize { - self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len()) - } } impl From<&RawElement<'_>> for DeadElementTag { @@ -195,12 +109,6 @@ impl From<&RawElement<'_>> for DeadElementTag { } } -impl Default for DeadProperty { - fn default() -> Self { - DeadProperty(Vec::with_capacity(4)) - } -} - #[cfg(test)] mod tests { use crate::{ diff --git a/crates/dav-proto/src/requests/report.rs b/crates/dav-proto/src/requests/report.rs index 7109fe62..033c6231 100644 --- a/crates/dav-proto/src/requests/report.rs +++ b/crates/dav-proto/src/requests/report.rs @@ -4,17 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use calcard::{ - icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, - vcard::VCardParameterName, -}; - use crate::{ - parser::{tokenizer::Tokenizer, DavParser, RawElement, Token, XmlValueParser}, + parser::{ + property::TimeRangeFromRaw, tokenizer::Tokenizer, DavParser, RawElement, Token, + XmlValueParser, + }, schema::{ - property::{DavProperty, TimeRange}, + property::DavProperty, request::{ - AclPrincipalPropSet, AddressbookQuery, CalendarQuery, DeadElementTag, ExpandProperty, + AclPrincipalPropSet, AddressbookQuery, CalendarQuery, ExpandProperty, ExpandPropertyItem, Filter, FilterOp, FreeBusyQuery, MultiGet, PrincipalMatch, PrincipalPropertySearch, PropFind, Report, SyncCollection, TextMatch, Timezone, VCardPropertyWithGroup, @@ -23,6 +21,11 @@ use crate::{ }, Depth, }; +use calcard::{ + icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, + vcard::VCardParameterName, +}; +use types::{dead_property::DeadElementTag, TimeRange}; impl DavParser for Report { fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { diff --git a/crates/dav-proto/src/responses/lock.rs b/crates/dav-proto/src/responses/lock.rs index 5ce0d8e1..4ef521d8 100644 --- a/crates/dav-proto/src/responses/lock.rs +++ b/crates/dav-proto/src/responses/lock.rs @@ -6,10 +6,13 @@ use std::fmt::Display; +use types::dead_property::DeadProperty; + use crate::{ + responses::DeadPropertyFormat, schema::{ property::{ActiveLock, LockDiscovery, LockEntry, LockScope, LockType, SupportedLock}, - request::{DeadProperty, LockInfo}, + request::LockInfo, response::{Href, List}, }, Depth, Timeout, @@ -36,7 +39,9 @@ impl Display for ActiveLock { )?; if let Some(owner) = &self.owner { - write!(f, "{}", owner)?; + f.write_str("")?; + owner.fmt(f)?; + f.write_str("")?; } write!(f, "{}", self.timeout)?; @@ -79,7 +84,9 @@ impl Display for LockInfo { write!(f, "{}{}", self.lock_scope, self.lock_type)?; if let Some(owner) = &self.owner { - write!(f, "{}", owner)?; + f.write_str("")?; + owner.fmt(f)?; + f.write_str("")?; } write!(f, "",) diff --git a/crates/dav-proto/src/responses/mod.rs b/crates/dav-proto/src/responses/mod.rs index 1f7a899b..bbb74688 100644 --- a/crates/dav-proto/src/responses/mod.rs +++ b/crates/dav-proto/src/responses/mod.rs @@ -13,9 +13,10 @@ pub mod property; pub mod propstat; pub mod schedule; +use types::dead_property::{DeadProperty, DeadPropertyTag}; + use crate::schema::{ property::{Comp, ResourceType, SupportedCollation}, - request::{DeadProperty, DeadPropertyTag}, response::{Href, List, Location, ResponseDescription, Status, SyncToken}, Namespaces, }; @@ -168,7 +169,11 @@ impl Display for SupportedCollation { } } -impl Display for DeadProperty { +pub trait DeadPropertyFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; +} + +impl DeadPropertyFormat for DeadProperty { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut last_tag = ""; @@ -203,6 +208,7 @@ mod tests { use calcard::{icalendar::ICalendar, vcard::VCard}; use hyper::StatusCode; use mail_parser::DateTime; + use types::dead_property::{DeadElementTag, DeadProperty, DeadPropertyTag}; use crate::{ parser::{tokenizer::Tokenizer, Token}, @@ -212,7 +218,7 @@ mod tests { ActiveLock, CalDavProperty, CardDavProperty, DavValue, LockScope, Privilege, ResourceType, Rfc1123DateTime, SupportedLock, WebDavProperty, }, - request::{DavPropertyValue, DeadElementTag, DeadProperty, DeadPropertyTag}, + request::DavPropertyValue, response::{ Ace, AclRestrictions, BaseCondition, ErrorResponse, GrantDeny, Href, List, MkColResponse, MultiStatus, Principal, PrincipalSearchProperty, diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs index 37d5bb48..3f353513 100644 --- a/crates/dav-proto/src/responses/property.rs +++ b/crates/dav-proto/src/responses/property.rs @@ -5,21 +5,25 @@ */ use super::{XmlCdataEscape, XmlEscape}; -use crate::schema::{ - property::{ - ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue, LockDiscovery, - LockEntry, PrincipalProperty, Privilege, ReportSet, ResourceType, Rfc1123DateTime, - SupportedCollation, SupportedLock, WebDavProperty, +use crate::{ + responses::DeadPropertyFormat, + schema::{ + property::{ + ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue, + LockDiscovery, LockEntry, PrincipalProperty, Privilege, ReportSet, ResourceType, + Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty, + }, + request::DavPropertyValue, + response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege}, + Namespace, Namespaces, }, - request::{DavPropertyValue, DeadProperty}, - response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege}, - Namespace, Namespaces, }; use mail_parser::{ parsers::fields::date::{DOW, MONTH}, DateTime, }; use std::fmt::Display; +use types::dead_property::DeadProperty; impl Display for PropResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index 998225e0..5b5f1176 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -4,17 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{ + request::DavPropertyValue, + response::{Ace, AclRestrictions, Href, List, Response, SupportedPrivilege}, + Collation, Namespace, +}; +use crate::{Depth, Timeout}; use calcard::{ icalendar::{ICalendar, ICalendarComponentType, ICalendarProperty}, vcard::{VCard, VCardProperty}, }; - -use crate::{Depth, Timeout}; - -use super::{ - request::{DavPropertyValue, DeadElementTag, DeadProperty}, - response::{Ace, AclRestrictions, Href, List, Response, SupportedPrivilege}, - Collation, Namespace, +use types::{ + dead_property::{DeadElementTag, DeadProperty}, + TimeRange, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -129,13 +131,6 @@ pub struct CalendarData { pub limit_freebusy: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -pub struct TimeRange { - pub start: i64, - pub end: i64, -} - #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct CalDavPropertyName { diff --git a/crates/dav-proto/src/schema/request.rs b/crates/dav-proto/src/schema/request.rs index c4f37485..397b07d0 100644 --- a/crates/dav-proto/src/schema/request.rs +++ b/crates/dav-proto/src/schema/request.rs @@ -4,17 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{ + property::{DavProperty, DavValue, LockScope, LockType}, + response::Ace, + Collation, MatchType, +}; +use crate::Depth; use calcard::{ icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, vcard::{VCardParameterName, VCardProperty}, }; - -use crate::Depth; - -use super::{ - property::{DavProperty, DavValue, LockScope, LockType, TimeRange}, - response::Ace, - Collation, MatchType, +use types::{ + dead_property::{ArchivedDeadProperty, ArchivedDeadPropertyTag, DeadElementTag, DeadProperty}, + TimeRange, }; #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -185,31 +187,6 @@ pub struct TextMatch { pub negate: bool, } -#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, serde(tag = "type", content = "data"))] -#[rkyv(derive(Debug))] -pub enum DeadPropertyTag { - ElementStart(DeadElementTag), - ElementEnd, - Text(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -#[rkyv(derive(Debug))] -pub struct DeadElementTag { - pub name: String, - pub attrs: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, serde(transparent))] -#[rkyv(derive(Debug))] -#[repr(transparent)] -pub struct DeadProperty(pub Vec); - #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct Acl { @@ -251,74 +228,26 @@ pub struct PropertySearch { pub match_: String, } -impl From<&ArchivedDeadProperty> for DeadProperty { - fn from(value: &ArchivedDeadProperty) -> Self { - DeadProperty(value.0.iter().map(|tag| tag.into()).collect::>()) +impl PropertyUpdate { + pub fn has_changes(&self) -> bool { + !self.set.is_empty() || !self.remove.is_empty() } } -impl From<&ArchivedDeadPropertyTag> for DeadPropertyTag { - fn from(tag: &ArchivedDeadPropertyTag) -> Self { - match tag { - ArchivedDeadPropertyTag::ElementStart(tag) => DeadPropertyTag::ElementStart(tag.into()), - ArchivedDeadPropertyTag::ElementEnd => DeadPropertyTag::ElementEnd, - ArchivedDeadPropertyTag::Text(tag) => DeadPropertyTag::Text(tag.to_string()), +impl FreeBusyQuery { + pub fn new(start: i64, end: i64) -> Self { + FreeBusyQuery { + range: Some(TimeRange { start, end }), } } } -impl From<&ArchivedDeadElementTag> for DeadElementTag { - fn from(tag: &ArchivedDeadElementTag) -> Self { - DeadElementTag { - name: tag.name.to_string(), - attrs: tag.attrs.as_ref().map(|s| s.to_string()), - } - } +pub trait DavDeadProperty { + fn to_dav_values(&self, output: &mut Vec); } -impl ArchivedDeadProperty { - pub fn find_tag(&self, needle: &str) -> Option { - let mut depth: u32 = 0; - let mut tags = Vec::new(); - let mut found_tag = false; - - for tag in self.0.iter() { - match tag { - ArchivedDeadPropertyTag::ElementStart(start) => { - if depth == 0 && start.name == needle { - found_tag = true; - } else if found_tag { - tags.push(tag.into()); - } - - depth += 1; - } - ArchivedDeadPropertyTag::ElementEnd => { - if found_tag { - if depth == 1 { - break; - } else { - tags.push(tag.into()); - } - } - depth = depth.saturating_sub(1); - } - ArchivedDeadPropertyTag::Text(_) => { - if found_tag { - tags.push(tag.into()); - } - } - } - } - - if found_tag { - Some(DeadProperty(tags)) - } else { - None - } - } - - pub fn to_dav_values(&self, output: &mut Vec) { +impl DavDeadProperty for ArchivedDeadProperty { + fn to_dav_values(&self, output: &mut Vec) { let mut depth: u32 = 0; let mut tags = Vec::new(); let mut tag_start = None; @@ -355,17 +284,3 @@ impl ArchivedDeadProperty { } } } - -impl PropertyUpdate { - pub fn has_changes(&self) -> bool { - !self.set.is_empty() || !self.remove.is_empty() - } -} - -impl FreeBusyQuery { - pub fn new(start: i64, end: i64) -> Self { - FreeBusyQuery { - range: Some(TimeRange { start, end }), - } - } -} diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs index 8f4e174c..064b1145 100644 --- a/crates/dav/src/calendar/freebusy.rs +++ b/crates/dav/src/calendar/freebusy.rs @@ -17,10 +17,7 @@ use calcard::{ }, }; use common::{DavResourcePath, DavResources, PROD_ID, Server, auth::AccessToken}; -use dav_proto::{ - RequestHeaders, - schema::{property::TimeRange, request::FreeBusyQuery}, -}; +use dav_proto::{RequestHeaders, schema::request::FreeBusyQuery}; use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; use http_proto::HttpResponse; use hyper::StatusCode; @@ -31,6 +28,7 @@ use store::{ }; use trc::AddContext; use types::{ + TimeRange, acl::Acl, collection::{Collection, SyncCollection}, }; @@ -152,13 +150,11 @@ impl CalendarFreebusyRequestHandler for Server { AHashMap::with_capacity(document_ids.len()); for document_id in document_ids { - let archive = if let Some(archive) = self + let Some(archive) = self .get_archive(account_id, Collection::CalendarEvent, document_id) .await .caused_by(trc::location!())? - { - archive - } else { + else { continue; }; let event = archive @@ -299,7 +295,7 @@ impl CalendarFreebusyRequestHandler for Server { fn merge_intervals(mut intervals: Vec<(i64, i64)>) -> Vec { if intervals.len() > 1 { - intervals.sort_by(|a, b| a.0.cmp(&b.0)); + intervals.sort_unstable_by(|a, b| a.0.cmp(&b.0)); let mut unique_intervals = Vec::new(); let mut start_time = intervals[0].0; diff --git a/crates/dav/src/calendar/mod.rs b/crates/dav/src/calendar/mod.rs index fabea3a7..3924b61f 100644 --- a/crates/dav/src/calendar/mod.rs +++ b/crates/dav/src/calendar/mod.rs @@ -20,6 +20,7 @@ use dav_proto::schema::{ property::{CalDavProperty, CalendarData, DavProperty, WebDavProperty}, response::CalCondition, }; +use groupware::scheduling::ItipError; use hyper::StatusCode; use store::query::Filter; use trc::AddContext; @@ -119,3 +120,31 @@ pub(crate) async fn assert_is_unique_uid( Ok(()) } + +pub(crate) trait ItipPrecondition { + fn failed_precondition(&self) -> Option; +} + +impl ItipPrecondition for ItipError { + fn failed_precondition(&self) -> Option { + match self { + ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents), + ItipError::OrganizerIsLocalAddress + | ItipError::SenderIsNotParticipant(_) + | ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer), + ItipError::CannotModifyProperty(_) + | ItipError::CannotModifyInstance + | ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange), + ItipError::MissingUid + | ItipError::MultipleUid + | ItipError::MultipleObjectTypes + | ItipError::MultipleObjectInstances + | ItipError::MissingMethod + | ItipError::InvalidComponentType + | ItipError::OutOfSequence + | ItipError::UnknownParticipant(_) + | ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage), + _ => None, + } + } +} diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs index 7b07cc7c..9dc880cc 100644 --- a/crates/dav/src/calendar/query.rs +++ b/crates/dav/src/calendar/query.rs @@ -26,7 +26,7 @@ use common::{DavResource, Server, auth::AccessToken}; use dav_proto::{ RequestHeaders, schema::{ - property::{CalDavProperty, CalendarData, DavProperty, TimeRange}, + property::{CalDavProperty, CalendarData, DavProperty}, request::{CalendarQuery, Filter, FilterOp, PropFind, Timezone}, response::MultiStatus, }, @@ -43,7 +43,7 @@ use store::{ write::serialize::rkyv_deserialize, }; use trc::AddContext; -use types::{acl::Acl, collection::SyncCollection}; +use types::{TimeRange, acl::Acl, collection::SyncCollection}; pub(crate) trait CalendarQueryRequestHandler: Sync + Send { fn handle_calendar_query_request( @@ -125,20 +125,8 @@ impl CalendarQueryRequestHandler for Server { } pub(crate) fn is_resource_in_time_range(resource: &DavResource, filter: &TimeRange) -> bool { + // Check whether the resource has a time range and if it overlaps with the filter if let Some((start, end)) = resource.event_time_range() { - /*let range_from = DateTime::from_timestamp(filter.start, 0).unwrap(); - let range_end = DateTime::from_timestamp(filter.end, 0).unwrap(); - let result = ((filter.start < end) || (filter.start <= start)) - && (filter.end > start || filter.end >= end); - - let c = println!( - "filter from {range_from} to {range_end}, resource is {} from {} to {}, result: {}", - resource.path(), - DateTime::from_timestamp(start, 0).unwrap(), - DateTime::from_timestamp(end, 0).unwrap(), - result - );*/ - ((filter.start < end) || (filter.start <= start)) && (filter.end > start || filter.end >= end) } else { diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index b25b8fe8..1b487290 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -7,6 +7,7 @@ use super::assert_is_unique_uid; use crate::{ DavError, DavErrorCondition, DavMethod, + calendar::ItipPrecondition, common::{ ETag, ExtractETag, lock::{LockRequestHandler, ResourceState}, diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 036f0ecb..5bcd74b9 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -10,7 +10,7 @@ use crate::{DavError, DavErrorCondition, DavMethod}; use common::KV_LOCK_DAV; use common::{Server, auth::AccessToken}; use dav_proto::schema::property::{ActiveLock, LockScope, WebDavProperty}; -use dav_proto::schema::request::{DavPropertyValue, DeadProperty}; +use dav_proto::schema::request::DavPropertyValue; use dav_proto::schema::response::{BaseCondition, List, PropResponse}; use dav_proto::{Condition, Depth, Timeout}; use dav_proto::{RequestHeaders, schema::request::LockInfo}; @@ -24,6 +24,7 @@ use store::write::{AlignedBytes, Archive, Archiver, now}; use store::{Serialize, U32_LEN}; use trc::AddContext; use types::collection::Collection; +use types::dead_property::DeadProperty; #[derive(Debug, Default, Clone)] pub struct ResourceState<'x> { diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 4f93cc62..99ce741c 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -13,10 +13,10 @@ use dav_proto::{ Depth, RequestHeaders, Return, schema::{ Namespace, - property::{DavProperty, ReportSet, ResourceType, TimeRange}, + property::{DavProperty, ReportSet, ResourceType}, request::{ - AddressbookQuery, ArchivedDeadProperty, CalendarQuery, ExpandProperty, Filter, - MultiGet, PropFind, SyncCollection, Timezone, VCardPropertyWithGroup, + AddressbookQuery, CalendarQuery, ExpandProperty, Filter, MultiGet, PropFind, + SyncCollection, Timezone, VCardPropertyWithGroup, }, }, }; @@ -31,7 +31,10 @@ use groupware::{ use propfind::PropFindItem; use rkyv::vec::ArchivedVec; use store::write::{AlignedBytes, Archive, BatchBuilder, Operation, ValueClass, ValueOp}; -use types::{acl::ArchivedAclGrant, collection::Collection, field::Field}; +use types::{ + TimeRange, acl::ArchivedAclGrant, collection::Collection, dead_property::ArchivedDeadProperty, + field::Field, +}; use uri::{OwnedUri, Urn}; pub mod acl; @@ -135,9 +138,9 @@ pub(crate) trait DavCollection { impl DavCollection for Collection { fn namespace(&self) -> Namespace { match self { - Collection::Calendar | Collection::CalendarEvent | Collection::CalendarEventNotification => { - Namespace::CalDav - } + Collection::Calendar + | Collection::CalendarEvent + | Collection::CalendarEventNotification => Namespace::CalDav, Collection::AddressBook | Collection::ContactCard => Namespace::CardDav, _ => Namespace::Dav, } @@ -359,7 +362,9 @@ impl<'x> ArchivedResource<'x> { ArchivedResource::AddressBook(archive) => archive.inner.created.to_native(), ArchivedResource::ContactCard(archive) => archive.inner.created.to_native(), ArchivedResource::FileNode(archive) => archive.inner.created.to_native(), - ArchivedResource::CalendarEventNotification(archive) => archive.inner.created.to_native(), + ArchivedResource::CalendarEventNotification(archive) => { + archive.inner.created.to_native() + } ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200, } } @@ -371,7 +376,9 @@ impl<'x> ArchivedResource<'x> { ArchivedResource::AddressBook(archive) => archive.inner.modified.to_native(), ArchivedResource::ContactCard(archive) => archive.inner.modified.to_native(), ArchivedResource::FileNode(archive) => archive.inner.modified.to_native(), - ArchivedResource::CalendarEventNotification(archive) => archive.inner.modified.to_native(), + ArchivedResource::CalendarEventNotification(archive) => { + archive.inner.modified.to_native() + } ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200, } } @@ -394,7 +401,9 @@ impl<'x> ArchivedResource<'x> { archive.inner.file.as_ref().map(|f| f.size.to_native()) } ArchivedResource::CalendarEvent(archive) => archive.inner.size.to_native().into(), - ArchivedResource::CalendarEventNotification(archive) => archive.inner.size.to_native().into(), + ArchivedResource::CalendarEventNotification(archive) => { + archive.inner.size.to_native().into() + } ArchivedResource::ContactCard(archive) => archive.inner.size.to_native().into(), ArchivedResource::AddressBook(_) | ArchivedResource::Calendar(_) diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 70a988a9..54556457 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -35,6 +35,7 @@ use common::{ use dav_proto::{ Depth, RequestHeaders, parser::header::dav_base_uri, + requests::NsDeadProperty, schema::{ Collation, Namespace, property::{ @@ -42,7 +43,7 @@ use dav_proto::{ Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty, }, - request::{DavPropertyValue, DeadProperty, PropFind}, + request::{DavDeadProperty, DavPropertyValue, PropFind}, response::{ AclRestrictions, BaseCondition, Href, List, MultiStatus, PropStat, Response, SupportedPrivilege, @@ -67,6 +68,7 @@ use trc::AddContext; use types::{ acl::Acl, collection::{Collection, SyncCollection}, + dead_property::DeadProperty, }; pub(crate) trait PropFindRequestHandler: Sync + Send { @@ -684,7 +686,9 @@ impl PropFindRequestHandler for Server { vec![SupportedPrivilege::all_scheduling_privileges(matches!( archive, ArchivedResource::CalendarEventNotification(_) - | ArchivedResource::CalendarEventNotificationCollection(true) + | ArchivedResource::CalendarEventNotificationCollection( + true + ) ))], )); } @@ -695,7 +699,9 @@ impl PropFindRequestHandler for Server { matches!( archive, ArchivedResource::CalendarEventNotification(_) - | ArchivedResource::CalendarEventNotificationCollection(true) + | ArchivedResource::CalendarEventNotificationCollection( + true + ) ), access_token.is_member(account_id), ) diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index 0c7e1f90..a50a9358 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -11,7 +11,6 @@ common = { path = "../common" } types = { path = "../types" } trc = { path = "../trc" } directory = { path = "../directory" } -dav-proto = { path = "../dav-proto" } calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } hashify = "0.2" tokio = { version = "1.47", features = ["net", "macros"] } diff --git a/crates/groupware/src/calendar/expand.rs b/crates/groupware/src/calendar/expand.rs index 06281279..632ba67b 100644 --- a/crates/groupware/src/calendar/expand.rs +++ b/crates/groupware/src/calendar/expand.rs @@ -9,8 +9,8 @@ use crate::calendar::CalendarEventData; use ahash::AHashSet; use calcard::common::timezone::Tz; use chrono::{DateTime, TimeZone}; -use dav_proto::schema::property::TimeRange; use store::write::bitpack::BitpackIterator; +use types::TimeRange; use utils::codec::leb128::Leb128Reader; #[derive(Debug, Clone, PartialEq, Eq)] @@ -35,6 +35,9 @@ impl ArchivedCalendarEventData { 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())?; + let is_todo = self.event.components[comp_id as usize] + .component_type + .is_todo(); if start_tz.is_floating() && !default_tz.is_floating() { start_tz = default_tz; @@ -65,9 +68,7 @@ impl ArchivedCalendarEventData { .single()? .timestamp(); - if ((start < limit.end) || (start <= limit.start)) - && (end > limit.start || end >= limit.end) - { + if limit.is_in_range(is_todo, start, end) { expansion.push(CalendarEventExpansion { comp_id, expansion_id, @@ -97,9 +98,7 @@ impl ArchivedCalendarEventData { .single()? .timestamp(); - if ((start < limit.end) || (start <= limit.start)) - && (end > limit.start || end >= limit.end) - { + if limit.is_in_range(is_todo, start, end) { expansion.push(CalendarEventExpansion { comp_id, expansion_id: base_expansion_id, diff --git a/crates/groupware/src/calendar/index.rs b/crates/groupware/src/calendar/index.rs index 7bd1a005..8193d644 100644 --- a/crates/groupware/src/calendar/index.rs +++ b/crates/groupware/src/calendar/index.rs @@ -4,14 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::calendar::{ArchivedCalendarEventNotification, CalendarEventNotification}; +use std::collections::HashSet; use super::{ ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert, ArchivedTimezone, Calendar, CalendarEvent, CalendarPreferences, DefaultAlert, Timezone, }; -use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; +use crate::calendar::{ + ArchivedCalendarEventNotification, ArchivedEventPreferences, CalendarEventNotification, + EventPreferences, +}; +use calcard::icalendar::{ + ArchivedICalendarParameterValue, ArchivedICalendarProperty, ArchivedICalendarValue, + ICalendarParameterValue, ICalendarProperty, ICalendarValue, +}; +use common::storage::index::{ + IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, +}; +use store::backend::MAX_TOKEN_LENGTH; use types::{acl::AclGrant, collection::SyncCollection, field::CalendarField}; +use utils::sanitize_email; impl IndexableObject for Calendar { fn index_values(&self) -> impl Iterator> { @@ -69,10 +81,39 @@ impl IndexableObject for CalendarEvent { field: CalendarField::Uid.into(), value: self.data.event.uids().next().into(), }, + IndexValue::Index { + field: CalendarField::Start.into(), + value: self.data.event_range_start().into(), + }, + IndexValue::Index { + field: CalendarField::Created.into(), + value: self.created.into(), + }, + IndexValue::Index { + field: CalendarField::Updated.into(), + value: self.modified.into(), + }, + IndexValue::IndexList { + field: CalendarField::Text.into(), + value: self + .text() + .filter_map(|v| { + if let Some(email) = v.strip_prefix("mailto:") { + sanitize_email(email) + } else { + Some(v.to_lowercase()) + } + }) + .map(Into::into) + .collect::>() + .into_iter() + .collect(), + }, IndexValue::Quota { used: self.dead_properties.size() as u32 + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + self.names.iter().map(|n| n.name.len() as u32).sum::() + + self.preferences.iter().map(|p| p.size()).sum::() as u32 + self.size, }, IndexValue::LogItem { @@ -91,10 +132,39 @@ impl IndexableObject for &ArchivedCalendarEvent { field: CalendarField::Uid.into(), value: self.data.event.uids().next().into(), }, + IndexValue::Index { + field: CalendarField::Start.into(), + value: self.data.event_range_start().into(), + }, + IndexValue::Index { + field: CalendarField::Created.into(), + value: self.created.to_native().into(), + }, + IndexValue::Index { + field: CalendarField::Updated.into(), + value: self.modified.to_native().into(), + }, + IndexValue::IndexList { + field: CalendarField::Text.into(), + value: self + .text() + .filter_map(|v| { + if let Some(email) = v.strip_prefix("mailto:") { + sanitize_email(email) + } else { + Some(v.to_lowercase()) + } + }) + .map(Into::into) + .collect::>() + .into_iter() + .collect(), + }, IndexValue::Quota { used: self.dead_properties.size() as u32 + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + self.names.iter().map(|n| n.name.len() as u32).sum::() + + self.preferences.iter().map(|p| p.size()).sum::() as u32 + self.size, }, IndexValue::LogItem { @@ -161,6 +231,7 @@ impl CalendarPreferences { + self.description.as_ref().map_or(0, |n| n.len()) + self.color.as_ref().map_or(0, |n| n.len()) + self.time_zone.size() + + std::mem::size_of::() } } @@ -171,6 +242,23 @@ impl ArchivedCalendarPreferences { + self.description.as_ref().map_or(0, |n| n.len()) + self.color.as_ref().map_or(0, |n| n.len()) + self.time_zone.size() + + std::mem::size_of::() + } +} + +impl EventPreferences { + pub fn size(&self) -> usize { + self.alerts.iter().map(|a| a.size()).sum::() + + self.properties.iter().map(|p| p.size()).sum::() + + std::mem::size_of::() + } +} + +impl ArchivedEventPreferences { + pub fn size(&self) -> usize { + self.alerts.iter().map(|a| a.size()).sum::() + + self.properties.iter().map(|p| p.size()).sum::() + + std::mem::size_of::() } } @@ -205,3 +293,83 @@ impl ArchivedDefaultAlert { std::mem::size_of::() + self.id.len() } } + +impl CalendarEvent { + pub fn text(&self) -> impl Iterator { + self.data + .event + .components + .iter() + .filter(|e| e.component_type.is_scheduling_object()) + .flat_map(|e| { + e.entries.iter().filter(|e| { + matches!( + e.name, + ICalendarProperty::Summary + | ICalendarProperty::Location + | ICalendarProperty::Description + | ICalendarProperty::Categories + | ICalendarProperty::Comment + | ICalendarProperty::Attendee + | ICalendarProperty::Organizer + ) + }) + }) + .flat_map(|e| { + e.values + .iter() + .filter_map(|v| match v { + ICalendarValue::Text(v) => Some(v.as_str()), + ICalendarValue::Uri(uri) => uri.as_str(), + _ => None, + }) + .chain(e.params.iter().filter_map(|p| match &p.value { + ICalendarParameterValue::Text(v) => Some(v.as_str()), + ICalendarParameterValue::Uri(uri) => uri.as_str(), + _ => None, + })) + }) + .flat_map(str::split_whitespace) + .filter(|s| s.len() < MAX_TOKEN_LENGTH) + } +} + +impl ArchivedCalendarEvent { + pub fn text(&self) -> impl Iterator { + self.data + .event + .components + .iter() + .filter(|e| e.component_type.is_scheduling_object()) + .flat_map(|e| { + e.entries.iter().filter(|e| { + matches!( + e.name, + ArchivedICalendarProperty::Summary + | ArchivedICalendarProperty::Location + | ArchivedICalendarProperty::Description + | ArchivedICalendarProperty::Categories + | ArchivedICalendarProperty::Comment + | ArchivedICalendarProperty::Attendee + | ArchivedICalendarProperty::Organizer + ) + }) + }) + .flat_map(|e| { + e.values + .iter() + .filter_map(|v| match v { + ArchivedICalendarValue::Text(v) => Some(v.as_str()), + ArchivedICalendarValue::Uri(uri) => uri.as_str(), + _ => None, + }) + .chain(e.params.iter().filter_map(|p| match &p.value { + ArchivedICalendarParameterValue::Text(v) => Some(v.as_str()), + ArchivedICalendarParameterValue::Uri(uri) => uri.as_str(), + _ => None, + })) + }) + .flat_map(str::split_whitespace) + .filter(|s| s.len() < MAX_TOKEN_LENGTH) + } +} diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index bde3d0dd..3059ef8f 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -13,8 +13,7 @@ pub mod storage; use calcard::icalendar::{ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry}; use common::{DavName, auth::AccessToken}; -use dav_proto::schema::request::DeadProperty; -use types::acl::AclGrant; +use types::{acl::AclGrant, dead_property::DeadProperty}; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, diff --git a/crates/groupware/src/contact/index.rs b/crates/groupware/src/contact/index.rs index 3f2cf74a..d07de352 100644 --- a/crates/groupware/src/contact/index.rs +++ b/crates/groupware/src/contact/index.rs @@ -10,6 +10,7 @@ use common::storage::index::{ IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject, }; use std::collections::HashSet; +use store::backend::MAX_TOKEN_LENGTH; use types::{acl::AclGrant, collection::SyncCollection, field::ContactField}; use utils::sanitize_email; @@ -199,6 +200,7 @@ impl ContactCard { }) .flat_map(|e| e.values.iter().filter_map(|v| v.as_text())) .flat_map(str::split_whitespace) + .filter(|s| s.len() < MAX_TOKEN_LENGTH) } pub fn emails(&self) -> impl Iterator { @@ -229,6 +231,7 @@ impl ArchivedContactCard { }) .flat_map(|e| e.values.iter().filter_map(|v| v.as_text())) .flat_map(str::split_whitespace) + .filter(|s| s.len() < MAX_TOKEN_LENGTH) } pub fn emails(&self) -> impl Iterator { diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index eef2ef4a..4f4012c9 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -9,8 +9,7 @@ pub mod storage; use calcard::vcard::VCard; use common::{DavName, auth::AccessToken}; -use dav_proto::schema::request::DeadProperty; -use types::acl::AclGrant; +use types::{acl::AclGrant, dead_property::DeadProperty}; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, diff --git a/crates/groupware/src/file/mod.rs b/crates/groupware/src/file/mod.rs index 447cfea7..8d000cfe 100644 --- a/crates/groupware/src/file/mod.rs +++ b/crates/groupware/src/file/mod.rs @@ -7,8 +7,7 @@ pub mod index; pub mod storage; -use dav_proto::schema::request::DeadProperty; -use types::{acl::AclGrant, blob_hash::BlobHash}; +use types::{acl::AclGrant, blob_hash::BlobHash, dead_property::DeadProperty}; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, diff --git a/crates/groupware/src/scheduling/mod.rs b/crates/groupware/src/scheduling/mod.rs index ba126298..2e40031b 100644 --- a/crates/groupware/src/scheduling/mod.rs +++ b/crates/groupware/src/scheduling/mod.rs @@ -14,7 +14,6 @@ use calcard::{ ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri, }, }; -use dav_proto::schema::response::CalCondition; use std::{fmt::Display, hash::Hash}; pub mod attendee; @@ -341,28 +340,6 @@ impl ItipDateTime<'_> { } impl ItipError { - pub fn failed_precondition(&self) -> Option { - match self { - ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents), - ItipError::OrganizerIsLocalAddress - | ItipError::SenderIsNotParticipant(_) - | ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer), - ItipError::CannotModifyProperty(_) - | ItipError::CannotModifyInstance - | ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange), - ItipError::MissingUid - | ItipError::MultipleUid - | ItipError::MultipleObjectTypes - | ItipError::MultipleObjectInstances - | ItipError::MissingMethod - | ItipError::InvalidComponentType - | ItipError::OutOfSequence - | ItipError::UnknownParticipant(_) - | ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage), - _ => None, - } - } - pub fn is_jmap_error(&self) -> bool { matches!( self, diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index 8843af98..8c28e8ef 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -183,7 +183,7 @@ impl SessionData { .caused_by(trc::location!())? .ids .into_iter() - .map(|id| id as u32), + .map(|id| id.document_id()), is_uid, arguments.result_options.contains(&ResultOption::Min), arguments.result_options.contains(&ResultOption::Max), diff --git a/crates/jmap-proto/src/method/availability.rs b/crates/jmap-proto/src/method/availability.rs index 4a78375e..a97b2657 100644 --- a/crates/jmap-proto/src/method/availability.rs +++ b/crates/jmap-proto/src/method/availability.rs @@ -40,7 +40,7 @@ pub struct BusyPeriod { pub event: Option>, } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum BusyStatus { Confirmed, diff --git a/crates/jmap-proto/src/method/query.rs b/crates/jmap-proto/src/method/query.rs index 7e4bc9de..fda327d8 100644 --- a/crates/jmap-proto/src/method/query.rs +++ b/crates/jmap-proto/src/method/query.rs @@ -18,7 +18,7 @@ use std::{ fmt::{self, Display, Formatter}, }; use store::fts::{FilterItem, FilterType, FtsFilter}; -use types::{id::Id}; +use types::id::Id; #[derive(Debug, Clone)] pub struct QueryRequest { @@ -200,8 +200,8 @@ where where V: MapAccess<'de>, { - let mut filter = T::default(); - let mut has_filter = false; + let mut filter = None; + let mut has_multiple_filters = false; let mut has_conditions = None; let mut op = None; @@ -231,13 +231,21 @@ where self.0.push(Filter::Close); } _ => { - filter.deserialize_argument(&key, &mut map)?; - has_filter = true; + if let Some(filter) = filter { + if !has_multiple_filters { + self.0.push(Filter::And); + has_multiple_filters = true; + } + self.0.push(Filter::Property(filter)); + } + let mut new_filter = T::default(); + new_filter.deserialize_argument(&key, &mut map)?; + filter = Some(new_filter); } } } - if has_filter { + if let Some(filter) = filter { if has_conditions.is_some() { return Err(de::Error::custom( "Cannot mix conditions with property filters", @@ -245,6 +253,9 @@ where } self.0.push(Filter::Property(filter)); + if has_multiple_filters { + self.0.push(Filter::Close); + } } Ok(()) diff --git a/crates/jmap-proto/src/object/calendar_event.rs b/crates/jmap-proto/src/object/calendar_event.rs index 37cfc21c..591f9f60 100644 --- a/crates/jmap-proto/src/object/calendar_event.rs +++ b/crates/jmap-proto/src/object/calendar_event.rs @@ -7,13 +7,13 @@ use crate::{ object::{AnyId, JmapObject, JmapObjectId}, request::{MaybeInvalid, deserialize::DeserializeArguments}, - types::date::UTCDate, }; use calcard::{ common::timezone::Tz, - jscalendar::{JSCalendarProperty, JSCalendarValue}, + jscalendar::{JSCalendarDateTime, JSCalendarProperty, JSCalendarValue}, }; use jmap_tools::{JsonPointerItem, Key}; +use mail_parser::DateTime; use std::{borrow::Cow, str::FromStr}; use types::{blob::BlobId, id::Id}; @@ -81,14 +81,14 @@ impl JmapObjectId for JSCalendarValue { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CalendarEventFilter { InCalendar(MaybeInvalid), - After(UTCDate), - Before(UTCDate), + After(JSCalendarDateTime), + Before(JSCalendarDateTime), Text(String), - Title(Option), - Description(Option), - Location(Option), - Owner(Option), - Attendee(Option), + Title(String), + Description(String), + Location(String), + Owner(String), + Attendee(String), Uid(String), _T(String), } @@ -105,8 +105,8 @@ pub enum CalendarEventComparator { #[derive(Debug, Clone, Default)] pub struct CalendarEventGetArguments { - pub recurrence_overrides_before: Option, - pub recurrence_overrides_after: Option, + pub recurrence_overrides_before: Option, + pub recurrence_overrides_after: Option, pub reduce_participants: Option, pub time_zone: Option, } @@ -132,28 +132,28 @@ impl<'de> DeserializeArguments<'de> for CalendarEventFilter { *self = CalendarEventFilter::InCalendar(map.next_value()?); }, b"after" => { - *self = CalendarEventFilter::After(map.next_value()?); + *self = CalendarEventFilter::After(map.next_value::()?.0); }, b"before" => { - *self = CalendarEventFilter::Before(map.next_value()?); + *self = CalendarEventFilter::Before(map.next_value::()?.0); }, b"text" => { *self = CalendarEventFilter::Text(map.next_value::>()?.to_lowercase()); }, b"title" => { - *self = CalendarEventFilter::Title(map.next_value::>>()?.map(|s| s.to_lowercase())); + *self = CalendarEventFilter::Title(map.next_value::>()?.to_lowercase()); }, b"description" => { - *self = CalendarEventFilter::Description(map.next_value::>>()?.map(|s| s.to_lowercase())); + *self = CalendarEventFilter::Description(map.next_value::>()?.to_lowercase()); }, b"location" => { - *self = CalendarEventFilter::Location(map.next_value::>>()?.map(|s| s.to_lowercase())); + *self = CalendarEventFilter::Location(map.next_value::>()?.to_lowercase()); }, b"owner" => { - *self = CalendarEventFilter::Owner(map.next_value::>>()?.map(|s| s.to_lowercase())); + *self = CalendarEventFilter::Owner(map.next_value::>()?.to_lowercase()); }, b"attendee" => { - *self = CalendarEventFilter::Attendee(map.next_value::>>()?.map(|s| s.to_lowercase())); + *self = CalendarEventFilter::Attendee(map.next_value::>()?.to_lowercase()); }, b"uid" => { *self = CalendarEventFilter::Uid(map.next_value()?); @@ -208,10 +208,10 @@ impl<'de> DeserializeArguments<'de> for CalendarEventGetArguments { { hashify::fnc_map!(key.as_bytes(), b"recurrenceOverridesBefore" => { - self.recurrence_overrides_before = map.next_value()?; + self.recurrence_overrides_before = map.next_value::>()?.map(|lt| lt.0) }, b"recurrenceOverridesAfter" => { - self.recurrence_overrides_after = map.next_value()?; + self.recurrence_overrides_after = map.next_value::>()?.map(|lt| lt.0); }, b"reduceParticipants" => { self.reduce_participants = map.next_value()?; @@ -309,6 +309,29 @@ impl Default for CalendarEventComparator { } } +struct LocalTime(JSCalendarDateTime); + +impl<'de> serde::Deserialize<'de> for LocalTime { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = <&str>::deserialize(deserializer)?; + + if let Some(dt) = DateTime::parse_rfc3339(value) { + Ok(LocalTime(JSCalendarDateTime { + timestamp: dt.to_timestamp_local(), + is_local: true, + })) + } else { + Err(serde::de::Error::custom(format!( + "Invalid datetime: {}", + value + ))) + } + } +} + impl JmapObjectId for JSCalendarProperty { fn as_id(&self) -> Option { if let JSCalendarProperty::IdValue(id) = self { diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index 8738fe43..7ab8fb8f 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -561,7 +561,7 @@ mod tests { "Email/query", { "accountId": "0", - "filter": { "conditions": [ { "hasKeyword": "music" }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" }, + "filter": { "conditions": [ { "hasKeyword": "music", "maxSize": 455 }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" }, "sort": [ { "property": "subject", diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 7b97182e..0da9f4c3 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -307,6 +307,7 @@ impl RequestHandler for Server { } GetRequestMethod::PrincipalAvailability(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::CalendarEvent)?; self.principal_get_availability(req, access_token) .await? diff --git a/crates/jmap/src/calendar_event/get.rs b/crates/jmap/src/calendar_event/get.rs index 7e0244a5..5de975e0 100644 --- a/crates/jmap/src/calendar_event/get.rs +++ b/crates/jmap/src/calendar_event/get.rs @@ -150,12 +150,12 @@ impl CalendarEventGet for Server { let after = request .arguments .recurrence_overrides_after - .map(|v| v.timestamp()) + .map(|v| v.timestamp) .unwrap_or(i64::MIN); let before = request .arguments .recurrence_overrides_before - .map(|v| v.timestamp()) + .map(|v| v.timestamp) .unwrap_or(i64::MAX); if after < before { Some(after..before) diff --git a/crates/jmap/src/calendar_event/query.rs b/crates/jmap/src/calendar_event/query.rs index 9f6ef01e..4e983a5e 100644 --- a/crates/jmap/src/calendar_event/query.rs +++ b/crates/jmap/src/calendar_event/query.rs @@ -4,27 +4,30 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{JmapMethods, UpdateResults, changes::state::JmapCacheState}; +use calcard::{common::timezone::Tz, jscalendar::JSCalendarDateTime}; +use chrono::offset::TimeZone; use common::{Server, auth::AccessToken}; -use groupware::cache::GroupwareCache; +use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; use jmap_proto::{ - method::query::{Comparator, Filter, QueryRequest, QueryResponse}, - object::calendar_event::{CalendarEvent, CalendarEventComparator, CalendarEventFilter}, + method::query::{Filter, QueryRequest, QueryResponse}, + object::calendar_event::{self, CalendarEventComparator, CalendarEventFilter}, request::MaybeInvalid, }; -use store::{SerializeInfallible, query, roaring::RoaringBitmap}; +use std::{cmp::Ordering, sync::Arc}; +use store::{query, roaring::RoaringBitmap}; +use trc::AddContext; use types::{ + TimeRange, acl::Acl, collection::{Collection, SyncCollection}, - field::ContactField, + field::CalendarField, }; -use utils::sanitize_email; - -use crate::{JmapMethods, changes::state::JmapCacheState}; pub trait CalendarEventQuery: Sync + Send { fn calendar_event_query( &self, - request: QueryRequest, + request: QueryRequest, access_token: &AccessToken, ) -> impl Future> + Send; } @@ -32,18 +35,33 @@ pub trait CalendarEventQuery: Sync + Send { impl CalendarEventQuery for Server { async fn calendar_event_query( &self, - mut request: QueryRequest, + mut request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { - todo!() - - /*let account_id = request.account_id.document_id(); + let account_id = request.account_id.document_id(); let mut filters = Vec::with_capacity(request.filter.len()); let cache = self .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) .await?; let filter_mask = (access_token.is_shared(account_id)) .then(|| cache.shared_items(access_token, [Acl::ReadItems], true)); + let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC); + let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false); + let mut filter_start = None; + let mut filter_end = None; + + // Extract from/to arguments + for cond in &request.filter { + if let Filter::Property(CalendarEventFilter::After(date)) = cond { + if let Some(after) = local_timestamp(date, default_tz) { + filter_start = Some(after); + } + } else if let Filter::Property(CalendarEventFilter::Before(date)) = cond + && let Some(before) = local_timestamp(date, default_tz) + { + filter_end = Some(before); + } + } for cond in std::mem::take(&mut request.filter) { match cond { @@ -54,32 +72,59 @@ impl CalendarEventQuery for Server { ))) } CalendarEventFilter::Uid(uid) => { - filters.push(query::Filter::eq(ContactField::Uid, uid.into_bytes())) + filters.push(query::Filter::eq(CalendarField::Uid, uid.into_bytes())) + } + CalendarEventFilter::Text(value) => { + filters.push(query::Filter::has_text(CalendarField::Text, value)) + } + CalendarEventFilter::After(_) => { + /* + The end of the event, or any recurrence of the event, + in the time zone given as the "timeZone" argument, + must be after this date to match the condition. + */ + + if let Some(filter_start) = filter_start { + if let Some(filter_end) = filter_end { + filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( + cache.resources.iter().filter_map(|r| { + r.event_time_range().and_then(|(start, end)| { + (((filter_start < end) || (filter_start <= start)) + && (filter_end > start || filter_end >= end)) + .then_some(r.document_id) + }) + }), + ))); + } else { + filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( + cache.resources.iter().filter_map(|r| { + r.event_time_range().and_then(|(_, end)| { + (end >= filter_start).then_some(r.document_id) + }) + }), + ))); + } + } + } + CalendarEventFilter::Before(_) => { + /* + The start of the event, or any recurrence of the event, + in the time zone given as the "timeZone" argument, + must be before this date to match the condition. + */ + + if filter_start.is_none() + && let Some(filter_end) = filter_end + { + filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( + cache.resources.iter().filter_map(|r| { + r.event_time_range().and_then(|(start, _)| { + (start < filter_end).then_some(r.document_id) + }) + }), + ))); + } } - CalendarEventFilter::Email(email) => filters.push(query::Filter::eq( - ContactField::Email, - sanitize_email(&email).unwrap_or(email).into_bytes(), - )), - CalendarEventFilter::Text(value) => filters.push(query::Filter::has_text( - ContactField::Text, - value.to_lowercase(), - )), - CalendarEventFilter::CreatedBefore(before) => filters.push(query::Filter::lt( - ContactField::Created, - (before.timestamp() as u64).serialize(), - )), - CalendarEventFilter::CreatedAfter(after) => filters.push(query::Filter::gt( - ContactField::Created, - (after.timestamp() as u64).serialize(), - )), - CalendarEventFilter::UpdatedBefore(before) => filters.push(query::Filter::lt( - ContactField::Updated, - (before.timestamp() as u64).serialize(), - )), - CalendarEventFilter::UpdatedAfter(after) => filters.push(query::Filter::gt( - ContactField::Updated, - (after.timestamp() as u64).serialize(), - )), unsupported => { return Err(trc::JmapEvent::UnsupportedFilter .into_err() @@ -101,37 +146,166 @@ impl CalendarEventQuery for Server { result_set.apply_mask(filter_mask); } - let (response, paginate) = self + let (mut response, paginate) = self .build_query_response(&result_set, cache.get_state(false), &request) .await?; - if let Some(paginate) = paginate { - // Parse sort criteria - let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len())); - for comparator in request - .sort - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| vec![Comparator::descending(CalendarEventComparator::Updated)]) - { - comparators.push(match comparator.property { - CalendarEventComparator::Created => { - query::Comparator::field(ContactField::Created, comparator.is_ascending) - } - CalendarEventComparator::Updated => { - query::Comparator::field(ContactField::Updated, comparator.is_ascending) - } - unsupported => { - return Err(trc::JmapEvent::UnsupportedSort - .into_err() - .details(unsupported.into_string())); - } - }); - } + if let Some(mut paginate) = paginate { + // Extract comparators + let comparators = request.sort.filter(|s| !s.is_empty()).unwrap_or_default(); + if expand_recurrences { + let (Some(start), Some(end)) = (filter_start, filter_end) else { + return Err(trc::JmapEvent::InvalidArguments.into_err().details( + "Both 'after' and 'before' filters are required when expanding recurrences", + )); + }; + let time_range = TimeRange { start, end }; + let max_instances = self.core.groupware.max_ical_instances; + let mut results = Vec::with_capacity(result_set.results.len() as usize); + let has_uid_comparator = comparators + .iter() + .any(|c| matches!(c.property, CalendarEventComparator::Uid)); - // Sort results - self.sort(result_set, comparators, paginate, response).await + for document_id in result_set.results { + let Some(_calendar_event) = self + .get_archive(account_id, Collection::CalendarEvent, document_id) + .await? + else { + continue; + }; + let calendar_event = _calendar_event + .unarchive::() + .caused_by(trc::location!())?; + + // Expand recurrences + let uid = if has_uid_comparator { + Arc::new( + calendar_event + .data + .event + .uids() + .next() + .unwrap_or_default() + .to_string(), + ) + } else { + Arc::new(String::new()) + }; + for expansion in calendar_event + .data + .expand(default_tz, time_range) + .unwrap_or_default() + { + if results.len() < max_instances { + results.push(SearchResult { + created: calendar_event.created.to_native().to_be_bytes(), + updated: calendar_event.modified.to_native().to_be_bytes(), + start: expansion.start.to_be_bytes(), + uid: uid.clone(), + document_id, + expansion_id: expansion.expansion_id, + }); + } else { + return Err(trc::JmapEvent::InvalidArguments.into_err().details( + "The number of expanded recurrences exceeds the server limit", + )); + } + } + } + + // Sort results + if !results.is_empty() { + results.sort_by(|a, b| { + for comparator in &comparators { + let ordering = a + .get_property(&comparator.property) + .cmp(b.get_property(&comparator.property)); + + let ordering = if comparator.is_ascending { + ordering + } else { + ordering.reverse() + }; + + if ordering != Ordering::Equal { + return ordering; + } + } + Ordering::Equal + }); + } + + // Add results + for result in results { + if !paginate.add(result.expansion_id + 1, result.document_id) { + break; + } + } + response.update_results(paginate.build())?; + + Ok(response) + } else { + let mut comparators_ = Vec::with_capacity(comparators.len()); + + for comparator in comparators { + comparators_.push(match comparator.property { + CalendarEventComparator::Uid => { + query::Comparator::field(CalendarField::Uid, comparator.is_ascending) + } + CalendarEventComparator::Start => { + query::Comparator::field(CalendarField::Start, comparator.is_ascending) + } + CalendarEventComparator::Created => query::Comparator::field( + CalendarField::Created, + comparator.is_ascending, + ), + CalendarEventComparator::Updated => query::Comparator::field( + CalendarField::Updated, + comparator.is_ascending, + ), + unsupported => { + return Err(trc::JmapEvent::UnsupportedSort + .into_err() + .details(unsupported.into_string())); + } + }); + } + + // Sort results + self.sort(result_set, comparators_, paginate, response) + .await + } } else { Ok(response) - }*/ + } + } +} + +fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option { + tz.from_local_datetime(&dt.to_naive_date_time()?) + .single() + .map(|dt| dt.timestamp()) +} + +struct SearchResult { + expansion_id: u32, + document_id: u32, + start: [u8; std::mem::size_of::()], + created: [u8; std::mem::size_of::()], + updated: [u8; std::mem::size_of::()], + uid: Arc, +} + +impl SearchResult { + fn get_property(&self, comparator: &CalendarEventComparator) -> &[u8] { + match comparator { + CalendarEventComparator::Uid => self.uid.as_bytes(), + CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => { + self.start.as_ref() + } + CalendarEventComparator::Created => self.created.as_ref(), + CalendarEventComparator::Updated => self.updated.as_ref(), + CalendarEventComparator::_T(_) => &[], + } } } diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs index 9f2a897f..0ef572a1 100644 --- a/crates/jmap/src/calendar_event/set.rs +++ b/crates/jmap/src/calendar_event/set.rs @@ -519,6 +519,8 @@ impl CalendarEventSet for Server { return Ok(Err(err)); } + let todo = "add default alarms + other calendar properties"; + // Verify that the calendar ids valid for name in &event.names { if !cache.has_container_id(&name.parent_id) { diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 49635e63..d3b3146f 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -216,11 +216,7 @@ impl UpdateResults for QueryResponse { // Prepare response if sorted_results.found_anchor { self.position = sorted_results.position; - self.ids = sorted_results - .ids - .into_iter() - .map(|id| id.into()) - .collect::>(); + self.ids = sorted_results.ids; Ok(()) } else { Err(trc::JmapEvent::AnchorNotFound.into_err()) diff --git a/crates/jmap/src/principal/availability.rs b/crates/jmap/src/principal/availability.rs index 19a0c1b3..a2ddf60a 100644 --- a/crates/jmap/src/principal/availability.rs +++ b/crates/jmap/src/principal/availability.rs @@ -4,9 +4,36 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::calendar_event::CalendarSyntheticId; +use calcard::{ + common::timezone::Tz, + icalendar::{ + ArchivedICalendarClassification, ArchivedICalendarParameterValue, + ArchivedICalendarParticipationStatus, ArchivedICalendarProperty, ArchivedICalendarStatus, + ArchivedICalendarTransparency, ArchivedICalendarValue, ICalendarParameterName, + }, + jscalendar::{JSCalendar, JSCalendarProperty, JSCalendarValue}, +}; use common::{Server, auth::AccessToken}; -use jmap_proto::method::availability::{GetAvailabilityRequest, GetAvailabilityResponse}; -use std::future::Future; +use groupware::{cache::GroupwareCache, calendar::CalendarEvent}; +use jmap_proto::{ + method::availability::{ + BusyPeriod, BusyStatus, GetAvailabilityRequest, GetAvailabilityResponse, + }, + request::IntoValid, + types::date::UTCDate, +}; +use jmap_tools::{Key, Map, Value}; +use std::{future::Future, sync::Arc}; +use store::ahash::AHashMap; +use trc::AddContext; +use types::{ + TimeRange, + acl::Acl, + collection::{Collection, SyncCollection}, + id::Id, +}; +use utils::sanitize_email; pub trait PrincipalGetAvailability: Sync + Send { fn principal_get_availability( @@ -19,9 +46,315 @@ pub trait PrincipalGetAvailability: Sync + Send { impl PrincipalGetAvailability for Server { async fn principal_get_availability( &self, - mut request: GetAvailabilityRequest, + request: GetAvailabilityRequest, access_token: &AccessToken, ) -> trc::Result { - todo!() + if !request.id.is_valid() { + return Err(trc::JmapEvent::InvalidArguments + .into_err() + .details("Missing principal id")); + } + let properties = request + .event_properties + .map(|props| props.into_valid().collect::>()) + .unwrap_or_default(); + if properties + .iter() + .any(|p| !matches!(p, JSCalendarProperty::Id | JSCalendarProperty::BaseEventId)) + { + return Err(trc::JmapEvent::InvalidArguments + .into_err() + .details("Only 'id' and 'baseEventId' properties are supported in results")); + } + let return_event_details = !properties.is_empty(); + + let account_id = request.account_id.document_id(); + let principal_id = request.id.document_id(); + let resources = self + .fetch_dav_resources(access_token, account_id, SyncCollection::Calendar) + .await + .caused_by(trc::location!())?; + + // Obtain shared ids + let shared_ids = if !access_token.is_member(account_id) { + // Condition: The user has the "mayReadFreeBusy" permission for the calendar. + resources + .shared_containers( + access_token, + [Acl::ReadItems, Acl::SchedulingReadFreeBusy], + true, + ) + .into() + } else { + None + }; + /* + + TODO: Implement the following conditions: + + - The Principal is subscribed to the calendar. + - The "includeInAvailability" property of the calendar for the Principal is "all" or "attending". + - If the "includeInAvailability" property of the calendar is "attending", + + */ + + // Obtain external principal + let is_user_account = principal_id == account_id; + let user_principal = if access_token.primary_id() != principal_id { + PrincipalAddresses::Owned( + self.get_access_token(principal_id) + .await + .caused_by(trc::location!())?, + ) + } else { + PrincipalAddresses::Shared(access_token) + }; + + let filter = TimeRange { + start: request.utc_start.timestamp(), + end: request.utc_end.timestamp(), + }; + + // Condition: The event finishes after the "utcStart" argument and starts before the "utcEnd" argument. + let mut periods = Vec::new(); + 'next_event: for document_id in resources.resources.iter().filter_map(|r| { + r.event_time_range().and_then(|(start, end)| { + (shared_ids + .as_ref() + .is_none_or(|ids| ids.contains(r.document_id)) + && ((filter.start < end) || (filter.start <= start)) + && (filter.end > start || filter.end >= end)) + .then_some(r.document_id) + }) + }) { + let Some(archive) = self + .get_archive(account_id, Collection::CalendarEvent, document_id) + .await + .caused_by(trc::location!())? + else { + continue; + }; + let event = archive + .unarchive::() + .caused_by(trc::location!())?; + + // Find the component ids that match the criteria + let mut matching_component_ids = AHashMap::new(); + 'next_component: for (component_id, component) in + event.data.event.components.iter().enumerate() + { + if !component.component_type.is_event_or_todo() { + continue 'next_component; + } + + let mut is_cancelled = false; + let mut is_main_event = true; + let mut busy_status = None; + + for entry in component.entries.iter() { + match (&entry.name, entry.values.first()) { + ( + ArchivedICalendarProperty::Status, + Some(ArchivedICalendarValue::Status( + ArchivedICalendarStatus::Cancelled, + )), + ) => { + // The "status" property of the event is not "cancelled". + is_cancelled = true; + } + (ArchivedICalendarProperty::RecurrenceId, _) => { + is_main_event = false; + } + ( + ArchivedICalendarProperty::Class, + Some(ArchivedICalendarValue::Classification( + ArchivedICalendarClassification::Confidential, + )), + ) => { + // Condition: The event's "privacy" property is not "secret". + continue 'next_component; + } + ( + ArchivedICalendarProperty::Transp, + Some(ArchivedICalendarValue::Transparency( + ArchivedICalendarTransparency::Transparent, + )), + ) => { + // Condition: The "freeBusyStatus" property of the event is "busy" (or omitted, as this is the default). + continue 'next_component; + } + (ArchivedICalendarProperty::Attendee, Some(value)) => { + if let Some(attendee) = value.as_text().and_then(|attendee| { + sanitize_email(attendee.strip_prefix("mailto:").unwrap_or(attendee)) + }) { + // Condition: the Principal is a participant of the event, and has a "participationStatus" of "accepted" or "tentative". + if user_principal.is_principal_addresses(&attendee) { + busy_status = Some( + entry + .parameters(&ICalendarParameterName::Partstat) + .next() + .map(|v| match v { + ArchivedICalendarParameterValue::Partstat( + ArchivedICalendarParticipationStatus::Accepted, + ) => BusyStatus::Confirmed, + ArchivedICalendarParameterValue::Partstat( + ArchivedICalendarParticipationStatus::Tentative, + ) => BusyStatus::Tentative, + ArchivedICalendarParameterValue::Partstat( + ArchivedICalendarParticipationStatus::Declined, + ) => { + is_cancelled = true; + BusyStatus::Unavailable + } + _ => BusyStatus::Unavailable, + }) + .unwrap_or(BusyStatus::Unavailable), + ); + } + } + } + _ => (), + } + } + + if is_cancelled { + if is_main_event { + continue 'next_event; + } else { + continue 'next_component; + } + } + + let busy_status = if let Some(busy_status) = busy_status { + busy_status + } else if is_user_account { + BusyStatus::Confirmed + } else { + continue 'next_component; + }; + + matching_component_ids.insert(component_id as u32, busy_status); + } + + if matching_component_ids.is_empty() { + // No events matched the criteria + continue 'next_event; + } + + let default_tz = resources + .container_resource_by_id(event.names[0].parent_id.to_native()) + .and_then(|r| r.timezone()) + .unwrap_or(Tz::UTC); + + for expansion in event.data.expand(default_tz, filter).unwrap_or_default() { + let Some(busy_status) = matching_component_ids.get(&expansion.comp_id) else { + continue; + }; + periods.push(FreeBusyResult { + utc_start: expansion.start, + utc_end: expansion.end, + busy_status: *busy_status, + expansion_id: expansion.comp_id, + document_id, + }); + } + } + + let mut result = GetAvailabilityResponse { + list: Vec::with_capacity(periods.len()), + }; + + if periods.is_empty() { + return Ok(result); + } + + // Sort by busy status and start time + periods.sort_unstable_by(|a, b| { + a.busy_status + .cmp(&b.busy_status) + .then_with(|| a.utc_start.cmp(&b.utc_start)) + }); + + if return_event_details { + for period in periods { + result.list.push(period.into()); + } + } else { + // Merge intervals with same busy status + let mut start_time = periods[0].utc_start; + let mut end_time = periods[0].utc_end; + let mut current_status = periods[0].busy_status; + + for curr in periods.iter().skip(1) { + if curr.utc_start <= end_time && curr.busy_status == current_status { + end_time = end_time.max(curr.utc_end); + } else { + result.list.push(BusyPeriod { + utc_start: UTCDate::from_timestamp(start_time), + utc_end: UTCDate::from_timestamp(end_time), + busy_status: Some(current_status), + event: None, + }); + start_time = curr.utc_start; + end_time = curr.utc_end; + current_status = curr.busy_status; + } + } + + result.list.push(BusyPeriod { + utc_start: UTCDate::from_timestamp(start_time), + utc_end: UTCDate::from_timestamp(end_time), + busy_status: Some(current_status), + event: None, + }); + } + + Ok(result) + } +} + +struct FreeBusyResult { + utc_start: i64, + utc_end: i64, + busy_status: BusyStatus, + expansion_id: u32, + document_id: u32, +} + +impl From for BusyPeriod { + fn from(value: FreeBusyResult) -> Self { + BusyPeriod { + utc_start: UTCDate::from_timestamp(value.utc_start), + utc_end: UTCDate::from_timestamp(value.utc_end), + busy_status: Some(value.busy_status), + event: JSCalendar(Value::Object(Map::from(vec![ + ( + Key::Property(JSCalendarProperty::Id), + Value::Element(JSCalendarValue::Id(::new( + value.expansion_id, + value.document_id, + ))), + ), + ( + Key::Property(JSCalendarProperty::BaseEventId), + Value::Element(JSCalendarValue::Id(Id::from(value.document_id))), + ), + ]))) + .into(), + } + } +} + +enum PrincipalAddresses<'x> { + Owned(Arc), + Shared(&'x AccessToken), +} + +impl<'x> PrincipalAddresses<'x> { + fn is_principal_addresses(&self, email: &String) -> bool { + match self { + PrincipalAddresses::Owned(token) => token.emails.contains(email), + PrincipalAddresses::Shared(token) => token.emails.contains(email), + } } } diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs index a3a875d4..f371beb2 100644 --- a/crates/jmap/src/thread/get.rs +++ b/crates/jmap/src/thread/get.rs @@ -98,7 +98,7 @@ impl ThreadGet for Server { .caused_by(trc::location!())? .ids .into_iter() - .map(|id| Id::from_parts(thread_id, id as u32)) + .map(|id| Id::from_parts(thread_id, id.document_id())) .collect::>(), ); } diff --git a/crates/migration/src/calendar.rs b/crates/migration/src/calendar.rs index 6ba925b7..2fd61e52 100644 --- a/crates/migration/src/calendar.rs +++ b/crates/migration/src/calendar.rs @@ -6,17 +6,14 @@ use calcard::{common::timezone::Tz, icalendar::ICalendar}; use common::{DavName, Server}; -use dav_proto::schema::request::DeadProperty; -use groupware::calendar::{ - AlarmDelta, CalendarEvent, CalendarEventData, ComponentTimeRange, UserProperties, -}; +use groupware::calendar::{AlarmDelta, CalendarEvent, CalendarEventData, ComponentTimeRange}; use store::{ Serialize, rand::{self, seq::SliceRandom}, write::{Archiver, BatchBuilder, serialize::rkyv_deserialize}, }; use trc::AddContext; -use types::{collection::Collection, field::Field}; +use types::{collection::Collection, dead_property::DeadProperty, field::Field}; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, @@ -33,6 +30,14 @@ pub struct CalendarEventV1 { pub modified: i64, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +pub struct UserProperties { + pub account_id: u32, + pub properties: ICalendar, +} + #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] @@ -103,7 +108,7 @@ pub(crate) async fn migrate_calendar_events(server: &Server) -> trc::Result<()> server.core.groupware.max_ical_instances, &mut next_email_alarm, ), - user_properties: event.user_properties, + preferences: Default::default(), flags: event.flags, dead_properties: event.dead_properties, size: event.size, diff --git a/crates/store/src/query/mod.rs b/crates/store/src/query/mod.rs index 848d8cdf..8345c2c8 100644 --- a/crates/store/src/query/mod.rs +++ b/crates/store/src/query/mod.rs @@ -10,7 +10,7 @@ pub mod log; pub mod sort; use roaring::RoaringBitmap; -use types::collection::Collection; +use types::{collection::Collection, id::Id}; use crate::{ BitmapKey, IterateParams, Key, @@ -63,7 +63,7 @@ pub struct ResultSet { pub struct SortedResultSet { pub position: i32, - pub ids: Vec, + pub ids: Vec, pub found_anchor: bool, } diff --git a/crates/store/src/query/sort.rs b/crates/store/src/query/sort.rs index 37dfc3a1..4fc78504 100644 --- a/crates/store/src/query/sort.rs +++ b/crates/store/src/query/sort.rs @@ -9,6 +9,7 @@ use crate::{IndexKeyPrefix, IterateParams, Store, U32_LEN, write::key::Deseriali use ahash::{AHashMap, AHashSet}; use std::cmp::Ordering; use trc::AddContext; +use types::id::Id; #[derive(Debug)] pub struct Pagination<'x> { @@ -19,7 +20,7 @@ pub struct Pagination<'x> { anchor_offset: i32, has_anchor: bool, anchor_found: bool, - pub ids: Vec, + pub ids: Vec, prefix_map: Option<&'x AHashMap>, prefix_unique: bool, } @@ -124,8 +125,9 @@ impl Store { let mut sorted_results = paginate.build(); if let Some(prefix_map) = prefix_map { for id in sorted_results.ids.iter_mut() { - if let Some(prefix_id) = prefix_map.get(&(*id as u32)) { - *id |= (*prefix_id as u64) << 32; + let document_id = id.document_id(); + if let Some(prefix_id) = prefix_map.get(&document_id) { + *id = Id::from_parts(*prefix_id, document_id); } } } @@ -320,7 +322,7 @@ impl<'x> Pagination<'x> { } pub fn add(&mut self, prefix_id: u32, document_id: u32) -> bool { - let id = ((prefix_id as u64) << 32) | document_id as u64; + let id = Id::from_parts(prefix_id, document_id); // Pagination if !self.has_anchor { diff --git a/crates/types/src/dead_property.rs b/crates/types/src/dead_property.rs new file mode 100644 index 00000000..73aaf4e6 --- /dev/null +++ b/crates/types/src/dead_property.rs @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[cfg_attr(feature = "test_mode", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "test_mode", serde(tag = "type", content = "data"))] +#[rkyv(derive(Debug))] +pub enum DeadPropertyTag { + ElementStart(DeadElementTag), + ElementEnd, + Text(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[cfg_attr(feature = "test_mode", derive(serde::Serialize, serde::Deserialize))] +#[rkyv(derive(Debug))] +pub struct DeadElementTag { + pub name: String, + pub attrs: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[cfg_attr(feature = "test_mode", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "test_mode", serde(transparent))] +#[rkyv(derive(Debug))] +#[repr(transparent)] +pub struct DeadProperty(pub Vec); + +impl From<&ArchivedDeadProperty> for DeadProperty { + fn from(value: &ArchivedDeadProperty) -> Self { + DeadProperty(value.0.iter().map(|tag| tag.into()).collect::>()) + } +} + +impl From<&ArchivedDeadPropertyTag> for DeadPropertyTag { + fn from(tag: &ArchivedDeadPropertyTag) -> Self { + match tag { + ArchivedDeadPropertyTag::ElementStart(tag) => DeadPropertyTag::ElementStart(tag.into()), + ArchivedDeadPropertyTag::ElementEnd => DeadPropertyTag::ElementEnd, + ArchivedDeadPropertyTag::Text(tag) => DeadPropertyTag::Text(tag.to_string()), + } + } +} + +impl From<&ArchivedDeadElementTag> for DeadElementTag { + fn from(tag: &ArchivedDeadElementTag) -> Self { + DeadElementTag { + name: tag.name.to_string(), + attrs: tag.attrs.as_ref().map(|s| s.to_string()), + } + } +} + +impl ArchivedDeadProperty { + pub fn find_tag(&self, needle: &str) -> Option { + let mut depth: u32 = 0; + let mut tags = Vec::new(); + let mut found_tag = false; + + for tag in self.0.iter() { + match tag { + ArchivedDeadPropertyTag::ElementStart(start) => { + if depth == 0 && start.name == needle { + found_tag = true; + } else if found_tag { + tags.push(tag.into()); + } + + depth += 1; + } + ArchivedDeadPropertyTag::ElementEnd => { + if found_tag { + if depth == 1 { + break; + } else { + tags.push(tag.into()); + } + } + depth = depth.saturating_sub(1); + } + ArchivedDeadPropertyTag::Text(_) => { + if found_tag { + tags.push(tag.into()); + } + } + } + } + + if found_tag { + Some(DeadProperty(tags)) + } else { + None + } + } +} + +impl DeadProperty { + pub fn remove_element(&mut self, element: &DeadElementTag) { + let mut depth = 0; + let mut remove = false; + self.0.retain(|item| match item { + DeadPropertyTag::ElementStart(tag) => { + if depth == 0 && !remove && tag.name == element.name { + remove = true; + } + depth += 1; + + !remove + } + DeadPropertyTag::ElementEnd => { + depth -= 1; + if remove && depth == 0 { + remove = false; + false + } else { + !remove + } + } + _ => !remove, + }); + } + + pub fn add_element(&mut self, element: DeadElementTag, values: Vec) { + self.0.push(DeadPropertyTag::ElementStart(element)); + self.0.extend(values); + self.0.push(DeadPropertyTag::ElementEnd); + } + + pub fn size(&self) -> usize { + let mut size = 0; + for item in &self.0 { + match item { + DeadPropertyTag::ElementStart(tag) => { + size += tag.size(); + } + DeadPropertyTag::ElementEnd => { + size += 1; + } + DeadPropertyTag::Text(text) => { + size += text.len(); + } + } + } + size + } +} + +impl ArchivedDeadProperty { + pub fn size(&self) -> usize { + let mut size = 0; + for item in self.0.iter() { + match item { + ArchivedDeadPropertyTag::ElementStart(tag) => { + size += tag.size(); + } + ArchivedDeadPropertyTag::ElementEnd => { + size += 1; + } + ArchivedDeadPropertyTag::Text(text) => { + size += text.len(); + } + } + } + size + } +} + +impl DeadElementTag { + pub fn new(name: String, attrs: Option) -> Self { + DeadElementTag { name, attrs } + } + + pub fn size(&self) -> usize { + self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len()) + } +} + +impl ArchivedDeadElementTag { + pub fn size(&self) -> usize { + self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len()) + } +} + +impl Default for DeadProperty { + fn default() -> Self { + DeadProperty(Vec::with_capacity(4)) + } +} diff --git a/crates/types/src/field.rs b/crates/types/src/field.rs index 4abce1ed..9a36567d 100644 --- a/crates/types/src/field.rs +++ b/crates/types/src/field.rs @@ -28,6 +28,9 @@ pub enum ContactField { pub enum CalendarField { Uid, Created, + Updated, + Start, + Text, Archive, } @@ -100,7 +103,10 @@ impl From for u8 { fn from(value: CalendarField) -> Self { match value { CalendarField::Uid => 0, + CalendarField::Text => 1, CalendarField::Created => 2, + CalendarField::Updated => 3, + CalendarField::Start => 4, CalendarField::Archive => ARCHIVE_FIELD, } } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 161feca6..20487209 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -8,6 +8,7 @@ pub mod acl; pub mod blob; pub mod blob_hash; pub mod collection; +pub mod dead_property; pub mod field; pub mod id; pub mod keyword; @@ -17,3 +18,22 @@ pub mod type_state; pub type DocumentId = u32; pub type ChangeId = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "test_mode", derive(serde::Serialize, serde::Deserialize))] +pub struct TimeRange { + pub start: i64, + pub end: i64, +} + +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)) + ((start < self.end) || (start <= self.start)) && (end > self.start || end >= self.end) + } + } +}