CalDAV basic implementation

This commit is contained in:
mdecimus
2025-04-20 12:17:21 +02:00
parent 10ae19f2eb
commit ee7c953279
32 changed files with 4241 additions and 837 deletions

View File

@@ -0,0 +1,185 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::storage::index::{
IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject,
};
use jmap_proto::types::{collection::Collection, value::AclGrant};
use store::SerializeInfallible;
use crate::{IDX_NAME, IDX_UID};
use super::{
ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert,
ArchivedTimezone, Calendar, CalendarEvent, CalendarPreferences, DefaultAlert, Timezone,
};
impl IndexableObject for Calendar {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
// Note: When adding a new value with index id above 0u8, tune `build_hierarchy`` to skip
// this value during iteration.
[
IndexValue::Index {
field: IDX_NAME,
value: self.name.as_str().into(),
},
IndexValue::Acl {
value: (&self.acls).into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.preferences.iter().map(|p| p.size()).sum::<usize>() as u32
+ self.default_alerts.iter().map(|a| a.size()).sum::<usize>() as u32
+ self.name.len() as u32,
},
IndexValue::LogChild { prefix: None },
]
.into_iter()
}
}
impl IndexableObject for &ArchivedCalendar {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Index {
field: IDX_NAME,
value: self.name.as_str().into(),
},
IndexValue::Acl {
value: self
.acls
.iter()
.map(AclGrant::from)
.collect::<Vec<_>>()
.into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
+ self.preferences.iter().map(|p| p.size()).sum::<usize>() as u32
+ self.default_alerts.iter().map(|a| a.size()).sum::<usize>() as u32
+ self.name.len() as u32,
},
IndexValue::LogChild { prefix: None },
]
.into_iter()
}
}
impl IndexableAndSerializableObject for Calendar {}
impl IndexableObject for CalendarEvent {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::IndexList {
field: IDX_NAME,
value: self
.names
.iter()
.map(|v| IndexItem::Vec(v.serialize()))
.collect::<Vec<_>>(),
},
IndexValue::Index {
field: IDX_UID,
value: self.event.uids().next().into(),
},
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.size,
},
IndexValue::LogChild { prefix: None },
IndexValue::LogParent {
collection: Collection::Calendar.into(),
ids: self.names.iter().map(|n| n.parent_id).collect(),
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedCalendarEvent {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::IndexList {
field: IDX_NAME,
value: self
.names
.iter()
.map(|v| IndexItem::Vec(v.serialize()))
.collect::<Vec<_>>(),
},
IndexValue::Index {
field: IDX_UID,
value: self.event.uids().next().into(),
},
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.size,
},
IndexValue::LogChild { prefix: None },
IndexValue::LogParent {
collection: Collection::Calendar.into(),
ids: self.names.iter().map(|n| n.parent_id.to_native()).collect(),
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for CalendarEvent {}
impl CalendarPreferences {
pub fn size(&self) -> usize {
self.name.len()
+ self.description.as_ref().map_or(0, |n| n.len())
+ self.color.as_ref().map_or(0, |n| n.len())
+ self.time_zone.size()
}
}
impl ArchivedCalendarPreferences {
pub fn size(&self) -> usize {
self.name.len()
+ self.description.as_ref().map_or(0, |n| n.len())
+ self.color.as_ref().map_or(0, |n| n.len())
+ self.time_zone.size()
}
}
impl Timezone {
pub fn size(&self) -> usize {
match self {
Timezone::IANA(s) => s.len(),
Timezone::Custom(c) => c.size(),
Timezone::Default => 0,
}
}
}
impl ArchivedTimezone {
pub fn size(&self) -> usize {
match self {
ArchivedTimezone::IANA(s) => s.len(),
ArchivedTimezone::Custom(c) => c.size(),
ArchivedTimezone::Default => 0,
}
}
}
impl DefaultAlert {
pub fn size(&self) -> usize {
self.alert.size() + self.id.len()
}
}
impl ArchivedDefaultAlert {
pub fn size(&self) -> usize {
self.alert.size() + self.id.len()
}
}

View File

@@ -4,46 +4,63 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::collections::HashMap;
use calcard::icalendar::ICalendar;
use dav_proto::schema::request::DeadProperty;
use jmap_proto::types::{acl::Acl, value::AclGrant};
use store::{SERIALIZE_CALENDAR_V1, SERIALIZE_CALENDAREVENT_V1, SerializedVersion, ahash};
use utils::map::vec_map::VecMap;
pub mod index;
pub mod storage;
use crate::DavName;
use calcard::icalendar::ICalendar;
use dav_proto::schema::request::DeadProperty;
use jmap_proto::types::{acl::Acl, value::AclGrant};
use store::{SERIALIZE_CALENDAR_V1, SERIALIZE_CALENDAREVENT_V1, SerializedVersion};
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct Calendar {
pub name: String,
pub preferences: HashMap<u32, CalendarPreferences, ahash::RandomState>,
pub preferences: Vec<CalendarPreferences>,
pub default_alerts: Vec<DefaultAlert>,
pub acls: Vec<AclGrant>,
pub dead_properties: DeadProperty,
pub created: i64,
pub modified: i64,
}
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;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarPreferences {
pub account_id: u32,
pub name: String,
pub description: Option<String>,
pub sort_order: u32,
pub color: Option<String>,
pub is_subscribed: bool,
pub is_default: bool,
pub is_visible: bool,
pub include_in_availability: IncludeInAvailability,
pub default_alerts_with_time: HashMap<String, ICalendar, ahash::RandomState>,
pub default_alerts_without_time: HashMap<String, ICalendar, ahash::RandomState>,
pub flags: u16,
pub time_zone: Timezone,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct DefaultAlert {
pub account_id: u32,
pub id: String,
pub alert: ICalendar,
pub with_time: bool,
}
pub const EVENT_INVITE_SELF: u16 = 1;
pub const EVENT_INVITE_OTHERS: u16 = 1 << 1;
pub const EVENT_HIDE_ATTENDEES: u16 = 1 << 2;
pub const EVENT_DRAFT: u16 = 1 << 3;
pub const EVENT_ORIGIN: u16 = 1 << 4;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
@@ -51,17 +68,22 @@ pub struct CalendarEvent {
pub names: Vec<DavName>,
pub display_name: Option<String>,
pub event: ICalendar,
pub user_properties: VecMap<u32, ICalendar>,
pub may_invite_self: bool,
pub may_invite_others: bool,
pub hide_attendees: bool,
pub is_draft: bool,
pub user_properties: Vec<UserProperties>,
pub flags: u16,
pub dead_properties: DeadProperty,
pub size: u32,
pub created: i64,
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,
)]
@@ -72,17 +94,6 @@ pub enum Timezone {
Default,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub enum IncludeInAvailability {
All,
Attending,
#[default]
None,
}
pub enum CalendarRight {
ReadFreeBusy,
ReadItems,
@@ -139,14 +150,43 @@ impl SerializedVersion for CalendarEvent {
}
}
impl ArchivedCalendar {
pub fn preferences(&self, account_id: u32) -> Option<&ArchivedCalendarPreferences> {
impl Calendar {
pub fn preferences(&self, account_id: u32) -> &CalendarPreferences {
if self.preferences.len() == 1 {
self.preferences.values().next()
&self.preferences[0]
} else {
self.preferences
.get(&rkyv::rend::u32_le::from_native(account_id))
.or_else(|| self.preferences.values().next())
.iter()
.find(|p| p.account_id == account_id)
.or_else(|| self.preferences.first())
.unwrap()
}
}
pub fn preferences_mut(&mut self, account_id: u32) -> &mut CalendarPreferences {
if self.preferences.len() == 1 {
&mut self.preferences[0]
} else {
let idx = self
.preferences
.iter()
.position(|p| p.account_id == account_id)
.unwrap_or(0);
&mut self.preferences[idx]
}
}
}
impl ArchivedCalendar {
pub fn preferences(&self, account_id: u32) -> &ArchivedCalendarPreferences {
if self.preferences.len() == 1 {
&self.preferences[0]
} else {
self.preferences
.iter()
.find(|p| p.account_id == account_id)
.or_else(|| self.preferences.first())
.unwrap()
}
}
}

View File

@@ -0,0 +1,246 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::DestroyArchive;
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use jmap_proto::types::collection::Collection;
use store::write::{Archive, BatchBuilder, now};
use trc::AddContext;
use super::{
ArchivedCalendar, ArchivedCalendarEvent, Calendar, CalendarEvent, CalendarPreferences,
};
impl CalendarEvent {
pub fn update<'x>(
self,
access_token: &AccessToken,
event: Archive<&ArchivedCalendarEvent>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
let mut new_event = self;
// Build event
new_event.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::CalendarEvent)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(event)
.with_changes(new_event)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
pub fn insert<'x>(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build event
let mut event = self;
let now = now() as i64;
event.modified = now;
event.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::CalendarEvent)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(event)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
}
impl Calendar {
pub fn insert<'x>(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build address calendar
let mut calendar = self;
let now = now() as i64;
calendar.modified = now;
calendar.created = now;
if calendar.preferences.is_empty() {
calendar.preferences.push(CalendarPreferences {
account_id,
name: "default".to_string(),
..Default::default()
});
}
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::Calendar)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(calendar)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
pub fn update<'x>(
self,
access_token: &AccessToken,
calendar: Archive<&ArchivedCalendar>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build address calendar
let mut new_calendar = self;
new_calendar.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::Calendar)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(calendar)
.with_changes(new_calendar)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
}
impl DestroyArchive<Archive<&ArchivedCalendar>> {
pub async fn delete_with_events(
self,
server: &Server,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
children_ids: Vec<u32>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Process deletions
let calendar_id = document_id;
for document_id in children_ids {
if let Some(event_) = server
.get_archive(account_id, Collection::CalendarEvent, document_id)
.await?
{
DestroyArchive(
event_
.to_unarchived::<CalendarEvent>()
.caused_by(trc::location!())?,
)
.delete(
access_token,
account_id,
document_id,
calendar_id,
batch,
)?;
}
}
self.delete(access_token, account_id, document_id, batch)
}
pub fn delete(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let calendar = self.0;
// Delete calendar
batch
.with_account_id(account_id)
.with_collection(Collection::Calendar)
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(calendar),
)
.caused_by(trc::location!())?
.commit_point();
Ok(())
}
}
impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
pub fn delete(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
calendar_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let event = self.0;
if let Some(delete_idx) = event
.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 {
// Unlink calendar id from event
let mut new_event = event
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?;
new_event.names.swap_remove(delete_idx);
batch
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_tenant_id(access_token)
.with_current(event)
.with_changes(new_event),
)
.caused_by(trc::location!())?;
} else {
// Delete event
batch
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(event),
)
.caused_by(trc::location!())?;
}
batch.commit_point();
}
Ok(())
}
}

