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:
@@ -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
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+281
@@ -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"));
|
||||
}
|
||||
}
|
||||
Vendored
+653
@@ -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!(),
|
||||
})
|
||||
}
|
||||
@@ -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 (¶m.name, ¶m.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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) = ¶m.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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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>()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(¶m.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)
|
||||
}
|
||||
@@ -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!(
|
||||
(¶m.name, ¶m.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!(
|
||||
(¶m.name, ¶m.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!(
|
||||
¶m.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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 (¶m.name, ¶m.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 (¶m.name, ¶m.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 (¶m.name, ¶m.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user