use http::Method;
use crate::{
FetchedResource,
dav::{WebDavError, extract_fetched_resources},
names,
requests::{DavRequest, ParseResponseError, PreparedRequest, xml_content_type_header},
xmlutils::XmlNode,
};
pub struct GetAddressBookResources<'a> {
collection_href: &'a str,
hrefs: Option<Vec<String>>,
}
impl<'a> GetAddressBookResources<'a> {
#[must_use]
pub fn new(collection_href: &'a str) -> Self {
Self {
collection_href,
hrefs: None,
}
}
#[must_use]
pub fn with_hrefs<S: AsRef<str>>(mut self, hrefs: impl IntoIterator<Item = S>) -> Self
where
Self: Sized,
{
self.hrefs = Some(hrefs.into_iter().map(|s| s.as_ref().to_string()).collect());
self
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct GetAddressBookResourcesResponse {
pub resources: Vec<FetchedResource>,
}
impl DavRequest for GetAddressBookResources<'_> {
type Response = GetAddressBookResourcesResponse;
type ParseError = ParseResponseError;
type Error<E> = WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
let mut prop = XmlNode::new(&names::PROP);
prop.children = vec![
XmlNode::new(&names::GETETAG),
XmlNode::new(&names::ADDRESS_DATA),
];
let body = if let Some(hrefs) = &self.hrefs {
let mut multiget = XmlNode::new(&names::ADDRESSBOOK_MULTIGET);
multiget.children = vec![prop];
for href in hrefs {
multiget
.children
.push(XmlNode::new(&names::HREF).with_text(href));
}
multiget.render_node()
} else {
let prop_filter =
XmlNode::new(&names::PROP_FILTER).with_attributes(vec![("name", "FN")]);
let filter = XmlNode::new(&names::ADDRESSBOOK_FILTER).with_children(vec![prop_filter]);
let mut query = XmlNode::new(&names::ADDRESSBOOK_QUERY);
query.children = vec![prop, filter];
query.render_node()
};
Ok(PreparedRequest {
method: Method::from_bytes(b"REPORT")?,
path: self.collection_href.to_string(),
body,
headers: vec![
("Depth".to_string(), "1".to_string()),
xml_content_type_header(),
],
})
}
fn parse_response(
&self,
parts: &http::response::Parts,
body: &[u8],
) -> Result<Self::Response, ParseResponseError> {
if !parts.status.is_success() {
return Err(ParseResponseError::BadStatusCode(parts.status));
}
let resources = extract_fetched_resources(body, &names::ADDRESS_DATA)?;
Ok(GetAddressBookResourcesResponse { resources })
}
}
#[cfg(test)]
mod tests {
use http::StatusCode;
use super::*;
#[test]
fn test_prepare_request_all_resources() {
let req = GetAddressBookResources::new("/addressbooks/personal/");
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"REPORT").unwrap());
assert_eq!(prepared.path, "/addressbooks/personal/");
assert_eq!(
prepared.body,
concat!(
r#"<CARD:addressbook-query xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:prop><D:getetag/><CARD:address-data/></D:prop>"#,
r#"<CARD:filter><CARD:prop-filter name="FN"/></CARD:filter>"#,
r#"</CARD:addressbook-query>"#,
)
);
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "1".to_string()))
);
}
#[test]
fn test_prepare_request_specific_hrefs() {
let req = GetAddressBookResources::new("/addressbooks/personal/").with_hrefs([
"/addressbooks/personal/contact1.vcf",
"/addressbooks/personal/contact2.vcf",
]);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"REPORT").unwrap());
assert_eq!(prepared.path, "/addressbooks/personal/");
assert_eq!(
prepared.body,
concat!(
r#"<CARD:addressbook-multiget xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:prop><D:getetag/><CARD:address-data/></D:prop>"#,
r#"<D:href>/addressbooks/personal/contact1.vcf</D:href>"#,
r#"<D:href>/addressbooks/personal/contact2.vcf</D:href>"#,
r#"</CARD:addressbook-multiget>"#,
)
);
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "1".to_string()))
);
}
#[test]
fn test_prepare_request_escapes_hrefs() {
let req = GetAddressBookResources::new("/addressbooks/personal/")
.with_hrefs(["/addressbooks/personal/<special>&contact.vcf"]);
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<CARD:addressbook-multiget xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:prop><D:getetag/><CARD:address-data/></D:prop>"#,
r#"<D:href>/addressbooks/personal/<special>&contact.vcf</D:href>"#,
r#"</CARD:addressbook-multiget>"#,
)
);
}
#[test]
fn test_parse_response_success() {
let req = GetAddressBookResources::new("/addressbooks/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<response>
<href>/addressbooks/personal/contact1.vcf</href>
<propstat>
<prop>
<getetag>"abc123"</getetag>
<C:address-data>BEGIN:VCARD
VERSION:3.0
FN:John Doe
END:VCARD
</C:address-data>
</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, body).unwrap();
assert_eq!(result.resources.len(), 1);
assert_eq!(
result.resources[0].href,
"/addressbooks/personal/contact1.vcf"
);
let content = result.resources[0].content.as_ref().unwrap();
assert_eq!(content.etag, "\"abc123\"");
assert!(content.data.contains("BEGIN:VCARD"));
}
#[test]
fn test_parse_response_with_missing_resource() {
let req = GetAddressBookResources::new("/addressbooks/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<response>
<href>/addressbooks/personal/contact1.vcf</href>
<propstat>
<prop>
<getetag>"abc123"</getetag>
<C:address-data>BEGIN:VCARD
VERSION:3.0
FN:John Doe
END:VCARD
</C:address-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/addressbooks/personal/missing.vcf</href>
<status>HTTP/1.1 404 Not Found</status>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, body).unwrap();
assert_eq!(result.resources.len(), 2);
assert!(result.resources[0].content.is_ok());
assert_eq!(result.resources[1].content, Err(StatusCode::NOT_FOUND));
}
#[test]
fn test_parse_response_bad_status() {
let req = GetAddressBookResources::new("/addressbooks/personal/");
let response = http::Response::builder()
.status(StatusCode::FORBIDDEN)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"");
assert!(matches!(
result,
Err(ParseResponseError::BadStatusCode(StatusCode::FORBIDDEN))
));
}
#[test]
fn test_parse_response_multiple_resources() {
let req = GetAddressBookResources::new("/addressbooks/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<response>
<href>/addressbooks/personal/contact1.vcf</href>
<propstat>
<prop>
<getetag>"etag1"</getetag>
<C:address-data>BEGIN:VCARD
VERSION:3.0
FN:John Doe
END:VCARD
</C:address-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/addressbooks/personal/contact2.vcf</href>
<propstat>
<prop>
<getetag>"etag2"</getetag>
<C:address-data>BEGIN:VCARD
VERSION:3.0
FN:Jane Doe
END:VCARD
</C:address-data>
</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, body).unwrap();
assert_eq!(result.resources.len(), 2);
assert_eq!(
result.resources[0].href,
"/addressbooks/personal/contact1.vcf"
);
assert_eq!(
result.resources[0].content.as_ref().unwrap().etag,
"\"etag1\""
);
assert_eq!(
result.resources[1].href,
"/addressbooks/personal/contact2.vcf"
);
assert_eq!(
result.resources[1].content.as_ref().unwrap().etag,
"\"etag2\""
);
}
}