JMAP for Calendars implementation (part 3)

This commit is contained in:
mdecimus
2025-10-08 19:52:23 +02:00
parent 2fb59edaa7
commit ec4963f46b
44 changed files with 1232 additions and 460 deletions

View File

@@ -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"] }

View File

@@ -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,

View File

@@ -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<Item = IndexValue<'_>> {
@@ -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::<HashSet<IndexItem>>()
.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::<u32>()
+ self.preferences.iter().map(|p| p.size()).sum::<usize>() 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::<HashSet<IndexItem>>()
.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::<u32>()
+ self.preferences.iter().map(|p| p.size()).sum::<usize>() 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::<CalendarPreferences>()
}
}
@@ -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::<CalendarPreferences>()
}
}
impl EventPreferences {
pub fn size(&self) -> usize {
self.alerts.iter().map(|a| a.size()).sum::<usize>()
+ self.properties.iter().map(|p| p.size()).sum::<usize>()
+ std::mem::size_of::<EventPreferences>()
}
}
impl ArchivedEventPreferences {
pub fn size(&self) -> usize {
self.alerts.iter().map(|a| a.size()).sum::<usize>()
+ self.properties.iter().map(|p| p.size()).sum::<usize>()
+ std::mem::size_of::<EventPreferences>()
}
}
@@ -205,3 +293,83 @@ impl ArchivedDefaultAlert {
std::mem::size_of::<Self>() + self.id.len()
}
}
impl CalendarEvent {
pub fn text(&self) -> impl Iterator<Item = &str> {
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<Item = &str> {
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)
}
}

View File

@@ -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,

View File

@@ -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<Item = String> {
@@ -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<Item = String> {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<CalCondition> {
match self {
ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents),
ItipError::OrganizerIsLocalAddress
| ItipError::SenderIsNotParticipant(_)
| ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer),
ItipError::CannotModifyProperty(_)
| ItipError::CannotModifyInstance
| ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange),
ItipError::MissingUid
| ItipError::MultipleUid
| ItipError::MultipleObjectTypes
| ItipError::MultipleObjectInstances
| ItipError::MissingMethod
| ItipError::InvalidComponentType
| ItipError::OutOfSequence
| ItipError::UnknownParticipant(_)
| ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage),
_ => None,
}
}
pub fn is_jmap_error(&self) -> bool {
matches!(
self,