1use core::{fmt, str::FromStr};
28
29use alloc::{
30 borrow::Cow,
31 boxed::Box,
32 format,
33 string::{String, ToString},
34 vec,
35 vec::Vec,
36};
37
38use crate::v1::{
39 field::MsgraphField,
40 rest::users::{
41 contacts::{MsgraphContact, MsgraphPhysicalAddress, MsgraphSingleValueExtendedProperty},
42 messages::MsgraphEmailAddress,
43 },
44};
45use serde_json::{from_value, to_value};
46use vcard::{
47 param::VcardParam,
48 prop::{
49 VcardProp, VcardPropKind, VcardPropName, adr::ADR, categories::CATEGORIES, email::EMAIL,
50 r#fn::FN, impp::IMPP, n::N, nickname::NICKNAME, note::NOTE, org::ORG, related::RELATED,
51 role::ROLE, tel::TEL, title::TITLE, url::URL,
52 },
53 tree::{cst::VcardCst, error::VcardParseError, line::VcardLine, prop::lens::VcardPropLens},
54 value::{
55 VcardValue,
56 adr::VcardAdr,
57 datetime::VcardDateAndOrTime,
58 n::VcardN,
59 org::VcardOrg,
60 text::{VcardText, VcardTextList},
61 uri::VcardUri,
62 },
63};
64
65pub const MSGRAPH_CONTACT_STASH_ID: &str =
72 "String {c8e5e5cf-3f6c-4f0a-9d4e-52f1e7b2a9d3} Name cardamum-vcard";
73
74pub const MSGRAPH_CONTACT_STASH_EXPAND: &str = "singleValueExtendedProperties($filter=id eq \
77 'String {c8e5e5cf-3f6c-4f0a-9d4e-52f1e7b2a9d3} Name cardamum-vcard')";
78
79const MAX_STASH_LINE: usize = 8 * 1024;
85
86const MINTED_PROPS: &[&str] = &[
92 "X-MSGRAPH-FILE-AS",
93 "X-MSGRAPH-OFFICE-LOCATION",
94 "X-MSGRAPH-ASSISTANT",
95 "X-MSGRAPH-MANAGER",
96];
97
98impl MsgraphContact {
99 pub fn to_vcard(&self) -> String {
109 let contact = self;
110 let mut card = VcardCst::v4();
111
112 let id = contact.id.trim();
113 if contact.stashed_uid().is_none() && !id.is_empty() {
114 card.push(VcardProp::text(VcardPropKind::Uid, vec![], id));
115 }
116
117 card.push(VcardProp::text(
118 VcardPropKind::Fn,
119 vec![],
120 display_name(contact),
121 ));
122
123 let n = VcardN {
126 family: component(&contact.surname),
127 given: component(&contact.given_name),
128 additional: component(&contact.middle_name),
129 prefixes: component(&contact.title),
130 suffixes: Vec::new(),
131 };
132
133 let empty = n.family.is_empty()
134 && n.given.is_empty()
135 && n.additional.is_empty()
136 && n.prefixes.is_empty();
137 if !empty {
138 card.push(VcardProp {
139 group: None,
140 name: VcardPropName::Kind(VcardPropKind::N),
141 params: vec![],
142 value: VcardValue::N(n),
143 });
144 }
145
146 if let Some(nick) = opt(&contact.nick_name) {
147 card.push(VcardProp {
148 group: None,
149 name: VcardPropName::Kind(VcardPropKind::Nickname),
150 params: vec![],
151 value: VcardValue::TextList(VcardTextList(vec![Cow::Owned(nick.to_string())])),
152 });
153 }
154
155 for email in list(&contact.email_addresses) {
156 if let Some(address) = email.address.as_deref() {
157 let address = address.trim();
158 if !address.is_empty() {
159 card.push(VcardProp::text(VcardPropKind::Email, vec![], address));
160 }
161 }
162 }
163
164 for im in list(&contact.im_addresses) {
165 let im = im.trim();
166 if !im.is_empty() {
167 card.push(VcardProp {
168 group: None,
169 name: VcardPropName::Kind(VcardPropKind::Impp),
170 params: vec![],
171 value: VcardValue::Uri(VcardUri(Cow::Owned(im.to_string()))),
172 });
173 }
174 }
175
176 for phone in list(&contact.business_phones) {
177 let phone = phone.trim();
178 if !phone.is_empty() {
179 card.push(VcardProp::text(
180 VcardPropKind::Tel,
181 vec![type_param("work")],
182 phone,
183 ));
184 }
185 }
186 for phone in list(&contact.home_phones) {
187 let phone = phone.trim();
188 if !phone.is_empty() {
189 card.push(VcardProp::text(
190 VcardPropKind::Tel,
191 vec![type_param("home")],
192 phone,
193 ));
194 }
195 }
196 if let Some(phone) = opt(&contact.mobile_phone) {
197 card.push(VcardProp::text(
198 VcardPropKind::Tel,
199 vec![type_param("cell")],
200 phone,
201 ));
202 }
203
204 if let Some(prop) = adr_prop(contact.home_address.as_option(), Some("home")) {
205 card.push(prop);
206 }
207 if let Some(prop) = adr_prop(contact.business_address.as_option(), Some("work")) {
208 card.push(prop);
209 }
210 if let Some(prop) = adr_prop(contact.other_address.as_option(), None) {
211 card.push(prop);
212 }
213
214 let company = opt(&contact.company_name);
215 let department = opt(&contact.department);
216 if company.is_some() || department.is_some() {
217 let mut components: Vec<Cow<'static, str>> =
218 vec![Cow::Owned(company.unwrap_or_default().to_string())];
219 if let Some(department) = department {
220 components.push(Cow::Owned(department.to_string()));
221 }
222 card.push(VcardProp {
223 group: None,
224 name: VcardPropName::Kind(VcardPropKind::Org),
225 params: vec![],
226 value: VcardValue::Org(VcardOrg(components)),
227 });
228 }
229
230 if let Some(title) = opt(&contact.job_title) {
231 card.push(VcardProp::text(VcardPropKind::Title, vec![], title));
232 }
233 if let Some(role) = opt(&contact.profession) {
234 card.push(VcardProp::text(VcardPropKind::Role, vec![], role));
235 }
236 if let Some(page) = opt(&contact.business_home_page) {
237 card.push(VcardProp {
238 group: None,
239 name: VcardPropName::Kind(VcardPropKind::Url),
240 params: vec![],
241 value: VcardValue::Uri(VcardUri(Cow::Owned(page.to_string()))),
242 });
243 }
244
245 if let Some(birthday) = opt(&contact.birthday) {
248 let date = birthday.split('T').next().unwrap_or(birthday);
249 if let Some(date) = VcardDateAndOrTime::from(date).full_date() {
250 card.push(VcardProp {
251 group: None,
252 name: VcardPropName::Kind(VcardPropKind::Bday),
253 params: vec![],
254 value: VcardValue::DateAndOrTime(VcardDateAndOrTime(Cow::Owned(date))),
255 });
256 }
257 }
258
259 if let Some(notes) = opt(&contact.personal_notes) {
260 card.push(VcardProp::text(VcardPropKind::Note, vec![], notes));
261 }
262
263 let categories: Vec<Cow<'static, str>> = list(&contact.categories)
264 .iter()
265 .map(|category| category.trim())
266 .filter(|category| !category.is_empty())
267 .map(|category| Cow::Owned(category.to_string()))
268 .collect();
269 if !categories.is_empty() {
270 card.push(VcardProp {
271 group: None,
272 name: VcardPropName::Kind(VcardPropKind::Categories),
273 params: vec![],
274 value: VcardValue::TextList(VcardTextList(categories)),
275 });
276 }
277
278 if let Some(spouse) = opt(&contact.spouse_name) {
279 card.push(related_prop("spouse", spouse));
280 }
281 for child in list(&contact.children) {
282 let child = child.trim();
283 if !child.is_empty() {
284 card.push(related_prop("child", child));
285 }
286 }
287
288 let minted = [
291 ("X-MSGRAPH-FILE-AS", opt(&contact.file_as)),
292 ("X-MSGRAPH-OFFICE-LOCATION", opt(&contact.office_location)),
293 ("X-MSGRAPH-ASSISTANT", opt(&contact.assistant_name)),
294 ("X-MSGRAPH-MANAGER", opt(&contact.manager)),
295 ];
296 for (name, value) in minted {
297 if let Some(value) = value {
298 card.push(VcardProp::text(name, vec![], value));
299 }
300 }
301
302 let stash = stash_lines(contact);
303 for line in &stash {
304 let _ = card.push_raw(line);
307 }
308
309 String::from_utf8_lossy(&card.to_bytes()).into_owned()
310 }
311
312 pub fn from_vcard(vcard: &str) -> Result<Self, MsgraphContactVcardError> {
322 let card = VcardCst::parse(vcard).map_err(MsgraphContactVcardError::Parse)?;
323 let version = card.version();
324
325 let mut display_name = None;
326 let mut surname = None;
327 let mut given_name = None;
328 let mut middle_name = None;
329 let mut title = None;
330 let mut nick_name = None;
331 let mut emails = Vec::new();
332 let mut ims = Vec::new();
333 let mut business_phones = Vec::new();
334 let mut home_phones = Vec::new();
335 let mut mobile_phone = None;
336 let mut home_address = None;
337 let mut business_address = None;
338 let mut other_address = None;
339 let mut company_name = None;
340 let mut department = None;
341 let mut job_title = None;
342 let mut profession = None;
343 let mut business_home_page = None;
344 let mut birthday = None;
345 let mut spouse_name = None;
346 let mut children = Vec::new();
347 let mut categories = Vec::new();
348 let mut notes = Vec::new();
349 let mut stash = Vec::new();
350 let mut name_seen = false;
351
352 for line in &card.props {
359 let consumed = match VcardPropKind::from_str(line.name.get()) {
360 Err(_) => {
364 let raw_name = line.name.get();
365 raw_name.eq_ignore_ascii_case("VERSION")
366 || MINTED_PROPS
367 .iter()
368 .any(|prop| raw_name.eq_ignore_ascii_case(prop))
369 }
370 Ok(VcardPropKind::Fn) => {
371 let name = FN::decode(line, version);
372 set_first(&mut display_name, &name.0)
373 }
374 Ok(VcardPropKind::N) => {
375 if !name_seen {
376 name_seen = true;
377 let n = N::decode(line, version);
378 set_first(&mut surname, n.family.join(" "));
379 set_first(&mut given_name, n.given.join(" "));
380 set_first(&mut middle_name, n.additional.join(" "));
381 set_first(&mut title, n.prefixes.join(" "));
382 true
383 } else {
384 false
385 }
386 }
387 Ok(VcardPropKind::Nickname) => {
388 let all = NICKNAME::decode(line, version);
389 match all.0.first() {
390 Some(nick) => set_first(&mut nick_name, nick),
391 None => false,
392 }
393 }
394 Ok(VcardPropKind::Email) => {
395 let email = EMAIL::decode(line, version);
396 let address = email.0.trim();
397 if emails.len() < 3 && !address.is_empty() {
398 emails.push(MsgraphEmailAddress {
399 name: None,
400 address: Some(address.to_string()),
401 });
402 true
403 } else {
404 false
405 }
406 }
407 Ok(VcardPropKind::Impp) => {
408 let impp = IMPP::decode(line, version);
409 let address = impp.0.trim();
410 if ims.len() < 3 && !address.is_empty() {
411 ims.push(address.to_string());
412 true
413 } else {
414 false
415 }
416 }
417 Ok(VcardPropKind::Tel) => {
418 let tel = TEL::decode(line, version);
419 let number = tel.0.trim();
420 if number.is_empty() {
421 false
422 } else {
423 let types = type_values(line);
424 if types.iter().any(|t| t == "cell") {
425 if mobile_phone.is_none() {
429 mobile_phone = Some(number.to_string());
430 true
431 } else {
432 false
433 }
434 } else if types.iter().any(|t| t == "home") {
435 if home_phones.len() < 2 {
436 home_phones.push(number.to_string());
437 true
438 } else {
439 false
440 }
441 } else if business_phones.len() < 2 {
442 business_phones.push(number.to_string());
443 true
444 } else {
445 false
446 }
447 }
448 }
449 Ok(VcardPropKind::Adr) => {
450 let adr = ADR::decode(line, version);
451
452 let street: Vec<&str> = adr
456 .po_box
457 .iter()
458 .chain(&adr.extended)
459 .chain(&adr.street)
460 .map(|component| component.as_ref().trim())
461 .filter(|component| !component.is_empty())
462 .collect();
463
464 let address = MsgraphPhysicalAddress {
465 street: (!street.is_empty()).then(|| street.join("\n")),
466 city: joined(&adr.locality),
467 state: joined(&adr.region),
468 country_or_region: joined(&adr.country),
469 postal_code: joined(&adr.postal_code),
470 };
471
472 let empty = address.street.is_none()
473 && address.city.is_none()
474 && address.state.is_none()
475 && address.country_or_region.is_none()
476 && address.postal_code.is_none();
477 if empty {
478 false
479 } else {
480 let types = type_values(line);
481 let slot = if types.iter().any(|t| t == "home") {
482 &mut home_address
483 } else if types.iter().any(|t| t == "work") {
484 &mut business_address
485 } else {
486 &mut other_address
487 };
488 if slot.is_none() {
489 *slot = Some(address);
490 true
491 } else {
492 false
493 }
494 }
495 }
496 Ok(VcardPropKind::Org) => {
497 if company_name.is_none() && department.is_none() {
498 let org = ORG::decode(line, version);
499 let mut components = org.0.iter().map(|component| component.as_ref());
500 let company = components.next().unwrap_or_default();
501 let rest = components.collect::<Vec<_>>().join(" ");
502 set_first(&mut company_name, company);
503 set_first(&mut department, rest);
504 true
505 } else {
506 false
507 }
508 }
509 Ok(VcardPropKind::Title) => {
510 let value = TITLE::decode(line, version);
511 set_first(&mut job_title, &value.0)
512 }
513 Ok(VcardPropKind::Role) => {
514 let role = ROLE::decode(line, version);
515 set_first(&mut profession, &role.0)
516 }
517 Ok(VcardPropKind::Url) => {
518 let url = URL::decode(line, version);
519 set_first(&mut business_home_page, &url.0)
520 }
521 Ok(VcardPropKind::Bday) => {
522 if birthday.is_none()
525 && let Some(date) =
526 VcardDateAndOrTime::from(line.raw_value_str()).full_date()
527 {
528 birthday = Some(format!("{date}T00:00:00Z"));
529 true
530 } else {
531 false
532 }
533 }
534 Ok(VcardPropKind::Note) => {
535 let note = NOTE::decode(line, version);
536 let note = note.0.trim();
537 if !note.is_empty() {
538 notes.push(note.to_string());
539 true
540 } else {
541 false
542 }
543 }
544 Ok(VcardPropKind::Categories) => {
545 let all = CATEGORIES::decode(line, version);
546 let mut pushed = false;
547 for category in &all.0 {
548 let category = category.trim();
549 if !category.is_empty() {
550 categories.push(category.to_string());
551 pushed = true;
552 }
553 }
554 pushed
555 }
556 Ok(VcardPropKind::Related) => {
557 let text = line.params.iter().any(|param| {
561 param.name.get().eq_ignore_ascii_case("VALUE")
562 && param
563 .values
564 .iter()
565 .any(|value| value.get().eq_ignore_ascii_case("text"))
566 });
567 if text {
568 let related = RELATED::decode(line, version);
569 let types = type_values(line);
570 if types.iter().any(|t| t == "spouse") {
571 set_first(&mut spouse_name, &related.0)
572 } else if types.iter().any(|t| t == "child") {
573 let child = related.0.trim();
574 if !child.is_empty() {
575 children.push(child.to_string());
576 true
577 } else {
578 false
579 }
580 } else {
581 false
582 }
583 } else {
584 false
585 }
586 }
587 Ok(_) => false,
588 };
589
590 if !consumed {
591 let raw = line.to_string();
592 let raw = raw.trim_end().to_string();
593 if raw.len() <= MAX_STASH_LINE {
594 stash.push(raw);
595 }
596 }
597 }
598
599 Ok(MsgraphContact {
600 display_name: MsgraphField::set_or_null(display_name),
601 given_name: MsgraphField::set_or_null(given_name),
602 middle_name: MsgraphField::set_or_null(middle_name),
603 surname: MsgraphField::set_or_null(surname),
604 nick_name: MsgraphField::set_or_null(nick_name),
605 title: MsgraphField::set_or_null(title),
606 email_addresses: MsgraphField::Set(emails),
607 im_addresses: MsgraphField::Set(ims),
608 business_phones: MsgraphField::Set(business_phones),
609 home_phones: MsgraphField::Set(home_phones),
610 mobile_phone: MsgraphField::set_or_null(mobile_phone),
611 home_address: MsgraphField::set_or_null(home_address),
612 business_address: MsgraphField::set_or_null(business_address),
613 other_address: MsgraphField::set_or_null(other_address),
614 job_title: MsgraphField::set_or_null(job_title),
615 company_name: MsgraphField::set_or_null(company_name),
616 department: MsgraphField::set_or_null(department),
617 profession: MsgraphField::set_or_null(profession),
618 business_home_page: MsgraphField::set_or_null(business_home_page),
619 birthday: MsgraphField::set_or_null(birthday),
620 spouse_name: MsgraphField::set_or_null(spouse_name),
621 children: MsgraphField::Set(children),
622 personal_notes: MsgraphField::set_or_null(
623 (!notes.is_empty()).then(|| notes.join("\n")),
624 ),
625 categories: MsgraphField::Set(categories),
626 single_value_extended_properties: MsgraphField::Set(vec![
630 MsgraphSingleValueExtendedProperty {
631 id: MSGRAPH_CONTACT_STASH_ID.to_string(),
632 value: stash.join("\n"),
633 },
634 ]),
635 ..Default::default()
636 })
637 }
638
639 pub fn update_from_vcard(
648 vcard: &str,
649 base_vcard: &str,
650 ) -> Result<Self, MsgraphContactVcardError> {
651 let mut contact = Self::from_vcard(vcard)?;
652 let base = Self::from_vcard(base_vcard)?;
653
654 macro_rules! unset_unchanged {
655 ($($field:ident),* $(,)?) => {$(
656 if contact.$field == base.$field {
657 contact.$field = MsgraphField::Unset;
658 }
659 )*};
660 }
661
662 unset_unchanged!(
663 display_name,
664 given_name,
665 middle_name,
666 surname,
667 nick_name,
668 title,
669 email_addresses,
670 im_addresses,
671 business_phones,
672 home_phones,
673 mobile_phone,
674 home_address,
675 business_address,
676 other_address,
677 job_title,
678 company_name,
679 department,
680 profession,
681 business_home_page,
682 birthday,
683 spouse_name,
684 children,
685 personal_notes,
686 categories,
687 single_value_extended_properties,
688 );
689
690 Ok(contact)
691 }
692
693 pub fn create_from_vcard(vcard: &str) -> Result<Self, MsgraphContactVcardError> {
699 let mut contact = Self::from_vcard(vcard)?;
700
701 if stash_lines(&contact).is_empty() {
703 contact.single_value_extended_properties = MsgraphField::Unset;
704 }
705
706 let mut body = to_value(&contact).map_err(MsgraphContactVcardError::Json)?;
707 if let Some(object) = body.as_object_mut() {
708 object.retain(|_, value| {
709 !value.is_null() && value.as_array().is_none_or(|array| !array.is_empty())
710 });
711 }
712
713 from_value(body).map_err(MsgraphContactVcardError::Json)
714 }
715}
716
717#[derive(Debug)]
719pub enum MsgraphContactVcardError {
720 Parse(VcardParseError),
722 Json(serde_json::Error),
724}
725
726impl fmt::Display for MsgraphContactVcardError {
727 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
728 match self {
729 Self::Parse(err) => write!(f, "Invalid vCard: {err}"),
730 Self::Json(err) => write!(f, "Cannot shape the Graph contact body: {err}"),
731 }
732 }
733}
734
735impl core::error::Error for MsgraphContactVcardError {}
736
737fn display_name(contact: &MsgraphContact) -> String {
740 if let Some(name) = opt(&contact.display_name) {
741 return name.to_string();
742 }
743
744 let composed: Vec<&str> = [&contact.given_name, &contact.middle_name, &contact.surname]
745 .into_iter()
746 .filter_map(opt)
747 .collect();
748 if !composed.is_empty() {
749 return composed.join(" ");
750 }
751
752 opt(&contact.file_as).unwrap_or_default().to_string()
753}
754
755fn adr_prop(
761 address: Option<&MsgraphPhysicalAddress>,
762 r#type: Option<&'static str>,
763) -> Option<VcardProp<'static>> {
764 let address = address?;
765
766 let street = address.street.as_deref().unwrap_or("");
767 let value = VcardAdr {
768 po_box: Vec::new(),
769 extended: Vec::new(),
770 street: street
771 .split('\n')
772 .map(str::trim)
773 .filter(|line| !line.is_empty())
774 .map(|line| Cow::Owned(line.to_string()))
775 .collect(),
776 locality: option_component(&address.city),
777 region: option_component(&address.state),
778 postal_code: option_component(&address.postal_code),
779 country: option_component(&address.country_or_region),
780 ..Default::default()
781 };
782
783 let empty = value.street.is_empty()
784 && value.locality.is_empty()
785 && value.region.is_empty()
786 && value.postal_code.is_empty()
787 && value.country.is_empty();
788 if empty {
789 return None;
790 }
791
792 Some(VcardProp {
793 group: None,
794 name: VcardPropName::Kind(VcardPropKind::Adr),
795 params: r#type.map(type_param).into_iter().collect(),
796 value: VcardValue::Adr(Box::new(value)),
797 })
798}
799
800fn related_prop(r#type: &'static str, name: &str) -> VcardProp<'static> {
803 VcardProp {
804 group: None,
805 name: VcardPropName::Kind(VcardPropKind::Related),
806 params: vec![type_param(r#type), VcardParam::Value(Cow::Borrowed("text"))],
807 value: VcardValue::Text(VcardText(Cow::Owned(name.to_string()))),
808 }
809}
810
811fn type_param(value: &'static str) -> VcardParam<'static> {
813 VcardParam::Type(vec![Cow::Borrowed(value)])
814}
815
816fn opt(field: &MsgraphField<String>) -> Option<&str> {
818 let value = field.as_deref()?.trim();
819 if value.is_empty() { None } else { Some(value) }
820}
821
822fn list<T>(field: &MsgraphField<Vec<T>>) -> &[T] {
824 field.as_option().map(Vec::as_slice).unwrap_or(&[])
825}
826
827fn component(field: &MsgraphField<String>) -> Vec<Cow<'static, str>> {
829 match opt(field) {
830 Some(value) => vec![Cow::Owned(value.to_string())],
831 None => Vec::new(),
832 }
833}
834
835fn option_component(value: &Option<String>) -> Vec<Cow<'static, str>> {
837 let value = value.as_deref().unwrap_or("").trim();
838 if value.is_empty() {
839 Vec::new()
840 } else {
841 vec![Cow::Owned(value.to_string())]
842 }
843}
844
845fn set_first(slot: &mut Option<String>, value: impl AsRef<str>) -> bool {
848 let value = value.as_ref().trim();
849 if slot.is_none() && !value.is_empty() {
850 *slot = Some(value.to_string());
851 true
852 } else {
853 false
854 }
855}
856
857impl MsgraphContact {
858 pub fn stashed_uid(&self) -> Option<String> {
865 stash_lines(self).iter().find_map(|line| {
866 let mut bytes = line.clone();
867 bytes.push_str("\r\n");
868 let (line, _) = VcardLine::take(bytes.as_bytes()).ok()?;
869 line.bare_name()
870 .eq_ignore_ascii_case("UID")
871 .then(|| line.raw_value_str().trim().to_string())
872 .filter(|uid| !uid.is_empty())
873 })
874 }
875}
876
877fn stash_lines(contact: &MsgraphContact) -> Vec<String> {
881 contact
882 .single_value_extended_properties
883 .as_option()
884 .map(Vec::as_slice)
885 .unwrap_or(&[])
886 .iter()
887 .filter(|prop| prop.id.to_ascii_lowercase().contains("cardamum-vcard"))
888 .flat_map(|prop| prop.value.split('\n'))
889 .filter(|line| !line.is_empty())
890 .map(str::to_string)
891 .collect()
892}
893
894fn joined(components: &[Cow<'_, str>]) -> Option<String> {
897 let joined = components
898 .iter()
899 .map(|component| component.as_ref().trim())
900 .filter(|component| !component.is_empty())
901 .collect::<Vec<_>>()
902 .join(" ");
903 if joined.is_empty() {
904 None
905 } else {
906 Some(joined)
907 }
908}
909
910fn type_values(line: &VcardLine) -> Vec<String> {
912 let mut types = Vec::new();
913 for param in &line.params {
914 if param.name.get().eq_ignore_ascii_case("TYPE") {
915 types.extend(
916 param
917 .values
918 .iter()
919 .map(|value| value.get().to_ascii_lowercase()),
920 );
921 }
922 }
923 types
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929
930 fn full_contact() -> MsgraphContact {
932 MsgraphContact {
933 display_name: MsgraphField::Set("Jane Doe".into()),
934 given_name: MsgraphField::Set("Jane".into()),
935 middle_name: MsgraphField::Set("Q.".into()),
936 surname: MsgraphField::Set("Doe".into()),
937 title: MsgraphField::Set("Dr.".into()),
938 nick_name: MsgraphField::Set("Janie".into()),
939 email_addresses: MsgraphField::Set(vec![MsgraphEmailAddress {
940 name: None,
941 address: Some("jane@doe.org".into()),
942 }]),
943 im_addresses: MsgraphField::Set(vec!["xmpp:jane@doe.org".into()]),
944 business_phones: MsgraphField::Set(vec!["+331111".into()]),
945 home_phones: MsgraphField::Set(vec!["+332222".into()]),
946 mobile_phone: MsgraphField::Set("+333333".into()),
947 home_address: MsgraphField::Set(MsgraphPhysicalAddress {
948 street: Some("12 Main St".into()),
949 city: Some("Paris".into()),
950 state: Some("IDF".into()),
951 country_or_region: Some("France".into()),
952 postal_code: Some("75000".into()),
953 }),
954 business_address: MsgraphField::Set(MsgraphPhysicalAddress {
955 street: Some("1 Work Way".into()),
956 city: Some("Lyon".into()),
957 state: None,
958 country_or_region: None,
959 postal_code: None,
960 }),
961 other_address: MsgraphField::Set(MsgraphPhysicalAddress {
962 street: Some("3 Other Rd".into()),
963 city: None,
964 state: None,
965 country_or_region: None,
966 postal_code: None,
967 }),
968 company_name: MsgraphField::Set("ACME".into()),
969 department: MsgraphField::Set("R&D".into()),
970 job_title: MsgraphField::Set("Boss".into()),
971 profession: MsgraphField::Set("Engineer".into()),
972 business_home_page: MsgraphField::Set("https://doe.org".into()),
973 birthday: MsgraphField::Set("1983-04-01T00:00:00Z".into()),
974 spouse_name: MsgraphField::Set("John Doe".into()),
975 children: MsgraphField::Set(vec!["Jimmy".into()]),
976 personal_notes: MsgraphField::Set("a note".into()),
977 categories: MsgraphField::Set(vec!["friends".into(), "band".into()]),
978 ..Default::default()
979 }
980 }
981
982 #[test]
983 fn to_vcard_projects_every_mapped_field() {
984 let mut contact = full_contact();
985 contact.id = "AAMkAGI2".into();
986
987 let vcard = (contact).to_vcard();
988 assert!(vcard.contains("VERSION:4.0\r\n"));
989 assert!(vcard.contains("UID:AAMkAGI2\r\n"));
990 assert!(vcard.contains("FN:Jane Doe\r\n"));
991 assert!(vcard.contains("N:Doe;Jane;Q.;Dr.;\r\n"));
992 assert!(vcard.contains("NICKNAME:Janie\r\n"));
993 assert!(vcard.contains("EMAIL:jane@doe.org\r\n"));
994 assert!(vcard.contains("IMPP:xmpp:jane@doe.org\r\n"));
995 assert!(vcard.contains("TEL;TYPE=work:+331111\r\n"));
996 assert!(vcard.contains("TEL;TYPE=home:+332222\r\n"));
997 assert!(vcard.contains("TEL;TYPE=cell:+333333\r\n"));
998 assert!(vcard.contains("ADR;TYPE=home:;;12 Main St;Paris;IDF;75000;France\r\n"));
999 assert!(vcard.contains("ADR;TYPE=work:;;1 Work Way;Lyon;;;\r\n"));
1000 assert!(vcard.contains("ADR:;;3 Other Rd;;;;\r\n"));
1001 assert!(vcard.contains("ORG:ACME;R&D\r\n"));
1002 assert!(vcard.contains("TITLE:Boss\r\n"));
1003 assert!(vcard.contains("ROLE:Engineer\r\n"));
1004 assert!(vcard.contains("URL:https://doe.org\r\n"));
1005 assert!(vcard.contains("BDAY:1983-04-01\r\n"));
1006 assert!(vcard.contains("NOTE:a note\r\n"));
1007 assert!(vcard.contains("CATEGORIES:friends,band\r\n"));
1008 assert!(vcard.contains("RELATED;TYPE=spouse;VALUE=text:John Doe\r\n"));
1009 assert!(vcard.contains("RELATED;TYPE=child;VALUE=text:Jimmy\r\n"));
1010 }
1011
1012 #[test]
1013 fn to_vcard_composes_fn_from_name_parts() {
1014 let contact = MsgraphContact {
1015 given_name: MsgraphField::Set("Jane".into()),
1016 surname: MsgraphField::Set("Doe".into()),
1017 ..Default::default()
1018 };
1019 assert!((contact).to_vcard().contains("FN:Jane Doe\r\n"));
1020 }
1021
1022 #[test]
1023 fn from_vcard_reads_managed_props() {
1024 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:abc\r\nFN:Jane Doe\r\n\
1025 N:Doe;Jane;Q.;Dr.;\r\nNICKNAME:Janie,JJ\r\nEMAIL:jane@doe.org\r\n\
1026 IMPP:xmpp:jane@doe.org\r\nTEL;TYPE=cell:+333333\r\n\
1027 TEL;TYPE=home,voice:+332222\r\nTEL:+331111\r\n\
1028 ADR;TYPE=home:;;12 Main St;Paris;IDF;75000;France\r\nORG:ACME;R&D\r\n\
1029 TITLE:Boss\r\nROLE:Engineer\r\nURL:https://doe.org\r\nBDAY:1983-04-01\r\n\
1030 NOTE:a note\r\nCATEGORIES:friends,band\r\n\
1031 RELATED;TYPE=spouse;VALUE=text:John Doe\r\n\
1032 RELATED;TYPE=child;VALUE=text:Jimmy\r\nEND:VCARD\r\n";
1033
1034 let contact = MsgraphContact::from_vcard(vcard).unwrap();
1035 assert_eq!(contact.id, "");
1036 assert_eq!(contact.display_name.as_deref(), Some("Jane Doe"));
1037 assert_eq!(contact.surname.as_deref(), Some("Doe"));
1038 assert_eq!(contact.given_name.as_deref(), Some("Jane"));
1039 assert_eq!(contact.middle_name.as_deref(), Some("Q."));
1040 assert_eq!(contact.title.as_deref(), Some("Dr."));
1041 assert_eq!(contact.nick_name.as_deref(), Some("Janie"));
1042 let emails = contact.email_addresses.as_option().unwrap();
1043 assert_eq!(emails[0].address.as_deref(), Some("jane@doe.org"));
1044 assert_eq!(
1045 contact.im_addresses.as_option().unwrap(),
1046 &vec!["xmpp:jane@doe.org".to_string()]
1047 );
1048 assert_eq!(contact.mobile_phone.as_deref(), Some("+333333"));
1049 assert_eq!(
1050 contact.home_phones.as_option().unwrap(),
1051 &vec!["+332222".to_string()]
1052 );
1053 assert_eq!(
1054 contact.business_phones.as_option().unwrap(),
1055 &vec!["+331111".to_string()]
1056 );
1057 let home = contact.home_address.as_option().unwrap();
1058 assert_eq!(home.street.as_deref(), Some("12 Main St"));
1059 assert_eq!(home.city.as_deref(), Some("Paris"));
1060 assert_eq!(home.state.as_deref(), Some("IDF"));
1061 assert_eq!(home.postal_code.as_deref(), Some("75000"));
1062 assert_eq!(home.country_or_region.as_deref(), Some("France"));
1063 assert_eq!(contact.company_name.as_deref(), Some("ACME"));
1064 assert_eq!(contact.department.as_deref(), Some("R&D"));
1065 assert_eq!(contact.job_title.as_deref(), Some("Boss"));
1066 assert_eq!(contact.profession.as_deref(), Some("Engineer"));
1067 assert_eq!(
1068 contact.business_home_page.as_deref(),
1069 Some("https://doe.org")
1070 );
1071 assert_eq!(contact.birthday.as_deref(), Some("1983-04-01T00:00:00Z"));
1072 assert_eq!(contact.spouse_name.as_deref(), Some("John Doe"));
1073 assert_eq!(
1074 contact.children.as_option().unwrap(),
1075 &vec!["Jimmy".to_string()]
1076 );
1077 assert_eq!(contact.personal_notes.as_deref(), Some("a note"));
1078 assert_eq!(
1079 contact.categories.as_option().unwrap(),
1080 &vec!["friends".to_string(), "band".to_string()]
1081 );
1082 }
1083
1084 #[test]
1085 fn from_vcard_clears_absent_managed_fields_and_keeps_unmanaged_unset() {
1086 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nEND:VCARD\r\n";
1087 let contact = MsgraphContact::from_vcard(vcard).unwrap();
1088
1089 assert_eq!(contact.spouse_name, MsgraphField::Null);
1090 assert_eq!(contact.mobile_phone, MsgraphField::Null);
1091 assert_eq!(contact.home_address, MsgraphField::Null);
1092 assert_eq!(contact.business_phones, MsgraphField::Set(vec![]));
1093 assert_eq!(contact.email_addresses, MsgraphField::Set(vec![]));
1094
1095 assert_eq!(contact.file_as, MsgraphField::Unset);
1096 assert_eq!(contact.assistant_name, MsgraphField::Unset);
1097 assert_eq!(contact.manager, MsgraphField::Unset);
1098 assert_eq!(contact.office_location, MsgraphField::Unset);
1099 }
1100
1101 #[test]
1102 fn update_from_vcard_keeps_only_changed_fields() {
1103 let base = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Jane Doe\r\n\
1104 TEL;TYPE=cell:+333333\r\nEMAIL:jane@doe.org\r\n\
1105 NOTE:a note\r\nEND:VCARD\r\n";
1106 let edited = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Jane Doe\r\n\
1107 TEL;TYPE=cell:+444444\r\nEMAIL:jane@doe.org\r\nEND:VCARD\r\n";
1108
1109 let contact = MsgraphContact::update_from_vcard(edited, base).unwrap();
1110
1111 assert_eq!(contact.mobile_phone.as_deref(), Some("+444444"));
1112 assert_eq!(contact.personal_notes, MsgraphField::Null);
1113
1114 assert_eq!(contact.display_name, MsgraphField::Unset);
1117 assert_eq!(contact.email_addresses, MsgraphField::Unset);
1118 assert_eq!(contact.birthday, MsgraphField::Unset);
1119 assert_eq!(contact.home_address, MsgraphField::Unset);
1120 assert_eq!(contact.categories, MsgraphField::Unset);
1121 }
1122
1123 #[test]
1124 fn update_from_vcard_clears_collections_with_empty_sets() {
1125 let base = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\n\
1126 TEL;TYPE=home:+332222\r\nCATEGORIES:friends\r\nEND:VCARD\r\n";
1127 let edited = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nEND:VCARD\r\n";
1128
1129 let contact = MsgraphContact::update_from_vcard(edited, base).unwrap();
1130 assert_eq!(contact.home_phones, MsgraphField::Set(vec![]));
1131 assert_eq!(contact.categories, MsgraphField::Set(vec![]));
1132 }
1133
1134 #[test]
1135 fn update_from_vcard_of_identical_cards_is_empty() {
1136 let vcard = (full_contact()).to_vcard();
1137 let contact = MsgraphContact::update_from_vcard(&vcard, &vcard).unwrap();
1138 assert_eq!(contact, MsgraphContact::default());
1139 }
1140
1141 #[test]
1142 fn create_from_vcard_drops_nulls_and_empty_collections() {
1143 let vcard = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:X\r\nTEL;TYPE=CELL:+331111\r\nEND:VCARD\r\n";
1144 let contact = MsgraphContact::create_from_vcard(vcard).unwrap();
1145
1146 assert_eq!(contact.display_name.as_deref(), Some("X"));
1147 assert_eq!(contact.mobile_phone.as_deref(), Some("+331111"));
1148 assert_eq!(contact.spouse_name, MsgraphField::Unset);
1149 assert_eq!(contact.home_address, MsgraphField::Unset);
1150 assert_eq!(contact.business_phones, MsgraphField::Unset);
1151 assert_eq!(contact.email_addresses, MsgraphField::Unset);
1152 }
1153
1154 #[test]
1155 fn from_vcard_respects_graph_slots() {
1156 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\n\
1157 TEL;TYPE=cell:+331111\r\nTEL;TYPE=cell:+332222\r\n\
1158 TEL;TYPE=work:+333331\r\nTEL;TYPE=work:+333332\r\nTEL:+333333\r\n\
1159 TEL;TYPE=home:+334441\r\nTEL;TYPE=home:+334442\r\nTEL;TYPE=home:+334443\r\n\
1160 ADR;TYPE=home:;;First St;;;;\r\nADR;TYPE=home:;;Second St;;;;\r\n\
1161 EMAIL:a@x.org\r\nEMAIL:b@x.org\r\nEMAIL:c@x.org\r\nEMAIL:d@x.org\r\n\
1162 IMPP:xmpp:a@x.org\r\nIMPP:xmpp:b@x.org\r\nIMPP:xmpp:c@x.org\r\nIMPP:xmpp:d@x.org\r\n\
1163 RELATED;TYPE=spouse:urn:uuid:03a0e51f\r\nEND:VCARD\r\n";
1164
1165 let contact = MsgraphContact::from_vcard(vcard).unwrap();
1166
1167 assert_eq!(contact.mobile_phone.as_deref(), Some("+331111"));
1170
1171 assert_eq!(
1172 contact.business_phones.as_option().unwrap(),
1173 &vec!["+333331".to_string(), "+333332".to_string()]
1174 );
1175 assert_eq!(
1176 contact.home_phones.as_option().unwrap(),
1177 &vec!["+334441".to_string(), "+334442".to_string()]
1178 );
1179 assert_eq!(contact.email_addresses.as_option().unwrap().len(), 3);
1180 assert_eq!(contact.im_addresses.as_option().unwrap().len(), 3);
1181
1182 let home = contact.home_address.as_option().unwrap();
1183 assert_eq!(home.street.as_deref(), Some("First St"));
1184 assert_eq!(contact.spouse_name, MsgraphField::Null);
1185 }
1186
1187 #[test]
1188 fn round_trip() {
1189 let contact = full_contact();
1190
1191 let mut expected = contact.clone();
1195 expected.single_value_extended_properties =
1196 MsgraphField::Set(vec![MsgraphSingleValueExtendedProperty {
1197 id: MSGRAPH_CONTACT_STASH_ID.to_string(),
1198 value: String::new(),
1199 }]);
1200
1201 assert_eq!(
1202 MsgraphContact::from_vcard(&contact.to_vcard()).unwrap(),
1203 expected
1204 );
1205 }
1206
1207 #[test]
1208 fn stash_preserves_unprojected_props() {
1209 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nX-FOO;TYPE=bar:baz\r\n\
1210 EMAIL:a@x.org\r\nEMAIL:b@x.org\r\nEMAIL:c@x.org\r\nEMAIL:d@x.org\r\nEND:VCARD\r\n";
1211
1212 let contact = MsgraphContact::from_vcard(vcard).unwrap();
1213 let props = contact
1214 .single_value_extended_properties
1215 .as_option()
1216 .unwrap();
1217 assert_eq!(props[0].id, MSGRAPH_CONTACT_STASH_ID);
1218 assert_eq!(props[0].value, "X-FOO;TYPE=bar:baz\nEMAIL:d@x.org");
1219
1220 let restored = (contact).to_vcard();
1221 assert!(restored.contains("X-FOO;TYPE=bar:baz\r\n"));
1222 assert!(restored.contains("EMAIL:d@x.org\r\n"));
1223 assert!(restored.ends_with("END:VCARD\r\n"));
1224 }
1225
1226 #[test]
1227 fn minted_props_project_and_consume() {
1228 let mut contact = full_contact();
1229 contact.file_as = MsgraphField::Set("Doe, Jane".into());
1230 contact.office_location = MsgraphField::Set("B2".into());
1231 contact.assistant_name = MsgraphField::Set("Sam".into());
1232 contact.manager = MsgraphField::Set("Alex".into());
1233
1234 let vcard = (contact).to_vcard();
1235 assert!(vcard.contains("X-MSGRAPH-FILE-AS:Doe\\, Jane\r\n"));
1236 assert!(vcard.contains("X-MSGRAPH-OFFICE-LOCATION:B2\r\n"));
1237 assert!(vcard.contains("X-MSGRAPH-ASSISTANT:Sam\r\n"));
1238 assert!(vcard.contains("X-MSGRAPH-MANAGER:Alex\r\n"));
1239
1240 let back = MsgraphContact::from_vcard(&vcard).unwrap();
1243 assert_eq!(back.file_as, MsgraphField::Unset);
1244 assert_eq!(back.office_location, MsgraphField::Unset);
1245 assert_eq!(back.assistant_name, MsgraphField::Unset);
1246 assert_eq!(back.manager, MsgraphField::Unset);
1247 let props = back.single_value_extended_properties.as_option().unwrap();
1248 assert_eq!(props[0].value, "");
1249 }
1250
1251 #[test]
1252 fn update_from_vcard_tracks_the_stash() {
1253 let base = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nX-FOO:bar\r\nEND:VCARD\r\n";
1254 let unchanged = MsgraphContact::update_from_vcard(base, base).unwrap();
1255 assert_eq!(
1256 unchanged.single_value_extended_properties,
1257 MsgraphField::Unset
1258 );
1259
1260 let cleared = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nEND:VCARD\r\n";
1261 let delta = MsgraphContact::update_from_vcard(cleared, base).unwrap();
1262 let props = delta.single_value_extended_properties.as_option().unwrap();
1263 assert_eq!(props[0].value, "");
1264 }
1265
1266 #[test]
1267 fn create_from_vcard_drops_the_empty_stash() {
1268 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nEND:VCARD\r\n";
1269 let contact = MsgraphContact::create_from_vcard(vcard).unwrap();
1270 assert_eq!(
1271 contact.single_value_extended_properties,
1272 MsgraphField::Unset
1273 );
1274
1275 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:X\r\nX-FOO:bar\r\nEND:VCARD\r\n";
1276 let contact = MsgraphContact::create_from_vcard(vcard).unwrap();
1277 let props = contact
1278 .single_value_extended_properties
1279 .as_option()
1280 .unwrap();
1281 assert_eq!(props[0].value, "X-FOO:bar");
1282 }
1283
1284 #[test]
1285 fn the_expand_clause_names_the_stash() {
1286 let quoted = format!("'{MSGRAPH_CONTACT_STASH_ID}'");
1287 assert!(MSGRAPH_CONTACT_STASH_EXPAND.contains("ed));
1288 }
1289
1290 #[test]
1291 fn a_vcard_uid_rides_the_stash_and_wins_over_the_graph_id() {
1292 let vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:urn:uuid:4fbe8971\r\n\
1293 FN:Jane Doe\r\nEND:VCARD\r\n";
1294
1295 let mut contact = MsgraphContact::create_from_vcard(vcard).unwrap();
1296 contact.id = "AAMkAGI2".into();
1299
1300 assert_eq!(contact.stashed_uid().as_deref(), Some("urn:uuid:4fbe8971"));
1301 let vcard = contact.to_vcard();
1302 assert!(vcard.contains("UID:urn:uuid:4fbe8971\r\n"));
1303 assert!(!vcard.contains("UID:AAMkAGI2"));
1304 }
1305
1306 #[test]
1307 fn a_contact_graph_created_mints_its_uid_from_the_graph_id() {
1308 let contact = MsgraphContact {
1309 id: "AAMkAGI2".into(),
1310 display_name: MsgraphField::Set("Jane Doe".into()),
1311 ..Default::default()
1312 };
1313
1314 assert_eq!(contact.stashed_uid(), None);
1315 assert!(contact.to_vcard().contains("UID:AAMkAGI2\r\n"));
1316 }
1317}