libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
// Copyright 2023-2024 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: ISC

//! Support for CardDAV, Address Book extensions for WebDAV.

pub mod create_addressbook;
pub mod find_address_book_home_set;
pub mod find_addressbooks;
pub mod get_addressbook_resources;

pub use create_addressbook::{CreateAddressBook, CreateAddressBookResponse};
pub use find_address_book_home_set::{FindAddressBookHomeSet, FindAddressBookHomeSetResponse};
pub use find_addressbooks::{FindAddressBooks, FindAddressBooksResponse};
pub use get_addressbook_resources::{GetAddressBookResources, GetAddressBookResourcesResponse};

use std::ops::Deref;

use http::Response;
use hyper::{Uri, body::Incoming};
use tower_service::Service;

use crate::{
    common::ServiceForUrlError,
    dav::WebDavClient,
    sd::{BootstrapError, DiscoverableService, FindContextUrlResult, find_context_url},
};

/// Client to communicate with a CardDAV server.
///
/// Instances are usually created via [`CardDavClient::new`]:
///
/// ```rust
/// # use libdav::CardDavClient;
/// # use libdav::dav::WebDavClient;
/// use http::Uri;
/// use hyper_rustls::HttpsConnectorBuilder;
/// use hyper_util::{client::legacy::Client, rt::TokioExecutor};
/// use tower_http::auth::AddAuthorization;
///
/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
/// let uri = Uri::try_from("https://example.com").unwrap();
///
/// let https_connector = HttpsConnectorBuilder::new()
///     .with_native_roots()
///     .unwrap()
///     .https_or_http()
///     .enable_http1()
///     .build();
/// let https_client = Client::builder(TokioExecutor::new()).build(https_connector);
/// let https_client = AddAuthorization::basic(https_client, "user", "secret");
/// let webdav = WebDavClient::new(uri, https_client);
/// let client = CardDavClient::new(webdav);
/// # })
/// ```
///
/// If the real CardDAV server needs to be resolved via automated service discovery, use
/// [`CardDavClient::bootstrap_via_service_discovery`].
///
/// For setting the `Authorization` header or applying other changes to outgoing requests, see the
/// documentation on [`WebDavClient`].
#[derive(Debug)]
pub struct CardDavClient<C>
where
    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + 'static,
{
    /// A WebDAV client used to send requests.
    pub webdav_client: WebDavClient<C>,
}

impl<C> Deref for CardDavClient<C>
where
    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
{
    type Target = WebDavClient<C>;

    fn deref(&self) -> &Self::Target {
        &self.webdav_client
    }
}

impl<C> CardDavClient<C>
where
    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
    <C as Service<http::Request<String>>>::Error: std::error::Error + Send + Sync,
{
    /// Create a new client instance.
    ///
    /// # See also
    ///
    /// [`CardDavClient::bootstrap_via_service_discovery`].
    pub fn new(webdav_client: WebDavClient<C>) -> CardDavClient<C> {
        CardDavClient { webdav_client }
    }

    /// Automatically bootstrap a new client instance.
    ///
    /// Creates a new client, with its `base_url` set to the context path retrieved using service
    /// discovery via [`find_context_url`].
    ///
    /// # Errors
    ///
    /// Returns an error if:
    ///
    /// - The URL has an invalid schema.
    /// - The underlying call to [`find_context_url`] returns an error.
    pub async fn bootstrap_via_service_discovery(
        mut webdav_client: WebDavClient<C>,
    ) -> Result<CardDavClient<C>, BootstrapError> {
        let service = service_for_url(&webdav_client.base_url)?;
        match find_context_url(&webdav_client, service).await {
            FindContextUrlResult::BaseUrl => {}
            FindContextUrlResult::Found(url) => webdav_client.base_url = url,
            FindContextUrlResult::NoneFound => return Err(BootstrapError::NoUsableUrl),
            FindContextUrlResult::Error(err) => return Err(err.into()),
        }
        Ok(CardDavClient { webdav_client })
    }
}

impl<C> Clone for CardDavClient<C>
where
    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + Clone,
{
    fn clone(&self) -> CardDavClient<C> {
        CardDavClient {
            webdav_client: self.webdav_client.clone(),
        }
    }
}

/// Return the service type based on a URL's scheme.
///
/// # Errors
///
/// If `url` is missing a scheme or has a scheme invalid for CardDAV usage.
pub fn service_for_url(url: &Uri) -> Result<DiscoverableService, ServiceForUrlError> {
    match url
        .scheme()
        .ok_or(ServiceForUrlError::MissingScheme)?
        .as_ref()
    {
        "https" | "carddavs" => Ok(DiscoverableService::CardDavs),
        "http" | "carddav" => Ok(DiscoverableService::CardDav),
        _ => Err(ServiceForUrlError::UnknownScheme),
    }
}