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 let mut elem = Element::new("CARD:addressbook-home-set");
106 elem.namespace = Some(NS_CARDDAV_URI.to_string());
107
108 let mut href = Element::new("D:href");
109 href.namespace = Some("DAV:".to_string());
110 href.children
111 .push(xmltree::XMLNode::Text(format!("{prefix}{path}")));
112
113 elem.children.push(xmltree::XMLNode::Element(href));
114 elem
115}
116
117pub(crate) fn is_path_in_carddav_directory(dav_path: &DavPath) -> bool {
119 let path_string = dav_path.to_string();
120 path_string.len() > DEFAULT_CARDDAV_DIRECTORY_ENDSLASH.len()
121 && path_string.starts_with(DEFAULT_CARDDAV_DIRECTORY_ENDSLASH)
122}
123
124pub fn is_vcard_data(content: &[u8]) -> bool {
126 if !content.starts_with(b"BEGIN:VCARD") {
127 return false;
128 }
129
130 let trimmed = content.trim_ascii_end();
131 trimmed.ends_with(b"END:VCARD")
132}
133
134#[cfg(feature = "carddav")]
152pub fn validate_vcard_data(content: &str) -> Result<VCard, String> {
153 VCard::parse(content).map_err(|e| format!("Invalid vCard data: {:?}", e))
154}
155
156#[cfg(feature = "carddav")]
163pub fn validate_vcard_strict(content: &str) -> Result<(), String> {
164 let vcard = validate_vcard_data(content)?;
166
167 if vcard.version().is_none() {
169 return Err("Missing required VERSION property".to_string());
170 }
171
172 if extract_vcard_fn(content).is_none() {
175 return Err("Missing required FN (formatted name) property".to_string());
176 }
177
178 Ok(())
179}
180
181pub fn extract_vcard_uid(content: &str) -> Option<String> {
186 for line in content.lines() {
187 let line = line.trim();
188 if let Some(uid) = extract_vcard_property_value(line, "UID") {
189 return Some(uid);
190 }
191 }
192 None
193}
194
195pub fn extract_vcard_fn(content: &str) -> Option<String> {
200 for line in content.lines() {
201 let line = line.trim();
202 if let Some(fn_value) = extract_vcard_property_value(line, "FN") {
203 return Some(fn_value);
204 }
205 }
206 None
207}
208
209fn extract_vcard_property_value(line: &str, property_name: &str) -> Option<String> {
217 let colon_pos = line.find(':')?;
219 let property_part = &line[..colon_pos];
220 let value = &line[colon_pos + 1..];
221
222 let name_part = property_part.split(';').next()?;
225
226 let actual_name = if let Some(dot_pos) = name_part.find('.') {
228 &name_part[dot_pos + 1..]
229 } else {
230 name_part
231 };
232
233 if actual_name.eq_ignore_ascii_case(property_name) {
234 Some(value.to_string())
235 } else {
236 None
237 }
238}