2021-02-22 00:13:29 +01:00
|
|
|
//! This module provides a client to connect to a CalDAV server
|
2021-02-20 00:10:05 +01:00
|
|
|
|
|
|
|
use std::error::Error;
|
2021-02-21 00:16:40 +01:00
|
|
|
use std::convert::TryFrom;
|
2021-03-05 23:32:42 +01:00
|
|
|
use std::collections::HashMap;
|
2021-03-18 23:59:06 +01:00
|
|
|
use std::sync::{Arc, Mutex};
|
2021-02-20 00:10:05 +01:00
|
|
|
|
2021-03-18 23:59:06 +01:00
|
|
|
use async_trait::async_trait;
|
2021-04-20 00:01:15 +02:00
|
|
|
use reqwest::Method;
|
2021-02-20 00:10:05 +01:00
|
|
|
use reqwest::header::CONTENT_TYPE;
|
|
|
|
use minidom::Element;
|
|
|
|
use url::Url;
|
|
|
|
|
2021-03-21 23:52:42 +01:00
|
|
|
use crate::resource::Resource;
|
2021-02-21 00:29:22 +01:00
|
|
|
use crate::utils::{find_elem, find_elems};
|
2021-03-18 23:59:06 +01:00
|
|
|
use crate::calendar::remote_calendar::RemoteCalendar;
|
2021-03-05 23:32:42 +01:00
|
|
|
use crate::calendar::CalendarId;
|
2021-04-04 00:35:59 +02:00
|
|
|
use crate::calendar::SupportedComponents;
|
2021-03-18 23:59:06 +01:00
|
|
|
use crate::traits::CalDavSource;
|
2021-03-28 01:22:24 +01:00
|
|
|
use crate::traits::BaseCalendar;
|
2021-04-04 00:35:59 +02:00
|
|
|
use crate::traits::DavCalendar;
|
2021-03-01 23:39:16 +01:00
|
|
|
|
2021-02-21 00:16:40 +01:00
|
|
|
|
2021-02-20 00:10:05 +01:00
|
|
|
static DAVCLIENT_BODY: &str = r#"
|
|
|
|
<d:propfind xmlns:d="DAV:">
|
|
|
|
<d:prop>
|
|
|
|
<d:current-user-principal />
|
|
|
|
</d:prop>
|
|
|
|
</d:propfind>
|
|
|
|
"#;
|
|
|
|
|
|
|
|
static HOMESET_BODY: &str = r#"
|
|
|
|
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
|
|
|
|
<d:self/>
|
|
|
|
<d:prop>
|
|
|
|
<c:calendar-home-set />
|
|
|
|
</d:prop>
|
|
|
|
</d:propfind>
|
|
|
|
"#;
|
|
|
|
|
2021-02-20 00:46:20 +01:00
|
|
|
static CAL_BODY: &str = r#"
|
|
|
|
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
|
|
|
|
<d:prop>
|
|
|
|
<d:displayname />
|
|
|
|
<d:resourcetype />
|
|
|
|
<c:supported-calendar-component-set />
|
|
|
|
</d:prop>
|
|
|
|
</d:propfind>
|
|
|
|
"#;
|
|
|
|
|
2021-02-21 23:29:21 +01:00
|
|
|
|
2021-02-21 23:30:09 +01:00
|
|
|
|
2021-03-22 00:07:50 +01:00
|
|
|
pub async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result<String, Box<dyn Error>> {
|
|
|
|
let method = method.parse()
|
|
|
|
.expect("invalid method name");
|
|
|
|
|
|
|
|
let res = reqwest::Client::new()
|
|
|
|
.request(method, resource.url().clone())
|
|
|
|
.header("Depth", depth)
|
|
|
|
.header(CONTENT_TYPE, "application/xml")
|
|
|
|
.basic_auth(resource.username(), Some(resource.password()))
|
|
|
|
.body(body)
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
let text = res.text().await?;
|
|
|
|
Ok(text)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn sub_request_and_extract_elem(resource: &Resource, body: String, items: &[&str]) -> Result<String, Box<dyn Error>> {
|
|
|
|
let text = sub_request(resource, "PROPFIND", body, 0).await?;
|
|
|
|
|
|
|
|
let mut current_element: &Element = &text.parse()?;
|
|
|
|
for item in items {
|
|
|
|
current_element = match find_elem(¤t_element, item) {
|
|
|
|
Some(elem) => elem,
|
|
|
|
None => return Err(format!("missing element {}", item).into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(current_element.text())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn sub_request_and_extract_elems(resource: &Resource, method: &str, body: String, item: &str) -> Result<Vec<Element>, Box<dyn Error>> {
|
|
|
|
let text = sub_request(resource, method, body, 1).await?;
|
|
|
|
|
|
|
|
let element: &Element = &text.parse()?;
|
|
|
|
Ok(find_elems(&element, item)
|
|
|
|
.iter()
|
|
|
|
.map(|elem| (*elem).clone())
|
|
|
|
.collect()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-02-28 18:02:01 +01:00
|
|
|
/// A CalDAV source that fetches its data from a CalDAV server
|
2021-02-20 00:10:05 +01:00
|
|
|
pub struct Client {
|
2021-03-21 23:52:42 +01:00
|
|
|
resource: Resource,
|
2021-02-20 00:10:05 +01:00
|
|
|
|
2021-03-18 23:59:06 +01:00
|
|
|
/// The interior mutable part of a Client.
|
|
|
|
/// This data may be retrieved once and then cached
|
|
|
|
cached_replies: Mutex<CachedReplies>,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
struct CachedReplies {
|
2021-03-21 23:52:42 +01:00
|
|
|
principal: Option<Resource>,
|
|
|
|
calendar_home_set: Option<Resource>,
|
2021-03-18 23:59:06 +01:00
|
|
|
calendars: Option<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>>,
|
2021-02-20 00:10:05 +01:00
|
|
|
}
|
2021-02-18 12:02:04 +01:00
|
|
|
|
|
|
|
impl Client {
|
2021-02-20 17:58:46 +01:00
|
|
|
/// Create a client. This does not start a connection
|
2021-02-20 00:10:05 +01:00
|
|
|
pub fn new<S: AsRef<str>, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result<Self, Box<dyn Error>> {
|
|
|
|
let url = Url::parse(url.as_ref())?;
|
|
|
|
|
|
|
|
Ok(Self{
|
2021-03-21 23:52:42 +01:00
|
|
|
resource: Resource::new(url, username.to_string(), password.to_string()),
|
2021-03-18 23:59:06 +01:00
|
|
|
cached_replies: Mutex::new(CachedReplies::default()),
|
2021-02-20 00:10:05 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-02-20 00:18:51 +01:00
|
|
|
/// Return the Principal URL, or fetch it from server if not known yet
|
2021-03-21 23:52:42 +01:00
|
|
|
async fn get_principal(&self) -> Result<Resource, Box<dyn Error>> {
|
2021-03-18 23:59:06 +01:00
|
|
|
if let Some(p) = &self.cached_replies.lock().unwrap().principal {
|
2021-02-20 00:18:51 +01:00
|
|
|
return Ok(p.clone());
|
|
|
|
}
|
2021-02-20 00:10:05 +01:00
|
|
|
|
2021-03-22 00:07:50 +01:00
|
|
|
let href = sub_request_and_extract_elem(&self.resource, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?;
|
2021-03-21 23:52:42 +01:00
|
|
|
let principal_url = self.resource.combine(&href);
|
2021-03-18 23:59:06 +01:00
|
|
|
self.cached_replies.lock().unwrap().principal = Some(principal_url.clone());
|
2021-02-21 00:14:55 +01:00
|
|
|
log::debug!("Principal URL is {}", href);
|
2021-02-20 00:10:05 +01:00
|
|
|
|
|
|
|
return Ok(principal_url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the Homeset URL, or fetch it from server if not known yet
|
2021-03-21 23:52:42 +01:00
|
|
|
async fn get_cal_home_set(&self) -> Result<Resource, Box<dyn Error>> {
|
2021-03-18 23:59:06 +01:00
|
|
|
if let Some(h) = &self.cached_replies.lock().unwrap().calendar_home_set {
|
2021-02-20 00:10:05 +01:00
|
|
|
return Ok(h.clone());
|
|
|
|
}
|
|
|
|
let principal_url = self.get_principal().await?;
|
|
|
|
|
2021-03-22 00:07:50 +01:00
|
|
|
let href = sub_request_and_extract_elem(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?;
|
2021-03-21 23:52:42 +01:00
|
|
|
let chs_url = self.resource.combine(&href);
|
2021-03-18 23:59:06 +01:00
|
|
|
self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone());
|
2021-03-21 23:52:42 +01:00
|
|
|
log::debug!("Calendar home set URL is {:?}", href);
|
2021-02-20 00:10:05 +01:00
|
|
|
|
|
|
|
Ok(chs_url)
|
|
|
|
}
|
2021-02-20 00:46:20 +01:00
|
|
|
|
2021-03-18 23:59:06 +01:00
|
|
|
async fn populate_calendars(&self) -> Result<(), Box<dyn Error>> {
|
|
|
|
let cal_home_set = self.get_cal_home_set().await?;
|
|
|
|
|
2021-03-22 00:07:50 +01:00
|
|
|
let reps = sub_request_and_extract_elems(&cal_home_set, "PROPFIND", CAL_BODY.to_string(), "response").await?;
|
2021-03-18 23:59:06 +01:00
|
|
|
let mut calendars = HashMap::new();
|
|
|
|
for rep in reps {
|
2021-03-22 00:07:50 +01:00
|
|
|
let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("<no name>".to_string());
|
2021-03-18 23:59:06 +01:00
|
|
|
log::debug!("Considering calendar {}", display_name);
|
|
|
|
|
|
|
|
// We filter out non-calendar items
|
2021-03-22 00:07:50 +01:00
|
|
|
let resource_types = match find_elem(&rep, "resourcetype") {
|
2021-03-18 23:59:06 +01:00
|
|
|
None => continue,
|
|
|
|
Some(rt) => rt,
|
|
|
|
};
|
|
|
|
let mut found_calendar_type = false;
|
|
|
|
for resource_type in resource_types.children() {
|
|
|
|
if resource_type.name() == "calendar" {
|
|
|
|
found_calendar_type = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if found_calendar_type == false {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We filter out the root calendar collection, that has an empty supported-calendar-component-set
|
2021-03-22 00:07:50 +01:00
|
|
|
let el_supported_comps = match find_elem(&rep, "supported-calendar-component-set") {
|
2021-03-18 23:59:06 +01:00
|
|
|
None => continue,
|
|
|
|
Some(comps) => comps,
|
|
|
|
};
|
|
|
|
if el_supported_comps.children().count() == 0 {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-03-22 00:07:50 +01:00
|
|
|
let calendar_href = match find_elem(&rep, "href") {
|
2021-03-18 23:59:06 +01:00
|
|
|
None => {
|
|
|
|
log::warn!("Calendar {} has no URL! Ignoring it.", display_name);
|
|
|
|
continue;
|
|
|
|
},
|
|
|
|
Some(h) => h.text(),
|
|
|
|
};
|
|
|
|
|
2021-03-21 23:52:42 +01:00
|
|
|
let this_calendar_url = self.resource.combine(&calendar_href);
|
2021-03-18 23:59:06 +01:00
|
|
|
|
|
|
|
let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) {
|
|
|
|
Err(err) => {
|
|
|
|
log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err);
|
|
|
|
continue;
|
|
|
|
},
|
|
|
|
Ok(sc) => sc,
|
|
|
|
};
|
|
|
|
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components);
|
|
|
|
log::info!("Found calendar {}", this_calendar.name());
|
|
|
|
calendars.insert(this_calendar.id().clone(), Arc::new(Mutex::new(this_calendar)));
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut replies = self.cached_replies.lock().unwrap();
|
|
|
|
replies.calendars = Some(calendars);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl CalDavSource<RemoteCalendar> for Client {
|
|
|
|
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
|
|
|
self.populate_calendars().await?;
|
|
|
|
|
|
|
|
match &self.cached_replies.lock().unwrap().calendars {
|
|
|
|
Some(cals) => {
|
|
|
|
return Ok(cals.clone())
|
|
|
|
},
|
|
|
|
None => return Err("No calendars available".into())
|
|
|
|
};
|
|
|
|
}
|
2021-02-21 23:29:21 +01:00
|
|
|
|
2021-03-21 19:27:55 +01:00
|
|
|
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
2021-04-19 23:23:39 +02:00
|
|
|
if let Err(err) = self.populate_calendars().await {
|
|
|
|
log::warn!("Unable to fetch calendars: {}", err);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
2021-03-18 23:59:06 +01:00
|
|
|
self.cached_replies.lock().unwrap()
|
|
|
|
.calendars
|
|
|
|
.as_ref()
|
2021-03-21 19:27:55 +01:00
|
|
|
.and_then(|cals| cals.get(id))
|
2021-03-18 23:59:06 +01:00
|
|
|
.map(|cal| cal.clone())
|
2021-04-04 00:35:59 +02:00
|
|
|
}
|
2021-03-31 08:32:28 +02:00
|
|
|
|
2021-04-04 00:35:59 +02:00
|
|
|
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
2021-04-19 23:23:39 +02:00
|
|
|
self.populate_calendars().await?;
|
|
|
|
|
|
|
|
match self.cached_replies.lock().unwrap().calendars.as_ref() {
|
|
|
|
None => return Err("No calendars have been fetched".into()),
|
|
|
|
Some(cals) => {
|
|
|
|
if cals.contains_key(&id) {
|
|
|
|
return Err("This calendar already exists".into());
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-04-20 00:01:15 +02:00
|
|
|
let creation_body = calendar_body(name, supported_components);
|
|
|
|
|
|
|
|
reqwest::Client::new()
|
|
|
|
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), id.clone())
|
|
|
|
.header(CONTENT_TYPE, "application/xml")
|
|
|
|
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
|
|
|
.body(creation_body)
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
self.get_calendar(&id).await.ok_or(format!("Unable to insert calendar {:?}", id).into())
|
|
|
|
}
|
2021-03-31 08:32:28 +02:00
|
|
|
}
|
2021-04-04 00:35:59 +02:00
|
|
|
|
2021-04-20 00:01:15 +02:00
|
|
|
fn calendar_body(name: String, supported_components: SupportedComponents) -> String {
|
|
|
|
// This is taken from https://tools.ietf.org/html/rfc4791#page-24
|
|
|
|
format!(r#"<?xml version="1.0" encoding="utf-8" ?>
|
|
|
|
<C:mkcalendar xmlns:D="DAV:"
|
|
|
|
xmlns:C="urn:ietf:params:xml:ns:caldav">
|
|
|
|
<D:set>
|
|
|
|
<D:prop>
|
|
|
|
<D:displayname>{}</D:displayname>
|
|
|
|
{}
|
|
|
|
</D:prop>
|
|
|
|
</D:set>
|
|
|
|
</C:mkcalendar>
|
|
|
|
"#,
|
|
|
|
name,
|
|
|
|
supported_components.to_xml_string(),
|
|
|
|
)
|
2021-02-20 00:10:05 +01:00
|
|
|
}
|
|
|
|
|