imsg_formats/vcard.rs
1//! PBAP contact normalisation using calcard.
2//!
3//! Use [`Contact::from_vcard_str`] to extract display name and phone numbers
4//! from a raw vCard 3.0 string pulled via PBAP.
5
6use calcard::vcard::{VCardProperty, VCardValue};
7use thiserror::Error;
8
9/// Contact parsing errors — calcard cannot parse the vCard input.
10#[derive(Debug, Error)]
11pub enum ContactError {
12 /// calcard could not parse the vCard; the input was not well-formed vCard.
13 #[error("vCard parse failed")]
14 ParseFailed,
15}
16
17/// Normalised contact extracted from a PBAP vCard.
18///
19/// Phone numbers are whitespace-stripped but otherwise preserved as-is; no
20/// E.164 normalisation is attempted.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Contact {
23 /// Value of the FN property; `None` if absent.
24 pub display_name: Option<String>,
25 phones: Vec<String>,
26}
27
28impl Contact {
29 /// Whitespace-stripped, non-empty TEL values in vCard order.
30 #[inline]
31 #[must_use]
32 pub fn phones(&self) -> &[String] {
33 &self.phones
34 }
35}
36
37impl Contact {
38 /// Extracts FN and TEL; input must be a single vCard.
39 ///
40 /// # Errors
41 ///
42 /// Returns [`ContactError::ParseFailed`] if calcard cannot parse the input
43 /// as a valid vCard.
44 pub fn from_vcard_str(input: &str) -> Result<Self, ContactError> {
45 let vcard = calcard::vcard::VCard::parse(input).map_err(|_| ContactError::ParseFailed)?;
46
47 let display_name = vcard
48 .property(&VCardProperty::Fn)
49 .and_then(|e| e.values.first())
50 .and_then(VCardValue::as_text)
51 .map(str::to_owned);
52
53 let phones = vcard
54 .properties(&VCardProperty::Tel)
55 .flat_map(|e| e.values.iter())
56 .filter_map(VCardValue::as_text)
57 .map(|s| s.split_whitespace().collect::<String>())
58 .filter(|s| !s.is_empty())
59 .collect();
60
61 Ok(Self { display_name, phones })
62 }
63}