View File

@@ -10,7 +10,7 @@ use common::storage::index::{
use jmap_proto::types::{collection::Collection, value::AclGrant};
use store::SerializeInfallible;
use crate::{IDX_CARD_UID, IDX_NAME};
use crate::{IDX_NAME, IDX_UID};
use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard};
@@ -79,7 +79,7 @@ impl IndexableObject for ContactCard {
.collect::<Vec<_>>(),
},
IndexValue::Index {
field: IDX_CARD_UID,
field: IDX_UID,
value: self.card.uid().into(),
},
IndexValue::Quota {
@@ -110,7 +110,7 @@ impl IndexableObject for &ArchivedContactCard {
.collect::<Vec<_>>(),
},
IndexValue::Index {
field: IDX_CARD_UID,
field: IDX_UID,
value: self.card.uid().into(),
},
IndexValue::Quota {

View File

@@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use jmap_proto::types::collection::Collection;
use store::write::{Archive, BatchBuilder, now};

View File

@@ -18,7 +18,12 @@ use store::{
use trc::AddContext;
use utils::bimap::IdBimap;
use crate::{DavName, DavResourceName, IDX_NAME, contact::AddressBook, file::FileNode};
use crate::{
DavName, DavResourceName, IDX_NAME,
calendar::{Calendar, CalendarPreferences},
contact::AddressBook,
file::FileNode,
};
pub trait DavHierarchy: Sync + Send {
fn fetch_dav_resources(
@@ -127,7 +132,27 @@ impl DavHierarchy for Server {
access_token: &AccessToken,
account_id: u32,
) -> trc::Result<()> {
todo!()
if let Some(name) = &self.core.dav.default_calendar_name {
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::Calendar, 1)
.await?;
Calendar {
name: name.clone(),
preferences: vec![CalendarPreferences {
account_id,
name: name.clone(),
description: self.core.dav.default_calendar_display_name.clone(),
..Default::default()
}],
..Default::default()
}
.insert(access_token, account_id, document_id, &mut batch)?;
self.commit_batch(batch).await?;
}
Ok(())
}
}

View File

@@ -14,7 +14,7 @@ pub mod file;
pub mod hierarchy;
pub const IDX_NAME: u8 = 0;
pub const IDX_CARD_UID: u8 = 1;
pub const IDX_UID: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DavResourceName {