mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-11 21:15:45 +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_lock_timeout: u64,
|
||||||
pub max_locks_per_user: usize,
|
pub max_locks_per_user: usize,
|
||||||
pub max_results: usize,
|
pub max_results: usize,
|
||||||
|
pub assisted_discovery: bool,
|
||||||
|
|
||||||
// Calendar settings
|
// Calendar settings
|
||||||
pub max_ical_size: usize,
|
pub max_ical_size: usize,
|
||||||
|
@ -81,6 +82,9 @@ impl GroupwareConfig {
|
||||||
.property_or_default::<Option<usize>>("dav.property.max-size.dead", "1024")
|
.property_or_default::<Option<usize>>("dav.property.max-size.dead", "1024")
|
||||||
.unwrap_or(Some(1024)),
|
.unwrap_or(Some(1024)),
|
||||||
live_property_size: config.property("dav.property.max-size.live").unwrap_or(250),
|
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
|
max_lock_timeout: config
|
||||||
.property::<Duration>("dav.lock.max-timeout")
|
.property::<Duration>("dav.lock.max-timeout")
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
|
|
|
@ -75,6 +75,10 @@ pub(crate) enum DavQueryResource<'x> {
|
||||||
parent_collection: Collection,
|
parent_collection: Collection,
|
||||||
items: Vec<PropFindItem>,
|
items: Vec<PropFindItem>,
|
||||||
},
|
},
|
||||||
|
Discovery {
|
||||||
|
parent_collection: Collection,
|
||||||
|
account_ids: Vec<u32>,
|
||||||
|
},
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
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 {
|
pub fn is_minimal(&self) -> bool {
|
||||||
self.ret == Return::Minimal
|
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 {
|
pub fn collection_path(&self) -> &'static str {
|
||||||
DavResourceName::from(self.collection).collection_path()
|
DavResourceName::from(self.collection).collection_path()
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
impl Urn {
|
impl Urn {
|
||||||
pub fn try_extract_sync_id(token: &str) -> Option<&str> {
|
pub fn try_extract_sync_id(token: &str) -> Option<&str> {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||||
*/
|
*/
|
||||||
|
#![warn(clippy::large_futures)]
|
||||||
|
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod card;
|
pub mod card;
|
||||||
|
|
|
@ -270,28 +270,54 @@ impl PrincipalPropFind for Server {
|
||||||
))],
|
))],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
PrincipalProperty::CalendarHomeSet => {
|
PrincipalProperty::CalendarHomeSet
|
||||||
fields.push(DavPropertyValue::new(
|
| PrincipalProperty::AddressbookHomeSet => {
|
||||||
property.clone(),
|
let mut hrefs = Vec::new();
|
||||||
vec![Href(format!(
|
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!(
|
||||||
"{}/{}/",
|
"{}/{}/",
|
||||||
DavResourceName::Cal.base_path(),
|
resource_name.base_path(),
|
||||||
percent_encoding::utf8_percent_encode(&name, RFC_3986),
|
percent_encoding::utf8_percent_encode(
|
||||||
))],
|
&access_token.name,
|
||||||
));
|
RFC_3986
|
||||||
response.set_namespace(Namespace::CalDav);
|
),
|
||||||
}
|
)
|
||||||
PrincipalProperty::AddressbookHomeSet => {
|
} else {
|
||||||
fields.push(DavPropertyValue::new(
|
let name = self
|
||||||
property.clone(),
|
.store()
|
||||||
vec![Href(format!(
|
.get_principal_name(account_id)
|
||||||
|
.await
|
||||||
|
.caused_by(trc::location!())?
|
||||||
|
.unwrap_or_else(|| format!("_{account_id}"));
|
||||||
|
format!(
|
||||||
"{}/{}/",
|
"{}/{}/",
|
||||||
DavResourceName::Card.base_path(),
|
resource_name.base_path(),
|
||||||
percent_encoding::utf8_percent_encode(&name, RFC_3986),
|
percent_encoding::utf8_percent_encode(&name, RFC_3986),
|
||||||
))],
|
)
|
||||||
));
|
};
|
||||||
response.set_namespace(Namespace::CardDav);
|
hrefs.push(Href(href));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields.push(DavPropertyValue::new(property.clone(), hrefs));
|
||||||
|
response.set_namespace(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
PrincipalProperty::PrincipalAddress => {
|
PrincipalProperty::PrincipalAddress => {
|
||||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||||
response.set_namespace(Namespace::CardDav);
|
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
|
.await
|
||||||
.caused_by(trc::location!())?
|
.caused_by(trc::location!())?
|
||||||
.unwrap_or_default();
|
.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 container_ids.is_empty() {
|
||||||
if is_calendar {
|
if is_calendar {
|
||||||
server
|
server
|
||||||
.create_default_calendar(access_token, account_id)
|
.create_default_calendar(access_token, account_id, &name)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
server
|
server
|
||||||
.create_default_addressbook(access_token, account_id)
|
.create_default_addressbook(access_token, account_id, &name)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
last_change_id = server
|
last_change_id = server
|
||||||
|
@ -84,13 +90,6 @@ pub(super) async fn build_calcard_resources(
|
||||||
.caused_by(trc::location!())?
|
.caused_by(trc::location!())?
|
||||||
.unwrap_or_default();
|
.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 {
|
let mut cache = DavResources {
|
||||||
base_path: format!(
|
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,
|
&self,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
account_name: &str,
|
||||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||||
|
|
||||||
fn create_default_calendar(
|
fn create_default_calendar(
|
||||||
&self,
|
&self,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
account_name: &str,
|
||||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||||
|
|
||||||
fn get_or_create_default_calendar(
|
fn get_or_create_default_calendar(
|
||||||
&self,
|
&self,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
account_name: &str,
|
||||||
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
|
||||||
|
|
||||||
fn cached_dav_resources(
|
fn cached_dav_resources(
|
||||||
|
@ -337,6 +340,7 @@ impl GroupwareCache for Server {
|
||||||
&self,
|
&self,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
account_name: &str,
|
||||||
) -> trc::Result<Option<u32>> {
|
) -> trc::Result<Option<u32>> {
|
||||||
if let Some(name) = &self.core.groupware.default_addressbook_name {
|
if let Some(name) = &self.core.groupware.default_addressbook_name {
|
||||||
let mut batch = BatchBuilder::new();
|
let mut batch = BatchBuilder::new();
|
||||||
|
@ -346,7 +350,12 @@ impl GroupwareCache for Server {
|
||||||
.await?;
|
.await?;
|
||||||
AddressBook {
|
AddressBook {
|
||||||
name: name.clone(),
|
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,
|
is_default: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -362,6 +371,7 @@ impl GroupwareCache for Server {
|
||||||
&self,
|
&self,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
account_name: &str,
|
||||||
) -> trc::Result<Option<u32>> {
|
) -> trc::Result<Option<u32>> {
|
||||||
if let Some(name) = &self.core.groupware.default_calendar_name {
|
if let Some(name) = &self.core.groupware.default_calendar_name {
|
||||||
let mut batch = BatchBuilder::new();
|
let mut batch = BatchBuilder::new();
|
||||||
|
@ -377,8 +387,11 @@ impl GroupwareCache for Server {
|
||||||
.core
|
.core
|
||||||
.groupware
|
.groupware
|
||||||
.default_calendar_display_name
|
.default_calendar_display_name
|
||||||
.clone()
|
.as_ref()
|
||||||
.unwrap_or_else(|| name.clone()),
|
.map_or_else(
|
||||||
|
|| name.clone(),
|
||||||
|
|display| format!("{display} ({account_name})",),
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -395,13 +408,17 @@ impl GroupwareCache for Server {
|
||||||
&self,
|
&self,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
|
account_name: &str,
|
||||||
) -> trc::Result<Option<u32>> {
|
) -> trc::Result<Option<u32>> {
|
||||||
match self
|
match self
|
||||||
.get_document_ids(account_id, Collection::Calendar)
|
.get_document_ids(account_id, Collection::Calendar)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(ids)) if !ids.is_empty() => Ok(ids.iter().next()),
|
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
|
// Obtain parent calendar
|
||||||
let Some(parent_id) = self
|
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
|
.await
|
||||||
.caused_by(trc::location!())?
|
.caused_by(trc::location!())?
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -62,8 +62,16 @@ pub mod prop;
|
||||||
pub mod put_get;
|
pub mod put_get;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
pub async fn webdav_tests() {
|
fn webdav_tests() {
|
||||||
|
//test_build_itip_templates(&handle.server).await;
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.thread_stack_size(8 * 1024 * 1024) // 8MB stack
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
// Prepare settings
|
// Prepare settings
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let delete = true;
|
let delete = true;
|
||||||
|
@ -74,8 +82,6 @@ pub async fn webdav_tests() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
//test_build_itip_templates(&handle.server).await;
|
|
||||||
|
|
||||||
basic::test(&handle).await;
|
basic::test(&handle).await;
|
||||||
put_get::test(&handle).await;
|
put_get::test(&handle).await;
|
||||||
mkcol::test(&handle).await;
|
mkcol::test(&handle).await;
|
||||||
|
@ -104,6 +110,7 @@ pub async fn webdav_tests() {
|
||||||
if delete {
|
if delete {
|
||||||
handle.temp_dir.delete();
|
handle.temp_dir.delete();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -1160,6 +1167,9 @@ minimum-interval = "1s"
|
||||||
[calendar.scheduling.inbound]
|
[calendar.scheduling.inbound]
|
||||||
auto-add = true
|
auto-add = true
|
||||||
|
|
||||||
|
[dav.collection]
|
||||||
|
assisted-discovery = false
|
||||||
|
|
||||||
[store."auth"]
|
[store."auth"]
|
||||||
type = "sqlite"
|
type = "sqlite"
|
||||||
path = "{TMP}/auth.db"
|
path = "{TMP}/auth.db"
|
||||||
|
|
Loading…
Add table
Reference in a new issue