#[cfg(feature = "carddav")]
use calcard::vcard::VCard;
use xmltree::Element;
use crate::davpath::DavPath;
pub use crate::dav_filters::{ParameterFilter, TextMatch};
pub const NS_CARDDAV_URI: &str = "urn:ietf:params:xml:ns:carddav";
pub const DEFAULT_CARDDAV_NAME: &str = "addressbooks";
pub const DEFAULT_CARDDAV_DIRECTORY: &str = "/addressbooks";
pub const DEFAULT_CARDDAV_DIRECTORY_ENDSLASH: &str = "/addressbooks/";
pub const DEFAULT_MAX_RESOURCE_SIZE: u64 = 1024 * 1024;
#[derive(Debug, Clone)]
pub struct AddressBookProperties {
pub description: Option<String>,
pub max_resource_size: Option<u64>,
pub display_name: Option<String>,
}
impl Default for AddressBookProperties {
fn default() -> Self {
Self {
description: None,
max_resource_size: Some(DEFAULT_MAX_RESOURCE_SIZE),
display_name: None,
}
}
}
#[derive(Debug, Clone)]
pub struct AddressBookQuery {
pub prop_filter: Option<PropertyFilter>,
pub properties: Vec<String>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct PropertyFilter {
pub name: String,
pub is_not_defined: bool,
pub text_match: Option<TextMatch>,
pub param_filters: Vec<ParameterFilter>,
}
#[derive(Debug, Clone)]
pub enum CardDavReportType {
AddressBookQuery(AddressBookQuery),
AddressBookMultiget { hrefs: Vec<String> },
}
pub fn create_supported_address_data() -> Element {
let mut elem = Element::new("CARD:supported-address-data");
elem.namespace = Some(NS_CARDDAV_URI.to_string());
let mut address_data = Element::new("CARD:address-data-type");
address_data.namespace = Some(NS_CARDDAV_URI.to_string());
address_data
.attributes
.insert("content-type".to_string(), "text/vcard".to_string());
address_data
.attributes
.insert("version".to_string(), "3.0".to_string());
elem.children.push(xmltree::XMLNode::Element(address_data));
let mut address_data_v4 = Element::new("CARD:address-data-type");
address_data_v4.namespace = Some(NS_CARDDAV_URI.to_string());
address_data_v4
.attributes
.insert("content-type".to_string(), "text/vcard".to_string());
address_data_v4
.attributes
.insert("version".to_string(), "4.0".to_string());
elem.children
.push(xmltree::XMLNode::Element(address_data_v4));
elem
}
pub fn create_addressbook_home_set(prefix: &str, path: &str) -> Element {
log::debug!("create_addressbook_home_set prefix: {:#?}", prefix);
log::debug!("create_addressbook_home_set path: {:#?}", path);
let mut elem = Element::new("CARD:addressbook-home-set");
elem.namespace = Some(NS_CARDDAV_URI.to_string());
let mut href = Element::new("D:href");
href.namespace = Some("DAV:".to_string());
href.children
.push(xmltree::XMLNode::Text(format!("{prefix}{path}")));
elem.children.push(xmltree::XMLNode::Element(href));
elem
}
pub(crate) fn is_path_in_carddav_directory(dav_path: &DavPath) -> bool {
let path_string = dav_path.to_string();
path_string.len() > DEFAULT_CARDDAV_DIRECTORY_ENDSLASH.len()
&& path_string.starts_with(DEFAULT_CARDDAV_DIRECTORY_ENDSLASH)
}
pub fn is_vcard_data(content: &[u8]) -> bool {
if !content.starts_with(b"BEGIN:VCARD") {
return false;
}
let trimmed = content.trim_ascii_end();
trimmed.ends_with(b"END:VCARD")
}
#[cfg(feature = "carddav")]
pub fn validate_vcard_data(content: &str) -> Result<VCard, String> {
VCard::parse(content).map_err(|e| format!("Invalid vCard data: {:?}", e))
}
#[cfg(feature = "carddav")]
pub fn validate_vcard_strict(content: &str) -> Result<(), String> {
let vcard = validate_vcard_data(content)?;
if vcard.version().is_none() {
return Err("Missing required VERSION property".to_string());
}
if extract_vcard_fn(content).is_none() {
return Err("Missing required FN (formatted name) property".to_string());
}
Ok(())
}
pub fn extract_vcard_uid(content: &str) -> Option<String> {
for line in content.lines() {
let line = line.trim();
if let Some(uid) = extract_vcard_property_value(line, "UID") {
return Some(uid);
}
}
None
}
pub fn extract_vcard_fn(content: &str) -> Option<String> {
for line in content.lines() {
let line = line.trim();
if let Some(fn_value) = extract_vcard_property_value(line, "FN") {
return Some(fn_value);
}
}
None
}
fn extract_vcard_property_value(line: &str, property_name: &str) -> Option<String> {
let colon_pos = line.find(':')?;
let property_part = &line[..colon_pos];
let value = &line[colon_pos + 1..];
let name_part = property_part.split(';').next()?;
let actual_name = if let Some(dot_pos) = name_part.find('.') {
&name_part[dot_pos + 1..]
} else {
name_part
};
if actual_name.eq_ignore_ascii_case(property_name) {
Some(value.to_string())
} else {
None
}
}