use std::time::{SystemTime, UNIX_EPOCH};
use ureq::Agent;
use url::Url;
pub fn propfind_get(
client: Agent,
username: &str,
password: &str,
url: &Url,
body: &str,
prop_path: &[&str],
depth: &str,
) -> Result<(String, xmltree::Element), Error> {
let auth = format!(
"Basic {}",
base64::encode(format!("{}:{}", username, password))
);
let reader = client
.request("PROPFIND", url.as_str())
.set("Authorization", &auth)
.set("CONTENT_TYPE", "application/xml")
.set("Depth", depth)
.send_bytes(body.as_bytes())?
.into_reader();
let root = xmltree::Element::parse(reader)?;
let mut element = &root;
let mut searched = 0;
for prop in prop_path {
for e in &element.children {
if let Some(child) = e.as_element() {
if child.name == *prop {
searched += 1;
element = child;
break;
}
}
}
}
if searched != prop_path.len() {
Err(Error {
kind: ErrorKind::Parsing,
message: format!("Could not find data {:?} in PROPFIND response.", prop_path),
})
} else {
Ok((
element
.get_text()
.map(|s| s.to_string())
.unwrap_or_else(|| "".to_string()),
root,
))
}
}
pub static USER_PRINCIPAL_REQUEST: &str = r#"
<d:propfind xmlns:d="DAV:">
<d:prop>
<d:current-user-principal />
</d:prop>
</d:propfind>
"#;
pub fn get_principal_url(
client: Agent,
username: &str,
password: &str,
url: &Url,
) -> Result<Url, Error> {
let principal_url = propfind_get(
client,
username,
password,
url,
USER_PRINCIPAL_REQUEST,
&[
"response",
"propstat",
"prop",
"current-user-principal",
"href",
],
"0",
)?
.0;
Ok(url.join(&principal_url)?)
}
pub static HOMESET_REQUEST: &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>
"#;
pub fn get_home_set_url(
client: Agent,
username: &str,
password: &str,
url: &Url,
) -> Result<Url, Error> {
let principal_url = get_principal_url(client.clone(), username, password, url)?;
let homeset_url = propfind_get(
client,
username,
password,
&principal_url,
HOMESET_REQUEST,
&["response", "propstat", "prop", "calendar-home-set", "href"],
"0",
)?
.0;
Ok(url.join(&homeset_url)?)
}
pub static CALENDARS_REQUEST: &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>
"#;
pub fn get_calendars(
client: Agent,
username: &str,
password: &str,
base_url: &Url,
) -> Result<Vec<CalendarRef>, Error> {
let homeset_url = get_home_set_url(client.clone(), username, password, base_url)?;
let mut calendars = Vec::new();
let root = propfind_get(
client,
username,
password,
&homeset_url,
CALENDARS_REQUEST,
&[],
"1",
)?
.1;
for response in &root.children {
if let Some(response) = response.as_element() {
let name = response
.get_child("propstat")
.and_then(|e| e.get_child("prop"))
.and_then(|e| e.get_child("displayname"))
.and_then(|e| e.get_text());
let is_calendar = response
.get_child("propstat")
.and_then(|e| e.get_child("prop"))
.and_then(|e| e.get_child("resourcetype"))
.map(|e| e.get_child("calendar").is_some())
.unwrap_or(false);
let supports_vevents = response
.get_child("propstat")
.and_then(|e| e.get_child("prop"))
.and_then(|e| e.get_child("supported-calendar-component-set"))
.map(|e| {
for c in &e.children {
if let Some(child) = c.as_element() {
if child.name == "comp" {
if let Some(name) = child.attributes.get("name") {
if name == "VEVENT" {
return true;
}
}
}
}
}
false
})
.unwrap_or(false);
let href = response.get_child("href").and_then(|e| e.get_text());
if href.is_none() || name.is_none() || !is_calendar || !supports_vevents {
continue;
}
calendars.push(CalendarRef {
url: base_url.join(&href.unwrap()).unwrap(),
name: name.unwrap().to_string(),
})
}
}
Ok(calendars)
}
pub struct CalendarRef {
pub url: Url,
pub name: String,
}
impl std::fmt::Debug for CalendarRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CalendarRef")
.field("url", &self.url.to_string())
.field("name", &self.name)
.finish()
}
}
#[derive(Clone)]
pub struct EventRef {
pub etag: Option<String>,
pub url: Url,
pub data: String,
}
impl std::fmt::Debug for EventRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventRef")
.field("etag", &self.etag)
.field("url", &self.url.to_string())
.field("data", &self.data)
.finish()
}
}
pub static CALENDAR_EVENTS_REQUEST: &str = r#"
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:getetag />
<c:calendar-data />
</d:prop>
<c:filter>
<c:comp-filter name="VCALENDAR">
<c:comp-filter name="VEVENT" />
</c:comp-filter>
</c:filter>
</c:calendar-query>
"#;
pub fn get_events(
client: Agent,
username: &str,
password: &str,
base_url: &Url,
calendar_ref: &CalendarRef,
) -> Result<Vec<EventRef>, Error> {
let auth = format!(
"Basic {}",
base64::encode(format!("{}:{}", username, password))
);
let reader = client
.request("REPORT", calendar_ref.url.as_str())
.set("Authorization", &auth)
.set("CONTENT_TYPE", "application/xml")
.send_bytes(CALENDAR_EVENTS_REQUEST.as_bytes())?
.into_reader();
let root = xmltree::Element::parse(reader)?;
let mut events = Vec::new();
for c in &root.children {
if let Some(child) = c.as_element() {
let href = child.get_child("href").and_then(|e| e.get_text());
let etag = child
.get_child("propstat")
.and_then(|e| e.get_child("prop"))
.and_then(|e| e.get_child("getetag"))
.and_then(|e| e.get_text())
.map(|e| e.to_string());
let data = child
.get_child("propstat")
.and_then(|e| e.get_child("prop"))
.and_then(|e| e.get_child("calendar-data"))
.and_then(|e| e.get_text());
if href.is_none() || etag.is_none() || data.is_none() {
continue;
}
if let Ok(url) = base_url.join(&href.unwrap()) {
events.push(EventRef {
url,
data: data.unwrap().to_string(),
etag,
})
}
}
}
Ok(events)
}
pub fn save_event(
client: Agent,
username: &str,
password: &str,
event_ref: EventRef,
) -> Result<EventRef, Error> {
let auth = format!(
"Basic {}",
base64::encode(format!("{}:{}", username, password))
);
let response = client
.put(event_ref.url.as_str())
.set("Content-Type", "text/calendar")
.set("Content-Length", &event_ref.data.len().to_string())
.set("Authorization", &auth)
.send(event_ref.data.as_bytes())?;
if let Some(etag) = response.header("ETag") {
Ok(EventRef {
etag: Some(etag.into()),
..event_ref
})
} else {
Ok(EventRef {
etag: Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|t| t.as_millis().to_string())
.unwrap_or_else(|_| "0".to_string()),
),
..event_ref
})
}
}
pub fn remove_event(
client: Agent,
username: &str,
password: &str,
event_ref: EventRef,
) -> Result<(), Error> {
let auth = format!(
"Basic {}",
base64::encode(format!("{}:{}", username, password))
);
let _response = client
.delete(event_ref.url.as_str())
.set("Authorization", &auth)
.call()?;
Ok(())
}
#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
pub message: String,
}
#[derive(Debug)]
pub enum ErrorKind {
Http,
Parsing,
}
impl From<ureq::Error> for Error {
fn from(e: ureq::Error) -> Self {
Self {
kind: ErrorKind::Http,
message: format!("{:?}", e),
}
}
}
impl From<xmltree::ParseError> for Error {
fn from(e: xmltree::ParseError) -> Self {
Self {
kind: ErrorKind::Parsing,
message: e.to_string(),
}
}
}
impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Self {
Self {
kind: ErrorKind::Parsing,
message: e.to_string(),
}
}
}