libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Get the user address set for a principal.

use crate::{
    Depth,
    dav::{Propfind, WebDavError},
    names,
    requests::{DavRequest, ParseResponseError, PreparedRequest},
    xmlutils::validate_xml_response,
};

/// Request to get the calendar user address set for a principal.
///
/// Sends a PROPFIND request with depth 0 to fetch the
/// `calendar-user-address-set` property, which typically includes at least
/// an email address in the format `mailto:alice@example.com`.
///
/// See: <https://www.rfc-editor.org/rfc/rfc4791#section-6.2.1>
///
/// # Example
///
/// ```
/// # use libdav::caldav::GetUserAddressSet;
/// # use libdav::CalDavClient;
/// # use tower_service::Service;
/// # use http::Uri;
/// # async fn example<C>(caldav: &CalDavClient<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,
/// # {
/// let principal = Uri::from_static("/principals/alice/");
/// let response = caldav.request(
///     GetUserAddressSet::new(&principal)
/// ).await?;
///
/// for address in response.addresses {
///     println!("User address: {}", address);
/// }
/// # Ok(())
/// # }
/// ```
pub struct GetUserAddressSet<'a> {
    propfind: Propfind<'a>,
}

impl<'a> GetUserAddressSet<'a> {
    /// Create a new request for the given principal URI.
    #[must_use]
    pub fn new(principal: &'a hyper::Uri) -> Self {
        Self {
            propfind: Propfind::new(principal.path())
                .with_properties(&[&names::CALENDAR_USER_ADDRESS_SET])
                .with_depth(Depth::Zero),
        }
    }
}

/// Response from a [`GetUserAddressSet`] request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetUserAddressSetResponse {
    /// The addresses associated with the principal.
    ///
    /// Typically includes at least an email address in the format `mailto:alice@example.com`.
    pub addresses: Vec<String>,
}

impl DavRequest for GetUserAddressSet<'_> {
    type Response = GetUserAddressSetResponse;
    type ParseError = ParseResponseError;
    type Error<E> = WebDavError<E>;

    fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
        self.propfind.prepare_request()
    }

    fn parse_response(
        &self,
        parts: &http::response::Parts,
        body: &[u8],
    ) -> Result<Self::Response, ParseResponseError> {
        let doc = validate_xml_response(parts, body)?;
        let root = doc.root_element();

        // Find the calendar-user-address-set property.
        let prop = root
            .descendants()
            .find(|node| node.tag_name() == names::CALENDAR_USER_ADDRESS_SET);

        if let Some(prop) = prop {
            // Extract all href elements within the property.
            let addresses = prop
                .descendants()
                .filter(|node| node.tag_name() == names::HREF)
                .map(|h| h.text().map(str::to_string))
                .collect::<Option<Vec<_>>>()
                .ok_or_else(|| {
                    ParseResponseError::InvalidResponse(
                        "DAV:href in response is missing text".into(),
                    )
                })?;
            return Ok(GetUserAddressSetResponse { addresses });
        }

        Ok(GetUserAddressSetResponse {
            addresses: Vec::new(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::{Method, StatusCode};

    #[test]
    fn test_prepare_request() {
        let principal = hyper::Uri::from_static("/principals/alice/");
        let req = GetUserAddressSet::new(&principal);
        let prepared = req.prepare_request().unwrap();

        assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
        assert_eq!(prepared.path, "/principals/alice/");
        assert_eq!(
            prepared.body,
            concat!(
                r#"<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
                r#"<D:prop><C:calendar-user-address-set/></D:prop>"#,
                r#"</D:propfind>"#,
            )
        );
        assert_eq!(
            prepared.headers,
            vec![
                ("Depth".to_string(), "0".to_string()),
                (
                    "Content-Type".to_string(),
                    "application/xml; charset=utf-8".to_string()
                )
            ]
        );
    }

    #[test]
    fn test_parse_response() {
        let principal = hyper::Uri::from_static("/principals/alice/");
        let req = GetUserAddressSet::new(&principal);
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
                <response>
                    <href>/principals/alice/</href>
                    <propstat>
                        <prop>
                            <C:calendar-user-address-set>
                                <href>mailto:alice@example.com</href>
                                <href>mailto:alice@work.example.com</href>
                            </C:calendar-user-address-set>
                        </prop>
                        <status>HTTP/1.1 200 OK</status>
                    </propstat>
                </response>
            </multistatus>"#;

        let response = http::Response::builder()
            .status(StatusCode::MULTI_STATUS)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();

        assert_eq!(result.addresses.len(), 2);
        assert_eq!(result.addresses[0], "mailto:alice@example.com");
        assert_eq!(result.addresses[1], "mailto:alice@work.example.com");
    }

    #[test]
    fn test_parse_response_empty_address_set() {
        let principal = hyper::Uri::from_static("/principals/alice/");
        let req = GetUserAddressSet::new(&principal);
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
                <response>
                    <href>/principals/alice/</href>
                    <propstat>
                        <prop>
                            <C:calendar-user-address-set />
                        </prop>
                        <status>HTTP/1.1 200 OK</status>
                    </propstat>
                </response>
            </multistatus>"#;

        let response = http::Response::builder()
            .status(StatusCode::MULTI_STATUS)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();

        assert_eq!(result.addresses.len(), 0);
    }

    #[test]
    fn test_parse_response_missing_property() {
        let principal = hyper::Uri::from_static("/principals/alice/");
        let req = GetUserAddressSet::new(&principal);
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:">
                <response>
                    <href>/principals/alice/</href>
                    <propstat>
                        <prop></prop>
                        <status>HTTP/1.1 200 OK</status>
                    </propstat>
                </response>
            </multistatus>"#;

        let response = http::Response::builder()
            .status(StatusCode::MULTI_STATUS)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, response_xml.as_bytes()).unwrap();

        // Should return empty list when property is missing
        assert_eq!(result.addresses.len(), 0);
    }

    #[test]
    fn test_parse_response_bad_status() {
        let principal = hyper::Uri::from_static("/principals/alice/");
        let req = GetUserAddressSet::new(&principal);
        let response_xml = b"";

        let response = http::Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, response_xml);

        assert!(result.is_err());
    }
}