use crate::common::property::{Parameter, Property};
use crate::error::Result;
use crate::vcard::name::{Address, StructuredName};
#[derive(Debug, Clone)]
pub struct Contact {
pub(crate) uid: Option<String>,
pub(crate) full_name: Option<String>,
pub(crate) name: Option<StructuredName>,
pub(crate) emails: Vec<TypedValue>,
pub(crate) phones: Vec<TypedValue>,
pub(crate) organization: Option<String>,
pub(crate) title: Option<String>,
pub(crate) role: Option<String>,
pub(crate) note: Option<String>,
pub(crate) url: Option<String>,
pub(crate) addresses: Vec<TypedAddress>,
pub(crate) birthday: Option<String>,
pub(crate) photo_uri: Option<String>,
pub(crate) extra_properties: Vec<Property>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypedValue {
pub value: String,
pub types: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypedAddress {
pub address: Address,
pub types: Vec<String>,
pub label: Option<String>,
}
impl Contact {
pub fn new() -> Self {
Self {
uid: None,
full_name: None,
name: None,
emails: Vec::new(),
phones: Vec::new(),
organization: None,
title: None,
role: None,
note: None,
url: None,
addresses: Vec::new(),
birthday: None,
photo_uri: None,
extra_properties: Vec::new(),
}
}
pub fn parse_all(input: &str) -> Result<Vec<Contact>> {
crate::vcard::parser::parse_vcards(input)
}
pub fn parse(input: &str) -> Result<Contact> {
let mut contacts = Self::parse_all(input)?;
contacts
.pop()
.ok_or_else(|| crate::error::Error::Other("no vCard found in input".to_string()))
}
pub fn uid(mut self, uid: impl Into<String>) -> Self {
self.uid = Some(uid.into());
self
}
pub fn full_name(mut self, name: impl Into<String>) -> Self {
self.full_name = Some(name.into());
self
}
pub fn structured_name(mut self, name: StructuredName) -> Self {
self.name = Some(name);
self
}
pub fn email(mut self, email: impl Into<String>) -> Self {
self.emails.push(TypedValue {
value: email.into(),
types: Vec::new(),
});
self
}
pub fn email_typed(mut self, email: impl Into<String>, types: Vec<String>) -> Self {
self.emails.push(TypedValue {
value: email.into(),
types,
});
self
}
pub fn phone(mut self, phone: impl Into<String>) -> Self {
self.phones.push(TypedValue {
value: phone.into(),
types: Vec::new(),
});
self
}
pub fn phone_typed(mut self, phone: impl Into<String>, types: Vec<String>) -> Self {
self.phones.push(TypedValue {
value: phone.into(),
types,
});
self
}
pub fn organization(mut self, org: impl Into<String>) -> Self {
self.organization = Some(org.into());
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn role(mut self, role: impl Into<String>) -> Self {
self.role = Some(role.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn address(mut self, address: Address) -> Self {
self.addresses.push(TypedAddress {
address,
types: Vec::new(),
label: None,
});
self
}
pub fn address_typed(
mut self,
address: Address,
types: Vec<String>,
label: Option<String>,
) -> Self {
self.addresses.push(TypedAddress {
address,
types,
label,
});
self
}
pub fn birthday(mut self, birthday: impl Into<String>) -> Self {
self.birthday = Some(birthday.into());
self
}
pub fn photo_uri(mut self, uri: impl Into<String>) -> Self {
self.photo_uri = Some(uri.into());
self
}
pub fn property(mut self, prop: Property) -> Self {
self.extra_properties.push(prop);
self
}
pub fn build(self) -> Self {
self
}
pub fn get_uid(&self) -> Option<&str> {
self.uid.as_deref()
}
pub fn get_full_name(&self) -> Option<&str> {
self.full_name.as_deref()
}
pub fn get_name(&self) -> Option<&StructuredName> {
self.name.as_ref()
}
pub fn get_emails(&self) -> &[TypedValue] {
&self.emails
}
pub fn get_email(&self) -> Option<&str> {
self.emails.first().map(|e| e.value.as_str())
}
pub fn get_phones(&self) -> &[TypedValue] {
&self.phones
}
pub fn get_phone(&self) -> Option<&str> {
self.phones.first().map(|p| p.value.as_str())
}
pub fn get_organization(&self) -> Option<&str> {
self.organization.as_deref()
}
pub fn get_title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn get_role(&self) -> Option<&str> {
self.role.as_deref()
}
pub fn get_note(&self) -> Option<&str> {
self.note.as_deref()
}
pub fn get_url(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn get_addresses(&self) -> &[TypedAddress] {
&self.addresses
}
pub fn get_birthday(&self) -> Option<&str> {
self.birthday.as_deref()
}
pub fn get_photo_uri(&self) -> Option<&str> {
self.photo_uri.as_deref()
}
pub fn get_extra_properties(&self) -> &[Property] {
&self.extra_properties
}
pub(crate) fn to_properties(&self) -> Vec<Property> {
let mut props = Vec::new();
props.push(Property::new("VERSION", "4.0"));
let uid = self
.uid
.clone()
.unwrap_or_else(|| format!("urn:uuid:{}", uuid::Uuid::new_v4()));
props.push(Property::new("UID", uid));
if let Some(ref fn_name) = self.full_name {
props.push(Property::new("FN", vcard_escape(fn_name)));
}
if let Some(ref name) = self.name {
props.push(Property::new("N", name.to_value()));
}
for email in &self.emails {
let mut prop = Property::new("EMAIL", &email.value);
if !email.types.is_empty() {
prop = prop.with_param(Parameter::with_values(
"TYPE".to_string(),
email.types.clone(),
));
}
props.push(prop);
}
for phone in &self.phones {
let mut prop = Property::new("TEL", &phone.value);
if !phone.types.is_empty() {
prop = prop.with_param(Parameter::with_values(
"TYPE".to_string(),
phone.types.clone(),
));
}
props.push(prop);
}
if let Some(ref org) = self.organization {
props.push(Property::new("ORG", vcard_escape(org)));
}
if let Some(ref title) = self.title {
props.push(Property::new("TITLE", vcard_escape(title)));
}
if let Some(ref role) = self.role {
props.push(Property::new("ROLE", vcard_escape(role)));
}
if let Some(ref note) = self.note {
props.push(Property::new("NOTE", vcard_escape(note)));
}
if let Some(ref url) = self.url {
props.push(Property::new("URL", url));
}
for typed_addr in &self.addresses {
let mut prop = Property::new("ADR", typed_addr.address.to_value());
if !typed_addr.types.is_empty() {
prop = prop.with_param(Parameter::with_values(
"TYPE".to_string(),
typed_addr.types.clone(),
));
}
if let Some(ref label) = typed_addr.label {
prop = prop.with_param(Parameter::new("LABEL", label.clone()));
}
props.push(prop);
}
if let Some(ref bday) = self.birthday {
props.push(Property::new("BDAY", bday));
}
if let Some(ref photo) = self.photo_uri {
props.push(Property::new("PHOTO", photo));
}
props.extend(self.extra_properties.clone());
props
}
pub(crate) fn from_properties(props: Vec<Property>) -> Result<Self> {
let mut contact = Contact::new();
let mut extra = Vec::new();
for prop in props {
match prop.name.as_str() {
"VERSION" => {} "UID" => contact.uid = Some(prop.value.clone()),
"FN" => contact.full_name = Some(vcard_unescape(&prop.value)),
"N" => contact.name = Some(StructuredName::parse(&prop.value)),
"EMAIL" => {
let types = extract_types(&prop);
contact.emails.push(TypedValue {
value: prop.value.clone(),
types,
});
}
"TEL" => {
let types = extract_types(&prop);
contact.phones.push(TypedValue {
value: prop.value.clone(),
types,
});
}
"ORG" => contact.organization = Some(vcard_unescape(&prop.value)),
"TITLE" => contact.title = Some(vcard_unescape(&prop.value)),
"ROLE" => contact.role = Some(vcard_unescape(&prop.value)),
"NOTE" => contact.note = Some(vcard_unescape(&prop.value)),
"URL" => contact.url = Some(prop.value.clone()),
"ADR" => {
let types = extract_types(&prop);
let label = prop.param_value("LABEL").map(|s| s.to_string());
contact.addresses.push(TypedAddress {
address: Address::parse(&prop.value),
types,
label,
});
}
"BDAY" => contact.birthday = Some(prop.value.clone()),
"PHOTO" => contact.photo_uri = Some(prop.value.clone()),
_ => extra.push(prop),
}
}
contact.extra_properties = extra;
Ok(contact)
}
}
impl Default for Contact {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for Contact {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", crate::vcard::writer::write_contact(self))
}
}
fn extract_types(prop: &Property) -> Vec<String> {
prop.param("TYPE")
.map(|p| p.values.clone())
.unwrap_or_default()
}
fn vcard_escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace(',', "\\,")
.replace('\n', "\\n")
}
fn vcard_unescape(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.peek() {
Some('n') | Some('N') => {
result.push('\n');
chars.next();
}
Some('\\') => {
result.push('\\');
chars.next();
}
Some(',') => {
result.push(',');
chars.next();
}
Some(';') => {
result.push(';');
chars.next();
}
_ => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contact_builder() {
let contact = Contact::new()
.full_name("Jane Doe")
.email("jane@example.com")
.phone("+1-555-0123")
.organization("Acme Corp")
.build();
assert_eq!(contact.get_full_name(), Some("Jane Doe"));
assert_eq!(contact.get_email(), Some("jane@example.com"));
assert_eq!(contact.get_phone(), Some("+1-555-0123"));
assert_eq!(contact.get_organization(), Some("Acme Corp"));
}
#[test]
fn contact_typed_email() {
let contact = Contact::new()
.full_name("Test")
.email_typed("work@example.com", vec!["WORK".to_string()])
.email_typed("home@example.com", vec!["HOME".to_string()])
.build();
assert_eq!(contact.get_emails().len(), 2);
assert_eq!(contact.get_emails()[0].types, vec!["WORK"]);
}
#[test]
fn vcard_escape_roundtrip() {
let original = "Hello, World\\test\nnewline";
let escaped = vcard_escape(original);
let unescaped = vcard_unescape(&escaped);
assert_eq!(unescaped, original);
}
}