Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "groupware"
version = "0.16.22"
edition = "2024"
[dependencies]
utils = { path = "../utils" }
store = { path = "../store" }
common = { path = "../common" }
types = { path = "../types" }
trc = { path = "../trc" }
nlp = { path = "../nlp" }
registry = { path = "../registry" }
calcard = { version = "0.3", features = ["rkyv"] }
hashify = "0.2"
rkyv = { version = "0.8.18", features = ["little_endian"] }
percent-encoding = "2.3.2"
compact_str = "0.10.0"
ahash = { version = "0.8" }
indexmap = "2.14.1"
chrono = "0.4.45"
icu_locale_core = "2"
icu_provider = { version = "2", features = ["sync"] }
icu_plurals = "2"
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"
icu_datetime = { version = "~2.3", features = ["unstable_chrono_0_4"] }
[features]
test_mode = []
enterprise = []
[dev-dependencies]
[lints]
workspace = true
+404
View File
@@ -0,0 +1,404 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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<UpdateLock>,
) -> trc::Result<DavResources> {
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::<DavResources>() 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::<Calendar>()?, document_id)
} else {
resource_from_addressbook(archive.unarchive::<AddressBook>()?, 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::<DavPath>()
+ std::mem::size_of::<DavResource>()
+ (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::<CalendarEvent>()?, document_id)
} else {
resource_from_card(archive.unarchive::<ContactCard>()?, 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::<DavPath>()
+ name.name.len()
+ path.path.len()) as u64;
cache.paths.insert(path);
}
}
cache.size += std::mem::size_of::<DavResource>() 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<UpdateLock>,
) -> trc::Result<DavResources> {
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::<DavResources>() 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::<DavPath>() + (path.path.len() * 2)) as u64
+ std::mem::size_of::<DavResource>() 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::<AHashMap<_, _>>();
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::<DavPath>() + 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::<DavPath>()
+ name.name.len()
+ path.path.len()) as u64;
cache.paths.insert(path);
}
}
}
_ => unreachable!(),
}
cache.size += std::mem::size_of::<DavResource>() 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(),
},
}
}
+281
View File
@@ -0,0 +1,281 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavResourceName, RFC_3986, encode_path_segment,
file::{ArchivedFileNode, FileNode},
};
use common::{DavPath, DavResource, DavResourceMetadata, DavResources, Server, 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, topological::TopologicalSort};
pub(super) async fn build_file_resources(
server: &Server,
account_id: u32,
update_lock: Arc<UpdateLock>,
) -> trc::Result<DavResources> {
let last_change_id = server
.core
.storage
.data
.get_last_change_id(account_id, SyncCollection::FileNode.into())
.await
.caused_by(trc::location!())?
.unwrap_or_default();
let account_info = server.account(account_id).await?;
let mut resources = Vec::with_capacity(16);
server
.archives(
account_id,
Collection::FileNode,
&(),
|document_id, archive| {
resources.push(resource_from_file(
archive.unarchive::<FileNode>()?,
document_id,
));
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
update_lock.set_revision(last_change_id);
let mut files = DavResources {
base_path: format!(
"{}/{}/",
DavResourceName::File.base_path(),
percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986),
),
size: std::mem::size_of::<DavResources>() as u64,
paths: AHashSet::with_capacity(resources.len()),
resources,
item_change_id: last_change_id,
container_change_id: last_change_id,
highest_change_id: last_change_id,
update_lock,
};
build_nested_hierarchy(&mut files);
Ok(files)
}
pub(super) fn build_nested_hierarchy(resources: &mut DavResources) {
let mut topological_sort = TopologicalSort::with_capacity(resources.resources.len());
let mut names = AHashMap::with_capacity(resources.resources.len());
for (resource_idx, resource) in resources.resources.iter().enumerate() {
if let DavResourceMetadata::File { parent_id, .. } = resource.data {
topological_sort.insert(
parent_id.map(|id| id + 1).unwrap_or_default(),
resource.document_id + 1,
);
names.insert(
resource.document_id,
DavPath {
path: encode_path_segment(resource.container_name().unwrap()).into_owned(),
parent_id,
hierarchy_seq: 0,
resource_idx,
},
);
}
}
for (hierarchy_sequence, folder_id) in topological_sort.into_iterator().enumerate() {
if folder_id != 0 {
let folder_id = folder_id - 1;
let path = names
.get(&folder_id)
.and_then(|folder| folder.parent_id.map(|parent_id| (&folder.path, parent_id)))
.and_then(|(name, parent_id)| {
names
.get(&parent_id)
.map(|parent| format!("{}/{}", parent.path, name))
});
if let Some(folder) = names.get_mut(&folder_id) {
if let Some(path) = path {
folder.path = path;
}
folder.hierarchy_seq = hierarchy_sequence as u32;
}
}
}
resources.paths = names
.into_values()
.inspect(|v| {
resources.size += (std::mem::size_of::<DavPath>()
+ std::mem::size_of::<u32>()
+ std::mem::size_of::<usize>()
+ std::mem::size_of::<DavResource>()
+ v.path.len()) as u64;
})
.collect();
}
pub(super) fn resource_from_file(node: &ArchivedFileNode, document_id: u32) -> DavResource {
let parent_id = node.parent_id.to_native();
DavResource {
document_id,
data: DavResourceMetadata::File {
name: node.name.as_str().to_string(),
size: node.file.as_ref().map(|f| f.size.to_native()),
parent_id: if parent_id > 0 {
Some(parent_id - 1)
} else {
None
},
acls: node
.acls
.iter()
.map(|acl| AclGrant {
account_id: acl.account_id.to_native(),
grants: Bitmap::from(&acl.grants),
})
.collect(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
const MISSING_DOCUMENT_ID: u32 = 9;
fn folder(document_id: u32, name: &str, parent_id: Option<u32>) -> DavResource {
DavResource {
document_id,
data: DavResourceMetadata::File {
name: name.to_string(),
size: None,
parent_id,
acls: Default::default(),
},
}
}
fn file(document_id: u32, name: &str, parent_id: Option<u32>) -> DavResource {
DavResource {
document_id,
data: DavResourceMetadata::File {
name: name.to_string(),
size: Some(1024),
parent_id,
acls: Default::default(),
},
}
}
fn build(resources: Vec<DavResource>) -> DavResources {
let mut files = DavResources {
base_path: "/dav/file/john/".to_string(),
paths: AHashSet::with_capacity(resources.len()),
resources,
item_change_id: 0,
container_change_id: 0,
highest_change_id: 0,
size: 0,
update_lock: Arc::new(UpdateLock::new()),
};
build_nested_hierarchy(&mut files);
files
}
fn sorted_paths(files: &DavResources) -> Vec<&str> {
let mut paths = files
.paths
.iter()
.map(|path| path.path.as_str())
.collect::<Vec<_>>();
paths.sort_unstable();
paths
}
fn hierarchy_seq(files: &DavResources, path: &str) -> u32 {
files.paths.get(path).expect(path).hierarchy_seq
}
#[test]
fn nested_hierarchy() {
let files = build(vec![
folder(0, "docs", None),
folder(1, "reports", Some(0)),
file(2, "q1.txt", Some(1)),
file(3, "readme.txt", None),
]);
assert_eq!(
sorted_paths(&files),
["docs", "docs/reports", "docs/reports/q1.txt", "readme.txt"]
);
assert!(hierarchy_seq(&files, "docs") < hierarchy_seq(&files, "docs/reports"));
assert!(
hierarchy_seq(&files, "docs/reports") < hierarchy_seq(&files, "docs/reports/q1.txt")
);
}
#[test]
fn nested_hierarchy_percent_encodes_paths() {
let files = build(vec![
folder(0, "My Documents", None),
folder(1, "Berichte 2026", Some(0)),
file(2, "Ünterlagen Q1.txt", Some(1)),
folder(3, "My%20Folder", None),
file(4, "file(1)+a:b.txt", Some(3)),
]);
assert_eq!(
sorted_paths(&files),
[
"My%20Documents",
"My%20Documents/Berichte%202026",
"My%20Documents/Berichte%202026/%C3%9Cnterlagen%20Q1.txt",
"My%20Folder",
"My%20Folder/file(1)+a:b.txt",
]
);
assert_eq!(
files.format_resource(files.by_path("My%20Documents").unwrap()),
"/dav/file/john/My%20Documents/"
);
assert_eq!(
files.format_resource(
files
.by_path("My%20Documents/Berichte%202026/%C3%9Cnterlagen%20Q1.txt")
.unwrap()
),
"/dav/file/john/My%20Documents/Berichte%202026/%C3%9Cnterlagen%20Q1.txt"
);
}
#[test]
fn nested_hierarchy_with_dangling_parent() {
let files = build(vec![
folder(0, "docs", None),
folder(1, "reports", Some(MISSING_DOCUMENT_ID)),
file(2, "q1.txt", Some(1)),
file(3, "orphan.txt", Some(MISSING_DOCUMENT_ID)),
]);
assert_eq!(
sorted_paths(&files),
["docs", "orphan.txt", "reports", "reports/q1.txt"]
);
assert!(hierarchy_seq(&files, "reports") < hierarchy_seq(&files, "reports/q1.txt"));
}
}
+653
View File
@@ -0,0 +1,653 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
cache::calcard::{build_scheduling_resources, path_from_scheduling, resource_from_scheduling},
calendar::{CALENDAR_SUBSCRIBED, Calendar, CalendarEvent, CalendarPreferences},
contact::{AddressBook, AddressBookPreferences, ContactCard},
file::FileNode,
};
use ahash::AHashSet;
use calcard::{
build_calcard_resources, build_simple_hierarchy, resource_from_addressbook,
resource_from_calendar, resource_from_card, resource_from_event,
};
use common::{
DavResource, DavResources, Server, UpdateLock, auth::AccountCache, cache::LockResult,
};
use file::{build_file_resources, build_nested_hierarchy, resource_from_file};
use std::{sync::Arc, time::Instant};
use store::{
SerializeInfallible, ValueKey,
ahash::AHashMap,
query::log::{Change, Query},
write::{AlignedBytes, Archive, BatchBuilder, ValueClass},
};
use trc::{AddContext, StoreEvent};
use types::{
collection::{Collection, SyncCollection},
field::PrincipalField,
};
use utils::cache::Cache;
pub mod calcard;
pub mod file;
pub trait GroupwareCache: Sync + Send {
fn fetch_dav_resources(
&self,
access_account_id: u32,
account_id: u32,
collection: SyncCollection,
) -> impl Future<Output = trc::Result<Arc<DavResources>>> + Send;
fn create_default_addressbook(
&self,
account_info_access: &AccountCache,
account_info_owner: &AccountCache,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
fn create_default_calendar(
&self,
account_info_access: &AccountCache,
account_info_owner: &AccountCache,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
fn get_or_create_default_calendar(
&self,
access_account_id: u32,
account_id: u32,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
fn cached_dav_resources(
&self,
account_id: u32,
collection: SyncCollection,
) -> Option<Arc<DavResources>>;
}
impl GroupwareCache for Server {
async fn fetch_dav_resources(
&self,
access_account_id: u32,
account_id: u32,
collection: SyncCollection,
) -> trc::Result<Arc<DavResources>> {
let cache_store = match collection {
SyncCollection::Calendar => &self.inner.cache.events,
SyncCollection::AddressBook => &self.inner.cache.contacts,
SyncCollection::FileNode => &self.inner.cache.files,
SyncCollection::CalendarEventNotification => &self.inner.cache.scheduling,
_ => unreachable!(),
};
let mut cache = match cache_store.get_value_or_guard_async(&account_id).await {
Ok(cache) => cache,
Err(guard) => {
let start_time = Instant::now();
let cache = full_cache_build(
self,
account_id,
collection,
Arc::new(UpdateLock::new()),
access_account_id,
)
.await?;
if guard.insert(cache.clone()).is_err() {
cache_store.update(account_id, cache.clone());
}
warn_if_uncacheable(cache_store, account_id, collection, &cache);
trc::event!(
Store(StoreEvent::CacheMiss),
AccountId = account_id,
Collection = collection.as_str(),
Total = cache.resources.len(),
ChangeId = cache.highest_change_id,
Elapsed = start_time.elapsed(),
);
return Ok(cache);
}
};
// Obtain current state
let start_time = Instant::now();
let changes = self
.core
.storage
.data
.changes(
account_id,
collection.into(),
Query::Since(cache.highest_change_id),
)
.await
.caused_by(trc::location!())?;
// Regenerate cache if the change log has been truncated
if changes.is_truncated {
let cache = full_cache_build(
self,
account_id,
collection,
cache.update_lock.clone(),
access_account_id,
)
.await?;
cache_store.update(account_id, cache.clone());
warn_if_uncacheable(cache_store, account_id, collection, &cache);
trc::event!(
Store(StoreEvent::CacheStale),
AccountId = account_id,
Collection = collection.as_str(),
ChangeId = cache.highest_change_id,
Total = cache.resources.len(),
Elapsed = start_time.elapsed(),
);
return Ok(cache);
}
// Verify changes
if changes.changes.is_empty() {
trc::event!(
Store(StoreEvent::CacheHit),
AccountId = account_id,
Collection = collection.as_str(),
ChangeId = cache.highest_change_id,
Elapsed = start_time.elapsed(),
);
return Ok(cache);
}
// Lock for updates
let lock = cache.update_lock.clone();
let _permit = match lock.acquire(cache.highest_change_id).await? {
LockResult::Acquired(permit) => permit,
LockResult::Stale(permit) => {
cache = cache_store.peek(&account_id).unwrap_or(cache.clone());
if cache.highest_change_id >= changes.to_change_id {
trc::event!(
Store(StoreEvent::CacheHit),
AccountId = account_id,
Collection = collection.as_str(),
ChangeId = cache.highest_change_id,
Elapsed = start_time.elapsed(),
);
return Ok(cache);
}
permit
}
};
let num_changes = changes.changes.len();
let cache = if !matches!(collection, SyncCollection::CalendarEventNotification) {
let mut updated_resources = AHashMap::with_capacity(8);
let has_no_children = collection == SyncCollection::FileNode;
process_changes(
self,
account_id,
collection,
has_no_children,
&mut updated_resources,
changes.changes,
)
.await?;
let mut rebuild_hierarchy = false;
let mut resources = Vec::with_capacity(cache.resources.len());
for resource in &cache.resources {
let is_container = has_no_children || resource.is_container();
if let Some(updated_resource) =
updated_resources.remove(&(is_container, resource.document_id))
{
if let Some(updated_resource) = updated_resource {
rebuild_hierarchy =
rebuild_hierarchy || updated_resource.has_hierarchy_changes(resource);
resources.push(updated_resource);
} else {
// Deleted resource
rebuild_hierarchy = true;
}
} else {
resources.push(resource.clone());
}
}
// Add new resources
for resource in updated_resources.into_values().flatten() {
resources.push(resource);
rebuild_hierarchy = true;
}
if rebuild_hierarchy {
let mut cache = DavResources {
base_path: cache.base_path.clone(),
paths: Default::default(),
resources,
item_change_id: changes.item_change_id.unwrap_or(cache.item_change_id),
container_change_id: changes
.container_change_id
.unwrap_or(cache.container_change_id),
highest_change_id: changes.to_change_id,
size: std::mem::size_of::<DavResources>() as u64,
update_lock: lock.clone(),
};
if matches!(collection, SyncCollection::FileNode) {
build_nested_hierarchy(&mut cache);
} else {
build_simple_hierarchy(&mut cache);
}
cache
} else {
DavResources {
base_path: cache.base_path.clone(),
paths: cache.paths.clone(),
resources,
item_change_id: changes.item_change_id.unwrap_or(cache.item_change_id),
container_change_id: changes
.container_change_id
.unwrap_or(cache.container_change_id),
highest_change_id: changes.to_change_id,
size: cache.size,
update_lock: lock.clone(),
}
}
} else {
let mut delete_ids = AHashSet::with_capacity(changes.changes.len());
let mut resources = Vec::with_capacity(cache.resources.len());
let mut paths = AHashSet::with_capacity(cache.paths.len());
for change in changes.changes {
match change {
Change::InsertItem(document_id) => {
let document_id = document_id as u32;
paths.insert(path_from_scheduling(document_id, resources.len(), false));
resources.push(resource_from_scheduling(document_id, false));
}
Change::DeleteItem(document_id) => {
delete_ids.insert(document_id as u32);
}
_ => {}
}
}
for resource in &cache.resources {
if !delete_ids.contains(&resource.document_id) {
paths.insert(path_from_scheduling(
resource.document_id,
resources.len(),
resource.is_container(),
));
resources.push(resource.clone());
}
}
DavResources {
base_path: cache.base_path.clone(),
paths,
resources,
item_change_id: changes.item_change_id.unwrap_or(cache.item_change_id),
container_change_id: changes
.container_change_id
.unwrap_or(cache.container_change_id),
highest_change_id: changes.to_change_id,
size: cache.size,
update_lock: cache.update_lock.clone(),
}
};
cache.update_lock.set_revision(cache.highest_change_id);
let cache = Arc::new(cache);
cache_store.update(account_id, cache.clone());
warn_if_uncacheable(cache_store, account_id, collection, &cache);
trc::event!(
Store(StoreEvent::CacheUpdate),
AccountId = account_id,
Collection = collection.as_str(),
ChangeId = cache.highest_change_id,
Details = num_changes,
Total = cache.resources.len(),
Elapsed = start_time.elapsed(),
);
Ok(cache)
}
async fn create_default_addressbook(
&self,
account_info_access: &AccountCache,
account_info_owner: &AccountCache,
) -> trc::Result<Option<u32>> {
if let Some(name) = &self.core.groupware.default_addressbook_name {
let mut batch = BatchBuilder::new();
let account_id = account_info_owner.account_id();
let account_name = account_info_owner.name();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::AddressBook, 1)
.await?;
AddressBook {
name: name.clone(),
preferences: vec![AddressBookPreferences {
account_id,
name: format!(
"{} ({})",
self.core
.groupware
.default_addressbook_display_name
.as_ref()
.unwrap_or(name),
account_name
),
..Default::default()
}],
subscribers: vec![account_id],
..Default::default()
}
.insert(
account_info_access.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)?;
batch
.with_collection(Collection::Principal)
.with_document(0)
.set(
PrincipalField::DefaultAddressBookId,
document_id.serialize(),
);
self.commit_batch(batch).await?;
Ok(Some(document_id))
} else {
Ok(None)
}
}
async fn create_default_calendar(
&self,
account_info_access: &AccountCache,
account_info_owner: &AccountCache,
) -> trc::Result<Option<u32>> {
if let Some(name) = &self.core.groupware.default_calendar_name {
let mut batch = BatchBuilder::new();
let account_id = account_info_owner.account_id();
let account_name = account_info_owner.name();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::Calendar, 1)
.await?;
Calendar {
name: name.clone(),
preferences: vec![CalendarPreferences {
account_id,
name: format!(
"{} ({})",
self.core
.groupware
.default_calendar_display_name
.as_ref()
.unwrap_or(name),
account_name
),
flags: CALENDAR_SUBSCRIBED,
..Default::default()
}],
..Default::default()
}
.insert(
account_info_access.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)?;
// Set default calendar
batch
.with_collection(Collection::Principal)
.with_document(0)
.set(PrincipalField::DefaultCalendarId, document_id.serialize());
self.commit_batch(batch).await?;
Ok(Some(document_id))
} else {
Ok(None)
}
}
async fn get_or_create_default_calendar(
&self,
access_account_id: u32,
account_id: u32,
) -> trc::Result<Option<u32>> {
let default_calendar_id = self
.store()
.get_value::<u32>(ValueKey {
account_id,
collection: Collection::Principal.into(),
document_id: 0,
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
})
.await
.caused_by(trc::location!())?;
if default_calendar_id.is_some() {
Ok(default_calendar_id)
} else {
self.fetch_dav_resources(access_account_id, account_id, SyncCollection::Calendar)
.await
.map(|c| c.document_ids(true).next())
}
}
#[inline(always)]
fn cached_dav_resources(
&self,
account_id: u32,
collection: SyncCollection,
) -> Option<Arc<DavResources>> {
(match collection {
SyncCollection::Calendar => &self.inner.cache.events,
SyncCollection::AddressBook => &self.inner.cache.contacts,
SyncCollection::FileNode => &self.inner.cache.files,
_ => unreachable!(),
})
.get(&account_id)
}
}
async fn process_changes(
server: &Server,
account_id: u32,
collection: SyncCollection,
has_no_children: bool,
updated_resources: &mut AHashMap<(bool, u32), Option<DavResource>>,
changes: Vec<Change>,
) -> trc::Result<()> {
for change in changes {
match change {
Change::InsertItem(id) | Change::UpdateItem(id) => {
let document_id = id as u32;
if let Some(archive) = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
collection.collection(false),
document_id,
))
.await
.caused_by(trc::location!())?
{
updated_resources.insert(
(has_no_children, document_id),
Some(resource_from_archive(
archive,
document_id,
collection,
false,
)?),
);
} else {
updated_resources.insert((has_no_children, document_id), None);
}
}
Change::DeleteItem(id) => {
updated_resources.insert((has_no_children, id as u32), None);
}
Change::InsertContainer(id) | Change::UpdateContainer(id) => {
let document_id = id as u32;
if let Some(archive) = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
collection.collection(true),
document_id,
))
.await
.caused_by(trc::location!())?
{
updated_resources.insert(
(true, document_id),
Some(resource_from_archive(
archive,
document_id,
collection,
true,
)?),
);
} else {
updated_resources.insert((true, document_id), None);
}
}
Change::DeleteContainer(id) => {
updated_resources.insert((true, id as u32), None);
}
Change::UpdateContainerProperty(_) => (),
}
}
Ok(())
}
#[inline(always)]
fn warn_if_uncacheable(
cache_store: &Cache<u32, Arc<DavResources>>,
account_id: u32,
collection: SyncCollection,
cache: &Arc<DavResources>,
) {
let capacity = cache_store.weight_capacity();
if cache.size > capacity {
trc::event!(
Store(StoreEvent::CacheEntryTooLarge),
AccountId = account_id,
Collection = collection.as_str(),
Size = cache.size,
Limit = capacity,
);
}
}
async fn full_cache_build(
server: &Server,
account_id: u32,
collection: SyncCollection,
update_lock: Arc<UpdateLock>,
access_account_id: u32,
) -> trc::Result<Arc<DavResources>> {
match collection {
SyncCollection::Calendar => {
build_calcard_resources(
server,
access_account_id,
account_id,
SyncCollection::Calendar,
Collection::Calendar,
Collection::CalendarEvent,
update_lock,
)
.await
}
SyncCollection::AddressBook => {
build_calcard_resources(
server,
access_account_id,
account_id,
SyncCollection::AddressBook,
Collection::AddressBook,
Collection::ContactCard,
update_lock,
)
.await
}
SyncCollection::FileNode => build_file_resources(server, account_id, update_lock).await,
SyncCollection::CalendarEventNotification => {
build_scheduling_resources(server, account_id, update_lock).await
}
_ => unreachable!(),
}
.map(Arc::new)
}
fn resource_from_archive(
archive: Archive<AlignedBytes>,
document_id: u32,
collection: SyncCollection,
is_container: bool,
) -> trc::Result<DavResource> {
Ok(match collection {
SyncCollection::Calendar => {
if is_container {
resource_from_calendar(
archive
.unarchive::<Calendar>()
.caused_by(trc::location!())?,
document_id,
)
} else {
resource_from_event(
archive
.unarchive::<CalendarEvent>()
.caused_by(trc::location!())?,
document_id,
)
}
}
SyncCollection::AddressBook => {
if is_container {
resource_from_addressbook(
archive
.unarchive::<AddressBook>()
.caused_by(trc::location!())?,
document_id,
)
} else {
resource_from_card(
archive
.unarchive::<ContactCard>()
.caused_by(trc::location!())?,
document_id,
)
}
}
SyncCollection::FileNode => resource_from_file(
archive
.unarchive::<FileNode>()
.caused_by(trc::location!())?,
document_id,
),
_ => unreachable!(),
})
}
+273
View File
@@ -0,0 +1,273 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
Alarm, AlarmDelta, ArchivedAlarmDelta, ArchivedCalendarEventData, expand::resolve_local,
};
use calcard::{
common::timezone::Tz,
icalendar::{
ICalendarComponent, ICalendarParameterName, ICalendarParameterValue, ICalendarProperty,
ICalendarRelated, ICalendarValue,
},
};
use std::str::FromStr;
use store::write::bitpack::BitpackIterator;
use utils::codec::leb128::Leb128Reader;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CalendarAlarm {
pub alarm_id: u16,
pub event_id: u16,
pub alarm_time: i64,
pub typ: CalendarAlarmType,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CalendarAlarmType {
Email {
event_start: i64,
event_start_tz: u16,
event_end: i64,
event_end_tz: u16,
},
Display {
recurrence_id: Option<i64>,
},
}
impl ArchivedCalendarEventData {
pub fn next_alarm(&self, start_time: i64, default_tz: Tz) -> Option<CalendarAlarm> {
if self.alarms.is_empty() {
return None;
}
let base_offset = self.base_offset.to_native();
let mut next_alarm: Option<CalendarAlarm> = None;
'outer: for range in self.time_ranges.iter() {
let comp_id = range.id.to_native();
let Some(alarm) = self.alarms.iter().find(|a| a.parent_id == comp_id) else {
continue;
};
let instances = range.instances.as_ref();
let (offset_or_count, bytes_read) = instances.read_leb128::<u32>()?;
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())?;
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 {
// Recurring event
let unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, 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;
let (Some(start), Some(end)) = (
resolve_local(start_tz, start_date_naive),
resolve_local(end_tz, end_date_naive),
) else {
continue;
};
if let Some(alarm_time) = alarm.delta.to_timestamp(start, end, default_tz)
&& alarm_time > start_time
&& next_alarm
.as_ref()
.is_none_or(|next| alarm_time < next.alarm_time)
{
next_alarm = Some(CalendarAlarm {
alarm_id: alarm.id.to_native(),
event_id: alarm.parent_id.to_native(),
alarm_time,
typ: if alarm.is_email_alert {
CalendarAlarmType::Email {
event_start: start_date_naive,
event_start_tz: start_tz.as_id(),
event_end: end_date_naive,
event_end_tz: end_tz.as_id(),
}
} else {
let comp =
&self.event.components[alarm.parent_id.to_native() as usize];
CalendarAlarmType::Display {
recurrence_id: if comp.is_recurrent_or_override() {
start_date_naive.into()
} else {
None
},
}
},
});
continue 'outer;
}
}
} else {
// Single event
let start_date_naive = offset_or_count as i64 + base_offset;
let end_date_naive = start_date_naive + duration;
let (Some(start), Some(end)) = (
resolve_local(start_tz, start_date_naive),
resolve_local(end_tz, end_date_naive),
) else {
continue;
};
if let Some(alarm_time) = alarm.delta.to_timestamp(start, end, default_tz)
&& alarm_time > start_time
&& next_alarm
.as_ref()
.is_none_or(|next| alarm_time < next.alarm_time)
{
next_alarm = Some(CalendarAlarm {
alarm_id: alarm.id.to_native(),
event_id: alarm.parent_id.to_native(),
alarm_time,
typ: if alarm.is_email_alert {
CalendarAlarmType::Email {
event_start: start_date_naive,
event_start_tz: start_tz.as_id(),
event_end: end_date_naive,
event_end_tz: end_tz.as_id(),
}
} else {
let comp = &self.event.components[alarm.parent_id.to_native() as usize];
CalendarAlarmType::Display {
recurrence_id: if comp.is_recurrent_or_override() {
start_date_naive.into()
} else {
None
},
}
},
});
}
}
}
next_alarm
}
}
pub trait ExpandAlarm {
fn expand_alarm(&self, id: u16, parent_id: u16) -> Option<Alarm>;
}
impl ExpandAlarm for ICalendarComponent {
fn expand_alarm(&self, id: u16, parent_id: u16) -> Option<Alarm> {
let mut trigger = None;
let mut is_email_alert = false;
for entry in self.entries.iter() {
match &entry.name {
ICalendarProperty::Trigger => {
let mut tz = None;
let mut trigger_start = true;
for param in entry.params.iter() {
match (&param.name, &param.value) {
(
ICalendarParameterName::Related,
ICalendarParameterValue::Related(related),
) => {
trigger_start = matches!(related, ICalendarRelated::Start);
}
(
ICalendarParameterName::Tzid,
ICalendarParameterValue::Text(tz_id),
) => {
tz = Tz::from_str(tz_id).ok();
}
_ => {}
}
}
trigger = match entry.values.first()? {
ICalendarValue::PartialDateTime(dt) => {
let tz = tz.unwrap_or(Tz::Floating);
dt.to_date_time_with_tz(tz).map(|dt| {
let timestamp = dt.timestamp();
if !dt.timezone().is_floating() {
AlarmDelta::FixedUtc(timestamp)
} else {
AlarmDelta::FixedFloating(timestamp)
}
})
}
ICalendarValue::Duration(duration) => {
if trigger_start {
Some(AlarmDelta::Start(duration.as_seconds()))
} else {
Some(AlarmDelta::End(duration.as_seconds()))
}
}
_ => None,
};
}
ICalendarProperty::Action => {
is_email_alert = is_email_alert
|| entry
.values
.first()
.and_then(|v| v.as_text())
.is_some_and(|v| v.eq_ignore_ascii_case("email"));
}
ICalendarProperty::Summary | ICalendarProperty::Description => {
is_email_alert = is_email_alert
|| entry
.values
.first()
.and_then(|v| v.as_text())
.is_some_and(|v| v.contains("@email"));
}
_ => {}
}
}
trigger.map(|delta| Alarm {
id,
parent_id,
delta,
is_email_alert,
})
}
}
impl AlarmDelta {
pub fn to_timestamp(&self, start: i64, end: i64, default_tz: Tz) -> Option<i64> {
match self {
AlarmDelta::Start(delta) => Some(start + delta),
AlarmDelta::End(delta) => Some(end + delta),
AlarmDelta::FixedUtc(timestamp) => Some(*timestamp),
AlarmDelta::FixedFloating(timestamp) => resolve_local(default_tz, *timestamp),
}
}
}
impl ArchivedAlarmDelta {
pub fn to_timestamp(&self, start: i64, end: i64, default_tz: Tz) -> Option<i64> {
match self {
ArchivedAlarmDelta::Start(delta) => Some(start + delta.to_native()),
ArchivedAlarmDelta::End(delta) => Some(end + delta.to_native()),
ArchivedAlarmDelta::FixedUtc(timestamp) => Some(timestamp.to_native()),
ArchivedAlarmDelta::FixedFloating(timestamp) => {
resolve_local(default_tz, timestamp.to_native())
}
}
}
}
+298
View File
@@ -0,0 +1,298 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ArchivedCalendarEventData, ArchivedTimezone, CalendarEventData, Timezone,
alarm::{CalendarAlarm, ExpandAlarm},
};
use crate::calendar::{ComponentTimeRange, alarm::CalendarAlarmType};
use calcard::{
common::timezone::Tz,
icalendar::{ICalendar, ICalendarComponentType, dates::TimeOrDelta},
};
use compact_str::ToCompactString;
use indexmap::IndexMap;
use store::{
ahash::{AHashMap, RandomState},
write::{key::KeySerializer, now},
};
const MAX_TIME_SPAN: i64 = u32::MAX as i64;
impl CalendarEventData {
pub fn new(
ical: ICalendar,
default_tz: Tz,
max_expansions: usize,
next_email_alarm: &mut Option<CalendarAlarm>,
) -> Self {
let mut ranges = TimeRanges::default();
let now = now() as i64;
let expanded = ical.expand_dates(default_tz, max_expansions);
let mut groups: IndexMap<(u16, u16, u16, i32), Vec<i64>, RandomState> =
IndexMap::with_capacity_and_hasher(16, RandomState::default());
let mut alarms = AHashMap::with_capacity(16);
for event in expanded.events {
let start_naive = event.start.naive_local();
let start_tz = event.start.timezone().as_id();
let start_timestamp_utc = event.start.timestamp();
let start_timestamp_naive = start_naive.and_utc().timestamp();
let (end_timestamp_utc, end_timestamp_naive, end_tz) = match event.end {
TimeOrDelta::Time(time) => {
let end_naive = time.naive_local();
let end_timestamp_utc = time.timestamp();
let end_timestamp_naive = end_naive.and_utc().timestamp();
(
end_timestamp_utc,
end_timestamp_naive,
time.timezone().as_id(),
)
}
TimeOrDelta::Delta(delta) => {
let delta = delta.num_seconds();
(
start_timestamp_utc + delta,
start_timestamp_naive + delta,
start_tz,
)
}
};
// Expand alarms
let mut min = std::cmp::min(start_timestamp_utc, end_timestamp_utc);
let mut max = std::cmp::max(start_timestamp_utc, end_timestamp_utc);
for alarm in alarms.entry(event.comp_id).or_insert_with(|| {
ical.component_by_id(event.comp_id)
.map_or(&[][..], |c| c.component_ids.as_slice())
.iter()
.filter_map(|alarm_id| {
ical.component_by_id(*alarm_id).and_then(|alarm| {
if alarm.component_type == ICalendarComponentType::VAlarm {
alarm.expand_alarm(*alarm_id as u16, event.comp_id as u16)
} else {
None
}
})
})
.collect::<Vec<_>>()
}) {
if let Some(alarm_time) =
alarm
.delta
.to_timestamp(start_timestamp_utc, end_timestamp_utc, default_tz)
{
if alarm_time < min {
min = alarm_time;
}
if alarm_time > max {
max = alarm_time;
}
if alarm_time > now
&& next_email_alarm
.as_ref()
.is_none_or(|next| alarm_time < next.alarm_time)
{
*next_email_alarm = Some(CalendarAlarm {
alarm_id: alarm.id,
event_id: alarm.parent_id,
alarm_time,
typ: if alarm.is_email_alert {
CalendarAlarmType::Email {
event_start: start_timestamp_naive,
event_end: end_timestamp_naive,
event_start_tz: start_tz,
event_end_tz: end_tz,
}
} else {
CalendarAlarmType::Display {
recurrence_id: if ical.components[alarm.parent_id as usize]
.is_recurrent_or_override()
{
start_timestamp_naive.into()
} else {
None
},
}
},
});
}
}
}
ranges.update_base_offset(start_timestamp_naive, end_timestamp_naive);
ranges.update_utc_min_max(min, max);
groups
.entry((
start_tz,
end_tz,
event.comp_id as u16,
(end_timestamp_naive - start_timestamp_naive)
.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
))
.or_default()
.push(start_timestamp_naive);
}
let mut events = Vec::with_capacity(groups.len());
for ((start_tz, end_tz, id, duration), mut instances) in groups {
instances.sort_unstable();
instances.truncate(instances.partition_point(|instance| {
instance.saturating_sub(ranges.base_offset) <= MAX_TIME_SPAN
}));
let instances = match instances.len() {
0 => continue,
1 => KeySerializer::new(std::mem::size_of::<u32>())
.write_leb128((instances[0] - ranges.base_offset) as u32)
.finalize(),
len => {
// Bitpack instances
let mut instance_offsets = Vec::with_capacity(len);
for instance in instances {
debug_assert!(instance >= ranges.base_offset);
instance_offsets.push((instance - ranges.base_offset) as u32);
}
KeySerializer::new(instance_offsets.len() * std::mem::size_of::<u32>())
.bitpack_sorted(&instance_offsets)
.finalize()
}
};
events.push(ComponentTimeRange {
id,
start_tz,
end_tz,
duration,
instances: instances.into_boxed_slice(),
});
}
if !expanded.errors.is_empty() {
trc::event!(
Calendar(trc::CalendarEvent::RuleExpansionError),
Reason = expanded
.errors
.into_iter()
.map(|e| e.error.to_compact_string())
.collect::<Vec<_>>(),
Details = ical.to_string(),
Limit = max_expansions,
);
}
CalendarEventData {
event: ical,
time_ranges: events.into_boxed_slice(),
alarms: alarms
.into_values()
.flatten()
.collect::<Vec<_>>()
.into_boxed_slice(),
base_offset: ranges.base_offset,
base_time_utc: (ranges.min_time_utc - ranges.base_offset).clamp(0, MAX_TIME_SPAN)
as u32,
duration: (ranges.max_time_utc - ranges.min_time_utc).clamp(0, MAX_TIME_SPAN) as u32,
}
}
pub fn event_range(&self) -> Option<(i64, u32)> {
if self.base_offset != 0 {
Some((self.base_offset + self.base_time_utc as i64, self.duration))
} else {
None
}
}
}
#[derive(Default, Debug)]
struct TimeRanges {
max_time_utc: i64,
min_time_utc: i64,
base_offset: i64,
}
impl TimeRanges {
pub fn update_base_offset(&mut self, t1: i64, t2: i64) {
let offset = std::cmp::min(t1, t2);
if offset < self.base_offset || self.base_offset == 0 {
self.base_offset = offset;
}
}
pub fn update_utc_min_max(&mut self, min: i64, max: i64) {
if min < self.min_time_utc || self.min_time_utc == 0 {
self.min_time_utc = min;
}
if max > self.max_time_utc {
self.max_time_utc = max;
}
if min < self.base_offset || self.base_offset == 0 {
self.base_offset = min;
}
}
}
impl ArchivedCalendarEventData {
pub fn event_range(&self) -> Option<(i64, u32)> {
if self.base_offset != 0 {
Some((
self.base_offset.to_native() + self.base_time_utc.to_native() as i64,
self.duration.to_native(),
))
} else {
None
}
}
pub fn event_range_start(&self) -> i64 {
self.base_offset.to_native() + self.base_time_utc.to_native() as i64
}
pub fn event_range_end(&self) -> i64 {
self.base_offset.to_native()
+ self.base_time_utc.to_native() as i64
+ self.duration.to_native() as i64
}
}
impl CalendarEventData {
pub fn event_range_start(&self) -> i64 {
self.base_offset + self.base_time_utc as i64
}
pub fn event_range_end(&self) -> i64 {
self.base_offset + self.base_time_utc as i64 + self.duration as i64
}
}
impl Timezone {
pub fn tz(&self) -> Option<Tz> {
match self {
Timezone::IANA(iana) => Tz::from_id(*iana),
Timezone::Custom(icalendar) => icalendar
.timezones()
.filter_map(|t| t.timezone().map(|x| x.1))
.next(),
Timezone::Default => None,
}
}
}
impl ArchivedTimezone {
pub fn tz(&self) -> Option<Tz> {
match self {
ArchivedTimezone::IANA(iana) => Tz::from_id(iana.to_native()),
ArchivedTimezone::Custom(icalendar) => icalendar
.timezones()
.filter_map(|t| t.timezone().map(|x| x.1))
.next(),
ArchivedTimezone::Default => None,
}
}
}
+483
View File
@@ -0,0 +1,483 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::ArchivedCalendarEventData;
use crate::calendar::CalendarEventData;
use ahash::AHashSet;
use calcard::{
common::{DateTimeResult, timezone::Tz},
icalendar::{ArchivedICalendarComponent, ICalendarComponent, ICalendarProperty},
};
use chrono::{DateTime, TimeZone};
use std::str::FromStr;
use store::write::bitpack::BitpackIterator;
use types::TimeRange;
use utils::codec::leb128::Leb128Reader;
const RECURRENCE_KEY_EPOCH: i64 = -2208988800;
const RECURRENCE_KEY_GRANULARITY: i64 = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RecurrenceKey(u32);
impl RecurrenceKey {
pub fn from_recurrence_id(recurrence_id_naive: i64) -> Option<Self> {
u32::try_from(
recurrence_id_naive
.checked_sub(RECURRENCE_KEY_EPOCH)?
.div_euclid(RECURRENCE_KEY_GRANULARITY),
)
.ok()?
.checked_add(1)
.map(RecurrenceKey)
}
pub fn from_prefix(prefix: u32) -> Option<Self> {
(prefix != 0).then_some(RecurrenceKey(prefix))
}
pub fn prefix(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecurrenceId {
pub utc: i64,
pub naive: i64,
}
pub trait ComponentRecurrenceId {
fn recurrence_id(&self, fallback_tz: Tz) -> Option<RecurrenceId>;
}
impl ComponentRecurrenceId for ArchivedICalendarComponent {
fn recurrence_id(&self, fallback_tz: Tz) -> Option<RecurrenceId> {
let entry = self.property(&ICalendarProperty::RecurrenceId)?;
resolve_recurrence_id(
entry.tz_id(),
entry
.values
.first()?
.as_partial_date_time()?
.to_date_time()?,
fallback_tz,
)
}
}
impl ComponentRecurrenceId for ICalendarComponent {
fn recurrence_id(&self, fallback_tz: Tz) -> Option<RecurrenceId> {
let entry = self.property(&ICalendarProperty::RecurrenceId)?;
resolve_recurrence_id(
entry.tz_id(),
entry
.values
.first()?
.as_partial_date_time()?
.to_date_time()?,
fallback_tz,
)
}
}
fn resolve_recurrence_id(
tz_id: Option<&str>,
date_time: DateTimeResult,
fallback_tz: Tz,
) -> Option<RecurrenceId> {
let tz = tz_id
.and_then(|tz_id| Tz::from_str(tz_id).ok())
.unwrap_or(fallback_tz);
let date_time = date_time.to_date_time_with_tz(tz)?.with_timezone(&tz);
Some(RecurrenceId {
utc: date_time.timestamp(),
naive: date_time.naive_local().and_utc().timestamp(),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CalendarEventExpansion {
pub comp_id: u32,
pub own_recurrence_id: Option<RecurrenceId>,
pub start: i64,
pub end: i64,
pub start_naive: i64,
}
impl CalendarEventExpansion {
pub fn recurrence_id(&self) -> RecurrenceId {
self.own_recurrence_id.unwrap_or(RecurrenceId {
utc: self.start,
naive: self.start_naive,
})
}
pub fn recurrence_key(&self) -> Option<RecurrenceKey> {
RecurrenceKey::from_recurrence_id(self.recurrence_id().naive)
}
}
impl ArchivedCalendarEventData {
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();
'outer: for (range_index, range) in self.time_ranges.iter().enumerate() {
let instances = range.instances.as_ref();
let (offset_or_count, bytes_read) = instances.read_leb128::<u32>()?;
let comp_id = range.id.to_native() as u32;
let component = self.event.components.get(comp_id as usize)?;
let duration = range.duration.to_native() as i64;
let component_tz = Tz::from_id(range.start_tz.to_native())?;
let mut own_recurrence_id = self
.time_ranges
.iter()
.take(range_index)
.all(|prior| prior.id != range.id)
.then(|| component.recurrence_id(component_tz))
.flatten();
let mut start_tz = component_tz;
let mut end_tz = Tz::from_id(range.end_tz.to_native())?;
let is_todo = component.component_type.is_todo();
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 unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count);
for start_offset in unpacker {
let own_recurrence_id = own_recurrence_id.take();
let start_date_naive = start_offset as i64 + base_offset;
let end_date_naive = start_date_naive + duration;
let (Some(start), Some(end)) = (
resolve_local(start_tz, start_date_naive),
resolve_local(end_tz, end_date_naive),
) else {
continue;
};
if limit.is_in_range(is_todo, start, end) {
expansion.push(CalendarEventExpansion {
comp_id,
own_recurrence_id,
start,
end,
start_naive: start_date_naive,
});
} else if start > limit.end {
continue 'outer;
}
}
} else {
let start_date_naive = offset_or_count as i64 + base_offset;
let end_date_naive = start_date_naive + duration;
if let (Some(start), Some(end)) = (
resolve_local(start_tz, start_date_naive),
resolve_local(end_tz, end_date_naive),
) && limit.is_in_range(is_todo, start, end)
{
expansion.push(CalendarEventExpansion {
comp_id,
own_recurrence_id,
start,
end,
start_naive: start_date_naive,
});
}
}
}
Some(expansion)
}
}
impl CalendarEventData {
pub fn component_tz(&self, comp_id: u32) -> Option<Tz> {
self.time_ranges
.iter()
.find(|range| range.id as u32 == comp_id)
.and_then(|range| Tz::from_id(range.start_tz))
}
pub fn expand_from_ids(
&self,
keys: &mut AHashSet<RecurrenceKey>,
default_tz: Tz,
) -> Option<Vec<CalendarEventExpansion>> {
let mut expansion = Vec::with_capacity(keys.len());
let base_offset = self.base_offset;
for (range_index, range) in self.time_ranges.iter().enumerate() {
let instances = range.instances.as_ref();
let (offset_or_count, bytes_read) = instances.read_leb128::<u32>()?;
let comp_id = range.id as u32;
let component = self.event.components.get(comp_id as usize)?;
let duration = range.duration as i64;
let component_tz = Tz::from_id(range.start_tz)?;
let mut own_recurrence_id = self
.time_ranges
.iter()
.take(range_index)
.all(|prior| prior.id != range.id)
.then(|| component.recurrence_id(component_tz))
.flatten();
let mut start_tz = component_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 mut push_instance = |own_recurrence_id: Option<RecurrenceId>, start_offset: u32| {
let start_date_naive = start_offset as i64 + base_offset;
let recurrence_id_naive =
own_recurrence_id.map_or(start_date_naive, |recurrence_id| recurrence_id.naive);
if RecurrenceKey::from_recurrence_id(recurrence_id_naive)
.is_none_or(|key| !keys.contains(&key))
{
return;
}
let end_date_naive = start_date_naive + duration;
if let (Some(start), Some(end)) = (
resolve_local(start_tz, start_date_naive),
resolve_local(end_tz, end_date_naive),
) {
expansion.push(CalendarEventExpansion {
comp_id,
own_recurrence_id,
start,
end,
start_naive: start_date_naive,
});
}
};
if instances.len() > bytes_read {
let unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count);
for start_offset in unpacker {
push_instance(own_recurrence_id.take(), start_offset);
}
} else {
push_instance(own_recurrence_id, offset_or_count);
}
}
keys.retain(|key| {
!expansion
.iter()
.any(|expansion| expansion.recurrence_key() == Some(*key))
});
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 component_tz = Tz::from_id(range.start_tz)?;
let own_recurrence_id = self
.event
.components
.get(comp_id as usize)
.and_then(|component| component.recurrence_id(component_tz));
let mut start_tz = component_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 {
let mut unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count);
unpacker.next()?
} else {
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 = resolve_local(start_tz, start_date_naive)?;
let end = resolve_local(end_tz, end_date_naive)?;
Some(CalendarEventExpansion {
comp_id,
own_recurrence_id,
start,
end,
start_naive: start_date_naive,
})
}
}
impl Default for CalendarEventExpansion {
fn default() -> Self {
Self {
comp_id: u32::MAX,
own_recurrence_id: None,
start: i64::MAX,
end: i64::MAX,
start_naive: i64::MAX,
}
}
}
pub fn resolve_local(tz: Tz, naive_secs: i64) -> Option<i64> {
tz.from_local_datetime(&DateTime::from_timestamp(naive_secs, 0)?.naive_local())
.earliest()
.map(|dt| dt.timestamp())
}
#[cfg(test)]
mod tests {
use super::*;
use calcard::{Entry, Parser};
use chrono::NaiveDate;
fn naive(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> i64 {
NaiveDate::from_ymd_opt(year, month, day)
.and_then(|date| date.and_hms_opt(hour, minute, second))
.map(|date_time| date_time.and_utc().timestamp())
.expect("valid date")
}
fn key(year: i32, month: u32, day: u32, hour: u32, minute: u32) -> RecurrenceKey {
RecurrenceKey::from_recurrence_id(naive(year, month, day, hour, minute, 0))
.expect("representable recurrence id")
}
fn event_data(ical: &str) -> CalendarEventData {
let entry = Parser::new(ical).entry();
let Entry::ICalendar(ical) = entry else {
panic!("failed to parse iCalendar: {entry:?}");
};
CalendarEventData::new(ical, Tz::UTC, 1000, &mut None)
}
fn expand_key(data: &CalendarEventData, key: RecurrenceKey) -> Vec<(u32, i64)> {
let mut keys = AHashSet::from_iter([key]);
data.expand_from_ids(&mut keys, Tz::UTC)
.expect("expansion")
.into_iter()
.map(|expansion| (expansion.comp_id, expansion.start_naive))
.collect()
}
const MASTER: &str = concat!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//EN\r\n",
"BEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTAMP:20270101T000000Z\r\n",
"DTSTART:20270301T090000Z\r\nDTEND:20270301T100000Z\r\n",
"RRULE:FREQ=WEEKLY;COUNT=5\r\nSUMMARY:Weekly\r\nEND:VEVENT\r\n",
);
const OVERRIDE: &str = concat!(
"BEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTAMP:20270101T000000Z\r\n",
"RECURRENCE-ID:20270308T090000Z\r\nDTSTART:20270308T140000Z\r\n",
"DTEND:20270308T150000Z\r\nSUMMARY:Moved\r\nEND:VEVENT\r\n",
);
#[test]
fn recurrence_key_encoding() {
assert_eq!(
RecurrenceKey::from_recurrence_id(RECURRENCE_KEY_EPOCH),
Some(RecurrenceKey(1))
);
assert_eq!(
RecurrenceKey::from_recurrence_id(RECURRENCE_KEY_EPOCH - 1),
None
);
assert_eq!(RecurrenceKey::from_recurrence_id(i64::MAX), None);
assert_eq!(RecurrenceKey::from_prefix(0), None);
assert_eq!(
RecurrenceKey::from_prefix(key(2027, 3, 15, 9, 0).prefix()),
Some(key(2027, 3, 15, 9, 0))
);
assert_ne!(key(2027, 3, 15, 9, 0), key(2027, 3, 15, 9, 1));
assert_eq!(
RecurrenceKey::from_recurrence_id(naive(2027, 3, 15, 9, 0, 30)),
Some(key(2027, 3, 15, 9, 0))
);
}
#[test]
fn recurrence_keys_survive_an_override() {
let before = event_data(&format!("{MASTER}END:VCALENDAR\r\n"));
let after = event_data(&format!("{MASTER}{OVERRIDE}END:VCALENDAR\r\n"));
for (day, comp_id) in [(1, 1), (15, 1), (22, 1), (29, 1)] {
let key = key(2027, 3, day, 9, 0);
let start_naive = naive(2027, 3, day, 9, 0, 0);
assert_eq!(expand_key(&before, key), [(comp_id, start_naive)]);
assert_eq!(expand_key(&after, key), [(comp_id, start_naive)]);
}
let overridden = key(2027, 3, 8, 9, 0);
assert_eq!(
expand_key(&before, overridden),
[(1, naive(2027, 3, 8, 9, 0, 0))]
);
assert_eq!(
expand_key(&after, overridden),
[(2, naive(2027, 3, 8, 14, 0, 0))]
);
}
#[test]
fn this_and_future_instances_get_distinct_keys() {
const THIS_AND_FUTURE: &str = concat!(
"BEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTAMP:20270101T000000Z\r\n",
"RECURRENCE-ID;RANGE=THISANDFUTURE:20270315T090000Z\r\n",
"DTSTART:20270315T100000Z\r\nDTEND:20270315T113000Z\r\n",
"SUMMARY:Longer\r\nEND:VEVENT\r\n",
);
let data = event_data(&format!("{MASTER}{THIS_AND_FUTURE}END:VCALENDAR\r\n"));
assert_eq!(
expand_key(&data, key(2027, 3, 15, 9, 0)),
[(2, naive(2027, 3, 15, 10, 0, 0))]
);
assert_eq!(
expand_key(&data, key(2027, 3, 22, 10, 0)),
[(2, naive(2027, 3, 22, 10, 0, 0))]
);
assert_eq!(
expand_key(&data, key(2027, 3, 29, 10, 0)),
[(2, naive(2027, 3, 29, 10, 0, 0))]
);
assert_eq!(
expand_key(&data, key(2027, 3, 1, 9, 0)),
[(1, naive(2027, 3, 1, 9, 0, 0))]
);
}
#[test]
fn unmatched_recurrence_keys_are_reported_back() {
let data = event_data(&format!("{MASTER}END:VCALENDAR\r\n"));
let missing = key(2027, 4, 5, 9, 0);
let present = key(2027, 3, 15, 9, 0);
let mut keys = AHashSet::from_iter([missing, present]);
let expansion = data.expand_from_ids(&mut keys, Tz::UTC).expect("expansion");
assert_eq!(expansion.len(), 1);
assert_eq!(keys.into_iter().collect::<Vec<_>>(), [missing]);
}
}
+495
View File
@@ -0,0 +1,495 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert,
ArchivedTimezone, Calendar, CalendarEvent, CalendarPreferences, DefaultAlert, Timezone,
};
use crate::{
calendar::{
ArchivedCalendarEventNotification, ArchivedChangedBy, ArchivedEventPreferences,
CalendarEventNotification, ChangedBy, EventPreferences,
},
strip_mailto_scheme,
};
use ahash::AHashSet;
use calcard::icalendar::{
ArchivedICalendarParameterValue, ArchivedICalendarProperty, ArchivedICalendarValue,
ICalendarParameterValue, ICalendarProperty, ICalendarValue,
};
use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject};
use nlp::language::{
Language,
detect::{LanguageDetector, MIN_LANGUAGE_SCORE},
};
use store::{
U32_LEN,
search::{CalendarSearchField, IndexDocument, SearchField},
write::{IndexPropertyClass, SearchIndex, ValueClass},
xxhash_rust::xxh3,
};
use types::{
acl::AclGrant,
collection::SyncCollection,
field::{CalendarEventField, CalendarNotificationField},
};
impl IndexableObject for Calendar {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Acl {
value: (&self.acls).into(),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogContainer {
sync_collection: SyncCollection::Calendar,
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedCalendar {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Acl {
value: self
.acls
.iter()
.map(AclGrant::from)
.collect::<Vec<_>>()
.into(),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogContainer {
sync_collection: SyncCollection::Calendar,
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for Calendar {
fn is_versioned() -> bool {
true
}
}
impl IndexableObject for CalendarEvent {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::SearchIndex {
index: SearchIndex::Calendar,
hash: self
.hashes()
.chain([self.data.event_range_start() as u64])
.fold(0, |acc, hash| acc ^ hash),
},
IndexValue::Index {
field: CalendarEventField::Uid.into(),
value: self.data.event.uids().next().into(),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogItem {
sync_collection: SyncCollection::Calendar,
prefix: None,
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedCalendarEvent {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::SearchIndex {
index: SearchIndex::Calendar,
hash: self
.hashes()
.chain([self.data.event_range_start() as u64])
.fold(0, |acc, hash| acc ^ hash),
},
IndexValue::Index {
field: CalendarEventField::Uid.into(),
value: self.data.event.uids().next().into(),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogItem {
sync_collection: SyncCollection::Calendar,
prefix: None,
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for CalendarEvent {
fn is_versioned() -> bool {
true
}
}
impl IndexableObject for CalendarEventNotification {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::Property {
field: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: self.created as u64,
}),
value: self.event_id.unwrap_or(u32::MAX).into(),
},
IndexValue::LogItem {
sync_collection: SyncCollection::CalendarEventNotification,
prefix: None,
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedCalendarEventNotification {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::Property {
field: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: self.created.to_native() as u64,
}),
value: self
.event_id
.as_ref()
.map(|v| v.to_native())
.unwrap_or(u32::MAX)
.into(),
},
IndexValue::LogItem {
sync_collection: SyncCollection::CalendarEventNotification,
prefix: None,
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for CalendarEventNotification {
fn is_versioned() -> bool {
false
}
}
impl Calendar {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.preferences.iter().map(|p| p.size()).sum::<usize>()
+ self.name.len()
+ std::mem::size_of::<Calendar>()
}
}
impl ArchivedCalendar {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.preferences.iter().map(|p| p.size()).sum::<usize>()
+ self.name.len()
+ std::mem::size_of::<Calendar>()
}
}
impl CalendarEvent {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.display_name.as_ref().map_or(0, |n| n.len())
+ self.names.iter().map(|n| n.name.len()).sum::<usize>()
+ self.preferences.iter().map(|p| p.size()).sum::<usize>()
+ self.size as usize
+ std::mem::size_of::<CalendarEvent>()
}
}
impl ArchivedCalendarEvent {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.display_name.as_ref().map_or(0, |n| n.len())
+ self.names.iter().map(|n| n.name.len()).sum::<usize>()
+ self.preferences.iter().map(|p| p.size()).sum::<usize>()
+ self.size.to_native() as usize
+ std::mem::size_of::<CalendarEvent>()
}
}
impl CalendarEventNotification {
pub fn size(&self) -> usize {
(match &self.changed_by {
ChangedBy::PrincipalId(_) => U32_LEN,
ChangedBy::CalendarAddress(v) => v.len(),
}) + std::mem::size_of::<CalendarEventNotification>()
+ self.size as usize
}
}
impl ArchivedCalendarEventNotification {
pub fn size(&self) -> usize {
(match &self.changed_by {
ArchivedChangedBy::PrincipalId(_) => U32_LEN,
ArchivedChangedBy::CalendarAddress(v) => v.len(),
}) + std::mem::size_of::<CalendarEventNotification>()
+ self.size.to_native() as usize
}
}
impl CalendarPreferences {
pub fn size(&self) -> usize {
self.name.len()
+ self.default_alerts.iter().map(|a| a.size()).sum::<usize>()
+ 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 ArchivedCalendarPreferences {
pub fn size(&self) -> usize {
self.name.len()
+ self.default_alerts.iter().map(|a| a.size()).sum::<usize>()
+ 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>()
}
}
impl Timezone {
pub fn size(&self) -> usize {
match self {
Timezone::IANA(_) => 2,
Timezone::Custom(c) => c.size(),
Timezone::Default => 0,
}
}
}
impl ArchivedTimezone {
pub fn size(&self) -> usize {
match self {
ArchivedTimezone::IANA(_) => 2,
ArchivedTimezone::Custom(c) => c.size(),
ArchivedTimezone::Default => 0,
}
}
}
impl DefaultAlert {
pub fn size(&self) -> usize {
std::mem::size_of::<DefaultAlert>() + self.id.len()
}
}
impl ArchivedDefaultAlert {
pub fn size(&self) -> usize {
std::mem::size_of::<DefaultAlert>() + self.id.len()
}
}
impl CalendarEvent {
pub fn hashes(&self) -> impl Iterator<Item = u64> {
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
| ICalendarProperty::Uid
)
})
})
.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,
}))
})
.map(|v| xxh3::xxh3_64(v.as_bytes()))
}
}
impl ArchivedCalendarEvent {
pub fn hashes(&self) -> impl Iterator<Item = u64> {
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
| ArchivedICalendarProperty::Uid
)
})
})
.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,
}))
})
.map(|v| xxh3::xxh3_64(v.as_bytes()))
}
}
impl ArchivedCalendarEvent {
pub fn index_document(
&self,
account_id: u32,
document_id: u32,
index_fields: &AHashSet<SearchField>,
default_language: Language,
) -> IndexDocument {
let mut document = IndexDocument::new(SearchIndex::Calendar)
.with_account_id(account_id)
.with_document_id(document_id);
if index_fields.is_empty()
|| index_fields.contains(&SearchField::Calendar(CalendarSearchField::Start))
{
document.index_integer(CalendarSearchField::Start, self.data.event_range_start());
}
let mut detector = LanguageDetector::new();
for component in self
.data
.event
.components
.iter()
.filter(|e| e.component_type.is_scheduling_object())
{
for entry in component.entries.iter() {
let (is_lang, is_keyword, field) = match entry.name {
ArchivedICalendarProperty::Summary => (true, false, CalendarSearchField::Title),
ArchivedICalendarProperty::Description => {
(true, false, CalendarSearchField::Description)
}
ArchivedICalendarProperty::Location => {
(false, false, CalendarSearchField::Location)
}
ArchivedICalendarProperty::Organizer => {
(false, false, CalendarSearchField::Owner)
}
ArchivedICalendarProperty::Attendee => {
(false, false, CalendarSearchField::Attendee)
}
ArchivedICalendarProperty::Uid => (false, true, CalendarSearchField::Uid),
_ => continue,
};
let field = SearchField::Calendar(field);
if index_fields.is_empty() || index_fields.contains(&field) {
for value in entry
.values
.iter()
.filter_map(|v| match v {
ArchivedICalendarValue::Text(v) => Some(v.as_str()),
ArchivedICalendarValue::Uri(uri) => uri.as_str(),
_ => None,
})
.chain(entry.params.iter().filter_map(|p| match &p.value {
ArchivedICalendarParameterValue::Text(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Uri(uri) => uri.as_str(),
_ => None,
}))
{
let value = strip_mailto_scheme(value);
let lang = if is_lang {
detector.detect(value, MIN_LANGUAGE_SCORE);
Language::Unknown
} else {
Language::None
};
if !is_keyword {
document.index_text(field.clone(), value, lang);
} else {
document.index_keyword(field.clone(), value);
}
}
}
}
}
document.set_unknown_language(
detector
.most_frequent_language()
.unwrap_or(default_language),
);
document
}
}
File diff suppressed because it is too large Load Diff
+439
View File
@@ -0,0 +1,439 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod alarm;
pub mod dates;
pub mod expand;
pub mod index;
pub mod itip;
pub mod storage;
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry,
};
use common::DavName;
use types::{acl::AclGrant, dead_property::DeadProperty};
use utils::map::bitmap::BitmapItem;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct Calendar {
pub name: String,
pub preferences: Vec<CalendarPreferences>,
pub acls: Vec<AclGrant>,
pub supported_components: u64,
pub dead_properties: DeadProperty,
pub created: i64,
pub modified: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SupportedComponent {
VCalendar, // [RFC5545, Section 3.4]
VEvent, // [RFC5545, Section 3.6.1]
VTodo, // [RFC5545, Section 3.6.2]
VJournal, // [RFC5545, Section 3.6.3]
VFreebusy, // [RFC5545, Section 3.6.4]
VTimezone, // [RFC5545, Section 3.6.5]
VAlarm, // [RFC5545, Section 3.6.6]
Standard, // [RFC5545, Section 3.6.5]
Daylight, // [RFC5545, Section 3.6.5]
VAvailability, // [RFC7953, Section 3.1]
Available, // [RFC7953, Section 3.1]
Participant, // [RFC9073, Section 7.1]
VLocation, // [RFC9073, Section 7.2] [RFC Errata 7381]
VResource, // [RFC9073, Section 7.3]
VStatus, // draft-ietf-calext-ical-tasks-14
Other,
}
pub const CALENDAR_SUBSCRIBED: u16 = 1;
pub const CALENDAR_INVISIBLE: u16 = 1 << 1;
pub const CALENDAR_AVAILABILITY_NONE: u16 = 1 << 2;
pub const CALENDAR_AVAILABILITY_ATTENDING: u16 = 1 << 3;
pub const CALENDAR_AVAILABILITY_ALL: 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 flags: u16,
pub time_zone: Timezone,
pub default_alerts: Vec<DefaultAlert>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct DefaultAlert {
pub id: String,
pub offset: ICalendarDuration,
pub flags: u16,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct ParticipantIdentities {
pub identities: Vec<ParticipantIdentity>,
pub default_name: String,
pub default: u32,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct ParticipantIdentity {
pub id: u32,
pub name: Option<String>,
pub calendar_address: String,
}
pub const ALERT_WITH_TIME: u16 = 1;
pub const ALERT_EMAIL: u16 = 1 << 1;
pub const ALERT_RELATIVE_TO_END: u16 = 1 << 2;
pub const SCHEDULE_INBOX_ID: u32 = u32::MAX - 1;
pub const SCHEDULE_OUTBOX_ID: u32 = u32::MAX - 2;
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_NOTIFICATION_IS_DRAFT: u16 = 1;
pub const EVENT_NOTIFICATION_IS_CHANGE: u16 = 1 << 1;
pub const PREF_USE_DEFAULT_ALERTS: u16 = 1;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarEvent {
pub names: Vec<DavName>,
pub display_name: Option<String>,
pub data: CalendarEventData,
pub preferences: Vec<EventPreferences>,
pub flags: u16,
pub dead_properties: DeadProperty,
pub size: u32,
pub created: i64,
pub modified: i64,
pub schedule_tag: Option<u32>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarEventNotification {
pub event: ICalendar,
pub event_id: Option<u32>,
pub changed_by: ChangedBy,
pub flags: u16,
pub size: u32,
pub created: i64,
pub modified: i64,
}
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)]
pub enum ChangedBy {
PrincipalId(u32),
CalendarAddress(String),
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarEventData {
pub event: ICalendar,
pub time_ranges: Box<[ComponentTimeRange]>,
pub alarms: Box<[Alarm]>,
pub base_offset: i64,
pub base_time_utc: u32,
pub duration: u32,
}
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)]
#[rkyv(compare(PartialEq), derive(Debug))]
pub struct Alarm {
pub id: u16,
pub parent_id: u16,
pub delta: AlarmDelta,
pub is_email_alert: bool,
}
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)]
#[rkyv(compare(PartialEq), derive(Debug))]
pub enum AlarmDelta {
Start(i64),
End(i64),
FixedUtc(i64),
FixedFloating(i64),
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct ComponentTimeRange {
pub id: u16,
pub start_tz: u16,
pub end_tz: u16,
pub duration: i32,
pub instances: Box<[u8]>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct EventPreferences {
pub account_id: u32,
pub flags: u16,
pub properties: Vec<ICalendarEntry>,
pub alerts: Vec<ICalendarComponent>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub enum Timezone {
IANA(u16),
Custom(ICalendar),
#[default]
Default,
}
impl Calendar {
pub fn preferences(&self, account_id: u32) -> &CalendarPreferences {
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()
}
}
pub fn preferences_mut(&mut self, account_id: u32) -> &mut CalendarPreferences {
let idx = if let Some(idx) = self
.preferences
.iter()
.position(|p| p.account_id == account_id)
{
idx
} else {
let mut preferences = self.preferences[0].clone();
preferences.account_id = account_id;
self.preferences.push(preferences);
self.preferences.len() - 1
};
&mut self.preferences[idx]
}
}
impl ArchivedCalendar {
pub fn default_alerts(
&self,
account_id: u32,
with_time: bool,
) -> impl Iterator<Item = &ArchivedDefaultAlert> {
self.preferences(account_id)
.default_alerts
.iter()
.filter(move |a| (a.flags & ALERT_WITH_TIME != 0) == with_time)
}
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()
}
}
}
impl CalendarEvent {
pub fn preferences(&self, account_id: u32) -> Option<&EventPreferences> {
self.preferences.iter().find(|p| p.account_id == account_id)
}
pub fn preferences_mut(&mut self, account_id: u32) -> &mut EventPreferences {
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: 0,
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, account_id: u32) -> Option<&ArchivedEventPreferences> {
self.preferences.iter().find(|p| p.account_id == account_id)
}
}
impl Default for ChangedBy {
fn default() -> Self {
ChangedBy::CalendarAddress("".into())
}
}
impl From<u64> for SupportedComponent {
fn from(value: u64) -> Self {
match value {
0 => SupportedComponent::VCalendar,
1 => SupportedComponent::VEvent,
2 => SupportedComponent::VTodo,
3 => SupportedComponent::VJournal,
4 => SupportedComponent::VFreebusy,
5 => SupportedComponent::VTimezone,
6 => SupportedComponent::VAlarm,
7 => SupportedComponent::Standard,
8 => SupportedComponent::Daylight,
9 => SupportedComponent::VAvailability,
10 => SupportedComponent::Available,
11 => SupportedComponent::Participant,
12 => SupportedComponent::VLocation,
13 => SupportedComponent::VResource,
14 => SupportedComponent::VStatus,
_ => SupportedComponent::Other,
}
}
}
impl From<SupportedComponent> for u64 {
fn from(value: SupportedComponent) -> Self {
match value {
SupportedComponent::VCalendar => 0,
SupportedComponent::VEvent => 1,
SupportedComponent::VTodo => 2,
SupportedComponent::VJournal => 3,
SupportedComponent::VFreebusy => 4,
SupportedComponent::VTimezone => 5,
SupportedComponent::VAlarm => 6,
SupportedComponent::Standard => 7,
SupportedComponent::Daylight => 8,
SupportedComponent::VAvailability => 9,
SupportedComponent::Available => 10,
SupportedComponent::Participant => 11,
SupportedComponent::VLocation => 12,
SupportedComponent::VResource => 13,
SupportedComponent::VStatus => 14,
SupportedComponent::Other => 15,
}
}
}
impl BitmapItem for SupportedComponent {
fn max() -> u64 {
u64::from(SupportedComponent::Other)
}
fn is_valid(&self) -> bool {
!matches!(self, SupportedComponent::Other)
}
}
impl From<ICalendarComponentType> for SupportedComponent {
fn from(value: ICalendarComponentType) -> Self {
match value {
ICalendarComponentType::VCalendar => SupportedComponent::VCalendar,
ICalendarComponentType::VEvent => SupportedComponent::VEvent,
ICalendarComponentType::VTodo => SupportedComponent::VTodo,
ICalendarComponentType::VJournal => SupportedComponent::VJournal,
ICalendarComponentType::VFreebusy => SupportedComponent::VFreebusy,
ICalendarComponentType::VTimezone => SupportedComponent::VTimezone,
ICalendarComponentType::VAlarm => SupportedComponent::VAlarm,
ICalendarComponentType::Standard => SupportedComponent::Standard,
ICalendarComponentType::Daylight => SupportedComponent::Daylight,
ICalendarComponentType::VAvailability => SupportedComponent::VAvailability,
ICalendarComponentType::Available => SupportedComponent::Available,
ICalendarComponentType::Participant => SupportedComponent::Participant,
ICalendarComponentType::VLocation => SupportedComponent::VLocation,
ICalendarComponentType::VResource => SupportedComponent::VResource,
ICalendarComponentType::VStatus => SupportedComponent::VStatus,
_ => SupportedComponent::Other,
}
}
}
impl From<SupportedComponent> for ICalendarComponentType {
fn from(value: SupportedComponent) -> Self {
match value {
SupportedComponent::VCalendar => ICalendarComponentType::VCalendar,
SupportedComponent::VEvent => ICalendarComponentType::VEvent,
SupportedComponent::VTodo => ICalendarComponentType::VTodo,
SupportedComponent::VJournal => ICalendarComponentType::VJournal,
SupportedComponent::VFreebusy => ICalendarComponentType::VFreebusy,
SupportedComponent::VTimezone => ICalendarComponentType::VTimezone,
SupportedComponent::VAlarm => ICalendarComponentType::VAlarm,
SupportedComponent::Standard => ICalendarComponentType::Standard,
SupportedComponent::Daylight => ICalendarComponentType::Daylight,
SupportedComponent::VAvailability => ICalendarComponentType::VAvailability,
SupportedComponent::Available => ICalendarComponentType::Available,
SupportedComponent::Participant => ICalendarComponentType::Participant,
SupportedComponent::VLocation => ICalendarComponentType::VLocation,
SupportedComponent::VResource => ICalendarComponentType::VResource,
SupportedComponent::VStatus => ICalendarComponentType::VStatus,
SupportedComponent::Other => ICalendarComponentType::Other(Default::default()),
}
}
}
+634
View File
@@ -0,0 +1,634 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ArchivedCalendar, ArchivedCalendarEvent, Calendar, CalendarEvent, CalendarPreferences,
alarm::CalendarAlarm,
};
use crate::{
DavResourceName, DestroyArchive, RFC_3986,
calendar::{
ArchivedCalendarEventNotification, CalendarEventNotification, alarm::CalendarAlarmType,
},
scheduling::{ItipMessages, event_cancel::itip_cancel},
};
use calcard::common::timezone::Tz;
use common::{
Server,
auth::{AccountInfo, AccountTenantIds},
storage::index::ObjectIndexBuilder,
};
use registry::{
schema::structs::{Task, TaskCalendarAlarmEmail, TaskCalendarAlarmNotification, TaskStatus},
types::{EnumImpl, ObjectImpl, datetime::UTCDateTime},
};
use store::{
IterateParams, SerializeInfallible, U32_LEN, ValueKey,
roaring::RoaringBitmap,
write::{
AlignedBytes, Archive, BatchBuilder, IndexPropertyClass, Operation, TaskQueueClass,
ValueClass, ValueOp, key::DeserializeBigEndian, now,
},
};
use trc::AddContext;
use types::{
collection::{Collection, VanishedCollection},
field::CalendarNotificationField,
id::Id,
};
pub trait ItipAutoExpunge: Sync + Send {
fn itip_ids(&self, account_id: u32) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn itip_auto_expunge(
&self,
account_id: u32,
hold_period: u64,
) -> impl Future<Output = trc::Result<()>> + Send;
}
impl ItipAutoExpunge for Server {
async fn itip_ids(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
let mut document_ids = RoaringBitmap::new();
self.store()
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: 0,
}),
},
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: u64::MAX,
}),
},
)
.no_values()
.ascending(),
|key, _| {
document_ids.insert(key.deserialize_be_u32(key.len() - U32_LEN)?);
Ok(true)
},
)
.await
.caused_by(trc::location!())
.map(|_| document_ids)
}
async fn itip_auto_expunge(&self, account_id: u32, hold_period: u64) -> trc::Result<()> {
let mut destroy_ids = RoaringBitmap::new();
self.store()
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: 0,
}),
},
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: now().saturating_sub(hold_period),
}),
},
)
.no_values()
.ascending(),
|key, _| {
destroy_ids.insert(key.deserialize_be_u32(key.len() - U32_LEN)?);
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
if destroy_ids.is_empty() {
return Ok(());
}
trc::event!(
Store(trc::StoreEvent::AutoExpunge),
AccountId = account_id,
Collection = Collection::CalendarEventNotification.as_str(),
Total = destroy_ids.len(),
);
// Tombstone messages
let mut batch = BatchBuilder::new();
let changed_by = self
.account(account_id)
.await
.caused_by(trc::location!())?
.account_tenant_ids();
for document_id in destroy_ids {
// Fetch event
if let Some(event_) = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEventNotification,
document_id,
))
.await
.caused_by(trc::location!())?
{
let event = event_
.to_unarchived::<CalendarEventNotification>()
.caused_by(trc::location!())?;
DestroyArchive(event)
.delete(changed_by, account_id, document_id, &mut batch)
.caused_by(trc::location!())?;
}
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(())
}
}
impl CalendarEvent {
pub fn update<'x>(
self,
changed_by: AccountTenantIds,
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)
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(event)
.with_changes(new_event)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
pub fn insert(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
next_alarm: Option<CalendarAlarm>,
batch: &mut BatchBuilder,
) -> trc::Result<&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)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(event)
.with_changed_by(changed_by),
)
.map(|batch| {
if let Some(next_alarm) = next_alarm {
next_alarm.write_task(batch);
}
batch.commit_point()
})
}
}
impl Calendar {
pub fn insert(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<&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)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(calendar)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
pub fn update<'x>(
self,
changed_by: AccountTenantIds,
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)
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(calendar)
.with_changes(new_calendar)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
}
impl CalendarEventNotification {
pub fn insert(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<&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::CalendarEventNotification)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(event)
.with_changed_by(changed_by),
)
.map(|batch| batch.commit_point())
}
}
impl DestroyArchive<Archive<&ArchivedCalendar>> {
#[allow(clippy::too_many_arguments)]
pub async fn delete_with_events(
self,
server: &Server,
account_info: &AccountInfo,
account_id: u32,
document_id: u32,
children_ids: Vec<u32>,
delete_path: Option<String>,
send_itip: bool,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Process deletions
let calendar_id = document_id;
for document_id in children_ids {
if let Some(event_) = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEvent,
document_id,
))
.await?
{
DestroyArchive(
event_
.to_unarchived::<CalendarEvent>()
.caused_by(trc::location!())?,
)
.delete(
account_info,
account_id,
document_id,
calendar_id,
None,
send_itip,
batch,
)?;
}
}
self.delete(
account_info.account_tenant_ids(),
account_id,
document_id,
delete_path,
batch,
)
}
pub fn delete(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
delete_path: Option<String>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let calendar = self.0;
// Delete calendar
batch
.with_account_id(account_id)
.with_collection(Collection::Calendar)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(changed_by)
.with_current(calendar),
)
.caused_by(trc::location!())?;
if let Some(delete_path) = delete_path {
batch.log_vanished_item(VanishedCollection::Calendar, delete_path);
}
batch.commit_point();
Ok(())
}
}
impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
#[allow(clippy::too_many_arguments)]
pub fn delete(
self,
account_info: &AccountInfo,
account_id: u32,
document_id: u32,
calendar_id: u32,
delete_path: Option<String>,
send_itip: bool,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
if let Some(delete_idx) = self
.0
.inner
.names
.iter()
.position(|name| name.parent_id == calendar_id)
{
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)
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_changed_by(account_info.account_tenant_ids())
.with_current(event)
.with_changes(new_event),
)
.caused_by(trc::location!())?;
} else {
self.delete_all(account_info, account_id, document_id, send_itip, batch)?;
}
if let Some(delete_path) = delete_path {
batch.log_vanished_item(VanishedCollection::Calendar, delete_path);
}
batch.commit_point();
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn delete_all(
self,
account_info: &AccountInfo,
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)
.with_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, account_info.addresses(), true) {
ItipMessages::new(vec![messages])
.queue(batch)
.caused_by(trc::location!())?;
}
}
batch
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(account_info.account_tenant_ids())
.with_current(event),
)
.caused_by(trc::location!())?;
Ok(())
}
}
impl DestroyArchive<Archive<&ArchivedCalendarEventNotification>> {
#[allow(clippy::too_many_arguments)]
pub fn delete(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Delete event
batch
.with_account_id(account_id)
.with_collection(Collection::CalendarEventNotification)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(changed_by)
.with_current(self.0),
)
.caused_by(trc::location!())?
.commit_point();
Ok(())
}
}
impl CalendarAlarm {
pub fn build_write_ops(&self, account_id: u32, document_id: u32) -> [Operation; 2] {
let task = match &self.typ {
CalendarAlarmType::Email {
event_start,
event_start_tz,
event_end,
event_end_tz,
} => Task::CalendarAlarmEmail(TaskCalendarAlarmEmail {
account_id: account_id.into(),
document_id: document_id.into(),
alarm_id: self.alarm_id.into(),
event_id: self.event_id.into(),
event_end: UTCDateTime::from_timestamp(*event_end),
event_end_tz: (*event_end_tz).into(),
event_start: UTCDateTime::from_timestamp(*event_start),
event_start_tz: (*event_start_tz).into(),
status: TaskStatus::at(self.alarm_time),
}),
CalendarAlarmType::Display { recurrence_id } => {
Task::CalendarAlarmNotification(TaskCalendarAlarmNotification {
account_id: account_id.into(),
document_id: document_id.into(),
alarm_id: self.alarm_id.into(),
event_id: self.event_id.into(),
recurrence_id: *recurrence_id,
status: TaskStatus::at(self.alarm_time),
})
}
};
let id = Id::from_parts(account_id, document_id).id();
[
Operation::Value {
class: ValueClass::TaskQueue(TaskQueueClass::Due {
id,
due: self.alarm_time as u64,
}),
op: ValueOp::Set(task.object_type().to_id().serialize()),
},
Operation::Value {
class: ValueClass::TaskQueue(TaskQueueClass::Task { id }),
op: ValueOp::Set(task.to_pickled_vec()),
},
]
}
pub fn write_task(&self, batch: &mut BatchBuilder) {
let account_id = batch.last_account_id().unwrap();
let document_id = batch.last_document_id().unwrap();
for op in self.build_write_ops(account_id, document_id) {
batch.any_op(op);
}
}
pub fn delete_task(&self, batch: &mut BatchBuilder) {
let account_id = batch.last_account_id().unwrap();
let document_id = batch.last_document_id().unwrap();
let id = Id::from_parts(account_id, document_id).id();
batch
.clear(ValueClass::TaskQueue(TaskQueueClass::Task { id }))
.clear(ValueClass::TaskQueue(TaskQueueClass::Due {
id,
due: self.alarm_time as u64,
}));
}
}
impl ArchivedCalendarEvent {
pub async fn webcal_uri(
&self,
server: &Server,
account_info: &AccountInfo,
) -> trc::Result<String> {
for event_name in self.names.iter() {
if let Some(calendar_) = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_info.account_id(),
Collection::Calendar,
event_name.parent_id.to_native(),
))
.await
.caused_by(trc::location!())?
{
let calendar = calendar_
.unarchive::<Calendar>()
.caused_by(trc::location!())?;
return Ok(format!(
"webcal://{}{}/{}/{}/{}",
server.core.network.server_name,
DavResourceName::Cal.base_path(),
percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986),
calendar.name,
event_name.name
));
}
}
Err(trc::StoreEvent::UnexpectedError
.into_err()
.details("Event is not linked to any calendar"))
}
}
+342
View File
@@ -0,0 +1,342 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard};
use ahash::AHashSet;
use calcard::{
common::IanaString,
vcard::{ArchivedVCardProperty, ArchivedVCardValue, VCardProperty},
};
use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject};
use nlp::language::{
Language,
detect::{LanguageDetector, MIN_LANGUAGE_SCORE},
};
use store::{
search::{ContactSearchField, IndexDocument, SearchField},
write::{IndexPropertyClass, SearchIndex, ValueClass},
xxhash_rust::xxh3,
};
use types::{acl::AclGrant, collection::SyncCollection, field::ContactField};
use utils::sanitize_email;
impl IndexableObject for AddressBook {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Acl {
value: (&self.acls).into(),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogContainer {
sync_collection: SyncCollection::AddressBook,
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedAddressBook {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Acl {
value: self
.acls
.iter()
.map(AclGrant::from)
.collect::<Vec<_>>()
.into(),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogContainer {
sync_collection: SyncCollection::AddressBook,
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for AddressBook {
fn is_versioned() -> bool {
true
}
}
impl IndexableObject for ContactCard {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Index {
field: ContactField::Uid.into(),
value: self.card.uid().into(),
},
IndexValue::Index {
field: ContactField::Email.into(),
value: self.emails().next().into(),
},
IndexValue::Property {
field: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: ContactField::CreatedToUpdated.into(),
value: self.created as u64,
}),
value: self.modified.into(),
},
IndexValue::SearchIndex {
index: SearchIndex::Contacts,
hash: self.hashes().fold(0, |acc, hash| acc ^ hash),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogItem {
sync_collection: SyncCollection::AddressBook,
prefix: None,
},
]
.into_iter()
}
}
impl IndexableObject for &ArchivedContactCard {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Index {
field: ContactField::Uid.into(),
value: self.card.uid().into(),
},
IndexValue::Index {
field: ContactField::Email.into(),
value: self.emails().next().into(),
},
IndexValue::Property {
field: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: ContactField::CreatedToUpdated.into(),
value: self.created.to_native() as u64,
}),
value: (self.modified.to_native() as u64).into(),
},
IndexValue::SearchIndex {
index: SearchIndex::Contacts,
hash: self.hashes().fold(0, |acc, hash| acc ^ hash),
},
IndexValue::Quota {
used: self.size() as u32,
},
IndexValue::LogItem {
sync_collection: SyncCollection::AddressBook,
prefix: None,
},
]
.into_iter()
}
}
impl IndexableAndSerializableObject for ContactCard {
fn is_versioned() -> bool {
true
}
}
impl AddressBook {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self
.preferences
.iter()
.map(|p| p.name.len() + p.description.as_ref().map_or(0, |n| n.len()))
.sum::<usize>()
+ self.name.len()
+ std::mem::size_of::<AddressBook>()
}
}
impl ArchivedAddressBook {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self
.preferences
.iter()
.map(|p| p.name.len() + p.description.as_ref().map_or(0, |n| n.len()))
.sum::<usize>()
+ self.name.len()
+ std::mem::size_of::<AddressBook>()
}
}
impl ContactCard {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.display_name.as_ref().map_or(0, |n| n.len())
+ self.names.iter().map(|n| n.name.len()).sum::<usize>()
+ self.size as usize
+ std::mem::size_of::<ContactCard>()
}
pub fn hashes(&self) -> impl Iterator<Item = u64> {
self.card
.entries
.iter()
.filter(|e| {
matches!(
e.name,
VCardProperty::Adr
| VCardProperty::N
| VCardProperty::Fn
| VCardProperty::Title
| VCardProperty::Org
| VCardProperty::Note
| VCardProperty::Nickname
| VCardProperty::Email
| VCardProperty::Kind
| VCardProperty::Uid
| VCardProperty::Member
| VCardProperty::Impp
| VCardProperty::Socialprofile
| VCardProperty::Tel
)
})
.flat_map(|e| e.values.iter().filter_map(|v| v.as_text()))
.map(|v| xxh3::xxh3_64(v.as_bytes()))
}
pub fn emails(&self) -> impl Iterator<Item = String> {
self.card.properties(&VCardProperty::Email).flat_map(|e| {
e.values
.iter()
.filter_map(|v| v.as_text().and_then(sanitize_email))
})
}
}
impl ArchivedContactCard {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.display_name.as_ref().map_or(0, |n| n.len())
+ self.names.iter().map(|n| n.name.len()).sum::<usize>()
+ self.size.to_native() as usize
+ std::mem::size_of::<ContactCard>()
}
pub fn hashes(&self) -> impl Iterator<Item = u64> {
self.card
.entries
.iter()
.filter(|e| {
matches!(
e.name,
ArchivedVCardProperty::Adr
| ArchivedVCardProperty::N
| ArchivedVCardProperty::Fn
| ArchivedVCardProperty::Title
| ArchivedVCardProperty::Org
| ArchivedVCardProperty::Note
| ArchivedVCardProperty::Nickname
| ArchivedVCardProperty::Email
| ArchivedVCardProperty::Kind
| ArchivedVCardProperty::Uid
| ArchivedVCardProperty::Member
| ArchivedVCardProperty::Impp
| ArchivedVCardProperty::Socialprofile
| ArchivedVCardProperty::Tel
)
})
.flat_map(|e| e.values.iter().filter_map(|v| v.as_text()))
.map(|v| xxh3::xxh3_64(v.as_bytes()))
}
pub fn emails(&self) -> impl Iterator<Item = String> {
self.card.properties(&VCardProperty::Email).flat_map(|e| {
e.values
.iter()
.filter_map(|v| v.as_text().and_then(sanitize_email))
})
}
pub fn index_document(
&self,
account_id: u32,
document_id: u32,
index_fields: &AHashSet<SearchField>,
default_language: Language,
) -> IndexDocument {
let mut document = IndexDocument::new(SearchIndex::Contacts)
.with_account_id(account_id)
.with_document_id(document_id);
let mut detector = LanguageDetector::new();
for entry in self.card.entries.iter() {
let (is_text, is_keyword, field) = match entry.name {
ArchivedVCardProperty::N => (false, false, ContactSearchField::Name),
ArchivedVCardProperty::Nickname => (false, false, ContactSearchField::Nickname),
ArchivedVCardProperty::Org => (false, false, ContactSearchField::Organization),
ArchivedVCardProperty::Email => (false, false, ContactSearchField::Email),
ArchivedVCardProperty::Tel => (false, false, ContactSearchField::Phone),
ArchivedVCardProperty::Impp | ArchivedVCardProperty::Socialprofile => {
(false, false, ContactSearchField::OnlineService)
}
ArchivedVCardProperty::Adr => (false, false, ContactSearchField::Address),
ArchivedVCardProperty::Note => (true, false, ContactSearchField::Note),
ArchivedVCardProperty::Kind => (false, true, ContactSearchField::Kind),
ArchivedVCardProperty::Uid => (false, true, ContactSearchField::Uid),
ArchivedVCardProperty::Member => (false, false, ContactSearchField::Member),
_ => continue,
};
let field = SearchField::Contact(field);
if index_fields.is_empty() || index_fields.contains(&field) {
for value in entry.values.iter() {
match value {
ArchivedVCardValue::Text(v) => {
if !is_keyword {
let lang = if is_text {
detector.detect(v.as_str().trim(), MIN_LANGUAGE_SCORE);
Language::Unknown
} else {
Language::None
};
document.index_text(field.clone(), v, lang);
} else {
document.index_keyword(field.clone(), v.as_str());
}
}
ArchivedVCardValue::Kind(v) => {
document.index_keyword(field.clone(), v.as_str());
}
ArchivedVCardValue::Component(v) => {
for item in v.iter() {
document.index_text(field.clone(), item.trim(), Language::None);
}
}
_ => (),
}
}
/*for param in entry.params.iter() {
if let ArchivedVCardParameterValue::Text(value) = &param.value {
let lang = if is_text {
detector.detect(value.as_str(), MIN_LANGUAGE_SCORE);
Language::Unknown
} else {
Language::None
};
document.index_text(field.clone(), value, lang);
}
}*/
}
}
document.set_unknown_language(
detector
.most_frequent_language()
.unwrap_or(default_language),
);
document
}
}
+128
View File
@@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod index;
pub mod storage;
use calcard::vcard::VCard;
use common::DavName;
use types::{acl::AclGrant, dead_property::DeadProperty};
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct AddressBook {
pub name: String,
pub preferences: Vec<AddressBookPreferences>,
pub subscribers: Vec<u32>,
pub dead_properties: DeadProperty,
pub acls: Vec<AclGrant>,
pub created: i64,
pub modified: i64,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct AddressBookPreferences {
pub account_id: u32,
pub name: String,
pub description: Option<String>,
pub sort_order: u32,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct ContactCard {
pub names: Vec<DavName>,
pub display_name: Option<String>,
pub card: VCard,
pub dead_properties: DeadProperty,
pub created: i64,
pub modified: i64,
pub size: u32,
}
impl AddressBook {
pub fn preferences(&self, account_id: u32) -> &AddressBookPreferences {
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()
}
}
pub fn preferences_mut(&mut self, account_id: u32) -> &mut AddressBookPreferences {
let idx = if let Some(idx) = self
.preferences
.iter()
.position(|p| p.account_id == account_id)
{
idx
} else {
let mut preferences = self.preferences[0].clone();
preferences.account_id = account_id;
self.preferences.push(preferences);
self.preferences.len() - 1
};
&mut self.preferences[idx]
}
}
impl ArchivedAddressBook {
pub fn preferences(&self, account_id: u32) -> &ArchivedAddressBookPreferences {
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()
}
}
}
impl ContactCard {
pub fn added_addressbook_ids(
&self,
prev_data: &ArchivedContactCard,
) -> 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_addressbook_ids(
&self,
prev_data: &ArchivedContactCard,
) -> 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_addressbook_ids(
&self,
prev_data: &ArchivedContactCard,
) -> 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)
}
}
+279
View File
@@ -0,0 +1,279 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard};
use crate::DestroyArchive;
use common::{Server, auth::AccountTenantIds, storage::index::ObjectIndexBuilder};
use store::{
ValueKey,
write::{AlignedBytes, Archive, BatchBuilder, now},
};
use trc::AddContext;
use types::collection::{Collection, VanishedCollection};
impl ContactCard {
pub fn update<'x>(
self,
changed_by: AccountTenantIds,
card: Archive<&ArchivedContactCard>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
let mut new_card = self;
// Build card
new_card.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(card)
.with_changes(new_card)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
pub fn insert(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<&mut BatchBuilder> {
// Build card
let mut card = self;
let now = now() as i64;
card.modified = now;
card.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(card)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
}
impl AddressBook {
pub fn insert(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<&mut BatchBuilder> {
// Build address book
let mut book = self;
let now = now() as i64;
book.modified = now;
book.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(book)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
pub fn update<'x>(
self,
changed_by: AccountTenantIds,
book: Archive<&ArchivedAddressBook>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build address book
let mut new_book = self;
new_book.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(book)
.with_changes(new_book)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
}
impl DestroyArchive<Archive<&ArchivedAddressBook>> {
#[allow(clippy::too_many_arguments)]
pub async fn delete_with_cards(
self,
server: &Server,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
children_ids: Vec<u32>,
delete_path: Option<String>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Process deletions
let addressbook_id = document_id;
for document_id in children_ids {
if let Some(card_) = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
document_id,
))
.await?
{
DestroyArchive(
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
)
.delete(
changed_by,
account_id,
document_id,
addressbook_id,
None,
batch,
)?;
}
}
self.delete(changed_by, account_id, document_id, delete_path, batch)
}
pub fn delete(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
delete_path: Option<String>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let book = self.0;
// Delete addressbook
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(changed_by)
.with_current(book),
)
.caused_by(trc::location!())?;
if let Some(delete_path) = delete_path {
batch.log_vanished_item(VanishedCollection::AddressBook, delete_path);
}
batch.commit_point();
Ok(())
}
}
impl DestroyArchive<Archive<&ArchivedContactCard>> {
pub fn delete(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
addressbook_id: u32,
delete_path: Option<String>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let card = self.0;
if let Some(delete_idx) = card
.inner
.names
.iter()
.position(|name| name.parent_id == addressbook_id)
{
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard);
if card.inner.names.len() > 1 {
// Unlink addressbook id from card
let mut new_card = card
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
new_card.names.swap_remove(delete_idx);
batch
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_changed_by(changed_by)
.with_current(card)
.with_changes(new_card),
)
.caused_by(trc::location!())?;
} else {
// Delete card
batch
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(changed_by)
.with_current(card),
)
.caused_by(trc::location!())?;
}
if let Some(delete_path) = delete_path {
batch.log_vanished_item(VanishedCollection::AddressBook, delete_path);
}
batch.commit_point();
}
Ok(())
}
pub fn delete_all(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(changed_by)
.with_current(self.0),
)
.caused_by(trc::location!())
.map(|b| {
b.commit_point();
})
}
}
+97
View File
@@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{ArchivedFileNode, FileNode};
use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject};
use types::{acl::AclGrant, collection::SyncCollection};
impl IndexableObject for FileNode {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
let mut values = Vec::with_capacity(6);
values.extend([
IndexValue::Acl {
value: (&self.acls).into(),
},
IndexValue::LogItem {
prefix: None,
sync_collection: SyncCollection::FileNode,
},
IndexValue::Quota {
used: self.size() as u32,
},
]);
if let Some(file) = &self.file {
values.extend([IndexValue::Blob {
value: file.blob_hash.clone(),
}]);
}
values.into_iter()
}
}
impl IndexableObject for &ArchivedFileNode {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
let mut values = Vec::with_capacity(6);
values.extend([
IndexValue::Acl {
value: self
.acls
.iter()
.map(AclGrant::from)
.collect::<Vec<_>>()
.into(),
},
IndexValue::LogItem {
prefix: None,
sync_collection: SyncCollection::FileNode,
},
IndexValue::Quota {
used: self.size() as u32,
},
]);
if let Some(file) = self.file.as_ref() {
values.extend([IndexValue::Blob {
value: (&file.blob_hash).into(),
}]);
}
values.into_iter()
}
}
impl IndexableAndSerializableObject for FileNode {
fn is_versioned() -> bool {
true
}
}
impl FileNode {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.display_name.as_ref().map_or(0, |n| n.len())
+ self.name.len()
+ self.file.as_ref().map_or(0, |f| f.size as usize)
+ std::mem::size_of::<FileNode>()
}
}
impl ArchivedFileNode {
pub fn size(&self) -> usize {
self.dead_properties.size()
+ self.display_name.as_ref().map_or(0, |n| n.len())
+ self.name.len()
+ self
.file
.as_ref()
.map_or(0, |f| f.size.to_native() as usize)
+ std::mem::size_of::<FileNode>()
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod index;
pub mod storage;
use types::{acl::AclGrant, blob_hash::BlobHash, dead_property::DeadProperty};
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct FileNode {
pub parent_id: u32,
pub name: String,
pub display_name: Option<String>,
pub file: Option<FileProperties>,
pub created: i64,
pub modified: i64,
pub dead_properties: DeadProperty,
pub acls: Vec<AclGrant>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
#[rkyv(derive(Debug))]
pub struct FileProperties {
pub blob_hash: BlobHash,
pub size: u32,
pub media_type: Option<String>,
pub executable: bool,
}
+170
View File
@@ -0,0 +1,170 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{ArchivedFileNode, FileNode};
use crate::DestroyArchive;
use common::{Server, auth::AccountTenantIds, storage::index::ObjectIndexBuilder};
use store::{
ValueKey,
write::{AlignedBytes, Archive, BatchBuilder, now},
};
use trc::AddContext;
use types::collection::{Collection, VanishedCollection};
impl FileNode {
pub fn insert(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
set_created: bool,
set_modified: bool,
batch: &mut BatchBuilder,
) -> trc::Result<&mut BatchBuilder> {
let mut node = self;
let now = now() as i64;
if set_created {
node.created = now;
}
if set_modified {
node.modified = now;
}
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
pub fn update<'x>(
self,
changed_by: AccountTenantIds,
node: Archive<&ArchivedFileNode>,
account_id: u32,
document_id: u32,
set_modified: bool,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
let mut new_node = self;
if set_modified {
new_node.modified = now() as i64;
}
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(node)
.with_changes(new_node)
.with_changed_by(changed_by),
)
.map(|b| b.commit_point())
}
}
impl DestroyArchive<Archive<&ArchivedFileNode>> {
pub fn delete(
self,
changed_by: AccountTenantIds,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
path: String,
) -> trc::Result<()> {
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_current(self.0)
.with_changed_by(changed_by),
)?
.log_vanished_item(VanishedCollection::FileNode, path)
.commit_point();
Ok(())
}
}
impl DestroyArchive<Vec<u32>> {
pub async fn delete(
self,
server: &Server,
changed_by: AccountTenantIds,
account_id: u32,
delete_path: Option<String>,
) -> trc::Result<()> {
// Process deletions
let mut batch = BatchBuilder::new();
self.delete_batch(server, changed_by, account_id, delete_path, &mut batch)
.await?;
// Write changes
if !batch.is_empty() {
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
}
Ok(())
}
pub async fn delete_batch(
self,
server: &Server,
changed_by: AccountTenantIds,
account_id: u32,
delete_path: Option<String>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Process deletions
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode);
for document_id in self.0 {
if let Some(node) = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
document_id,
))
.await?
{
// Delete record
batch
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(changed_by)
.with_current(
node.to_unarchived::<FileNode>()
.caused_by(trc::location!())?,
),
)
.caused_by(trc::location!())?
.commit_point();
}
}
if !batch.is_empty()
&& let Some(delete_path) = delete_path
{
batch.log_vanished_item(VanishedCollection::FileNode, delete_path);
}
Ok(())
}
}
+287
View File
@@ -0,0 +1,287 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#![warn(clippy::large_futures)]
use calcard::common::timezone::Tz;
use common::DavResources;
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
use std::borrow::Cow;
use types::collection::{Collection, SyncCollection};
pub mod cache;
pub mod calendar;
pub mod contact;
pub mod file;
pub mod scheduling;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DavResourceName {
Card,
Cal,
File,
Principal,
Scheduling,
}
pub const RFC_3986: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'!')
.add(b'"')
.add(b'#')
.add(b'$')
.add(b'%')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
fn is_pchar(byte: u8) -> bool {
matches!(byte,
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'-'
| b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@')
}
pub fn is_uri_segment(name: &str) -> bool {
let mut bytes = name.as_bytes().iter();
while let Some(&byte) = bytes.next() {
if byte == b'%' {
if !bytes.next().is_some_and(u8::is_ascii_hexdigit)
|| !bytes.next().is_some_and(u8::is_ascii_hexdigit)
{
return false;
}
} else if !is_pchar(byte) {
return false;
}
}
true
}
pub fn encode_path_segment(name: &str) -> Cow<'_, str> {
if is_uri_segment(name) {
Cow::Borrowed(name)
} else {
utf8_percent_encode(name, RFC_3986).into()
}
}
pub struct DestroyArchive<T>(pub T);
impl DavResourceName {
pub fn parse(service: &str) -> Option<Self> {
hashify::tiny_map!(service.as_bytes(),
"card" => DavResourceName::Card,
"cal" => DavResourceName::Cal,
"file" => DavResourceName::File,
"pal" => DavResourceName::Principal,
"itip" => DavResourceName::Scheduling,
)
}
pub fn base_path(&self) -> &'static str {
match self {
DavResourceName::Card => "/dav/card",
DavResourceName::Cal => "/dav/cal",
DavResourceName::File => "/dav/file",
DavResourceName::Principal => "/dav/pal",
DavResourceName::Scheduling => "/dav/itip",
}
}
pub fn collection_path(&self) -> &'static str {
match self {
DavResourceName::Card => "/dav/card/",
DavResourceName::Cal => "/dav/cal/",
DavResourceName::File => "/dav/file/",
DavResourceName::Principal => "/dav/pal/",
DavResourceName::Scheduling => "/dav/itip/",
}
}
pub fn name(&self) -> &'static str {
match self {
DavResourceName::Card => "CardDAV",
DavResourceName::Cal => "CalDAV",
DavResourceName::File => "WebDAV",
DavResourceName::Principal => "Principal",
DavResourceName::Scheduling => "Scheduling",
}
}
}
impl From<DavResourceName> for Collection {
fn from(value: DavResourceName) -> Self {
match value {
DavResourceName::Card => Collection::AddressBook,
DavResourceName::Cal => Collection::Calendar,
DavResourceName::File => Collection::FileNode,
DavResourceName::Principal => Collection::Principal,
DavResourceName::Scheduling => Collection::CalendarEventNotification,
}
}
}
impl From<Collection> for DavResourceName {
fn from(value: Collection) -> Self {
match value {
Collection::AddressBook => DavResourceName::Card,
Collection::Calendar => DavResourceName::Cal,
Collection::FileNode => DavResourceName::File,
Collection::Principal => DavResourceName::Principal,
Collection::CalendarEventNotification => DavResourceName::Scheduling,
_ => unreachable!(),
}
}
}
impl From<SyncCollection> for DavResourceName {
fn from(value: SyncCollection) -> Self {
match value {
SyncCollection::AddressBook => DavResourceName::Card,
SyncCollection::Calendar => DavResourceName::Cal,
SyncCollection::FileNode => DavResourceName::File,
SyncCollection::CalendarEventNotification => DavResourceName::Scheduling,
_ => unreachable!(),
}
}
}
pub trait DavCalendarResource {
fn calendar_default_tz(&self, calendar_id: u32, account_id: u32) -> Option<Tz>;
}
impl DavCalendarResource for DavResources {
fn calendar_default_tz(&self, calendar_id: u32, account_id: u32) -> Option<Tz> {
self.container_resource_by_id(calendar_id)
.and_then(|c| c.calendar_preferences(account_id))
.map(|p| p.tz)
}
}
pub fn strip_mailto_scheme(value: &str) -> &str {
value
.split_once(':')
.filter(|(scheme, _)| scheme.eq_ignore_ascii_case("mailto"))
.map_or(value, |(_, address)| address.trim())
}
pub fn decode_mailto_address(value: &str) -> Cow<'_, str> {
match value.split_once(':') {
Some((scheme, address)) if scheme.eq_ignore_ascii_case("mailto") => {
let address = address.trim();
let address = address.split_once('?').map_or(address, |(to, _)| to);
percent_decode_str(address).decode_utf8_lossy()
}
_ => Cow::Borrowed(value),
}
}
pub fn extract_addr_spec(value: &str) -> Option<&str> {
value
.rsplit_once('<')
.and_then(|(_, rest)| rest.split_once('>'))
.map(|(addr, _)| addr.trim())
.filter(|addr| !addr.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_segments_from_uris_are_preserved() {
for name in [
"readme.txt",
"My%20Folder",
"%C3%9Cnterlagen.txt",
"file(1).txt",
"a+b.txt",
"Q&A.txt",
"it's.txt",
"[email protected]",
"a:b.txt",
"notes;v=2,rev=3!$*=.txt",
"~backup_1-2.txt",
] {
assert!(is_uri_segment(name), "{name:?}");
assert_eq!(encode_path_segment(name), name);
}
}
#[test]
fn path_segments_from_names_are_encoded() {
for (name, expected) in [
("My Folder", "My%20Folder"),
("Ünterlagen.txt", "%C3%9Cnterlagen.txt"),
("Ünterlagen 2026.txt", "%C3%9Cnterlagen%202026.txt"),
("100%", "100%25"),
("100%2", "100%252"),
("100%zz", "100%25zz"),
("a/b.txt", "a%2Fb.txt"),
("a<b>c.txt", "a%3Cb%3Ec.txt"),
("a\"b#c?d.txt", "a%22b%23c%3Fd.txt"),
("a\tb.txt", "a%09b.txt"),
] {
assert!(!is_uri_segment(name), "{name:?}");
assert_eq!(encode_path_segment(name), expected, "{name:?}");
}
}
#[test]
fn encoded_path_segments_are_stable() {
for name in [
"My Folder",
"Ünterlagen 2026.txt",
"100%",
"a/b.txt",
"file(1).txt",
] {
let encoded = encode_path_segment(name).into_owned();
assert!(is_uri_segment(&encoded), "{encoded:?}");
assert_eq!(encode_path_segment(&encoded), encoded, "{name:?}");
}
}
}
+377
View File
@@ -0,0 +1,377 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
Email, InstanceId, ItipEntries, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot,
ItipSnapshots, ItipSummary,
itip::{
ItipExportAs, can_attendee_modify_property, itip_add_tz, itip_build_envelope,
itip_export_component,
},
organizer::organizer_request_full,
};
use ahash::{AHashMap, AHashSet};
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarParameter,
ICalendarParticipationStatus, ICalendarProperty, ICalendarValue,
},
};
pub(crate) fn attendee_handle_update(
new_ical: &ICalendar,
old_itip: ItipSnapshots<'_>,
new_itip: ItipSnapshots<'_>,
) -> Result<Vec<ItipMessage<ICalendar>>, ItipError> {
let dt_stamp = PartialDateTime::now();
let mut message = ICalendar {
components: Vec::with_capacity(2),
};
message
.components
.push(itip_build_envelope(ICalendarMethod::Reply));
let mut mail_from = None;
let mut email_rcpt = AHashSet::new();
let mut new_delegates = AHashSet::new();
let mut part_stat = &ICalendarParticipationStatus::NeedsAction;
for (instance_id, instance) in &new_itip.components {
if let Some(old_instance) = old_itip.components.get(instance_id) {
match (instance.local_attendee(), old_instance.local_attendee()) {
(Some(local_attendee), Some(old_local_attendee))
if local_attendee.email == old_local_attendee.email =>
{
// Distinguish a genuine add/remove of a restricted property from a value-only drift
// caused by a client re-encoding the same property
let old_name_counts = count_entry_names(&old_instance.entries);
let new_name_counts = count_entry_names(&instance.entries);
// Check added fields
let mut send_update = false;
for new_entry in instance.entries.difference(&old_instance.entries) {
match (new_entry.name, &new_entry.value) {
(ICalendarProperty::Exdate, ItipEntryValue::DateTime(date))
if instance_id == &InstanceId::Main =>
{
if let Some((mut cancel_comp, attendee_email)) = attendee_decline(
instance_id,
&old_itip,
old_instance,
&dt_stamp,
&mut email_rcpt,
false,
) {
// Add EXDATE as RECURRENCE-ID
cancel_comp
.entries
.push(date.to_entry(ICalendarProperty::RecurrenceId));
part_stat = &ICalendarParticipationStatus::Declined;
// Add cancel component
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(cancel_comp);
mail_from = Some(&attendee_email.email);
}
}
_ => {
// Changing these properties is not allowed
if !can_attendee_modify_property(
&instance.comp.component_type,
new_entry.name,
) {
if name_count(&new_name_counts, new_entry.name)
> name_count(&old_name_counts, new_entry.name)
{
return Err(ItipError::CannotModifyProperty(
new_entry.name.clone(),
));
}
} else {
send_update = send_update
|| (instance.comp.component_type
== ICalendarComponentType::VTodo
&& matches!(
new_entry.name,
ICalendarProperty::Status
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed
));
}
}
}
}
// Send participation status update
if local_attendee.is_server_scheduling
&& ((local_attendee.part_stat != old_local_attendee.part_stat)
|| local_attendee.force_send.is_some()
|| send_update)
{
// Build the attendee list
if let Some(new_partstat) = local_attendee.part_stat {
part_stat = new_partstat;
}
let mut attendee_entry_uids = vec![local_attendee.entry_id];
let old_delegates = old_instance
.external_attendees()
.filter(|a| a.is_delegated_from(old_local_attendee))
.map(|a| a.email.email.as_str())
.collect::<AHashSet<_>>();
for external_attendee in instance.external_attendees() {
if external_attendee.is_delegated_from(local_attendee) {
if external_attendee.send_invite_messages()
&& !old_delegates
.contains(&external_attendee.email.email.as_str())
{
new_delegates.insert(external_attendee.email.email.as_str());
}
} else if external_attendee.is_delegated_to(local_attendee) {
if external_attendee.send_update_messages() {
email_rcpt.insert(external_attendee.email.email.as_str());
}
} else {
continue;
}
attendee_entry_uids.push(external_attendee.entry_id);
}
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(itip_export_component(
instance.comp,
new_itip.uid,
&dt_stamp,
instance.sequence.unwrap_or_default(),
ItipExportAs::Attendee(attendee_entry_uids),
));
mail_from = Some(&local_attendee.email.email);
}
// Check removed fields
for removed_entry in old_instance.entries.difference(&instance.entries) {
if !can_attendee_modify_property(
&instance.comp.component_type,
removed_entry.name,
) && name_count(&old_name_counts, removed_entry.name)
> name_count(&new_name_counts, removed_entry.name)
{
// Removing these properties is not allowed
return Err(ItipError::CannotModifyProperty(
removed_entry.name.clone(),
));
}
}
}
_ => {
// Change in local attendee email is not allowed
return Err(ItipError::CannotModifyAddress);
}
}
} else if let Some(local_attendee) = instance
.local_attendee()
.filter(|_| instance_id != &InstanceId::Main)
{
let mut attendee_entry_uids = vec![local_attendee.entry_id];
for external_attendee in instance.external_attendees() {
if external_attendee.is_delegated_from(local_attendee) {
if external_attendee.send_invite_messages() {
new_delegates.insert(external_attendee.email.email.as_str());
}
} else if external_attendee.is_delegated_to(local_attendee) {
if external_attendee.send_update_messages() {
email_rcpt.insert(external_attendee.email.email.as_str());
}
} else {
continue;
}
attendee_entry_uids.push(external_attendee.entry_id);
}
// A new instance has been added
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(itip_export_component(
instance.comp,
new_itip.uid,
&dt_stamp,
instance.sequence.unwrap_or_default(),
ItipExportAs::Attendee(attendee_entry_uids),
));
mail_from = Some(&local_attendee.email.email);
} else {
return Err(ItipError::CannotModifyInstance);
}
}
for (instance_id, old_instance) in &old_itip.components {
if !new_itip.components.contains_key(instance_id) {
if instance_id != &InstanceId::Main && old_instance.has_local_attendee() {
// Send cancel message for removed instances
if let Some((cancel_comp, attendee_email)) = attendee_decline(
instance_id,
&old_itip,
old_instance,
&dt_stamp,
&mut email_rcpt,
false,
) {
// Add cancel component
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(cancel_comp);
mail_from = Some(&attendee_email.email);
}
} else {
// Removing instances is not allowed
return Err(ItipError::CannotModifyInstance);
}
}
}
if let Some(from) = mail_from {
email_rcpt.insert(&new_itip.organizer.email.email);
// Add timezones if needed
itip_add_tz(&mut message, new_ical);
let mut responses = vec![ItipMessage {
from: from.to_string(),
from_organizer: false,
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
summary: ItipSummary::Rsvp {
part_stat: part_stat.clone(),
current: new_itip
.main_instance_or_default()
.build_summary(Some(&new_itip.organizer), &[]),
},
message,
}];
// Invite new delegates
if !new_delegates.is_empty() {
let from = from.to_string();
let new_delegates = new_delegates
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<_>>();
if let Ok(messages_) = organizer_request_full(new_ical, &new_itip, None, true) {
for mut message in messages_ {
message.from = from.clone();
message.to = new_delegates.clone();
message.from_organizer = false;
responses.push(message);
}
}
}
Ok(responses)
} else {
Err(ItipError::NothingToSend)
}
}
pub(crate) fn attendee_decline<'x>(
instance_id: &'x InstanceId,
itip: &'x ItipSnapshots<'x>,
comp: &'x ItipSnapshot<'x>,
dt_stamp: &'x PartialDateTime,
email_rcpt: &mut AHashSet<&'x str>,
skip_needs_action: bool,
) -> Option<(ICalendarComponent, &'x Email)> {
let component = comp.comp;
let mut cancel_comp = ICalendarComponent {
component_type: component.component_type.clone(),
entries: Vec::with_capacity(5),
component_ids: vec![],
};
let mut local_attendee = None;
let mut delegated_from = None;
for attendee in &comp.attendees {
if attendee.email.is_local {
if attendee.is_server_scheduling
&& attendee.rsvp.is_none_or(|rsvp| rsvp)
&& match attendee.part_stat {
Some(
ICalendarParticipationStatus::Declined
| ICalendarParticipationStatus::Delegated,
) => attendee.force_send.is_some(),
Some(ICalendarParticipationStatus::NeedsAction) => !skip_needs_action,
_ => true,
}
{
local_attendee = Some(attendee);
}
} else if attendee.delegated_to.iter().any(|d| d.is_local) {
cancel_comp
.entries
.push(component.entries[attendee.entry_id as usize].clone());
delegated_from = Some(&attendee.email.email);
}
}
local_attendee.map(|local_attendee| {
cancel_comp.add_property(
ICalendarProperty::Organizer,
ICalendarValue::Text(itip.organizer.email.to_string()),
);
cancel_comp.add_property_with_params(
ICalendarProperty::Attendee,
[ICalendarParameter::partstat(
ICalendarParticipationStatus::Declined,
)],
ICalendarValue::Text(local_attendee.email.to_string()),
);
cancel_comp.add_uid(itip.uid);
cancel_comp.add_dtstamp(dt_stamp.clone());
cancel_comp.add_sequence(comp.sequence.unwrap_or_default());
cancel_comp.entries.extend(
component
.entries
.iter()
.filter(|e| {
matches!(
e.name,
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Description
| ICalendarProperty::Summary
)
})
.cloned(),
);
if let InstanceId::Recurrence(recurrence_id) = instance_id {
cancel_comp
.entries
.push(component.entries[recurrence_id.entry_id as usize].clone());
}
if let Some(delegated_from) = delegated_from {
email_rcpt.insert(delegated_from);
}
(cancel_comp, &local_attendee.email)
})
}
fn count_entry_names<'x>(entries: &'x ItipEntries<'x>) -> AHashMap<&'x ICalendarProperty, usize> {
let mut counts = AHashMap::with_capacity(entries.len());
for entry in entries {
*counts.entry(entry.name).or_insert(0) += 1;
}
counts
}
#[inline]
fn name_count(counts: &AHashMap<&ICalendarProperty, usize>, name: &ICalendarProperty) -> usize {
counts.get(name).copied().unwrap_or(0)
}
@@ -0,0 +1,183 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
InstanceId, ItipError, ItipMessage, ItipSummary,
attendee::attendee_decline,
itip::{itip_add_tz, itip_build_envelope},
snapshot::itip_snapshot,
};
use ahash::AHashSet;
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod,
ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus, ICalendarValue,
},
};
pub fn itip_cancel(
ical: &ICalendar,
account_emails: &[String],
is_deletion: bool,
) -> Result<ItipMessage<ICalendar>, ItipError> {
// Prepare iTIP message
let itip = itip_snapshot(ical, account_emails, false)?;
let dt_stamp = PartialDateTime::now();
let mut message = ICalendar {
components: Vec::with_capacity(2),
};
if itip.organizer.email.is_local {
// Send cancel message
let mut comp = itip_build_envelope(ICalendarMethod::Cancel);
comp.component_ids.push(1);
message.components.push(comp);
// Fetch guest emails
let mut recipients = AHashSet::new();
let mut cancel_guests = AHashSet::new();
let mut component_type = &ICalendarComponentType::VEvent;
let mut sequence = 0;
for (instance_id, comp) in &itip.components {
component_type = &comp.comp.component_type;
for attendee in &comp.attendees {
if attendee.send_update_messages() {
recipients.insert(attendee.email.email.clone());
}
cancel_guests.insert(&attendee.email);
}
// Increment sequence if needed
if instance_id == &InstanceId::Main {
sequence = comp.sequence.unwrap_or_default() + 1;
}
}
if !recipients.is_empty() && component_type != &ICalendarComponentType::VFreebusy {
let instance = itip.main_instance_or_default();
message.components.push(build_cancel_component(
instance.comp,
sequence,
dt_stamp,
&[],
));
// Add timezones
itip_add_tz(&mut message, ical);
Ok(ItipMessage {
to: recipients.into_iter().collect(),
summary: ItipSummary::Cancel(instance.build_summary(None, &[])),
from: itip.organizer.email.email,
from_organizer: true,
message,
})
} else {
Err(ItipError::NothingToSend)
}
} else {
// Send decline message
message
.components
.push(itip_build_envelope(ICalendarMethod::Reply));
// Decline attendance for all instances that have local attendees
let mut mail_from = None;
let mut email_rcpt = AHashSet::new();
for (instance_id, comp) in &itip.components {
if let Some((cancel_comp, attendee_email)) = attendee_decline(
instance_id,
&itip,
comp,
&dt_stamp,
&mut email_rcpt,
is_deletion,
) {
// Add cancel component
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(cancel_comp);
mail_from = Some(&attendee_email.email);
}
}
if let Some(from) = mail_from {
// Add timezone information if needed
itip_add_tz(&mut message, ical);
email_rcpt.insert(&itip.organizer.email.email);
Ok(ItipMessage {
from: from.to_string(),
from_organizer: false,
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
summary: ItipSummary::Rsvp {
part_stat: ICalendarParticipationStatus::Declined,
current: itip.main_instance_or_default().build_summary(None, &[]),
},
message,
})
} else {
Err(ItipError::NothingToSend)
}
}
}
pub(crate) fn build_cancel_component(
component: &ICalendarComponent,
sequence: i64,
dt_stamp: PartialDateTime,
attendees: &[&str],
) -> ICalendarComponent {
let mut cancel_comp = ICalendarComponent {
component_type: component.component_type.clone(),
entries: Vec::with_capacity(7),
component_ids: vec![],
};
cancel_comp.add_property(
ICalendarProperty::Status,
ICalendarValue::Status(ICalendarStatus::Cancelled),
);
cancel_comp.add_dtstamp(dt_stamp);
cancel_comp.add_sequence(sequence);
cancel_comp.entries.extend(
component
.entries
.iter()
.filter(|e| match e.name {
ICalendarProperty::Organizer
| ICalendarProperty::Uid
| ICalendarProperty::Summary
| ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::RecurrenceId
| ICalendarProperty::Created
| ICalendarProperty::LastModified
| ICalendarProperty::Description
| ICalendarProperty::Location => true,
ICalendarProperty::Attendee => {
attendees.is_empty()
|| e.values
.first()
.and_then(|v| v.as_text())
.is_some_and(|email| {
attendees.iter().any(|attendee| {
email
.strip_suffix(attendee)
.is_some_and(|v| v.ends_with(':') || v.is_empty())
})
})
}
_ => false,
})
.cloned(),
);
cancel_comp
}
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
ItipError, ItipMessage, itip::itip_finalize, organizer::organizer_request_full,
snapshot::itip_snapshot,
};
use calcard::icalendar::ICalendar;
pub fn itip_create(
ical: &mut ICalendar,
account_emails: &[String],
) -> Result<Vec<ItipMessage<ICalendar>>, ItipError> {
let itip = itip_snapshot(ical, account_emails, false)?;
if !itip.organizer.is_server_scheduling {
Err(ItipError::OtherSchedulingAgent)
} else if !itip.organizer.email.is_local {
Err(ItipError::NotOrganizer)
} else {
organizer_request_full(ical, &itip, None, true).inspect(|_| {
itip_finalize(ical, &[]);
})
}
}
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
ItipError, ItipMessage, attendee::attendee_handle_update, event_cancel::itip_cancel,
itip::itip_finalize, organizer::organizer_handle_update, snapshot::itip_snapshot,
};
use calcard::icalendar::ICalendar;
pub fn itip_update(
ical: &mut ICalendar,
old_ical: &ICalendar,
account_emails: &[String],
) -> Result<Vec<ItipMessage<ICalendar>>, ItipError> {
let old_itip = itip_snapshot(old_ical, account_emails, false)?;
match itip_snapshot(ical, account_emails, false) {
Ok(new_itip) => {
let mut sequences = Vec::new();
if old_itip.organizer.email != new_itip.organizer.email {
// RFC 6638 does not support replacing the organizer
Err(ItipError::OrganizerMismatch)
} else if old_itip.organizer.email.is_local {
organizer_handle_update(old_ical, ical, old_itip, new_itip, &mut sequences)
} else {
attendee_handle_update(ical, old_itip, new_itip)
}
.inspect(|_| {
itip_finalize(ical, &sequences);
})
}
Err(err) => {
match &err {
ItipError::NoSchedulingInfo
| ItipError::NotOrganizer
| ItipError::NotOrganizerNorAttendee
| ItipError::OtherSchedulingAgent => {
if old_itip.organizer.email.is_local {
// RFC 6638 does not support replacing the organizer, so we cancel the event
itip_cancel(old_ical, account_emails, false).map(|message| vec![message])
} else {
Err(ItipError::CannotModifyAddress)
}
}
_ => Err(err),
}
}
}
}
+594
View File
@@ -0,0 +1,594 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::ItipValue;
use calcard::{
common::timezone::Tz,
icalendar::{ICalendarDay, ICalendarFrequency, ICalendarRecurrenceRule, ICalendarWeekday},
};
use chrono::{DateTime, NaiveDate, TimeZone, Weekday};
use common::i18n::{self, Locale, PluralForms};
use icu_datetime::{DateTimeFormatter, fieldsets};
use icu_locale_core::{Locale as IcuLocale, locale};
use icu_plurals::{PluralCategory, PluralRuleType, PluralRules, PluralRulesOptions};
use std::fmt::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateStyle {
Short,
Long,
}
pub struct TextFormatter {
pub locale: &'static Locale,
date_short: DateTimeFormatter<fieldsets::YMDT>,
date_long: DateTimeFormatter<fieldsets::YMDT>,
weekday_short: DateTimeFormatter<fieldsets::E>,
weekday: DateTimeFormatter<fieldsets::E>,
month: DateTimeFormatter<fieldsets::M>,
cardinal: PluralRules,
ordinal: PluralRules,
}
impl TextFormatter {
pub fn new(language: &str) -> trc::Result<Self> {
let locale = i18n::locale_or_default(language);
let icu_locale = IcuLocale::try_from_str(locale.name).unwrap_or(locale!("en-US"));
let failed = |detail: &'static str| {
move |err: icu_datetime::DateTimeFormatterLoadError| {
trc::EventType::Calendar(trc::CalendarEvent::ItipMessageError)
.into_err()
.caused_by(trc::location!())
.details(detail)
.ctx(trc::Key::Reason, err.to_string())
}
};
let plural_prefs = (&icu_locale).into();
let datetime_prefs = (&icu_locale).into();
let plural_rules = |options| {
PluralRules::try_new(plural_prefs, options).map_err(|err| {
trc::EventType::Calendar(trc::CalendarEvent::ItipMessageError)
.into_err()
.caused_by(trc::location!())
.details("Failed to load plural rules")
.ctx(trc::Key::Reason, err.to_string())
})
};
Ok(Self {
locale,
date_short: DateTimeFormatter::try_new(
datetime_prefs,
fieldsets::YMD::medium().with_time_hm(),
)
.map_err(failed("Failed to load short date formatter"))?,
date_long: DateTimeFormatter::try_new(
datetime_prefs,
fieldsets::YMD::long().with_time_hm(),
)
.map_err(failed("Failed to load long date formatter"))?,
weekday_short: DateTimeFormatter::try_new(datetime_prefs, fieldsets::E::short())
.map_err(failed("Failed to load short weekday formatter"))?,
weekday: DateTimeFormatter::try_new(datetime_prefs, fieldsets::E::long())
.map_err(failed("Failed to load weekday formatter"))?,
month: DateTimeFormatter::try_new(datetime_prefs, fieldsets::M::long())
.map_err(failed("Failed to load month formatter"))?,
cardinal: plural_rules(PluralRulesOptions::default())?,
ordinal: plural_rules(
PluralRulesOptions::default().with_type(PluralRuleType::Ordinal),
)?,
})
}
pub fn field(&self, out: &mut String, value: &ItipValue, style: DateStyle) {
match value {
ItipValue::Text(text) => out.push_str(text),
ItipValue::Time(time) => {
let tz = Tz::from_id(time.tz_id).unwrap_or(Tz::UTC);
let (weekday, date) = match style {
DateStyle::Short => (&self.weekday_short, &self.date_short),
DateStyle::Long => (&self.weekday, &self.date_long),
};
let local = tz
.from_utc_datetime(
&DateTime::from_timestamp(time.start, 0)
.unwrap_or_default()
.naive_local(),
)
.naive_local();
let _ = write!(
out,
"{}, {}",
weekday.format(&local.date()),
date.format(&local)
);
if let Some(name) = tz.name().filter(|name| !name.is_empty()) {
let _ = write!(out, " ({name})");
}
}
ItipValue::Rrule(rrule) => self.recurrence(out, rrule),
ItipValue::Participants(_) => {}
}
}
pub fn field_to_string(&self, value: &ItipValue, style: DateStyle) -> String {
let mut out = String::with_capacity(32);
self.field(&mut out, value, style);
out
}
pub fn recurrence(&self, out: &mut String, rule: &ICalendarRecurrenceRule) {
let start = out.len();
self.write_frequency(out, &rule.freq, rule.interval.unwrap_or(1));
if !rule.byday.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_on, |out| {
self.write_list(out, rule.byday.len(), |out, index| {
self.write_day(out, &rule.byday[index])
})
});
}
if !rule.byhour.is_empty() || !rule.byminute.is_empty() {
let hours = rule.byhour.len().max(1);
let minutes = rule.byminute.len().max(1);
self.write_clause(out, start, self.locale.calendar_rrule_at, |out| {
self.write_list(out, hours * minutes, |out, index| {
let hour = rule.byhour.get(index / minutes).copied().unwrap_or(0);
let minute = rule.byminute.get(index % minutes).copied().unwrap_or(0);
let _ = write!(out, "{hour:02}:{minute:02}");
})
});
}
if !rule.bymonthday.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_on_the, |out| {
self.write_list(out, rule.bymonthday.len(), |out, index| {
self.write_signed_ordinal(out, rule.bymonthday[index] as i32)
})
});
}
if !rule.bymonth.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_in, |out| {
self.write_list(out, rule.bymonth.len(), |out, index| {
self.write_month(out, rule.bymonth[index].month())
})
});
}
if !rule.byyearday.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_on, |out| {
self.write_list(out, rule.byyearday.len(), |out, index| {
self.write_counted(
out,
rule.byyearday[index] as i32,
self.locale.calendar_rrule_year_day,
)
})
});
}
if !rule.byweekno.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_in, |out| {
self.write_list(out, rule.byweekno.len(), |out, index| {
self.write_counted(
out,
rule.byweekno[index] as i32,
self.locale.calendar_rrule_week_no,
)
})
});
}
if !rule.bysetpos.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_setpos, |out| {
self.write_list(out, rule.bysetpos.len(), |out, index| {
self.write_signed_ordinal(out, rule.bysetpos[index])
})
});
}
if let Some(count) = rule.count.as_ref() {
if out.len() > start {
out.push_str(", ");
}
let form = self
.cardinal
.category_for(*count)
.plural_form(&self.locale.calendar_rrule_count);
write_number(out, form, "$n", *count as u64);
}
}
fn write_clause(
&self,
out: &mut String,
start: usize,
template: &str,
write_items: impl FnOnce(&mut String),
) {
let restore = out.len();
if out.len() > start {
out.push(' ');
}
let (before, after) = template.split_once("$list").unwrap_or((template, ""));
out.push_str(before);
let items_start = out.len();
write_items(out);
if out.len() == items_start {
out.truncate(restore);
} else {
out.push_str(after);
}
}
fn write_list(
&self,
out: &mut String,
len: usize,
mut write_item: impl FnMut(&mut String, usize),
) {
match len {
0 => {}
1 => write_item(out, 0),
_ => {
let conjunction = self.locale.calendar_rrule_and;
let (prefix, rest) = conjunction.split_once("$a").unwrap_or(("", conjunction));
let (separator, suffix) = rest.split_once("$b").unwrap_or((rest, ""));
out.push_str(prefix);
for index in 0..len - 1 {
if index > 0 {
out.push_str(", ");
}
write_item(out, index);
}
out.push_str(separator);
write_item(out, len - 1);
out.push_str(suffix);
}
}
}
fn write_frequency(&self, out: &mut String, freq: &ICalendarFrequency, interval: u16) {
let entry = match freq {
ICalendarFrequency::Secondly => self.locale.calendar_rrule_secondly,
ICalendarFrequency::Minutely => self.locale.calendar_rrule_minutely,
ICalendarFrequency::Hourly => self.locale.calendar_rrule_hourly,
ICalendarFrequency::Daily => self.locale.calendar_rrule_daily,
ICalendarFrequency::Weekly => self.locale.calendar_rrule_weekly,
ICalendarFrequency::Monthly => self.locale.calendar_rrule_monthly,
ICalendarFrequency::Yearly => self.locale.calendar_rrule_yearly,
};
let form = self
.cardinal
.category_for(interval as u32)
.plural_form(&entry);
write_number(out, form, "$n", interval as u64);
}
fn write_ordinal(&self, out: &mut String, n: u32) {
let form = self
.ordinal
.category_for(n)
.plural_form(&self.locale.calendar_rrule_ordinal);
write_number(out, form, "$n", n as u64);
}
fn write_signed_ordinal(&self, out: &mut String, value: i32) {
if value < 0 {
let (before, after) = self
.locale
.calendar_rrule_from_end
.split_once("$ordinal")
.unwrap_or((self.locale.calendar_rrule_from_end, ""));
out.push_str(before);
self.write_ordinal(out, value.unsigned_abs());
out.push_str(after);
} else {
self.write_ordinal(out, value.unsigned_abs());
}
}
fn write_counted(&self, out: &mut String, value: i32, template: &str) {
let count = value.unsigned_abs() as u64;
if value < 0 {
let (before, after) = self
.locale
.calendar_rrule_from_end
.split_once("$ordinal")
.unwrap_or((self.locale.calendar_rrule_from_end, ""));
out.push_str(before);
write_number(out, template, "$n", count);
out.push_str(after);
} else {
write_number(out, template, "$n", count);
}
}
fn write_weekday(&self, out: &mut String, weekday: ICalendarWeekday) {
let weekday = match weekday {
ICalendarWeekday::Monday => Weekday::Mon,
ICalendarWeekday::Tuesday => Weekday::Tue,
ICalendarWeekday::Wednesday => Weekday::Wed,
ICalendarWeekday::Thursday => Weekday::Thu,
ICalendarWeekday::Friday => Weekday::Fri,
ICalendarWeekday::Saturday => Weekday::Sat,
ICalendarWeekday::Sunday => Weekday::Sun,
};
if let Some(date) = NaiveDate::from_isoywd_opt(2024, 1, weekday) {
let _ = write!(out, "{}", self.weekday.format(&date));
}
}
fn write_month(&self, out: &mut String, month: u8) {
if let Some(date) = NaiveDate::from_ymd_opt(2024, month.clamp(1, 12) as u32, 1) {
let _ = write!(out, "{}", self.month.format(&date));
}
}
fn write_day(&self, out: &mut String, day: &ICalendarDay) {
let Some(occurrence) = day.ordwk.filter(|occurrence| *occurrence != 0) else {
self.write_weekday(out, day.weekday);
return;
};
let (wrap_before, wrap_after) = if occurrence < 0 {
let from_end = self.locale.calendar_rrule_from_end;
from_end.split_once("$ordinal").unwrap_or((from_end, ""))
} else {
("", "")
};
out.push_str(wrap_before);
let mut rest = self.locale.calendar_rrule_nth_weekday;
loop {
let Some((at, placeholder)) = ["$ordinal", "$weekday"]
.into_iter()
.filter_map(|placeholder| rest.find(placeholder).map(|at| (at, placeholder)))
.min()
else {
out.push_str(rest);
break;
};
out.push_str(&rest[..at]);
if placeholder == "$ordinal" {
self.write_ordinal(out, occurrence.unsigned_abs() as u32);
} else {
self.write_weekday(out, day.weekday);
}
rest = &rest[at + placeholder.len()..];
}
out.push_str(wrap_after);
}
}
trait PluralCategoryExt {
fn plural_form(&self, forms: &PluralForms) -> &'static str;
}
impl PluralCategoryExt for PluralCategory {
fn plural_form(&self, forms: &PluralForms) -> &'static str {
match self {
PluralCategory::Zero => forms.zero,
PluralCategory::One => forms.one,
PluralCategory::Two => forms.two,
PluralCategory::Few => forms.few,
PluralCategory::Many => forms.many,
PluralCategory::Other => forms.other,
}
}
}
fn write_number(out: &mut String, template: &str, placeholder: &str, value: u64) {
let mut rest = template;
while let Some((before, after)) = rest.split_once(placeholder) {
out.push_str(before);
let _ = write!(out, "{value}");
rest = after;
}
out.push_str(rest);
}
pub fn hyperlink(value: &str) -> Option<&str> {
let (scheme, _) = value.split_once(':')?;
["https", "http", "tel", "sip", "sips", "xmpp"]
.iter()
.any(|candidate| scheme.eq_ignore_ascii_case(candidate))
.then_some(value)
}
#[cfg(test)]
mod tests {
use super::{PluralCategoryExt, PluralForms, TextFormatter, i18n};
use calcard::icalendar::{
ICalendarDay, ICalendarFrequency, ICalendarRecurrenceRule, ICalendarWeekday,
};
use icu_plurals::PluralCategory;
fn rule(freq: ICalendarFrequency, interval: Option<u16>) -> ICalendarRecurrenceRule {
ICalendarRecurrenceRule {
freq,
interval,
..Default::default()
}
}
fn format(language: &str, rule: &ICalendarRecurrenceRule) -> String {
let mut out = String::new();
TextFormatter::new(language)
.expect("formatter")
.recurrence(&mut out, rule);
out
}
#[test]
fn every_shipped_locale_loads_icu_data() {
for locale in i18n::ALL_LOCALES {
let formatter = TextFormatter::new(locale.name)
.unwrap_or_else(|err| panic!("{}: {err:?}", locale.name));
assert_eq!(formatter.locale.name, locale.name);
let mut out = String::new();
formatter.recurrence(
&mut out,
&ICalendarRecurrenceRule {
freq: ICalendarFrequency::Monthly,
interval: Some(2),
count: Some(5),
bymonthday: vec![3, -1],
byday: vec![ICalendarDay {
ordwk: Some(2),
weekday: ICalendarWeekday::Monday,
}],
..Default::default()
},
);
assert!(!out.is_empty(), "{} produced no text", locale.name);
}
}
#[test]
fn plural_form_selects_category_and_falls_back_to_other() {
let forms = PluralForms {
zero: "many",
one: "single",
two: "many",
few: "a few",
many: "many",
other: "many",
};
assert_eq!(PluralCategory::One.plural_form(&forms), "single");
assert_eq!(PluralCategory::Few.plural_form(&forms), "a few");
assert_eq!(PluralCategory::Many.plural_form(&forms), "many");
assert_eq!(PluralCategory::Other.plural_form(&forms), "many");
// Categories a locale omits are filled from "other" at build time
let polish = i18n::locale("pl-PL").expect("locale must exist");
assert_eq!(polish.calendar_rrule_secondly.many, "Co $n sekund");
assert_eq!(
polish.calendar_rrule_secondly.zero,
polish.calendar_rrule_secondly.other
);
}
#[test]
fn frequency_uses_cardinal_plural_rules() {
assert_eq!(
format("en", &rule(ICalendarFrequency::Weekly, None)),
"Every week"
);
assert_eq!(
format("en", &rule(ICalendarFrequency::Weekly, Some(2))),
"Every 2 weeks"
);
// Polish distinguishes one / few / many, unlike English
assert_eq!(
format("pl", &rule(ICalendarFrequency::Weekly, Some(1))),
"Co tydzień"
);
assert_eq!(
format("pl", &rule(ICalendarFrequency::Weekly, Some(2))),
"Co 2 tygodnie"
);
assert_eq!(
format("pl", &rule(ICalendarFrequency::Weekly, Some(5))),
"Co 5 tygodni"
);
}
#[test]
fn weekday_names_are_localized_without_translation_keys() {
let mut byday = rule(ICalendarFrequency::Weekly, None);
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Monday,
ordwk: None,
}];
assert_eq!(format("en", &byday), "Every week on Monday");
assert_eq!(format("es", &byday), "Cada semana los lunes");
}
#[test]
fn ordinal_weekday_uses_ordinal_plural_rules() {
let mut byday = rule(ICalendarFrequency::Monthly, None);
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Tuesday,
ordwk: Some(2),
}];
assert_eq!(format("en", &byday), "Every month on the 2nd Tuesday");
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Tuesday,
ordwk: Some(3),
}];
assert_eq!(format("en", &byday), "Every month on the 3rd Tuesday");
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Tuesday,
ordwk: Some(-1),
}];
assert_eq!(
format("en", &byday),
"Every month on the 1st Tuesday from the end"
);
}
#[test]
fn count_is_pluralized_and_appended() {
let mut counted = rule(ICalendarFrequency::Daily, None);
counted.count = Some(1);
assert_eq!(format("en", &counted), "Every day, 1 time");
counted.count = Some(5);
assert_eq!(format("en", &counted), "Every day, 5 times");
}
#[test]
fn multiple_days_are_joined_with_the_localized_conjunction() {
let mut byday = rule(ICalendarFrequency::Weekly, None);
byday.byday = vec![
ICalendarDay {
weekday: ICalendarWeekday::Monday,
ordwk: None,
},
ICalendarDay {
weekday: ICalendarWeekday::Wednesday,
ordwk: None,
},
ICalendarDay {
weekday: ICalendarWeekday::Friday,
ordwk: None,
},
];
assert_eq!(
format("en", &byday),
"Every week on Monday, Wednesday and Friday"
);
}
#[test]
fn unknown_language_falls_back_to_english_rules() {
assert_eq!(
format("zz", &rule(ICalendarFrequency::Weekly, Some(3))),
"Every 3 weeks"
);
}
}
+624
View File
@@ -0,0 +1,624 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
InstanceId, ItipError, ItipMessage, ItipSnapshots, organizer::organizer_request_full,
};
use ahash::AHashSet;
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParameterName, ICalendarProperty, ICalendarStatus, ICalendarValue,
Uri,
};
#[derive(Debug)]
pub enum MergeAction {
AddEntries {
component_id: u16,
entries: Vec<ICalendarEntry>,
},
RemoveEntries {
component_id: u16,
entries: AHashSet<ICalendarProperty>,
},
AddParameters {
component_id: u16,
entry_id: u16,
parameters: Vec<ICalendarParameter>,
},
RemoveParameters {
component_id: u16,
entry_id: u16,
parameters: Vec<ICalendarParameterName>,
},
AddComponent {
component: ICalendarComponent,
},
RemoveComponent {
component_id: u16,
},
}
pub enum MergeResult {
Actions(Vec<MergeAction>),
Message(ItipMessage<ICalendar>),
None,
}
pub fn itip_process_message(
ical: &ICalendar,
snapshots: ItipSnapshots<'_>,
itip: &ICalendar,
itip_snapshots: ItipSnapshots<'_>,
sender: String,
) -> Result<MergeResult, ItipError> {
if snapshots.organizer.email != itip_snapshots.organizer.email {
return Err(ItipError::OrganizerMismatch);
}
let method = itip_method(itip)?;
let mut merge_actions = Vec::new();
if snapshots.organizer.email.is_local {
// Handle attendee updates
if snapshots.organizer.email.email == sender {
return Err(ItipError::OrganizerIsLocalAddress);
}
match method {
ICalendarMethod::Reply => {
handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?;
}
ICalendarMethod::Refresh => {
return organizer_request_full(ical, &snapshots, None, false).and_then(
|messages| {
messages
.into_iter()
.next()
.map(|mut message| {
message.to = vec![sender];
MergeResult::Message(message)
})
.ok_or(ItipError::NothingToSend)
},
);
}
_ => return Err(ItipError::UnsupportedMethod(method.clone())),
}
} else {
// Handle organizer and attendees updates
match method {
ICalendarMethod::Request => {
let mut is_full_update = false;
for (instance_id, itip_snapshot) in &itip_snapshots.components {
is_full_update = is_full_update || instance_id == &InstanceId::Main;
let itip_component = &itip.components[itip_snapshot.comp_id as usize];
if let Some(snapshot) = snapshots.components.get(instance_id) {
// Merge instances
if itip_snapshot.sequence.unwrap_or_default()
>= snapshot.sequence.unwrap_or_default()
{
let mut changed_entries = itip_snapshot
.entries
.symmetric_difference(&snapshot.entries)
.map(|entry| entry.name.clone())
.collect::<AHashSet<_>>();
if itip_snapshot.attendees != snapshot.attendees {
changed_entries.insert(ICalendarProperty::Attendee);
}
if itip_snapshot.dtstamp.is_some()
&& itip_snapshot.dtstamp != snapshot.dtstamp
{
changed_entries.insert(ICalendarProperty::Dtstamp);
}
changed_entries.insert(ICalendarProperty::Sequence);
if !changed_entries.is_empty() {
let entries = itip_component
.entries
.iter()
.filter(|entry| changed_entries.contains(&entry.name))
.cloned()
.collect();
merge_actions.push(MergeAction::RemoveEntries {
component_id: snapshot.comp_id,
entries: changed_entries,
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries,
});
}
} else {
return Err(ItipError::OutOfSequence);
}
} else {
// Add instance
merge_actions.push(MergeAction::AddComponent {
component: ICalendarComponent {
component_type: itip_component.component_type.clone(),
entries: itip_component
.entries
.iter()
.filter(|entry| {
!matches!(entry.name, ICalendarProperty::Other(_))
})
.cloned()
.collect(),
component_ids: vec![],
},
});
}
}
if is_full_update {
for (instance_id, snapshot) in &snapshots.components {
if !itip_snapshots.components.contains_key(instance_id) {
// Remove instance
merge_actions.push(MergeAction::RemoveComponent {
component_id: snapshot.comp_id,
});
}
}
}
}
ICalendarMethod::Add => {
for (instance_id, itip_snapshot) in &itip_snapshots.components {
if !snapshots.components.contains_key(instance_id) {
let itip_component = &itip.components[itip_snapshot.comp_id as usize];
merge_actions.push(MergeAction::AddComponent {
component: ICalendarComponent {
component_type: itip_component.component_type.clone(),
entries: itip_component
.entries
.iter()
.filter(|entry| {
!matches!(entry.name, ICalendarProperty::Other(_))
})
.cloned()
.collect(),
component_ids: vec![],
},
});
}
}
}
ICalendarMethod::Cancel => {
let mut cancel_all_instances = false;
for (instance_id, itip_snapshot) in &itip_snapshots.components {
if let Some(snapshot) = snapshots.components.get(instance_id) {
if itip_snapshot.sequence.unwrap_or_default()
>= snapshot.sequence.unwrap_or_default()
{
// Cancel instance
let itip_component = itip_snapshot.comp;
merge_actions.push(MergeAction::RemoveEntries {
component_id: snapshot.comp_id,
entries: [
ICalendarProperty::Organizer,
ICalendarProperty::Attendee,
ICalendarProperty::Status,
ICalendarProperty::Sequence,
]
.into_iter()
.collect(),
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: itip_component
.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
ICalendarProperty::Organizer
| ICalendarProperty::Attendee
)
})
.cloned()
.chain([ICalendarEntry {
name: ICalendarProperty::Status,
params: vec![],
values: vec![ICalendarValue::Status(
ICalendarStatus::Cancelled,
)],
}])
.collect(),
});
cancel_all_instances =
cancel_all_instances || instance_id == &InstanceId::Main;
} else {
return Err(ItipError::OutOfSequence);
}
} else {
let itip_component = itip_snapshot.comp;
merge_actions.push(MergeAction::AddComponent {
component: ICalendarComponent {
component_type: itip_component.component_type.clone(),
entries: itip_component
.entries
.iter()
.filter(|entry| {
!matches!(
entry.name,
ICalendarProperty::Status | ICalendarProperty::Other(_)
)
})
.cloned()
.chain([ICalendarEntry {
name: ICalendarProperty::Status,
params: vec![],
values: vec![ICalendarValue::Status(
ICalendarStatus::Cancelled,
)],
}])
.collect(),
component_ids: vec![],
},
});
}
}
if cancel_all_instances {
// Remove all instances
let itip_main = itip_snapshots.components.get(&InstanceId::Main).unwrap();
let itip_component = itip_main.comp;
for (instance_id, snapshot) in &snapshots.components {
if !itip_snapshots.components.contains_key(instance_id) {
merge_actions.push(MergeAction::RemoveEntries {
component_id: snapshot.comp_id,
entries: [
ICalendarProperty::Organizer,
ICalendarProperty::Attendee,
ICalendarProperty::Status,
]
.into_iter()
.collect(),
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: itip_component
.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
ICalendarProperty::Organizer
| ICalendarProperty::Attendee
)
})
.cloned()
.chain([ICalendarEntry {
name: ICalendarProperty::Status,
params: vec![],
values: vec![ICalendarValue::Status(
ICalendarStatus::Cancelled,
)],
}])
.collect(),
});
}
}
}
}
ICalendarMethod::Reply
if itip_snapshots.components.values().any(|snapshot| {
snapshot.external_attendees().any(|a| {
a.email.email == sender && a.delegated_from.iter().any(|a| a.is_local)
})
}) =>
{
handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?;
}
_ => return Err(ItipError::UnsupportedMethod(method.clone())),
}
}
if !merge_actions.is_empty() {
Ok(MergeResult::Actions(merge_actions))
} else {
Ok(MergeResult::None)
}
}
pub fn itip_import_message(ical: &mut ICalendar) -> Result<(), ItipError> {
let mut expect_object_type = None;
for comp in ical.components.iter_mut() {
if comp.component_type.is_scheduling_object() {
match expect_object_type {
Some(expected) if expected != &comp.component_type => {
return Err(ItipError::MultipleObjectTypes);
}
None => {
expect_object_type = Some(&comp.component_type);
}
_ => {}
}
} else if comp.component_type == ICalendarComponentType::VCalendar {
comp.entries
.retain(|entry| !matches!(entry.name, ICalendarProperty::Method));
}
}
Ok(())
}
fn handle_reply(
snapshots: &ItipSnapshots<'_>,
itip_snapshots: &ItipSnapshots<'_>,
sender: &str,
merge_actions: &mut Vec<MergeAction>,
) -> Result<(), ItipError> {
for (instance_id, itip_snapshot) in &itip_snapshots.components {
if let Some(snapshot) = snapshots.components.get(instance_id) {
if let (Some(attendee), Some(updated_attendee)) = (
snapshot.attendee_by_email(sender),
itip_snapshot.attendee_by_email(sender),
) {
let itip_component = itip_snapshot.comp;
let changed_part_stat = attendee.part_stat != updated_attendee.part_stat;
let changed_rsvp = attendee.rsvp != updated_attendee.rsvp;
let changed_delegated_to = attendee.delegated_to != updated_attendee.delegated_to;
let has_request_status = !itip_snapshot.request_status.is_empty();
if changed_part_stat || changed_rsvp || changed_delegated_to || has_request_status {
// Update participant status
let mut add_parameters = Vec::new();
let mut remove_parameters = Vec::new();
if changed_part_stat {
remove_parameters.push(ICalendarParameterName::Partstat);
if let Some(part_stat) = updated_attendee.part_stat {
add_parameters.push(ICalendarParameter::partstat(part_stat.clone()));
}
}
if changed_rsvp {
remove_parameters.push(ICalendarParameterName::Rsvp);
if let Some(rsvp) = updated_attendee.rsvp {
add_parameters.push(ICalendarParameter::rsvp(rsvp));
}
}
if changed_delegated_to {
remove_parameters.push(ICalendarParameterName::DelegatedTo);
if !updated_attendee.delegated_to.is_empty() {
add_parameters.extend(updated_attendee.delegated_to.iter().map(
|email| {
ICalendarParameter::delegated_to(Uri::Location(
email.to_string(),
))
},
));
}
}
// RFC 6638 3.2.5: the status defaults to 2.0 when the reply carries none
remove_parameters.push(ICalendarParameterName::ScheduleStatus);
add_parameters.push(ICalendarParameter::schedule_status(
if has_request_status {
itip_snapshot.request_status.join(",")
} else {
"2.0".to_string()
},
));
merge_actions.push(MergeAction::RemoveParameters {
component_id: snapshot.comp_id,
entry_id: attendee.entry_id,
parameters: remove_parameters,
});
merge_actions.push(MergeAction::AddParameters {
component_id: snapshot.comp_id,
entry_id: attendee.entry_id,
parameters: add_parameters,
});
// Add unknown delegated attendees
for delegated_to in &updated_attendee.delegated_to {
if let Some(itip_delegated) =
itip_snapshot.attendee_by_email(&delegated_to.email)
{
if let Some(delegated) = snapshot.attendee_by_email(&delegated_to.email)
{
if delegated != itip_delegated {
merge_actions.push(MergeAction::RemoveParameters {
component_id: snapshot.comp_id,
entry_id: delegated.entry_id,
parameters: vec![
ICalendarParameterName::DelegatedTo,
ICalendarParameterName::DelegatedFrom,
ICalendarParameterName::Partstat,
ICalendarParameterName::Rsvp,
ICalendarParameterName::ScheduleStatus,
ICalendarParameterName::Role,
],
});
merge_actions.push(MergeAction::AddParameters {
component_id: snapshot.comp_id,
entry_id: delegated.entry_id,
parameters: itip_component.entries
[itip_delegated.entry_id as usize]
.params
.iter()
.filter(|param| {
matches!(
param.name,
ICalendarParameterName::DelegatedTo
| ICalendarParameterName::DelegatedFrom
| ICalendarParameterName::Partstat
| ICalendarParameterName::Rsvp
| ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::Role
)
})
.cloned()
.collect(),
});
}
} else {
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: vec![
itip_component.entries[itip_delegated.entry_id as usize]
.clone(),
],
});
}
}
}
}
// Add changed properties for VTODO
if snapshot.comp.component_type == ICalendarComponentType::VTodo {
let mut remove_entries = AHashSet::new();
let mut add_entries = Vec::new();
for entry in itip_component.entries.iter() {
if matches!(
entry.name,
ICalendarProperty::PercentComplete
| ICalendarProperty::Status
| ICalendarProperty::Completed
) {
remove_entries.insert(entry.name.clone());
add_entries.push(entry.clone());
}
}
if !add_entries.is_empty() {
merge_actions.push(MergeAction::RemoveEntries {
component_id: snapshot.comp_id,
entries: remove_entries,
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: add_entries,
});
}
}
} else {
return Err(ItipError::SenderIsNotParticipant(sender.to_string()));
}
} else if itip_snapshot.attendee_by_email(sender).is_some() {
// Add component
let itip_component = itip_snapshot.comp;
let is_todo = itip_component.component_type == ICalendarComponentType::VTodo;
merge_actions.push(MergeAction::AddComponent {
component: ICalendarComponent {
component_type: itip_component.component_type.clone(),
entries: itip_component
.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
ICalendarProperty::Organizer
| ICalendarProperty::Attendee
| ICalendarProperty::Uid
| ICalendarProperty::Dtstamp
| ICalendarProperty::Sequence
| ICalendarProperty::RecurrenceId
) || (is_todo
&& matches!(
entry.name,
ICalendarProperty::PercentComplete
| ICalendarProperty::Status
| ICalendarProperty::Completed
))
})
.cloned()
.collect(),
component_ids: vec![],
},
});
} else {
return Err(ItipError::SenderIsNotParticipant(sender.to_string()));
}
}
Ok(())
}
pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec<MergeAction>) {
let mut remove_component_ids: Vec<u32> = Vec::new();
for action in changes {
match action {
MergeAction::AddEntries {
component_id,
entries,
} => {
let component = &mut ical.components[component_id as usize];
component.entries.extend(entries);
}
MergeAction::RemoveEntries {
component_id,
entries,
} => {
let component = &mut ical.components[component_id as usize];
component
.entries
.retain(|entry| !entries.contains(&entry.name));
}
MergeAction::AddParameters {
component_id,
entry_id,
parameters,
} => {
ical.components[component_id as usize].entries[entry_id as usize]
.params
.extend(parameters);
}
MergeAction::RemoveParameters {
component_id,
entry_id,
parameters,
} => {
ical.components[component_id as usize].entries[entry_id as usize]
.params
.retain(|param| !parameters.contains(&param.name));
}
MergeAction::AddComponent { component } => {
let comp_id = ical.components.len() as u32;
if let Some(root) = ical
.components
.get_mut(0)
.filter(|c| c.component_type == ICalendarComponentType::VCalendar)
{
root.component_ids.push(comp_id);
ical.components.push(component);
}
}
MergeAction::RemoveComponent { component_id } => {
remove_component_ids.push(component_id as u32);
}
}
}
if !remove_component_ids.is_empty() {
ical.remove_component_ids(&remove_component_ids);
}
}
pub fn itip_method(ical: &ICalendar) -> Result<&ICalendarMethod, ItipError> {
ical.components
.first()
.and_then(|comp| {
comp.entries.iter().find_map(|entry| {
if entry.name == ICalendarProperty::Method {
entry.values.first().and_then(|value| {
if let ICalendarValue::Method(method) = value {
Some(method)
} else {
None
}
})
} else {
None
}
})
})
.ok_or(ItipError::MissingMethod)
}
+480
View File
@@ -0,0 +1,480 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{Email, ItipMessage, ItipMessages, ItipSummary};
use calcard::{
common::{IanaString, PartialDateTime},
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParameterName, ICalendarParameterValue,
ICalendarParticipationStatus, ICalendarProperty, ICalendarScheduleAgentValue,
ICalendarValue,
},
};
use common::PROD_ID;
use registry::schema::structs::{
Task, TaskCalendarItipContents, TaskCalendarItipMessage, TaskStatus,
};
use store::write::BatchBuilder;
pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent {
ICalendarComponent {
component_type: ICalendarComponentType::VCalendar,
entries: vec![
ICalendarEntry {
name: ICalendarProperty::Version,
params: vec![],
values: vec![ICalendarValue::Text("2.0".to_string())],
},
ICalendarEntry {
name: ICalendarProperty::Prodid,
params: vec![],
values: vec![ICalendarValue::Text(PROD_ID.to_string())],
},
ICalendarEntry {
name: ICalendarProperty::Method,
params: vec![],
values: vec![ICalendarValue::Method(method)],
},
],
component_ids: Default::default(),
}
}
pub fn itip_assign_organizer(ical: &mut ICalendar, organizer_address: &str) -> bool {
let mut assigned = false;
for component in &mut ical.components {
if !component.component_type.is_scheduling_object() {
continue;
}
let mut has_organizer = false;
let mut has_attendee = false;
for entry in &component.entries {
match entry.name {
ICalendarProperty::Organizer => has_organizer = true,
ICalendarProperty::Attendee => has_attendee = true,
_ => {}
}
}
if has_attendee && !has_organizer {
component.entries.push(ICalendarEntry {
name: ICalendarProperty::Organizer,
params: vec![],
values: vec![ICalendarValue::Text(format!("mailto:{organizer_address}"))],
});
assigned = true;
}
}
assigned
}
pub(crate) const ITIP_STATUS_INVALID_USER: &str = "3.7";
fn itip_is_server_scheduling(entry: &ICalendarEntry) -> bool {
!entry.params.iter().any(|param| {
matches!(
(&param.name, &param.value),
(
ICalendarParameterName::ScheduleAgent,
ICalendarParameterValue::ScheduleAgent(
ICalendarScheduleAgentValue::Client | ICalendarScheduleAgentValue::None
)
)
)
})
}
fn itip_unreachable_entries(
ical: &ICalendar,
account_emails: &[String],
) -> Option<Vec<(usize, usize)>> {
let (comp_id, entry_id, entry) = ical
.components
.iter()
.enumerate()
.filter(|(_, comp)| comp.component_type.is_scheduling_object())
.find_map(|(comp_id, comp)| {
comp.entries
.iter()
.enumerate()
.find(|(_, entry)| entry.name == ICalendarProperty::Organizer)
.map(|(entry_id, entry)| (comp_id, entry_id, entry))
})?;
if !itip_is_server_scheduling(entry) {
return None;
}
match Email::new(entry.values.first()?.as_text()?, account_emails) {
Some(email) if !email.is_local => return None,
Some(_) => {}
None => return Some(vec![(comp_id, entry_id)]),
}
let mut unreachable = Vec::new();
for (comp_id, comp) in ical.components.iter().enumerate() {
if !comp.component_type.is_scheduling_object() {
continue;
}
for (entry_id, entry) in comp.entries.iter().enumerate() {
if entry.name != ICalendarProperty::Attendee || !itip_is_server_scheduling(entry) {
continue;
}
let rsvp = !entry.params.iter().any(|param| {
matches!(
(&param.name, &param.value),
(
ICalendarParameterName::Rsvp,
ICalendarParameterValue::Bool(false)
)
)
});
if rsvp
&& entry
.values
.first()
.and_then(|value| value.as_text())
.is_some_and(|value| Email::new(value, account_emails).is_none())
{
unreachable.push((comp_id, entry_id));
}
}
}
Some(unreachable)
}
pub fn itip_unreachable_recipient<'x>(
ical: &'x ICalendar,
account_emails: &[String],
) -> Option<&'x str> {
itip_unreachable_entries(ical, account_emails)?
.first()
.and_then(|(comp_id, entry_id)| {
ical.components[*comp_id].entries[*entry_id]
.values
.first()
.and_then(|value| value.as_text())
})
}
pub fn itip_set_unreachable_status(ical: &mut ICalendar, account_emails: &[String]) {
let Some(unreachable) = itip_unreachable_entries(ical, account_emails) else {
return;
};
for (comp_id, comp) in ical.components.iter_mut().enumerate() {
if !comp.component_type.is_scheduling_object() {
continue;
}
for (entry_id, entry) in comp.entries.iter_mut().enumerate() {
if !matches!(
entry.name,
ICalendarProperty::Organizer | ICalendarProperty::Attendee
) {
continue;
}
entry.params.retain(|param| {
param.name != ICalendarParameterName::ScheduleStatus
|| param.value.as_text() != Some(ITIP_STATUS_INVALID_USER)
});
if unreachable.contains(&(comp_id, entry_id)) {
entry.params.push(ICalendarParameter::schedule_status(
ITIP_STATUS_INVALID_USER.to_string(),
));
}
}
}
}
pub(crate) enum ItipExportAs<'x> {
Organizer(&'x ICalendarParticipationStatus),
Attendee(Vec<u16>),
}
pub(crate) fn itip_export_component(
component: &ICalendarComponent,
uid: &str,
dt_stamp: &PartialDateTime,
sequence: i64,
export_as: ItipExportAs<'_>,
) -> ICalendarComponent {
let is_todo = component.component_type == ICalendarComponentType::VTodo;
let mut comp = ICalendarComponent {
component_type: component.component_type.clone(),
entries: Vec::with_capacity(component.entries.len() + 1),
component_ids: Default::default(),
};
comp.add_dtstamp(dt_stamp.clone());
comp.add_sequence(sequence);
comp.add_uid(uid);
for (entry_id, entry) in component.entries.iter().enumerate() {
match (&entry.name, &export_as) {
(
ICalendarProperty::Organizer | ICalendarProperty::Attendee,
ItipExportAs::Organizer(partstat),
) => {
let mut new_entry = ICalendarEntry {
name: entry.name.clone(),
params: Vec::with_capacity(entry.params.len()),
values: entry.values.clone(),
};
let mut has_partstat = false;
let mut rsvp = true;
for entry in &entry.params {
match &entry.name {
ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::ScheduleAgent
| ICalendarParameterName::ScheduleForceSend => {}
_ => {
match &entry.name {
ICalendarParameterName::Rsvp => {
rsvp = !matches!(
entry.value,
ICalendarParameterValue::Bool(false)
);
}
ICalendarParameterName::Partstat => {
has_partstat = true;
}
_ => {}
}
new_entry.params.push(entry.clone())
}
}
}
if !has_partstat && rsvp && entry.name == ICalendarProperty::Attendee {
new_entry
.params
.push(ICalendarParameter::partstat((*partstat).clone()));
}
comp.entries.push(new_entry);
}
(
ICalendarProperty::Organizer | ICalendarProperty::Attendee,
ItipExportAs::Attendee(attendee_entry_ids),
) if attendee_entry_ids.contains(&(entry_id as u16))
|| entry.name == ICalendarProperty::Organizer =>
{
comp.entries.push(ICalendarEntry {
name: entry.name.clone(),
params: entry
.params
.iter()
.filter(|param| {
!matches!(
&param.name,
ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::ScheduleAgent
| ICalendarParameterName::ScheduleForceSend
)
})
.cloned()
.collect(),
values: entry.values.clone(),
});
}
(
ICalendarProperty::RequestStatus
| ICalendarProperty::Dtstamp
| ICalendarProperty::Sequence
| ICalendarProperty::Uid,
_,
) => {}
(_, ItipExportAs::Organizer(_))
| (
ICalendarProperty::RecurrenceId
| ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Description
| ICalendarProperty::Summary,
_,
) => {
comp.entries.push(entry.clone());
}
(
ICalendarProperty::Status
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed,
_,
) if is_todo => {
comp.entries.push(entry.clone());
}
_ => {}
}
}
if matches!(export_as, ItipExportAs::Attendee(_)) {
comp.entries.push(ICalendarEntry {
name: ICalendarProperty::RequestStatus,
params: vec![],
values: vec![
ICalendarValue::Text("2.0".to_string()),
ICalendarValue::Text("Success".to_string()),
],
});
}
comp
}
pub(crate) fn itip_finalize(ical: &mut ICalendar, scheduling_object_ids: &[u16]) {
for comp in ical.components.iter_mut() {
if comp.component_type.is_scheduling_object() {
// Remove scheduling info from non-updated components
for entry in comp.entries.iter_mut() {
if matches!(
entry.name,
ICalendarProperty::Organizer | ICalendarProperty::Attendee
) {
entry.params.retain(|param| {
!matches!(param.name, ICalendarParameterName::ScheduleForceSend)
});
}
}
}
}
for comp_id in scheduling_object_ids {
let comp = &mut ical.components[*comp_id as usize];
let mut found_sequence = false;
for entry in &mut comp.entries {
if entry.name == ICalendarProperty::Sequence {
if let Some(ICalendarValue::Integer(seq)) = entry.values.first_mut() {
*seq += 1;
} else {
entry.values = vec![ICalendarValue::Integer(1)];
}
found_sequence = true;
break;
}
}
if !found_sequence {
comp.add_sequence(1);
}
}
}
pub(crate) fn itip_add_tz(message: &mut ICalendar, ical: &ICalendar) {
let mut has_timezones = false;
if message.components.iter().any(|c| {
has_timezones = has_timezones || c.component_type == ICalendarComponentType::VTimezone;
!has_timezones
&& c.entries.iter().any(|e| {
e.params
.iter()
.any(|p| matches!(p.name, ICalendarParameterName::Tzid))
})
}) && !has_timezones
{
message.copy_timezones(ical);
}
message.add_missing_timezones();
}
#[inline]
pub(crate) fn can_attendee_modify_property(
component_type: &ICalendarComponentType,
property: &ICalendarProperty,
) -> bool {
match component_type {
ICalendarComponentType::VEvent | ICalendarComponentType::VJournal => {
matches!(
property,
ICalendarProperty::Exdate
| ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Comment
)
}
ICalendarComponentType::VTodo => matches!(
property,
ICalendarProperty::Exdate
| ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Status
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed
| ICalendarProperty::Comment
),
_ => false,
}
}
impl ItipMessages {
pub fn new(messages: Vec<ItipMessage<ICalendar>>) -> Self {
ItipMessages {
messages: messages
.into_iter()
.map(|m| TaskCalendarItipContents {
from: m.from,
i_calendar_data: m.message.to_string(),
is_from_organizer: m.from_organizer,
summary: serde_json::to_string(&m.summary).unwrap_or_default(),
to: m.to.into(),
})
.collect(),
}
}
pub fn queue(self, batch: &mut BatchBuilder) -> trc::Result<()> {
batch.schedule_task(Task::CalendarItipMessage(TaskCalendarItipMessage {
account_id: batch.last_account_id().unwrap().into(),
document_id: batch.last_document_id().unwrap().into(),
messages: self.messages.into(),
status: TaskStatus::now(),
}));
Ok(())
}
}
impl From<ItipMessage<ICalendar>> for ItipMessage<String> {
fn from(message: ItipMessage<ICalendar>) -> Self {
ItipMessage {
from: message.from,
from_organizer: message.from_organizer,
to: message.to,
summary: message.summary,
message: message.message.to_string(),
}
}
}
impl ItipSummary {
pub fn method(&self) -> &str {
match self {
ItipSummary::Invite(_) => ICalendarMethod::Request.as_str(),
ItipSummary::Update { method, .. } => method.as_str(),
ItipSummary::Cancel(_) => ICalendarMethod::Cancel.as_str(),
ItipSummary::Rsvp { .. } => ICalendarMethod::Reply.as_str(),
}
}
}
+449
View File
@@ -0,0 +1,449 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{decode_mailto_address, extract_addr_spec};
use ahash::{AHashMap, AHashSet};
use calcard::{
common::{IanaString, PartialDateTime},
icalendar::{
ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParticipationRole, ICalendarParticipationStatus,
ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule,
ICalendarScheduleForceSendValue, ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri,
},
};
use indexmap::IndexSet;
use registry::schema::structs::TaskCalendarItipContents;
use std::{fmt::Display, hash::Hash};
use utils::sanitize_email;
pub mod attendee;
pub mod event_cancel;
pub mod event_create;
pub mod event_update;
pub mod format;
pub mod inbound;
pub mod itip;
pub mod organizer;
pub mod snapshot;
#[derive(Debug)]
pub struct ItipSnapshots<'x> {
pub organizer: Organizer<'x>,
pub uid: &'x str,
pub components: AHashMap<InstanceId, ItipSnapshot<'x>>,
}
#[derive(Debug)]
pub struct ItipSnapshot<'x> {
pub comp_id: u16,
pub comp: &'x ICalendarComponent,
pub attendees: AHashSet<Attendee<'x>>,
pub dtstamp: Option<&'x PartialDateTime>,
pub entries: ItipEntries<'x>,
pub sequence: Option<i64>,
pub request_status: Vec<&'x str>,
}
pub type ItipEntries<'x> = IndexSet<ItipEntry<'x>, ahash::RandomState>;
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ItipEntry<'x> {
pub name: &'x ICalendarProperty,
pub value: ItipEntryValue<'x>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ItipEntryValue<'x> {
DateTime(ItipDateTime<'x>),
Period(&'x ICalendarPeriod),
Duration(&'x ICalendarDuration),
Status(&'x ICalendarStatus),
RRule(&'x ICalendarRecurrenceRule),
Text(&'x str),
Integer(i64),
}
#[derive(Debug)]
pub struct ItipDateTime<'x> {
pub date: &'x PartialDateTime,
pub tz_id: Option<&'x str>,
pub tz_code: u16,
pub timestamp: i64,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InstanceId {
Main,
Recurrence(RecurrenceId),
}
#[derive(Debug, PartialOrd, Ord)]
pub struct RecurrenceId {
pub entry_id: u16,
pub date: i64,
pub this_and_future: bool,
}
#[derive(Debug)]
pub struct Attendee<'x> {
pub entry_id: u16,
pub email: Email,
pub name: Option<&'x str>,
pub part_stat: Option<&'x ICalendarParticipationStatus>,
pub delegated_from: Vec<Email>,
pub delegated_to: Vec<Email>,
pub role: Option<&'x ICalendarParticipationRole>,
pub cu_type: Option<&'x ICalendarUserTypes>,
pub sent_by: Option<Email>,
pub rsvp: Option<bool>,
pub is_server_scheduling: bool,
pub force_send: Option<&'x ICalendarScheduleForceSendValue>,
}
#[derive(Debug)]
pub struct Organizer<'x> {
pub entry_id: u16,
pub email: Email,
pub name: Option<&'x str>,
pub is_server_scheduling: bool,
pub force_send: Option<&'x ICalendarScheduleForceSendValue>,
}
#[derive(Debug)]
pub struct Email {
pub email: String,
pub is_local: bool,
}
#[derive(Debug)]
pub enum ItipError {
NoSchedulingInfo,
OtherSchedulingAgent,
NotOrganizer,
NotOrganizerNorAttendee,
NothingToSend,
MissingUid,
MultipleUid,
MultipleOrganizer,
MultipleObjectTypes,
MultipleObjectInstances,
CannotModifyProperty(ICalendarProperty),
CannotModifyInstance,
CannotModifyAddress,
OrganizerMismatch,
MissingMethod,
InvalidComponentType,
OutOfSequence,
OrganizerIsLocalAddress,
SenderIsNotOrganizerNorAttendee,
SenderIsNotParticipant(String),
UnknownParticipant(String),
UnsupportedMethod(ICalendarMethod),
ICalendarParseError,
EventNotFound,
EventTooLarge,
QuotaExceeded,
NoDefaultCalendar,
AutoAddDisabled,
}
#[derive(Debug)]
pub struct ItipMessage<T> {
pub from: String,
pub from_organizer: bool,
pub to: Vec<String>,
pub summary: ItipSummary,
pub message: T,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ItipSummary {
Invite(Vec<ItipField>),
Update {
method: ICalendarMethod,
current: Vec<ItipField>,
previous: Vec<ItipField>,
},
Cancel(Vec<ItipField>),
Rsvp {
part_stat: ICalendarParticipationStatus,
current: Vec<ItipField>,
},
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ItipField {
pub name: ICalendarProperty,
pub value: ItipValue,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ItipValue {
Text(String),
Time(ItipTime),
Rrule(Box<ICalendarRecurrenceRule>),
Participants(Vec<ItipParticipant>),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ItipTime {
pub start: i64,
pub tz_id: u16,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ItipParticipant {
pub email: String,
pub name: Option<String>,
pub is_organizer: bool,
}
pub struct ItipMessages {
pub messages: Vec<TaskCalendarItipContents>,
}
impl Attendee<'_> {
pub fn send_invite_messages(&self) -> bool {
!self.email.is_local
&& self.is_server_scheduling
&& self.rsvp.is_none_or(|rsvp| rsvp)
&& (self.force_send.is_some()
|| self.part_stat.is_none_or(|part_stat| {
part_stat == &ICalendarParticipationStatus::NeedsAction
}))
}
pub fn send_update_messages(&self) -> bool {
!self.email.is_local
&& self.is_server_scheduling
&& self.rsvp.is_none_or(|rsvp| rsvp)
&& (self.force_send.is_some()
|| self
.part_stat
.is_none_or(|part_stat| part_stat != &ICalendarParticipationStatus::Declined))
}
pub fn is_delegated_from(&self, attendee: &Attendee<'_>) -> bool {
self.delegated_from
.iter()
.any(|d| d.email == attendee.email.email)
}
pub fn is_delegated_to(&self, attendee: &Attendee<'_>) -> bool {
self.delegated_to
.iter()
.any(|d| d.email == attendee.email.email)
}
}
impl Email {
pub fn new(email: &str, local_addresses: &[String]) -> Option<Self> {
let decoded = decode_mailto_address(email.trim());
let email = sanitize_email(decoded.as_ref())
.or_else(|| extract_addr_spec(decoded.as_ref()).and_then(sanitize_email))?;
let is_local = local_addresses.contains(&email);
Some(Email { email, is_local })
}
pub fn from_uri(uri: &Uri, local_addresses: &[String]) -> Option<Self> {
if let Uri::Location(uri) = uri {
Email::new(uri.as_str(), local_addresses)
} else {
None
}
}
}
pub fn ical_size(ical: &ICalendar) -> usize {
struct SizeWriter(usize);
impl std::fmt::Write for SizeWriter {
fn write_str(&mut self, text: &str) -> std::fmt::Result {
self.0 += text.len();
Ok(())
}
}
let mut writer = SizeWriter(0);
let _ = ical.write_to(&mut writer);
writer.0
}
impl PartialEq for Attendee<'_> {
fn eq(&self, other: &Self) -> bool {
self.email == other.email
&& self.part_stat == other.part_stat
&& self.delegated_from == other.delegated_from
&& self.delegated_to == other.delegated_to
&& self.role == other.role
&& self.cu_type == other.cu_type
&& self.sent_by == other.sent_by
}
}
impl Eq for Attendee<'_> {}
impl Hash for Attendee<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.email.hash(state);
self.part_stat.hash(state);
self.delegated_from.hash(state);
self.delegated_to.hash(state);
self.role.hash(state);
self.cu_type.hash(state);
self.sent_by.hash(state);
}
}
impl Display for Email {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "mailto:{}", self.email)
}
}
impl Hash for Email {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.email.hash(state);
}
}
impl PartialEq for Email {
fn eq(&self, other: &Self) -> bool {
self.email == other.email
}
}
impl Eq for Email {}
impl PartialEq for RecurrenceId {
fn eq(&self, other: &Self) -> bool {
self.date == other.date && self.this_and_future == other.this_and_future
}
}
impl Eq for RecurrenceId {}
impl Hash for RecurrenceId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.date.hash(state);
self.this_and_future.hash(state);
}
}
impl PartialEq for ItipDateTime<'_> {
fn eq(&self, other: &Self) -> bool {
self.timestamp == other.timestamp
}
}
impl Eq for ItipDateTime<'_> {}
impl Hash for ItipDateTime<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.timestamp.hash(state);
}
}
impl ItipDateTime<'_> {
pub fn to_entry(&self, name: ICalendarProperty) -> ICalendarEntry {
ICalendarEntry {
name,
params: self
.tz_id
.map(|tz_id| vec![ICalendarParameter::tzid(tz_id.to_string())])
.unwrap_or_default(),
values: vec![ICalendarValue::PartialDateTime(Box::new(self.date.clone()))],
}
}
}
impl ItipError {
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ItipError::NoSchedulingInfo => write!(f, "No scheduling information found"),
ItipError::OtherSchedulingAgent => write!(f, "Other scheduling agent"),
ItipError::NotOrganizer => write!(f, "Not the organizer of the event"),
ItipError::NotOrganizerNorAttendee => write!(f, "Not an organizer or attendee"),
ItipError::NothingToSend => write!(f, "No iTIP messages to send"),
ItipError::MissingUid => write!(f, "Missing UID in iCalendar object"),
ItipError::MultipleUid => write!(f, "Multiple UIDs found in iCalendar object"),
ItipError::MultipleOrganizer => {
write!(f, "Multiple organizers found in iCalendar object")
}
ItipError::MultipleObjectTypes => {
write!(f, "Multiple object types found in iCalendar object")
}
ItipError::MultipleObjectInstances => {
write!(f, "Multiple object instances found in iCalendar object")
}
ItipError::CannotModifyProperty(prop) => {
write!(f, "Cannot modify property {}", prop.as_str())
}
ItipError::CannotModifyInstance => write!(f, "Cannot modify instance of the event"),
ItipError::CannotModifyAddress => write!(f, "Cannot modify address of the event"),
ItipError::OrganizerMismatch => write!(f, "Organizer mismatch in iCalendar object"),
ItipError::MissingMethod => write!(f, "Missing method in the iTIP message"),
ItipError::InvalidComponentType => {
write!(f, "Invalid component type in iCalendar object")
}
ItipError::OutOfSequence => write!(f, "Old sequence number found"),
ItipError::OrganizerIsLocalAddress => {
write!(
f,
"Organizer matches one of the recipient's account addresses"
)
}
ItipError::SenderIsNotParticipant(participant) => {
write!(f, "Sender {participant:?} is not a participant")
}
ItipError::SenderIsNotOrganizerNorAttendee => {
write!(f, "Sender is neither organizer nor attendee")
}
ItipError::UnknownParticipant(participant) => {
write!(f, "Unknown participant: {}", participant)
}
ItipError::UnsupportedMethod(method) => {
write!(f, "Unsupported method: {}", method.as_str())
}
ItipError::ICalendarParseError => write!(f, "Failed to parse iCalendar object"),
ItipError::EventNotFound => write!(f, "Event found in index but not in database"),
ItipError::EventTooLarge => write!(
f,
"Applying the iTIP message would exceed the maximum event size"
),
ItipError::QuotaExceeded => write!(f, "Quota exceeded"),
ItipError::NoDefaultCalendar => write!(f, "No default calendar found for the account"),
ItipError::AutoAddDisabled => {
write!(f, "Auto-adding events is disabled for this account")
}
}
}
}
@@ -0,0 +1,403 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
InstanceId, ItipError, ItipMessage, ItipSnapshots, ItipSummary,
event_cancel::build_cancel_component,
itip::{ItipExportAs, itip_add_tz, itip_build_envelope, itip_export_component},
};
use ahash::{AHashMap, AHashSet};
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod,
ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus,
},
};
use std::collections::hash_map::Entry;
pub(crate) fn organizer_handle_update(
old_ical: &ICalendar,
new_ical: &ICalendar,
old_itip: ItipSnapshots<'_>,
new_itip: ItipSnapshots<'_>,
increment_sequences: &mut Vec<u16>,
) -> Result<Vec<ItipMessage<ICalendar>>, ItipError> {
let mut changed_instances: Vec<(&InstanceId, &str, &ICalendarMethod)> = Vec::new();
let mut increment_sequence = false;
let mut changed_properties = AHashSet::new();
for (instance_id, instance) in &new_itip.components {
if let Some(old_instance) = old_itip.components.get(instance_id) {
let changed_entries = instance.entries != old_instance.entries;
let changed_attendees = instance.attendees != old_instance.attendees;
if changed_entries || changed_attendees {
if changed_entries {
for entry in instance.entries.symmetric_difference(&old_instance.entries) {
increment_sequence = increment_sequence
|| matches!(
entry.name,
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Rrule
| ICalendarProperty::Rdate
| ICalendarProperty::Exdate
| ICalendarProperty::Status
| ICalendarProperty::Location
);
changed_properties.insert(entry.name);
}
}
if changed_attendees {
changed_instances.extend(
old_instance
.external_attendees()
.filter(|attendee| attendee.send_update_messages())
.map(|attendee| attendee.email.email.as_str())
.collect::<AHashSet<_>>()
.difference(
&instance
.external_attendees()
.map(|attendee| attendee.email.email.as_str())
.collect::<AHashSet<_>>(),
)
.map(|attendee| (instance_id, *attendee, &ICalendarMethod::Cancel)),
);
changed_properties.insert(&ICalendarProperty::Attendee);
increment_sequence = true;
}
changed_instances.extend(instance.attendees.iter().filter_map(|attendee| {
if attendee.send_update_messages() {
Some((
instance_id,
attendee.email.email.as_str(),
&ICalendarMethod::Request,
))
} else {
None
}
}));
}
} else if instance_id != &InstanceId::Main {
changed_properties.insert(&ICalendarProperty::Exdate);
let method = if matches!(instance.comp.status(), Some(ICalendarStatus::Cancelled)) {
&ICalendarMethod::Cancel
} else {
&ICalendarMethod::Request
};
changed_instances.extend(instance.attendees.iter().filter_map(|attendee| {
if attendee.send_invite_messages() {
Some((instance_id, attendee.email.email.as_str(), method))
} else {
None
}
}));
increment_sequence = true;
} else {
return Err(ItipError::CannotModifyInstance);
}
}
for (instance_id, old_instance) in &old_itip.components {
if !new_itip.components.contains_key(instance_id) {
if instance_id != &InstanceId::Main {
changed_instances.extend(old_instance.attendees.iter().filter_map(|attendee| {
if attendee.send_update_messages() {
Some((
instance_id,
attendee.email.email.as_str(),
&ICalendarMethod::Cancel,
))
} else {
None
}
}));
changed_properties.insert(&ICalendarProperty::Exdate);
increment_sequence = true;
} else {
return Err(ItipError::CannotModifyInstance);
}
}
}
if changed_instances.is_empty() {
return Err(ItipError::NothingToSend);
}
// Remove partial notifications for attendees that receive a full update for the main instance
// or, that will receive both add and remove messages
let mut send_full_update: AHashSet<&str> = AHashSet::new();
let mut send_partial_update: AHashMap<&str, AHashMap<&ICalendarMethod, Vec<&InstanceId>>> =
AHashMap::new();
for (instance_id, email, method) in &changed_instances {
if *instance_id == &InstanceId::Main && *method == &ICalendarMethod::Request {
send_full_update.insert(*email);
send_partial_update.remove(email);
} else if !send_full_update.contains(email) {
match send_partial_update.entry(email) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
let is_empty = entry.is_empty();
match entry.entry(method) {
Entry::Occupied(mut method_entry) => {
method_entry.get_mut().push(*instance_id);
}
Entry::Vacant(method_entry) if is_empty => {
method_entry.insert(vec![*instance_id]);
}
_ => {
// Switch to full update for this participant
send_full_update.insert(*email);
send_partial_update.remove(email);
}
}
}
Entry::Vacant(entry) => {
entry.insert(AHashMap::from_iter([(*method, vec![*instance_id])]));
}
}
}
}
// Build summary of changed properties
let new_summary = new_itip
.main_instance_or_default()
.build_summary(Some(&new_itip.organizer), &[]);
let old_summary = old_itip
.main_instance_or_default()
.build_summary(Some(&old_itip.organizer), &new_summary);
// Prepare full updates
let mut messages = Vec::new();
if !send_full_update.is_empty() {
match organizer_request_full(
new_ical,
&new_itip,
increment_sequence.then_some(increment_sequences),
false,
) {
Ok(messages_) => {
for mut message in messages_ {
message.summary = ItipSummary::Update {
method: ICalendarMethod::Request,
current: new_summary.clone(),
previous: old_summary.clone(),
};
messages.push(message);
}
}
Err(err) => {
if send_partial_update.is_empty() {
return Err(err);
}
}
}
}
// Prepare partial updates
if !send_partial_update.is_empty() {
// Group updates by email and method
let mut updates: AHashMap<(&ICalendarMethod, Vec<&InstanceId>), Vec<&str>> =
AHashMap::new();
for (email, partial_updates) in send_partial_update {
for (method, mut instances) in partial_updates {
instances.sort_unstable();
instances.dedup();
updates.entry((method, instances)).or_default().push(email);
}
}
let dt_stamp = PartialDateTime::now();
for ((method, instances), emails) in updates {
let (mut ical, mut itip, is_cancel) = if matches!(method, ICalendarMethod::Cancel) {
(old_ical, &old_itip, true)
} else {
(new_ical, &new_itip, false)
};
// Prepare iTIP message
let mut message = ICalendar {
components: Vec::with_capacity(instances.len() + 1),
};
message.components.push(itip_build_envelope(method.clone()));
let mut increment_sequences = Vec::new();
for instance_id in instances {
let comp = match itip.components.get(instance_id) {
Some(comp) => comp,
None => {
// New component added with CANCELLED status
ical = new_ical;
itip = &new_itip;
itip.components.get(instance_id).unwrap()
}
};
// Prepare component for iTIP
let sequence = if increment_sequence {
comp.sequence.unwrap_or_default() + 1
} else {
comp.sequence.unwrap_or_default()
};
let orig_component = comp.comp;
let component = if !is_cancel {
if increment_sequence {
increment_sequences.push(comp.comp_id);
}
// Export component with updated sequence and participation status
itip_export_component(
orig_component,
itip.uid,
&dt_stamp,
sequence,
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
)
} else {
build_cancel_component(orig_component, sequence, dt_stamp.clone(), &emails)
};
// Add component to message
let comp_id = message.components.len() as u32;
message.components.push(component);
message.components[0].component_ids.push(comp_id);
}
// Add timezones
itip_add_tz(&mut message, ical);
messages.push(ItipMessage {
from: itip.organizer.email.email.clone(),
from_organizer: true,
to: emails.into_iter().map(|e| e.to_string()).collect(),
summary: if method == &ICalendarMethod::Cancel {
ItipSummary::Cancel(
new_summary
.iter()
.chain(old_summary.iter())
.map(|summary| (&summary.name, summary))
.collect::<AHashMap<_, _>>()
.into_values()
.cloned()
.collect(),
)
} else {
ItipSummary::Update {
method: method.clone(),
current: new_summary.clone(),
previous: old_summary.clone(),
}
},
message,
});
}
}
Ok(messages)
}
pub(crate) fn organizer_request_full(
ical: &ICalendar,
itip: &ItipSnapshots<'_>,
mut increment_sequence: Option<&mut Vec<u16>>,
is_first_request: bool,
) -> Result<Vec<ItipMessage<ICalendar>>, ItipError> {
// Prepare iTIP message
let dt_stamp = PartialDateTime::now();
let mut message = ICalendar {
components: vec![ICalendarComponent::default(); ical.components.len()],
};
message.components[0] = itip_build_envelope(ICalendarMethod::Request);
let mut recipients = AHashSet::new();
let mut copy_components = AHashSet::new();
for comp in itip.components.values() {
// Skip private components
if comp.attendees.is_empty() {
continue;
}
// Prepare component for iTIP
let sequence = if let Some(increment_sequence) = &mut increment_sequence {
increment_sequence.push(comp.comp_id);
comp.sequence.unwrap_or_default() + 1
} else {
comp.sequence.unwrap_or_default()
};
let orig_component = &ical.components[comp.comp_id as usize];
let mut component = itip_export_component(
orig_component,
itip.uid,
&dt_stamp,
sequence,
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
);
// Add VALARM sub-components
if is_first_request {
for sub_comp_id in &orig_component.component_ids {
if matches!(
ical.components[*sub_comp_id as usize].component_type,
ICalendarComponentType::VAlarm
) {
copy_components.insert(*sub_comp_id);
component.component_ids.push(*sub_comp_id);
}
}
}
// Add component to message
message.components[comp.comp_id as usize] = component;
message.components[0]
.component_ids
.push(comp.comp_id as u32);
// Add attendees
for attendee in &comp.attendees {
if (is_first_request && attendee.send_invite_messages())
|| (!is_first_request && attendee.send_update_messages())
{
recipients.insert(&attendee.email.email);
}
}
}
// Copy timezones and alarms
for (comp_id, comp) in ical.components.iter().enumerate() {
if matches!(comp.component_type, ICalendarComponentType::VTimezone) {
copy_components.extend(comp.component_ids.iter().copied());
message.components[0].component_ids.push(comp_id as u32);
} else if !copy_components.contains(&(comp_id as u32)) {
continue;
}
message.components[comp_id] = comp.clone();
}
message.components[0].component_ids.sort_unstable();
message.add_missing_timezones();
if !recipients.is_empty() {
Ok(vec![ItipMessage {
from: itip.organizer.email.email.clone(),
from_organizer: true,
to: recipients.into_iter().map(|e| e.to_string()).collect(),
summary: ItipSummary::Invite(
itip.main_instance_or_default()
.build_summary(Some(&itip.organizer), &[]),
),
message,
}])
} else {
Err(ItipError::NothingToSend)
}
}
+489
View File
@@ -0,0 +1,489 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
Attendee, Email, InstanceId, ItipDateTime, ItipEntry, ItipEntryValue, ItipError, ItipField,
ItipParticipant, ItipSnapshot, ItipSnapshots, ItipTime, ItipValue, Organizer, RecurrenceId,
};
use ahash::AHashMap;
use calcard::icalendar::{
ICalendar, ICalendarParameterName, ICalendarParameterValue, ICalendarProperty,
ICalendarScheduleAgentValue, ICalendarValue, Uri,
};
pub fn itip_snapshot<'x, 'y>(
ical: &'x ICalendar,
account_emails: &'y [String],
force_add_client_scheduling: bool,
) -> Result<ItipSnapshots<'x>, ItipError> {
if !ical.components.iter().any(|comp| {
comp.component_type.is_scheduling_object()
&& comp
.entries
.iter()
.any(|e| matches!(e.name, ICalendarProperty::Organizer))
}) {
return Err(ItipError::NoSchedulingInfo);
}
let mut organizer: Option<Organizer<'x>> = None;
let mut uid: Option<&'x str> = None;
let mut components = AHashMap::new();
let mut expect_object_type = None;
let mut has_local_emails = false;
let mut tz_resolver = None;
for (comp_id, comp) in ical.components.iter().enumerate() {
if comp.component_type.is_scheduling_object() {
match expect_object_type {
Some(expected) if expected != &comp.component_type => {
return Err(ItipError::MultipleObjectTypes);
}
None => {
expect_object_type = Some(&comp.component_type);
}
_ => {}
}
let mut sched_comp = ItipSnapshot {
comp_id: comp_id as u16,
comp,
attendees: Default::default(),
dtstamp: Default::default(),
entries: Default::default(),
sequence: Default::default(),
request_status: Default::default(),
};
let mut instance_id = InstanceId::Main;
for (entry_id, entry) in comp.entries.iter().enumerate() {
match &entry.name {
ICalendarProperty::Organizer => {
if let Some(email) = entry
.values
.first()
.and_then(|v| v.as_text())
.and_then(|v| Email::new(v, account_emails))
{
let mut part = Organizer {
entry_id: entry_id as u16,
email,
is_server_scheduling: true,
name: None,
force_send: None,
};
has_local_emails |= part.email.is_local;
for param in &entry.params {
match (&param.name, &param.value) {
(
ICalendarParameterName::ScheduleAgent,
ICalendarParameterValue::ScheduleAgent(
ICalendarScheduleAgentValue::Client
| ICalendarScheduleAgentValue::None,
),
) => {
part.is_server_scheduling = false;
}
(
ICalendarParameterName::ScheduleForceSend,
ICalendarParameterValue::ScheduleForceSend(force_send),
) => {
part.force_send = Some(force_send);
}
(
ICalendarParameterName::Cn,
ICalendarParameterValue::Text(name),
) => {
part.name = Some(name.as_str());
}
_ => {}
}
}
if !part.is_server_scheduling && !force_add_client_scheduling {
return Err(ItipError::OtherSchedulingAgent);
}
match organizer {
Some(existing_organizer)
if existing_organizer.email.email != part.email.email =>
{
return Err(ItipError::MultipleOrganizer);
}
None => {
organizer = Some(part);
}
_ => {}
}
}
}
ICalendarProperty::Attendee => {
if let Some(email) = entry
.values
.first()
.and_then(|v| v.as_text())
.and_then(|v| Email::new(v, account_emails))
{
let mut part = Attendee {
entry_id: entry_id as u16,
email,
name: None,
rsvp: None,
is_server_scheduling: true,
force_send: None,
part_stat: None,
delegated_from: vec![],
delegated_to: vec![],
cu_type: None,
role: None,
sent_by: None,
};
for param in &entry.params {
match (&param.name, &param.value) {
(
ICalendarParameterName::ScheduleAgent,
ICalendarParameterValue::ScheduleAgent(agent),
) => {
part.is_server_scheduling =
agent == &ICalendarScheduleAgentValue::Server;
}
(
ICalendarParameterName::Rsvp,
ICalendarParameterValue::Bool(rsvp),
) => {
part.rsvp = Some(*rsvp);
}
(
ICalendarParameterName::ScheduleForceSend,
ICalendarParameterValue::ScheduleForceSend(force_send),
) => {
part.force_send = Some(force_send);
}
(
ICalendarParameterName::Partstat,
ICalendarParameterValue::Partstat(value),
) => {
part.part_stat = Some(value);
}
(
ICalendarParameterName::Cutype,
ICalendarParameterValue::Cutype(value),
) => {
part.cu_type = Some(value);
}
(
ICalendarParameterName::DelegatedFrom,
ICalendarParameterValue::Uri(uri),
) => {
if let Some(uri) = Email::from_uri(uri, account_emails) {
part.delegated_from.push(uri);
}
}
(
ICalendarParameterName::DelegatedTo,
ICalendarParameterValue::Uri(uri),
) => {
if let Some(uri) = Email::from_uri(uri, account_emails) {
part.delegated_to.push(uri);
}
}
(
ICalendarParameterName::Role,
ICalendarParameterValue::Role(value),
) => {
part.role = Some(value);
}
(
ICalendarParameterName::SentBy,
ICalendarParameterValue::Uri(value),
) => {
part.sent_by = Email::from_uri(value, account_emails);
}
(
ICalendarParameterName::Cn,
ICalendarParameterValue::Text(name),
) => {
part.name = Some(name.as_str());
}
_ => {}
}
}
has_local_emails |= part.email.is_local
&& (force_add_client_scheduling || part.is_server_scheduling);
sched_comp.attendees.insert(part);
}
}
ICalendarProperty::Uid => {
if let Some(uid_) = entry
.values
.first()
.and_then(|v| v.as_text())
.map(|v| v.trim())
.filter(|v| !v.is_empty())
{
match uid {
Some(existing_uid) if existing_uid != uid_ => {
return Err(ItipError::MultipleUid);
}
None => {
uid = Some(uid_);
}
_ => {}
}
}
}
ICalendarProperty::Sequence => {
if let Some(sequence) = entry.values.first().and_then(|v| v.as_integer()) {
sched_comp.sequence = Some(sequence);
}
}
ICalendarProperty::RecurrenceId => {
if let Some(date) =
entry.values.first().and_then(|v| v.as_partial_date_time())
{
let mut this_and_future = false;
let mut tz_id = None;
for param in &entry.params {
match (&param.name, &param.value) {
(
ICalendarParameterName::Tzid,
ICalendarParameterValue::Text(id),
) => {
tz_id = Some(id.as_str());
}
(ICalendarParameterName::Range, _) => {
this_and_future = true;
}
_ => (),
}
}
instance_id = InstanceId::Recurrence(RecurrenceId {
entry_id: entry_id as u16,
date: date
.to_date_time_with_tz(
tz_resolver
.get_or_insert_with(|| ical.build_tz_resolver())
.resolve_or_default(tz_id),
)
.map(|dt| dt.timestamp())
.unwrap_or_else(|| date.to_timestamp().unwrap_or_default()),
this_and_future,
});
}
}
ICalendarProperty::RequestStatus => {
if let Some(value) = entry.values.first().and_then(|v| v.as_text()) {
sched_comp.request_status.push(value);
}
}
ICalendarProperty::Dtstamp => {
sched_comp.dtstamp =
entry.values.first().and_then(|v| v.as_partial_date_time());
}
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Rrule
| ICalendarProperty::Rdate
| ICalendarProperty::Exdate
| ICalendarProperty::Status
| ICalendarProperty::Location
| ICalendarProperty::Conference
| ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Priority
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed => {
let tz_id = entry.tz_id();
for value in &entry.values {
let value = match value {
ICalendarValue::Uri(Uri::Location(v)) => {
ItipEntryValue::Text(v.as_str())
}
ICalendarValue::PartialDateTime(date) => {
let tz = tz_resolver
.get_or_insert_with(|| ical.build_tz_resolver())
.resolve_or_default(tz_id);
ItipEntryValue::DateTime(ItipDateTime {
date: date.as_ref(),
tz_id,
tz_code: tz.as_id(),
timestamp: date
.to_date_time_with_tz(tz)
.map(|dt| dt.timestamp())
.unwrap_or_else(|| {
date.to_timestamp().unwrap_or_default()
}),
})
}
ICalendarValue::Duration(v) => ItipEntryValue::Duration(v),
ICalendarValue::RecurrenceRule(v) => ItipEntryValue::RRule(v),
ICalendarValue::Period(v) => ItipEntryValue::Period(v),
ICalendarValue::Integer(v) => ItipEntryValue::Integer(*v),
ICalendarValue::Text(v) => ItipEntryValue::Text(v.as_str()),
ICalendarValue::Status(v) => ItipEntryValue::Status(v),
_ => continue,
};
sched_comp.entries.insert(ItipEntry {
name: &entry.name,
value,
});
}
}
_ => {}
}
}
if components.insert(instance_id, sched_comp).is_some() {
return Err(ItipError::MultipleObjectInstances);
}
}
}
if has_local_emails {
Ok(ItipSnapshots {
organizer: organizer.ok_or(ItipError::NoSchedulingInfo)?,
uid: uid.ok_or(ItipError::MissingUid)?,
components,
})
} else {
Err(ItipError::NotOrganizerNorAttendee)
}
}
impl ItipSnapshots<'_> {
pub fn sender_is_organizer_or_attendee(&self, email: &str) -> bool {
self.organizer.email.email == email
|| self.components.values().any(|snapshot| {
snapshot
.attendees
.iter()
.any(|attendee| attendee.email.email == email)
})
}
pub fn main_instance(&self) -> Option<&ItipSnapshot<'_>> {
self.components.get(&InstanceId::Main)
}
pub fn main_instance_or_default(&self) -> &ItipSnapshot<'_> {
self.main_instance()
.unwrap_or_else(|| self.components.values().next().unwrap())
}
}
impl ItipSnapshot<'_> {
pub fn has_local_attendee(&self) -> bool {
self.attendees
.iter()
.any(|attendee| attendee.email.is_local)
}
pub fn local_attendee(&self) -> Option<&Attendee<'_>> {
self.attendees
.iter()
.find(|attendee| attendee.email.is_local)
}
pub fn external_attendees(&self) -> impl Iterator<Item = &Attendee<'_>> + '_ {
self.attendees.iter().filter(|item| !item.email.is_local)
}
pub fn attendee_by_email(&self, email: &str) -> Option<&Attendee<'_>> {
self.attendees
.iter()
.find(|attendee| attendee.email.email == email)
}
pub fn build_summary(
&self,
include_guests: Option<&Organizer<'_>>,
skip_fields: &[ItipField],
) -> Vec<ItipField> {
let mut fields = Vec::with_capacity(5);
for entry in &self.entries {
if matches!(
entry.name,
ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Dtstart
| ICalendarProperty::Location
| ICalendarProperty::Conference
| ICalendarProperty::Rrule
) {
let value = match &entry.value {
ItipEntryValue::DateTime(dt) => ItipValue::Time(ItipTime {
start: dt.timestamp,
tz_id: dt.tz_code,
}),
ItipEntryValue::RRule(rule) => ItipValue::Rrule(Box::new((*rule).clone())),
ItipEntryValue::Text(value) => ItipValue::Text(value.to_string()),
_ => continue,
};
let field = ItipField {
name: entry.name.clone(),
value,
};
if !skip_fields.contains(&field) {
fields.push(field);
}
}
}
if let Some(organizer) = include_guests {
let mut attendees = Vec::with_capacity(self.attendees.len());
for attendee in &self.attendees {
if attendee.email.email != organizer.email.email {
attendees.push(ItipParticipant {
email: attendee.email.email.to_string(),
name: attendee.name.map(|n| n.to_string()),
is_organizer: false,
});
}
}
attendees.push(ItipParticipant {
email: organizer.email.email.to_string(),
name: organizer.name.map(|n| n.to_string()),
is_organizer: true,
});
attendees.sort_by(|a, b| {
if a.is_organizer && !b.is_organizer {
std::cmp::Ordering::Less
} else if !a.is_organizer && b.is_organizer {
std::cmp::Ordering::Greater
} else if let (Some(a_name), Some(b_name)) = (a.name.as_deref(), b.name.as_deref())
{
match a_name.cmp(b_name) {
std::cmp::Ordering::Equal => a.email.cmp(&b.email),
ord => ord,
}
} else {
a.email.cmp(&b.email)
}
});
let field = ItipField {
name: ICalendarProperty::Attendee,
value: ItipValue::Participants(attendees),
};
if !skip_fields.contains(&field) {
fields.push(field);
}
}
fields
}
}