libdav 0.10.3

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

//! Support for CalDAV, Calendar extensions for WebDAV.

pub mod create_calendar;
pub mod find_calendar_home_set;
pub mod find_calendars;
pub mod get_calendar_resources;
pub mod get_supported_calendar_data;
pub mod get_supported_components;
pub mod get_user_address_set;
pub mod list_calendar_resources;

pub use create_calendar::{CreateCalendar, CreateCalendarResponse};
pub use find_calendar_home_set::{FindCalendarHomeSet, FindCalendarHomeSetResponse};
pub use find_calendars::{FindCalendars, FindCalendarsResponse};
pub use get_calendar_resources::{GetCalendarResources, GetCalendarResourcesResponse};
pub use get_supported_calendar_data::{
    CalendarDataType, GetSupportedCalendarData, GetSupportedCalendarDataResponse,
};
pub use get_supported_components::{GetSupportedComponents, GetSupportedComponentsResponse};
pub use get_user_address_set::{GetUserAddressSet, GetUserAddressSetResponse};
pub use list_calendar_resources::{
    ComponentFilterError, InvalidComponentType, InvalidTimeRange, ListCalendarResources,
    ListCalendarResourcesResponse, VALID_COMPONENT_TYPES,
};

use std::ops::Deref;

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

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

/// Client to communicate with a CalDAV server.
///
/// Instances are usually created via [`CalDavClient::new`]:
///
/// ```rust
/// # use libdav::CalDavClient;
/// # 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 = CalDavClient::new(webdav);
/// # })
/// ```
///
/// If the real CalDAV server needs to be resolved via automated service discovery, use
/// [`CalDavClient::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 CalDavClient<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 CalDavClient<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> CalDavClient<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
    ///
    /// [`CalDavClient::bootstrap_via_service_discovery`].
    pub fn new(webdav_client: WebDavClient<C>) -> CalDavClient<C> {
        CalDavClient { webdav_client }
    }

    /// Automatically bootstrap a new client instance using service discovery.
    ///
    /// Creates a new client, with its `base_url` set to the context path retrieved using service
    /// discovery via [`find_context_url`].
    ///
    /// # Errors
    ///
    /// - [`BootstrapError::ServiceForUrl`] if the webdav client uses a URL with an invalid schema.
    /// - [`BootstrapError::ContextUrl`] if underlying call to [`find_context_url`] returns an error.
    pub async fn bootstrap_via_service_discovery(
        mut webdav_client: WebDavClient<C>,
    ) -> Result<CalDavClient<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(CalDavClient { webdav_client })
    }
}
impl<C> Clone for CalDavClient<C>
where
    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + Clone,
{
    fn clone(&self) -> CalDavClient<C> {
        CalDavClient {
            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 CalDAV usage.
pub fn service_for_url(url: &Uri) -> Result<DiscoverableService, ServiceForUrlError> {
    match url
        .scheme()
        .ok_or(ServiceForUrlError::MissingScheme)?
        .as_ref()
    {
        "https" | "caldavs" => Ok(DiscoverableService::CalDavs),
        "http" | "caldav" => Ok(DiscoverableService::CalDav),
        _ => Err(ServiceForUrlError::UnknownScheme),
    }
}

/// Calendar component types defined in RFC 4791 and related standards.
///
/// From: <https://www.rfc-editor.org/rfc/rfc4791#section-5.2.3>
/// and <https://www.rfc-editor.org/rfc/rfc6638>
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarComponent {
    /// Event component (VEVENT).
    VEvent,
    /// Todo component (VTODO).
    VTodo,
    /// Journal component (VJOURNAL).
    VJournal,
    /// Free/busy component (VFREEBUSY).
    VFreeBusy,
    /// Availability component (VAVAILABILITY).
    VAvailability,
    /// Non-standard or unknown component type.
    Other(String),
}

impl CalendarComponent {
    /// Returns the iCalendar component name.
    #[must_use]
    pub fn as_str(&self) -> &str {
        match self {
            CalendarComponent::VEvent => "VEVENT",
            CalendarComponent::VTodo => "VTODO",
            CalendarComponent::VJournal => "VJOURNAL",
            CalendarComponent::VFreeBusy => "VFREEBUSY",
            CalendarComponent::VAvailability => "VAVAILABILITY",
            CalendarComponent::Other(name) => name,
        }
    }
}

/// Helper to create a `supported-calendar-component-set` property node.
///
/// Commonly used when creating calendars to specify which component types are supported
/// by the calendar (e.g., VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY).
///
/// # Examples
///
/// Creating a calendar that supports both events and todos:
///
/// ```rust
/// # use libdav::caldav::CalendarComponent;
/// # use libdav::caldav::CreateCalendar;
/// # use libdav::dav::WebDavClient;
/// # use tower_service::Service;
/// # async fn example<C>(caldav: &WebDavClient<C>) -> Result<(), Box<dyn std::error::Error>>
/// # where
/// #     C: Service<http::Request<String>, Response = http::Response<hyper::body::Incoming>> + Send + Sync,
/// #     C::Error: std::error::Error + Send + Sync,
/// # {
/// caldav.request(
///     CreateCalendar::new("/calendars/tasks/")
///         .with_components(&[CalendarComponent::VEvent, CalendarComponent::VTodo])
/// ).await?;
/// # Ok(())
/// # }
/// ```
///
///  # See also
///
///  - [`CreateCalendar::with_components`](crate::caldav::CreateCalendar::with_components)
#[must_use]
pub fn supported_calendar_component_set(components: &'_ [CalendarComponent]) -> XmlNode<'_> {
    let children = components
        .iter()
        .map(|component| {
            XmlNode::new(&names::COMP).with_attributes(vec![("name", component.as_str())])
        })
        .collect();

    XmlNode::new(&names::SUPPORTED_CALENDAR_COMPONENT_SET).with_children(children)
}