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