mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-09-29 23:34:38 +08:00
Assisted CalDAV/CardDAV shared resource discovery (closes #1691)
This commit is contained in:
parent
020bcaef8b
commit
01f7c8f56d
10 changed files with 772 additions and 528 deletions
|
@ -17,6 +17,7 @@ pub struct GroupwareConfig {
|
|||
pub max_lock_timeout: u64,
|
||||
pub max_locks_per_user: usize,
|
||||
pub max_results: usize,
|
||||
pub assisted_discovery: bool,
|
||||
|
||||
// Calendar settings
|
||||
pub max_ical_size: usize,
|
||||
|
@ -81,6 +82,9 @@ impl GroupwareConfig {
|
|||
.property_or_default::<Option<usize>>("dav.property.max-size.dead", "1024")
|
||||
.unwrap_or(Some(1024)),
|
||||
live_property_size: config.property("dav.property.max-size.live").unwrap_or(250),
|
||||
assisted_discovery: config
|
||||
.property("dav.collection.assisted-discovery")
|
||||
.unwrap_or(false),
|
||||
max_lock_timeout: config
|
||||
.property::<Duration>("dav.lock.max-timeout")
|
||||
.map(|d| d.as_secs())
|
||||
|
|
|
@ -75,6 +75,10 @@ pub(crate) enum DavQueryResource<'x> {
|
|||
parent_collection: Collection,
|
||||
items: Vec<PropFindItem>,
|
||||
},
|
||||
Discovery {
|
||||
parent_collection: Collection,
|
||||
account_ids: Vec<u32>,
|
||||
},
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
@ -299,6 +303,29 @@ impl<'x> DavQuery<'x> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn discovery(
|
||||
propfind: PropFind,
|
||||
account_ids: Vec<u32>,
|
||||
collection: Collection,
|
||||
headers: &RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
Self {
|
||||
resource: DavQueryResource::Discovery {
|
||||
parent_collection: collection,
|
||||
account_ids,
|
||||
},
|
||||
propfind,
|
||||
depth: 0,
|
||||
ret: headers.ret,
|
||||
depth_no_root: headers.depth_no_root,
|
||||
uri: headers.uri,
|
||||
sync_type: Default::default(),
|
||||
limit: Default::default(),
|
||||
max_vcard_version: headers.max_vcard_version,
|
||||
expand: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_minimal(&self) -> bool {
|
||||
self.ret == Return::Minimal
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -178,11 +178,11 @@ impl OwnedUri<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<A, R> UriResource<A, R> {
|
||||
/*impl<A, R> UriResource<A, R> {
|
||||
pub fn collection_path(&self) -> &'static str {
|
||||
DavResourceName::from(self.collection).collection_path()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
impl Urn {
|
||||
pub fn try_extract_sync_id(token: &str) -> Option<&str> {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod calendar;
|
||||
pub mod card;
|
||||
|
|
|
@ -270,28 +270,54 @@ impl PrincipalPropFind for Server {
|
|||
))],
|
||||
));
|
||||
}
|
||||
PrincipalProperty::CalendarHomeSet => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Cal.base_path(),
|
||||
percent_encoding::utf8_percent_encode(&name, RFC_3986),
|
||||
))],
|
||||
));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
PrincipalProperty::AddressbookHomeSet => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Card.base_path(),
|
||||
percent_encoding::utf8_percent_encode(&name, RFC_3986),
|
||||
))],
|
||||
));
|
||||
response.set_namespace(Namespace::CardDav);
|
||||
PrincipalProperty::CalendarHomeSet
|
||||
| PrincipalProperty::AddressbookHomeSet => {
|
||||
let mut hrefs = Vec::new();
|
||||
let (collection, resource_name, namespace) =
|
||||
if principal_property == &PrincipalProperty::CalendarHomeSet {
|
||||
(
|
||||
Collection::Calendar,
|
||||
DavResourceName::Cal,
|
||||
Namespace::CalDav,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Collection::AddressBook,
|
||||
DavResourceName::Card,
|
||||
Namespace::CardDav,
|
||||
)
|
||||
};
|
||||
|
||||
for account_id in access_token.all_ids_by_collection(collection) {
|
||||
let href = if account_id == access_token.primary_id() {
|
||||
format!(
|
||||
"{}/{}/",
|
||||
resource_name.base_path(),
|
||||
percent_encoding::utf8_percent_encode(
|
||||
&access_token.name,
|
||||
RFC_3986
|
||||
),
|
||||
)
|
||||
} else {
|
||||
let name = self
|
||||
.store()
|
||||
.get_principal_name(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| format!("_{account_id}"));
|
||||
format!(
|
||||
"{}/{}/",
|
||||
resource_name.base_path(),
|
||||
percent_encoding::utf8_percent_encode(&name, RFC_3986),
|
||||
)
|
||||
};
|
||||
hrefs.push(Href(href));
|
||||
}
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), hrefs));
|
||||
response.set_namespace(namespace);
|
||||
}
|
||||
|
||||
PrincipalProperty::PrincipalAddress => {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
response.set_namespace(Namespace::CardDav);
|
||||
|
|
17
crates/groupware/src/cache/calcard.rs
vendored
17
crates/groupware/src/cache/calcard.rs
vendored
|
@ -53,14 +53,20 @@ pub(super) async fn build_calcard_resources(
|
|||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_default();
|
||||
let name = server
|
||||
.store()
|
||||
.get_principal_name(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| format!("_{account_id}"));
|
||||
if container_ids.is_empty() {
|
||||
if is_calendar {
|
||||
server
|
||||
.create_default_calendar(access_token, account_id)
|
||||
.create_default_calendar(access_token, account_id, &name)
|
||||
.await?;
|
||||
} else {
|
||||
server
|
||||
.create_default_addressbook(access_token, account_id)
|
||||
.create_default_addressbook(access_token, account_id, &name)
|
||||
.await?;
|
||||
}
|
||||
last_change_id = server
|
||||
|
@ -84,13 +90,6 @@ pub(super) async fn build_calcard_resources(
|
|||
.caused_by(trc::location!())?
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = server
|
||||
.store()
|
||||
.get_principal_name(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| format!("_{account_id}"));
|
||||
|
||||
let mut cache = DavResources {
|
||||
base_path: format!(
|
||||
"{}/{}/",
|
||||
|
|
25
crates/groupware/src/cache/mod.rs
vendored
25
crates/groupware/src/cache/mod.rs
vendored
|
@ -44,18 +44,21 @@ pub trait GroupwareCache: Sync + Send {
|
|||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
account_name: &str,
|
||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||
|
||||
fn create_default_calendar(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
account_name: &str,
|
||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||
|
||||
fn get_or_create_default_calendar(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
account_name: &str,
|
||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||
|
||||
fn cached_dav_resources(
|
||||
|
@ -337,6 +340,7 @@ impl GroupwareCache for Server {
|
|||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
account_name: &str,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
if let Some(name) = &self.core.groupware.default_addressbook_name {
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
@ -346,7 +350,12 @@ impl GroupwareCache for Server {
|
|||
.await?;
|
||||
AddressBook {
|
||||
name: name.clone(),
|
||||
display_name: self.core.groupware.default_addressbook_display_name.clone(),
|
||||
display_name: self
|
||||
.core
|
||||
.groupware
|
||||
.default_addressbook_display_name
|
||||
.as_ref()
|
||||
.map(|display| format!("{display} ({account_name})")),
|
||||
is_default: true,
|
||||
..Default::default()
|
||||
}
|
||||
|
@ -362,6 +371,7 @@ impl GroupwareCache for Server {
|
|||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
account_name: &str,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
if let Some(name) = &self.core.groupware.default_calendar_name {
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
@ -377,8 +387,11 @@ impl GroupwareCache for Server {
|
|||
.core
|
||||
.groupware
|
||||
.default_calendar_display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| name.clone()),
|
||||
.as_ref()
|
||||
.map_or_else(
|
||||
|| name.clone(),
|
||||
|display| format!("{display} ({account_name})",),
|
||||
),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
|
@ -395,13 +408,17 @@ impl GroupwareCache for Server {
|
|||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
account_name: &str,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
match self
|
||||
.get_document_ids(account_id, Collection::Calendar)
|
||||
.await
|
||||
{
|
||||
Ok(Some(ids)) if !ids.is_empty() => Ok(ids.iter().next()),
|
||||
_ => self.create_default_calendar(access_token, account_id).await,
|
||||
_ => {
|
||||
self.create_default_calendar(access_token, account_id, account_name)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -249,7 +249,7 @@ impl ItipIngest for Server {
|
|||
|
||||
// Obtain parent calendar
|
||||
let Some(parent_id) = self
|
||||
.get_or_create_default_calendar(access_token, account_id)
|
||||
.get_or_create_default_calendar(access_token, account_id, &access_token.name)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
|
|
|
@ -62,48 +62,55 @@ pub mod prop;
|
|||
pub mod put_get;
|
||||
pub mod sync;
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn webdav_tests() {
|
||||
// Prepare settings
|
||||
let start_time = Instant::now();
|
||||
let delete = true;
|
||||
let handle = init_webdav_tests(
|
||||
&std::env::var("STORE")
|
||||
.expect("Missing store type. Try running `STORE=<store_type> cargo test`"),
|
||||
delete,
|
||||
)
|
||||
.await;
|
||||
|
||||
#[test]
|
||||
fn webdav_tests() {
|
||||
//test_build_itip_templates(&handle.server).await;
|
||||
|
||||
basic::test(&handle).await;
|
||||
put_get::test(&handle).await;
|
||||
mkcol::test(&handle).await;
|
||||
copy_move::test(&handle).await;
|
||||
prop::test(&handle).await;
|
||||
multiget::test(&handle).await;
|
||||
sync::test(&handle).await;
|
||||
lock::test(&handle).await;
|
||||
principals::test(&handle).await;
|
||||
acl::test(&handle).await;
|
||||
card_query::test(&handle).await;
|
||||
cal_query::test(&handle).await;
|
||||
cal_alarm::test(&handle).await;
|
||||
cal_itip::test();
|
||||
cal_scheduling::test(&handle).await;
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.thread_stack_size(8 * 1024 * 1024) // 8MB stack
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
// Prepare settings
|
||||
let start_time = Instant::now();
|
||||
let delete = true;
|
||||
let handle = init_webdav_tests(
|
||||
&std::env::var("STORE")
|
||||
.expect("Missing store type. Try running `STORE=<store_type> cargo test`"),
|
||||
delete,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Print elapsed time
|
||||
let elapsed = start_time.elapsed();
|
||||
println!(
|
||||
"Elapsed: {}.{:03}s",
|
||||
elapsed.as_secs(),
|
||||
elapsed.subsec_millis()
|
||||
);
|
||||
basic::test(&handle).await;
|
||||
put_get::test(&handle).await;
|
||||
mkcol::test(&handle).await;
|
||||
copy_move::test(&handle).await;
|
||||
prop::test(&handle).await;
|
||||
multiget::test(&handle).await;
|
||||
sync::test(&handle).await;
|
||||
lock::test(&handle).await;
|
||||
principals::test(&handle).await;
|
||||
acl::test(&handle).await;
|
||||
card_query::test(&handle).await;
|
||||
cal_query::test(&handle).await;
|
||||
cal_alarm::test(&handle).await;
|
||||
cal_itip::test();
|
||||
cal_scheduling::test(&handle).await;
|
||||
|
||||
// Remove test data
|
||||
if delete {
|
||||
handle.temp_dir.delete();
|
||||
}
|
||||
// Print elapsed time
|
||||
let elapsed = start_time.elapsed();
|
||||
println!(
|
||||
"Elapsed: {}.{:03}s",
|
||||
elapsed.as_secs(),
|
||||
elapsed.subsec_millis()
|
||||
);
|
||||
|
||||
// Remove test data
|
||||
if delete {
|
||||
handle.temp_dir.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -1160,6 +1167,9 @@ minimum-interval = "1s"
|
|||
[calendar.scheduling.inbound]
|
||||
auto-add = true
|
||||
|
||||
[dav.collection]
|
||||
assisted-discovery = false
|
||||
|
||||
[store."auth"]
|
||||
type = "sqlite"
|
||||
path = "{TMP}/auth.db"
|
||||
|
|
Loading…
Add table
Reference in a new issue