//! This module provides a client to connect to a CalDAV server use std::error::Error; use std::convert::TryFrom; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use async_trait::async_trait; use reqwest::header::CONTENT_TYPE; use minidom::Element; use url::Url; use crate::resource::Resource; use crate::utils::{find_elem, find_elems}; use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::CalendarId; use crate::traits::CalDavSource; use crate::traits::PartialCalendar; static DAVCLIENT_BODY: &str = r#" "#; static HOMESET_BODY: &str = r#" "#; static CAL_BODY: &str = r#" "#; pub async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result> { 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> { 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, Box> { 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() ) } /// A CalDAV source that fetches its data from a CalDAV server pub struct Client { resource: Resource, /// The interior mutable part of a Client. /// This data may be retrieved once and then cached cached_replies: Mutex, } #[derive(Default)] struct CachedReplies { principal: Option, calendar_home_set: Option, calendars: Option>>>, } impl Client { /// Create a client. This does not start a connection pub fn new, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result> { let url = Url::parse(url.as_ref())?; Ok(Self{ resource: Resource::new(url, username.to_string(), password.to_string()), cached_replies: Mutex::new(CachedReplies::default()), }) } /// Return the Principal URL, or fetch it from server if not known yet async fn get_principal(&self) -> Result> { if let Some(p) = &self.cached_replies.lock().unwrap().principal { return Ok(p.clone()); } let href = sub_request_and_extract_elem(&self.resource, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?; let principal_url = self.resource.combine(&href); self.cached_replies.lock().unwrap().principal = Some(principal_url.clone()); log::debug!("Principal URL is {}", href); return Ok(principal_url); } /// Return the Homeset URL, or fetch it from server if not known yet async fn get_cal_home_set(&self) -> Result> { if let Some(h) = &self.cached_replies.lock().unwrap().calendar_home_set { return Ok(h.clone()); } let principal_url = self.get_principal().await?; let href = sub_request_and_extract_elem(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?; let chs_url = self.resource.combine(&href); self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone()); log::debug!("Calendar home set URL is {:?}", href); Ok(chs_url) } async fn populate_calendars(&self) -> Result<(), Box> { let cal_home_set = self.get_cal_home_set().await?; let reps = sub_request_and_extract_elems(&cal_home_set, "PROPFIND", CAL_BODY.to_string(), "response").await?; let mut calendars = HashMap::new(); for rep in reps { let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("".to_string()); log::debug!("Considering calendar {}", display_name); // We filter out non-calendar items let resource_types = match find_elem(&rep, "resourcetype") { 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 let el_supported_comps = match find_elem(&rep, "supported-calendar-component-set") { None => continue, Some(comps) => comps, }; if el_supported_comps.children().count() == 0 { continue; } let calendar_href = match find_elem(&rep, "href") { None => { log::warn!("Calendar {} has no URL! Ignoring it.", display_name); continue; }, Some(h) => h.text(), }; let this_calendar_url = self.resource.combine(&calendar_href); 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 for Client { async fn get_calendars(&self) -> Result>>, Box> { self.populate_calendars().await?; match &self.cached_replies.lock().unwrap().calendars { Some(cals) => { return Ok(cals.clone()) }, None => return Err("No calendars available".into()) }; } async fn get_calendar(&self, id: &CalendarId) -> Option>> { self.cached_replies.lock().unwrap() .calendars .as_ref() .and_then(|cals| cals.get(id)) .map(|cal| cal.clone()) } }