1#[cfg(feature = "carddav")]
8use calcard::vcard::VCard;
9use xmltree::Element;
10
11use crate::davpath::DavPath;
12
13pub use crate::dav_filters::{ParameterFilter, TextMatch};
15
16pub const NS_CARDDAV_URI: &str = "urn:ietf:params:xml:ns:carddav";
18
19pub const DEFAULT_CARDDAV_NAME: &str = "addressbooks";
21pub const DEFAULT_CARDDAV_DIRECTORY: &str = "/addressbooks";
22pub const DEFAULT_CARDDAV_DIRECTORY_ENDSLASH: &str = "/addressbooks/";
23
24pub const DEFAULT_MAX_RESOURCE_SIZE: u64 = 1024 * 1024;
26
27#[derive(Debug, Clone)]
29pub struct AddressBookProperties {
30 pub description: Option<String>,
31 pub max_resource_size: Option<u64>,
32 pub display_name: Option<String>,
33}
34
35impl Default for AddressBookProperties {
36 fn default() -> Self {
37 Self {
38 description: None,
39 max_resource_size: Some(DEFAULT_MAX_RESOURCE_SIZE),
40 display_name: None,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct AddressBookQuery {
48 pub prop_filter: Option<PropertyFilter>,
49 pub properties: Vec<String>,
50 pub limit: Option<u32>,
51}
52
53#[derive(Debug, Clone)]
58pub struct PropertyFilter {
59 pub name: String,
60 pub is_not_defined: bool,
61 pub text_match: Option<TextMatch>,
62 pub param_filters: Vec<ParameterFilter>,
63}
64
65#[derive(Debug, Clone)]
67pub enum CardDavReportType {
68 AddressBookQuery(AddressBookQuery),
69 AddressBookMultiget { hrefs: Vec<String> },
70}
71
72pub fn create_supported_address_data() -> Element {
74 let mut elem = Element::new("CARD:supported-address-data");
75 elem.namespace = Some(NS_CARDDAV_URI.to_string());
76
77 let mut address_data = Element::new("CARD:address-data-type");
78 address_data.namespace = Some(NS_CARDDAV_URI.to_string());
79 address_data
80 .attributes
81 .insert("content-type".to_string(), "text/vcard".to_string());
82 address_data
83 .attributes
84 .insert("version".to_string(), "3.0".to_string());
85
86 elem.children.push(xmltree::XMLNode::Element(address_data));
87
88 let mut address_data_v4 = Element::new("CARD:address-data-type");
90 address_data_v4.namespace = Some(NS_CARDDAV_URI.to_string());
91 address_data_v4
92 .attributes
93 .insert("content-type".to_string(), "text/vcard".to_string());
94 address_data_v4
95 .attributes
96 .insert("version".to_string(), "4.0".to_string());
97
98 elem.children
99 .push(xmltree::XMLNode::Element(address_data_v4));
100
101 elem
102}
103
104pub fn create_addressbook_home_set(prefix: &str, path: &str) -> Element {
105 log::debug!("create_addressbook_home_set prefix: {:#?}", prefix);
106 log::debug!("create_addressbook_home_set path: {:#?}", path);
107
108 let mut elem = Element::new("CARD:addressbook-home-set");
109 elem.namespace = Some(NS_CARDDAV_URI.to_string());
110
111 let mut href = Element::new("D:href");
112 href.namespace = Some("DAV:".to_string());
113 href.children
114 .push(xmltree::XMLNode::Text(format!("{prefix}{path}")));
115
116 elem.children.push(xmltree::XMLNode::Element(href));
117 elem
118}
119
120pub(crate) fn is_path_in_carddav_directory(dav_path: &DavPath) -> bool {
122 let path_string = dav_path.to_string();
123 path_string.len() > DEFAULT_CARDDAV_DIRECTORY_ENDSLASH.len()
124 && path_string.starts_with(DEFAULT_CARDDAV_DIRECTORY_ENDSLASH)
125}
126
127pub fn is_vcard_data(content: &[u8]) -> bool {
129 if !content.starts_with(b"BEGIN:VCARD") {
130 return false;
131 }
132
133 let trimmed = content.trim_ascii_end();
134 trimmed.ends_with(b"END:VCARD")
135}
136
137#[cfg(feature = "carddav")]
155pub fn validate_vcard_data(content: &str) -> Result<VCard, String> {
156 VCard::parse(content).map_err(|e| format!("Invalid vCard data: {:?}", e))
157}
158
159#[cfg(feature = "carddav")]
166pub fn validate_vcard_strict(content: &str) -> Result<(), String> {
167 let vcard = validate_vcard_data(content)?;
169
170 if vcard.version().is_none() {
172 return Err("Missing required VERSION property".to_string());
173 }
174
175 if extract_vcard_fn(content).is_none() {
178 return Err("Missing required FN (formatted name) property".to_string());
179 }
180
181 Ok(())
182}
183
184pub fn extract_vcard_uid(content: &str) -> Option<String> {
189 for line in content.lines() {
190 let line = line.trim();
191 if let Some(uid) = extract_vcard_property_value(line, "UID") {
192 return Some(uid);
193 }
194 }
195 None
196}
197
198pub fn extract_vcard_fn(content: &str) -> Option<String> {
203 for line in content.lines() {
204 let line = line.trim();
205 if let Some(fn_value) = extract_vcard_property_value(line, "FN") {
206 return Some(fn_value);
207 }
208 }
209 None
210}
211
212fn extract_vcard_property_value(line: &str, property_name: &str) -> Option<String> {
220 let colon_pos = line.find(':')?;
222 let property_part = &line[..colon_pos];
223 let value = &line[colon_pos + 1..];
224
225 let name_part = property_part.split(';').next()?;
228
229 let actual_name = if let Some(dot_pos) = name_part.find('.') {
231 &name_part[dot_pos + 1..]
232 } else {
233 name_part
234 };
235
236 if actual_name.eq_ignore_ascii_case(property_name) {
237 Some(value.to_string())
238 } else {
239 None
240 }
241}