/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use super::GroupwareCache; use crate::{ DavResourceName, RFC_3986, calendar::{ ArchivedCalendar, ArchivedCalendarEvent, Calendar, CalendarEvent, SCHEDULE_INBOX_ID, SCHEDULE_OUTBOX_ID, storage::ItipAutoExpunge, }, contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard}, encode_path_segment, }; use calcard::common::timezone::Tz; use common::{ DavName, DavPath, DavResource, DavResourceMetadata, DavResources, Server, TinyCalendarPreferences, UpdateLock, }; use std::sync::Arc; use store::ahash::{AHashMap, AHashSet}; use trc::AddContext; use types::{ acl::AclGrant, collection::{Collection, SyncCollection}, }; use utils::map::bitmap::Bitmap; pub(super) async fn build_calcard_resources( server: &Server, access_account_id: u32, account_id: u32, sync_collection: SyncCollection, container_collection: Collection, item_collection: Collection, update_lock: Arc, ) -> trc::Result { let is_calendar = matches!(sync_collection, SyncCollection::Calendar); let owner_account_info = server.account(account_id).await?; let access_account_info = if account_id == access_account_id { owner_account_info.clone() } else { server.account(access_account_id).await? }; let mut cache = DavResources { base_path: format!( "{}/{}/", if is_calendar { DavResourceName::Cal } else { DavResourceName::Card } .base_path(), percent_encoding::utf8_percent_encode(owner_account_info.name(), RFC_3986), ), paths: AHashSet::with_capacity(16), resources: Vec::with_capacity(16), item_change_id: 0, container_change_id: 0, highest_change_id: 0, size: std::mem::size_of::() as u64, update_lock, }; let mut is_first_check = true; loop { let last_change_id = server .core .storage .data .get_last_change_id(account_id, sync_collection.into()) .await .caused_by(trc::location!())? .unwrap_or_default(); cache.item_change_id = last_change_id; cache.container_change_id = last_change_id; cache.highest_change_id = last_change_id; cache.update_lock.set_revision(last_change_id); server .archives( account_id, container_collection, &(), |document_id, archive| { let resource = if is_calendar { resource_from_calendar(archive.unarchive::()?, document_id) } else { resource_from_addressbook(archive.unarchive::()?, document_id) }; let path = DavPath { path: encode_path_segment(resource.container_name().unwrap()).into_owned(), parent_id: None, hierarchy_seq: 1, resource_idx: cache.resources.len(), }; cache.size += (std::mem::size_of::() + std::mem::size_of::() + (path.path.len()) * 2) as u64; cache.paths.insert(path); cache.resources.push(resource); Ok(true) }, ) .await .caused_by(trc::location!())?; if cache.paths.is_empty() { if is_first_check { if is_calendar { server .create_default_calendar(&access_account_info, &owner_account_info) .await?; } else { server .create_default_addressbook(&access_account_info, &owner_account_info) .await?; } is_first_check = false; continue; } else { return Ok(cache); } } let parent_range = cache.resources.len(); server .archives(account_id, item_collection, &(), |document_id, archive| { let resource = if is_calendar { resource_from_event(archive.unarchive::()?, document_id) } else { resource_from_card(archive.unarchive::()?, document_id) }; let resource_idx = cache.resources.len(); for name in resource.child_names().unwrap_or_default().iter() { if let Some(parent) = cache.resources.get(..parent_range).and_then(|resources| { resources.iter().find(|r| r.document_id == name.parent_id) }) { let path = DavPath { path: format!( "{}/{}", encode_path_segment(parent.container_name().unwrap()), encode_path_segment(&name.name) ), parent_id: Some(name.parent_id), hierarchy_seq: 0, resource_idx, }; cache.size += (std::mem::size_of::() + name.name.len() + path.path.len()) as u64; cache.paths.insert(path); } } cache.size += std::mem::size_of::() as u64; cache.resources.push(resource); Ok(true) }) .await .caused_by(trc::location!())?; return Ok(cache); } } pub(super) async fn build_scheduling_resources( server: &Server, account_id: u32, update_lock: Arc, ) -> trc::Result { let last_change_id = server .core .storage .data .get_last_change_id(account_id, SyncCollection::CalendarEventNotification.into()) .await .caused_by(trc::location!())? .unwrap_or_default(); let account_info = server.account(account_id).await?; let item_ids = server .itip_ids(account_id) .await .caused_by(trc::location!())?; update_lock.set_revision(last_change_id); let mut cache = DavResources { base_path: format!( "{}/{}/", DavResourceName::Scheduling.base_path(), percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986), ), paths: AHashSet::with_capacity((2 + item_ids.len()) as usize), resources: Vec::with_capacity((2 + item_ids.len()) as usize), item_change_id: last_change_id, container_change_id: last_change_id, highest_change_id: last_change_id, size: std::mem::size_of::() as u64, update_lock, }; for (document_id, is_container) in item_ids .into_iter() .map(|document_id| (document_id, false)) .chain([(SCHEDULE_INBOX_ID, true), (SCHEDULE_OUTBOX_ID, true)]) { let path = path_from_scheduling(document_id, cache.resources.len(), is_container); cache.size += (std::mem::size_of::() + (path.path.len() * 2)) as u64 + std::mem::size_of::() as u64; cache.paths.insert(path); cache .resources .push(resource_from_scheduling(document_id, is_container)); } Ok(cache) } pub(super) fn build_simple_hierarchy(cache: &mut DavResources) { cache.paths = AHashSet::with_capacity(cache.resources.len()); let name_idx = cache .resources .iter() .filter_map(|resource| { resource .container_name() .map(|name| (resource.document_id, name)) }) .collect::>(); for (resource_idx, resource) in cache.resources.iter().enumerate() { match &resource.data { DavResourceMetadata::Calendar { name, .. } | DavResourceMetadata::AddressBook { name, .. } => { let path = DavPath { path: encode_path_segment(name).into_owned(), parent_id: None, hierarchy_seq: 1, resource_idx, }; cache.size += (std::mem::size_of::() + name.len() + path.path.len()) as u64; cache.paths.insert(path); } DavResourceMetadata::CalendarEvent { names, .. } | DavResourceMetadata::ContactCard { names } => { for name in names { if let Some(parent_name) = name_idx.get(&name.parent_id) { let path = DavPath { path: format!( "{}/{}", encode_path_segment(parent_name), encode_path_segment(&name.name) ), parent_id: Some(name.parent_id), hierarchy_seq: 0, resource_idx, }; cache.size += (std::mem::size_of::() + name.name.len() + path.path.len()) as u64; cache.paths.insert(path); } } } _ => unreachable!(), } cache.size += std::mem::size_of::() as u64; } } pub(super) fn resource_from_calendar(calendar: &ArchivedCalendar, document_id: u32) -> DavResource { DavResource { document_id, data: DavResourceMetadata::Calendar { name: calendar.name.to_string(), acls: calendar .acls .iter() .map(|acl| AclGrant { account_id: acl.account_id.to_native(), grants: Bitmap::from(&acl.grants), }) .collect(), preferences: calendar .preferences .iter() .map(|pref| TinyCalendarPreferences { account_id: pref.account_id.to_native(), flags: pref.flags.to_native(), tz: pref.time_zone.tz().unwrap_or(Tz::UTC), }) .collect(), }, } } pub(super) fn resource_from_event(event: &ArchivedCalendarEvent, document_id: u32) -> DavResource { let (start, duration) = event.data.event_range().unwrap_or_default(); DavResource { document_id, data: DavResourceMetadata::CalendarEvent { names: event .names .iter() .map(|name| DavName { name: name.name.to_string(), parent_id: name.parent_id.to_native(), }) .collect(), start, duration, }, } } pub(super) fn resource_from_scheduling(document_id: u32, is_container: bool) -> DavResource { DavResource { document_id, data: DavResourceMetadata::CalendarEventNotification { names: if !is_container { [DavName { name: format!("{document_id}.ics"), parent_id: SCHEDULE_INBOX_ID, }] .into_iter() .collect() } else { Default::default() }, }, } } pub(super) fn path_from_scheduling( document_id: u32, resource_idx: usize, is_container: bool, ) -> DavPath { if is_container { DavPath { path: if document_id == SCHEDULE_INBOX_ID { "inbox".to_string() } else { "outbox".to_string() }, parent_id: None, hierarchy_seq: 1, resource_idx, } } else { DavPath { path: format!("inbox/{document_id}.ics"), parent_id: Some(SCHEDULE_INBOX_ID), hierarchy_seq: 0, resource_idx, } } } pub(super) fn resource_from_addressbook( book: &ArchivedAddressBook, document_id: u32, ) -> DavResource { DavResource { document_id, data: DavResourceMetadata::AddressBook { name: book.name.to_string(), acls: book .acls .iter() .map(|acl| AclGrant { account_id: acl.account_id.to_native(), grants: Bitmap::from(&acl.grants), }) .collect(), }, } } pub(super) fn resource_from_card(card: &ArchivedContactCard, document_id: u32) -> DavResource { DavResource { document_id, data: DavResourceMetadata::ContactCard { names: card .names .iter() .map(|name| DavName { name: name.name.to_string(), parent_id: name.parent_id.to_native(), }) .collect(), }, } }