JMAP for Calendars implementation (part 2)

This commit is contained in:
mdecimus
2025-10-07 19:06:49 +02:00
parent b7df05dd3b
commit 2fb59edaa7
22 changed files with 1809 additions and 330 deletions

View File

@@ -4,18 +4,28 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::{common::timezone::Tz, icalendar::dates::CalendarEvent};
use super::ArchivedCalendarEventData;
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 utils::codec::leb128::Leb128Reader;
use super::ArchivedCalendarEventData;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CalendarEventExpansion {
pub comp_id: u32,
pub expansion_id: u32,
pub start: i64,
pub end: i64,
}
impl ArchivedCalendarEventData {
pub fn expand(&self, default_tz: Tz, limit: TimeRange) -> Option<Vec<CalendarEvent<i64, i64>>> {
pub fn expand(&self, default_tz: Tz, limit: TimeRange) -> Option<Vec<CalendarEventExpansion>> {
let mut expansion = Vec::with_capacity(self.time_ranges.len());
let base_offset = self.base_offset.to_native();
let mut base_expansion_id = 0;
'outer: for range in self.time_ranges.iter() {
let instances = range.instances.as_ref();
@@ -37,6 +47,8 @@ impl ArchivedCalendarEventData {
// Recurring event
let unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count);
let mut expansion_id = base_expansion_id;
base_expansion_id += offset_or_count;
for start_offset in unpacker {
let start_date_naive = start_offset as i64 + base_offset;
let end_date_naive = start_date_naive + duration;
@@ -56,14 +68,17 @@ impl ArchivedCalendarEventData {
if ((start < limit.end) || (start <= limit.start))
&& (end > limit.start || end >= limit.end)
{
expansion.push(CalendarEvent {
expansion.push(CalendarEventExpansion {
comp_id,
expansion_id,
start,
end,
});
} else if start > limit.end {
continue 'outer;
}
expansion_id += 1;
}
} else {
// Single event
@@ -85,15 +100,201 @@ impl ArchivedCalendarEventData {
if ((start < limit.end) || (start <= limit.start))
&& (end > limit.start || end >= limit.end)
{
expansion.push(CalendarEvent {
expansion.push(CalendarEventExpansion {
comp_id,
expansion_id: base_expansion_id,
start,
end,
});
}
base_expansion_id += 1;
}
}
Some(expansion)
}
}
impl CalendarEventData {
pub fn expand_from_ids(
&self,
expansion_ids: &mut AHashSet<u32>,
default_tz: Tz,
) -> Option<Vec<CalendarEventExpansion>> {
let mut expansion = Vec::with_capacity(expansion_ids.len());
let base_offset = self.base_offset;
let mut base_expansion_id = 0;
'outer: for range in self.time_ranges.iter() {
let instances = range.instances.as_ref();
let (offset_or_count, bytes_read) = instances.read_leb128::<u32>()?;
let mut start_tz = Tz::from_id(range.start_tz)?;
let mut end_tz = Tz::from_id(range.end_tz)?;
if start_tz.is_floating() && !default_tz.is_floating() {
start_tz = default_tz;
}
if end_tz.is_floating() && !default_tz.is_floating() {
end_tz = default_tz;
}
if instances.len() > bytes_read {
let match_range = base_expansion_id..base_expansion_id + offset_or_count;
let mut match_count = expansion_ids
.iter()
.filter(|id| match_range.contains(id))
.count();
let mut expansion_id = base_expansion_id;
base_expansion_id += offset_or_count;
if match_count > 0 {
let unpacker = BitpackIterator::from_bytes_and_offset(
instances,
bytes_read,
offset_or_count,
);
for start_offset in unpacker {
if expansion_ids.remove(&expansion_id) {
let start_date_naive = start_offset as i64 + base_offset;
let end_date_naive = start_date_naive + range.duration as i64;
let start = start_tz
.from_local_datetime(
&DateTime::from_timestamp(start_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
let end = end_tz
.from_local_datetime(
&DateTime::from_timestamp(end_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
expansion.push(CalendarEventExpansion {
comp_id: range.id as u32,
expansion_id,
start,
end,
});
match_count -= 1;
if match_count == 0 {
if expansion_ids.is_empty() {
break 'outer;
} else {
continue 'outer;
}
}
}
expansion_id += 1;
}
}
} else {
if expansion_ids.remove(&base_expansion_id) {
// Single event
let start_date_naive = offset_or_count as i64 + base_offset;
let end_date_naive = start_date_naive + range.duration as i64;
let start = start_tz
.from_local_datetime(
&DateTime::from_timestamp(start_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
let end = end_tz
.from_local_datetime(
&DateTime::from_timestamp(end_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
expansion.push(CalendarEventExpansion {
comp_id: range.id as u32,
expansion_id: base_expansion_id,
start,
end,
});
if expansion_ids.is_empty() {
break 'outer;
}
}
base_expansion_id += 1;
}
}
if !expansion_ids.is_empty() {
expansion.extend(
expansion_ids
.drain()
.map(|expansion_id| CalendarEventExpansion {
comp_id: u32::MAX,
expansion_id,
start: i64::MAX,
end: i64::MAX,
}),
);
}
Some(expansion)
}
pub fn expand_single(&self, comp_id: u32, default_tz: Tz) -> Option<CalendarEventExpansion> {
let range = self.time_ranges.iter().find(|r| r.id as u32 == comp_id)?;
let instances = range.instances.as_ref();
let (offset_or_count, bytes_read) = instances.read_leb128::<u32>()?;
let mut start_tz = Tz::from_id(range.start_tz)?;
let mut end_tz = Tz::from_id(range.end_tz)?;
if start_tz.is_floating() && !default_tz.is_floating() {
start_tz = default_tz;
}
if end_tz.is_floating() && !default_tz.is_floating() {
end_tz = default_tz;
}
let start_offset = if instances.len() > bytes_read {
// Recurring event
let mut unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count);
unpacker.next()?
} else {
// Single event
offset_or_count
};
let start_date_naive = start_offset as i64 + self.base_offset;
let end_date_naive = start_date_naive + range.duration as i64;
let start = start_tz
.from_local_datetime(&DateTime::from_timestamp(start_date_naive, 0)?.naive_local())
.single()?
.timestamp();
let end = end_tz
.from_local_datetime(&DateTime::from_timestamp(end_date_naive, 0)?.naive_local())
.single()?
.timestamp();
Some(CalendarEventExpansion {
comp_id,
expansion_id: u32::MAX,
start,
end,
})
}
}
impl Default for CalendarEventExpansion {
fn default() -> Self {
Self {
comp_id: u32::MAX,
expansion_id: u32::MAX,
start: i64::MAX,
end: i64::MAX,
}
}
}
impl CalendarEventExpansion {
pub fn is_valid(&self) -> bool {
self.comp_id != u32::MAX && self.start != i64::MAX && self.end != i64::MAX
}
}

View File

@@ -224,7 +224,11 @@ impl ItipIngest for Server {
// Build event for schedule inbox
let itip_document_id = self
.store()
.assign_document_ids(account_id, Collection::CalendarEventNotification, 1)
.assign_document_ids(
account_id,
Collection::CalendarEventNotification,
1,
)
.await
.caused_by(trc::location!())?;
let itip_message = CalendarEventNotification {
@@ -412,14 +416,8 @@ impl ItipIngest for Server {
'outer: for entry in &mut component.entries {
if entry.name == ICalendarProperty::Attendee
&& entry
.values
.first()
.and_then(|v| v.as_text())
.is_some_and(|v| {
v.strip_prefix("mailto:")
.unwrap_or(v)
.eq_ignore_ascii_case(&rsvp.attendee)
})
.calendar_address()
.is_some_and(|v| v.eq_ignore_ascii_case(&rsvp.attendee))
{
let mut add_partstat = true;
for param in &mut entry.params {

View File

@@ -11,7 +11,7 @@ pub mod index;
pub mod itip;
pub mod storage;
use calcard::icalendar::{ICalendar, ICalendarDuration};
use calcard::icalendar::{ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry};
use common::{DavName, auth::AccessToken};
use dav_proto::schema::request::DeadProperty;
use types::acl::AclGrant;
@@ -29,10 +29,9 @@ pub struct Calendar {
}
pub const CALENDAR_SUBSCRIBED: u16 = 1;
pub const CALENDAR_DEFAULT: u16 = 1 << 1;
pub const CALENDAR_VISIBLE: u16 = 1 << 2;
pub const CALENDAR_AVAILABILITY_ALL: u16 = 1 << 3;
pub const CALENDAR_AVAILABILITY_ATTENDING: u16 = 1 << 4;
pub const CALENDAR_INVISIBLE: u16 = 1 << 1;
pub const CALENDAR_AVAILABILITY_NONE: u16 = 1 << 2;
pub const CALENDAR_AVAILABILITY_ATTENDING: u16 = 1 << 3;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
@@ -52,7 +51,6 @@ pub struct CalendarPreferences {
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct DefaultAlert {
pub account_id: u32,
pub id: String,
pub offset: ICalendarDuration,
pub flags: u16,
@@ -71,6 +69,8 @@ pub const EVENT_HIDE_ATTENDEES: u16 = 1 << 2;
pub const EVENT_DRAFT: u16 = 1 << 3;
pub const EVENT_ORIGIN: u16 = 1 << 4;
pub const PREF_USE_DEFAULT_ALERTS: u16 = 1;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
@@ -78,7 +78,7 @@ pub struct CalendarEvent {
pub names: Vec<DavName>,
pub display_name: Option<String>,
pub data: CalendarEventData,
pub user_properties: Vec<UserProperties>,
pub preferences: Vec<EventPreferences>,
pub flags: u16,
pub dead_properties: DeadProperty,
pub size: u32,
@@ -143,9 +143,11 @@ pub struct ComponentTimeRange {
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct UserProperties {
pub struct EventPreferences {
pub account_id: u32,
pub properties: ICalendar,
pub flags: u16,
pub properties: Vec<ICalendarEntry>,
pub alerts: Vec<ICalendarComponent>,
}
#[derive(
@@ -192,6 +194,17 @@ impl Calendar {
}
impl ArchivedCalendar {
pub fn default_alerts(
&self,
access_token: &AccessToken,
with_time: bool,
) -> impl Iterator<Item = &ArchivedDefaultAlert> {
self.preferences(access_token)
.default_alerts
.iter()
.filter(move |a| (a.flags & ALERT_WITH_TIME != 0) == with_time)
}
pub fn preferences(&self, access_token: &AccessToken) -> &ArchivedCalendarPreferences {
if self.preferences.len() == 1 {
&self.preferences[0]
@@ -205,3 +218,71 @@ impl ArchivedCalendar {
}
}
}
impl CalendarEvent {
pub fn preferences(&self, access_token: &AccessToken) -> Option<&EventPreferences> {
self.preferences
.iter()
.find(|p| p.account_id == access_token.primary_id())
}
pub fn preferences_mut(&mut self, access_token: &AccessToken) -> &mut EventPreferences {
let account_id = access_token.primary_id();
let idx = if let Some(idx) = self
.preferences
.iter()
.position(|p| p.account_id == account_id)
{
idx
} else {
self.preferences.push(EventPreferences {
account_id,
flags: PREF_USE_DEFAULT_ALERTS,
properties: Vec::new(),
alerts: Vec::new(),
});
self.preferences.len() - 1
};
&mut self.preferences[idx]
}
pub fn added_calendar_ids(
&self,
prev_data: &ArchivedCalendarEvent,
) -> impl Iterator<Item = u32> {
self.names
.iter()
.filter(|m| prev_data.names.iter().all(|pm| pm.parent_id != m.parent_id))
.map(|m| m.parent_id)
}
pub fn removed_calendar_ids(
&self,
prev_data: &ArchivedCalendarEvent,
) -> impl Iterator<Item = u32> {
prev_data
.names
.iter()
.filter(|m| self.names.iter().all(|pm| pm.parent_id != m.parent_id))
.map(|m| m.parent_id.to_native())
}
pub fn unchanged_calendar_ids(
&self,
prev_data: &ArchivedCalendarEvent,
) -> impl Iterator<Item = u32> {
self.names
.iter()
.filter(|m| prev_data.names.iter().any(|pm| pm.parent_id == m.parent_id))
.map(|m| m.parent_id)
}
}
impl ArchivedCalendarEvent {
pub fn preferences(&self, access_token: &AccessToken) -> Option<&ArchivedEventPreferences> {
self.preferences
.iter()
.find(|p| p.account_id == access_token.primary_id())
}
}

View File

@@ -351,24 +351,23 @@ impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
send_itip: bool,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let event = self.0;
if let Some(delete_idx) = event
if let Some(delete_idx) = self
.0
.inner
.names
.iter()
.position(|name| name.parent_id == calendar_id)
{
batch
.with_account_id(account_id)
.with_collection(Collection::CalendarEvent);
if event.inner.names.len() > 1 {
if self.0.inner.names.len() > 1 {
// Unlink calendar id from event
let event = self.0;
let mut new_event = event
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?;
new_event.names.swap_remove(delete_idx);
batch
.with_account_id(account_id)
.with_collection(Collection::CalendarEvent)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
@@ -378,40 +377,7 @@ impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
)
.caused_by(trc::location!())?;
} else {
// Delete event
batch.delete_document(document_id);
// Remove next alarm if it exists
let now = now() as i64;
if let Some(next_alarm) = event.inner.data.next_alarm(now, Tz::Floating) {
next_alarm.delete_task(batch);
}
// Scheduling
if send_itip
&& event.inner.schedule_tag.is_some()
&& event.inner.data.event_range_end() > now
{
let event = event
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?;
if let Ok(messages) =
itip_cancel(&event.data.event, access_token.emails.as_slice(), true)
{
ItipMessages::new(vec![messages])
.queue(batch)
.caused_by(trc::location!())?;
}
}
batch
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(event),
)
.caused_by(trc::location!())?;
self.delete_all(access_token, account_id, document_id, send_itip, batch)?;
}
if let Some(delete_path) = delete_path {
@@ -423,6 +389,57 @@ impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn delete_all(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
send_itip: bool,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let event = self.0;
// Delete event
batch
.with_account_id(account_id)
.with_collection(Collection::CalendarEvent)
.delete_document(document_id);
// Remove next alarm if it exists
let now = now() as i64;
if let Some(next_alarm) = event.inner.data.next_alarm(now, Tz::Floating) {
next_alarm.delete_task(batch);
}
// Scheduling
if send_itip
&& event.inner.schedule_tag.is_some()
&& event.inner.data.event_range_end() > now
{
let event = event
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?;
if let Ok(messages) =
itip_cancel(&event.data.event, access_token.emails.as_slice(), true)
{
ItipMessages::new(vec![messages])
.queue(batch)
.caused_by(trc::location!())?;
}
}
batch
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(event),
)
.caused_by(trc::location!())?;
Ok(())
}
}
impl DestroyArchive<Archive<&ArchivedCalendarEventNotification>> {

View File

@@ -362,6 +362,28 @@ impl ItipError {
_ => None,
}
}
pub fn is_jmap_error(&self) -> bool {
matches!(
self,
ItipError::MultipleOrganizer
| ItipError::OrganizerIsLocalAddress
| ItipError::SenderIsNotParticipant(_)
| ItipError::OrganizerMismatch
| ItipError::CannotModifyProperty(_)
| ItipError::CannotModifyInstance
| ItipError::CannotModifyAddress
//| ItipError::MissingUid
| ItipError::MultipleUid
| ItipError::MultipleObjectTypes
| ItipError::MultipleObjectInstances
| ItipError::MissingMethod
| ItipError::InvalidComponentType
| ItipError::OutOfSequence
| ItipError::UnknownParticipant(_)
| ItipError::UnsupportedMethod(_)
)
}
}
impl Display for ItipError {