carddav 0.1.1

CardDAV client library
Documentation
extern crate failure;
#[macro_use]
extern crate hyper;
#[macro_use]
extern crate lazy_static;
extern crate minidom;
extern crate reqwest;

use reqwest::{Client, Method, StatusCode};
use reqwest::header::{ContentType, IfNoneMatch};
use minidom::Element;
use failure::Error;

#[derive(Clone, Debug)]
pub struct Credentials {
    username: String,
    password: String,
    server: String,
}

impl Credentials {
    pub fn new(username: &str, password: &str, server: &str) -> Self {
        Credentials {
            username: username.into(),
            password: password.into(),
            server: server.into(),
        }
    }
}

header! { (Depth, "Depth") => [u8] }
lazy_static! {
    static ref PROPFIND: Method = Method::Extension("PROPFIND".into());
    static ref REPORT: Method = Method::Extension("REPORT".into());
}

#[derive(Debug, Clone)]
pub struct CardDAV {
    cred: Credentials,
    client: reqwest::Client,
}

impl CardDAV {
    pub fn from_credentials(cred: Credentials) -> Self {
        CardDAV {
            cred: cred,
            client: Client::new(),
        }
    }

    pub fn get_endpoint_url(&self) -> Result<String, Error> {
        let well_known = format!("{}/.well-known/carddav", self.cred.server);
        let resp = self.client
            .request(PROPFIND.clone(), well_known.as_str())
            .header(Depth(0))
            .basic_auth(
                self.cred.username.as_str(),
                Some(self.cred.password.as_str()),
            )
            .send()?;

        if resp.status() == StatusCode::NotFound {
            return Ok(self.cred.server.to_owned() + "/");
        }

        Ok(resp.url().clone().into_string())
    }

    pub fn get_principal(&self) -> Result<String, Error> {
        let endpoint = self.get_endpoint_url()?;
        let mut resp = self.client.request(PROPFIND.clone(), endpoint.as_str())
            .header(Depth(0))
            .header(ContentType("application/xml".parse()?))
            .basic_auth(self.cred.username.as_str(), Some(self.cred.password.as_str()))
            .body("<d:propfind xmlns:d=\"DAV:\"><d:prop><d:current-user-principal /></d:prop></d:propfind>")
            .send()?;

        let xml = resp.text()?;
        let el: Element = xml.parse()?;
        let principal = find_element_recursive(&el, "current-user-principal").unwrap();
        let href = find_element_recursive(&principal, "href")
            .unwrap()
            .texts()
            .next()
            .unwrap();

        Ok(self.cred.server.to_string() + href.into())
    }

    pub fn addressbook_home_set(&self) -> Result<String, Error> {
        let principal = self.get_principal()?;
        let mut resp = self.client
            .request(PROPFIND.clone(), principal.as_str())
            .header(Depth(0))
            .header(ContentType("application/xml".parse()?))
            .basic_auth(self.cred.username.as_str(), Some(self.cred.password.as_str()))
            .body("<d:propfind xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:carddav\"><d:prop><c:addressbook-home-set /></d:prop></d:propfind>")
            .send()?;

        let xml = resp.text()?;
        let el: Element = xml.parse()?;
        let home_set = find_element_recursive(&el, "addressbook-home-set").unwrap();
        let href = find_element_recursive(&home_set, "href")
            .unwrap()
            .texts()
            .next()
            .unwrap();

        Ok(self.cred.server.to_string() + href.into())
    }

    pub fn addressbooks(&self) -> Result<Vec<Addressbook>, Error> {
        let home_set = self.addressbook_home_set()?;
        let mut resp = self.client
            .request(PROPFIND.clone(), home_set.as_str())
            .header(Depth(1))
            .basic_auth(
                self.cred.username.as_str(),
                Some(self.cred.password.as_str()),
            )
            .send()?;

        let xml = resp.text()?;
        let el: Element = xml.parse()?;
        let books = el.children().skip(1);

        let mut address_books = Vec::new();
        for book in books {
            let prop = find_element_recursive(&book, "prop").unwrap();
            let href = book.children().find(|e| e.name() == "href").unwrap();
            let name = prop.children().find(|e| e.name() == "displayname").unwrap();

            let etag = {
                match prop.children().find(|e| e.name() == "getetag") {
                    Some(etag) => Some(etag.texts().next().unwrap_or("").into()),
                    None => None,
                }
            };

            let ctag = {
                match prop.children().find(|e| e.name() == "getctag") {
                    Some(etag) => Some(etag.texts().next().unwrap_or("").into()),
                    None => None,
                }
            };

            let addr = Addressbook {
                cd: self.clone(),
                url: href.texts().next().unwrap().into(),
                display_name: name.texts().next().unwrap().into(),
                etag: etag,
                ctag: ctag,
            };
            address_books.push(addr);
        }

        Ok(address_books)
    }
}

#[derive(Debug)]
pub struct Addressbook {
    cd: CardDAV,
    url: String,
    display_name: String,
    etag: Option<String>,
    ctag: Option<String>,
}

impl Addressbook {
    pub fn vcard_dump(&self) -> Result<String, Error> {
        let dump_url = self.cd.cred.server.to_string() + &self.url;
        let mut resp = self.cd
            .client
            .request(REPORT.clone(), dump_url.as_str())
            .header(Depth(1))
            .header(ContentType("application/xml".parse()?))
            .body("<c:addressbook-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:carddav\"><d:prop><d:getetag /><c:address-data /></d:prop></c:addressbook-query>")
            .basic_auth(
                self.cd.cred.username.as_str(),
                Some(self.cd.cred.password.as_str()),
            )
            .send()?;

        Ok(resp.text()?)
    }

    pub fn create_contact(&self, id: &str, vcard: &str) -> Result<(), Error> {
        let create_url = self.cd.cred.server.to_string() + &self.url + id + ".vcf";
        let mut resp = self.cd
            .client
            .put(create_url.as_str())
            .header(ContentType("text/vcard".parse()?))
            .header(IfNoneMatch::Any)
            .body(vcard.clone().to_string())
            .basic_auth(
                self.cd.cred.username.as_str(),
                Some(self.cd.cred.password.as_str()),
            ).send()?;

        Ok(())
    }
}

fn find_element_recursive<'a>(el: &'a Element, name: &str) -> Option<&'a Element> {
    for elem in el.children() {
        if elem.name() == name {
            return Some(elem);
        } else if elem.children().next().is_some() {
            return find_element_recursive(elem, name);
        }
    }

    None
}