Skip to main content

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}