contact.rs
โއ
Raw
1//! Photos and birthday events derived from contacts.
2
3use chrono::{DateTime, Utc};
4use pimdav::calcard::icalendar::ICalendar;
5use pimdav::contact;
6use pimdav::expand::expand;
7use pimdav::zone::Zone;
8
9fn card(lines: &str) -> String {
10 format!(
11 "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:c1\r\nFN:Anna Berg\r\n{}END:VCARD\r\n",
12 lines.replace('\n', "\r\n")
13 )
14}
15
16fn utc(s: &str) -> DateTime<Utc> {
17 format!("{s}T00:00:00Z").parse().unwrap()
18}
19
20/// The start dates of an event's instances in a range.
21fn days(ics: &str, from: &str, to: &str) -> Vec<String> {
22 let cal = ICalendar::parse(ics).unwrap();
23 expand(&cal, utc(from)..utc(to), Zone::Utc)
24 .instances
25 .iter()
26 .map(|i| i.start.format("%Y-%m-%d").to_string())
27 .collect()
28}
29
30#[test]
31fn photo_forms() {
32 let v3 = card("PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQ\n");
33 assert_eq!(
34 contact::photo(&v3),
35 Some(vec![0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10])
36 );
37 let v4 = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c\r\nFN:X\r\nPHOTO:data:image/png;base64,iVBORw0K\r\nEND:VCARD\r\n";
38 assert_eq!(contact::photo(v4).unwrap()[..4], [0x89, b'P', b'N', b'G']);
39 assert_eq!(
40 contact::photo(&card("PHOTO;VALUE=uri:http://example.com/a.jpg\n")),
41 None
42 );
43 assert_eq!(contact::photo(&card("")), None);
44}
45
46#[test]
47fn birthday_forms() {
48 let full = contact::dates(&card("BDAY:1980-03-15\n"), "k");
49 assert_eq!(full.len(), 1);
50 let (uid, ics) = &full[0];
51 assert!(ics.contains("SUMMARY:๐ŸŽ‚ Anna Berg (1980)\r\n"));
52 assert!(ics.contains("TRANSP:TRANSPARENT\r\n"));
53 assert_eq!(
54 days(ics, "2025-01-01", "2027-01-01"),
55 ["2025-03-15", "2026-03-15"]
56 );
57
58 // Stable per key and kind, different for another key.
59 assert_eq!(contact::dates(&card("BDAY:19800315\n"), "k")[0].0, *uid);
60 assert_ne!(contact::dates(&card("BDAY:19800315\n"), "other")[0].0, *uid);
61
62 for yearless in [
63 "BDAY:--0315\n",
64 "BDAY:--03-15\n",
65 "BDAY;X-APPLE-OMIT-YEAR=1604:1604-03-15\n",
66 ] {
67 let (_, ics) = &contact::dates(&card(yearless), "k")[0];
68 assert!(ics.contains("SUMMARY:๐ŸŽ‚ Anna Berg\r\n"), "{yearless}");
69 assert_eq!(
70 days(ics, "2025-01-01", "2026-01-01"),
71 ["2025-03-15"],
72 "{yearless}"
73 );
74 }
75 assert!(contact::dates(&card("BDAY;VALUE=text:circa 1800\n"), "k").is_empty());
76 assert!(contact::dates(&card("BDAY:1981-02-29\n"), "k").is_empty());
77}
78
79#[test]
80fn leap_day_birthday() {
81 let (_, ics) = &contact::dates(&card("BDAY:1980-02-29\n"), "k")[0];
82 assert_eq!(
83 days(ics, "2027-01-01", "2029-01-01"),
84 ["2027-02-28", "2028-02-29"]
85 );
86}
87
88#[test]
89fn anniversaries() {
90 let v4 = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c\r\nFN:Anna\r\nBDAY:--0315\r\nANNIVERSARY:20010601\r\nEND:VCARD\r\n";
91 let both = contact::dates(v4, "k");
92 assert_eq!(both.len(), 2);
93 assert!(both[1].1.contains("SUMMARY:๐Ÿ’ Anna (2001)\r\n"));
94 assert_ne!(both[0].0, both[1].0);
95
96 let evolution = contact::dates(&card("X-ANNIVERSARY:2001-06-01\n"), "k");
97 assert!(evolution[0].1.contains("๐Ÿ’"));
98
99 let apple = card(
100 "item1.X-ABDATE:2001-06-01\nitem1.X-ABLabel:_$!<Anniversary>!$_\nitem2.X-ABDATE:2010-01-01\nitem2.X-ABLabel:Other\n",
101 );
102 let dates = contact::dates(&apple, "k");
103 assert_eq!(dates.len(), 1);
104 assert!(dates[0].1.contains("๐Ÿ’ Anna Berg (2001)"));
105}
106
107#[test]
108fn non_ascii_dates_are_ignored() {
109 // Byte offsets on these would split a character.
110 for bday in ["BDAY:1980ๅนด3\n", "BDAY:--0ๅนด\n", "BDAY:รฉ1234567\n"] {
111 assert!(contact::dates(&card(bday), "k").is_empty(), "{bday}");
112 assert_eq!(pimdav::view::card(&card(bday)).birthday, None, "{bday}");
113 }
114 // A line without a value that ends in a multi-byte character.
115 let odd = card("X-NOTE;รฉ\nBDAY:1980-03-15\n");
116 let v4 = pimdav::render::AddressData {
117 props: None,
118 version: Some(pimdav::calcard::vcard::VCardVersion::V4_0),
119 };
120 assert!(pimdav::render::address_data(&odd, &v4).contains("BEGIN:VCARD"));
121}
122