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 "BDAY:1604-03-15\n",
67 "BDAY:0000-03-15\n",
68 ] {
69 let (_, ics) = &contact::dates(&card(yearless), "k")[0];
70 assert!(ics.contains("SUMMARY:๐ŸŽ‚ Anna Berg\r\n"), "{yearless}");
71 assert_eq!(
72 days(ics, "2025-01-01", "2026-01-01"),
73 ["2025-03-15"],
74 "{yearless}"
75 );
76 }
77 assert!(contact::dates(&card("BDAY;VALUE=text:circa 1800\n"), "k").is_empty());
78 let (_, ics) = &contact::dates(&card("BDAY:1900-02-29\n"), "k")[0];
79 assert!(ics.contains("SUMMARY:๐ŸŽ‚ Anna Berg\r\n"));
80 assert!(ics.contains("DTSTART;VALUE=DATE:19720229"));
81}
82
83#[test]
84fn leap_day_birthday() {
85 let (_, ics) = &contact::dates(&card("BDAY:1980-02-29\n"), "k")[0];
86 assert_eq!(
87 days(ics, "2027-01-01", "2029-01-01"),
88 ["2027-02-28", "2028-02-29"]
89 );
90}
91
92#[test]
93fn anniversaries() {
94 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";
95 let both = contact::dates(v4, "k");
96 assert_eq!(both.len(), 2);
97 assert!(both[1].1.contains("SUMMARY:๐Ÿ’ Anna (2001)\r\n"));
98 assert_ne!(both[0].0, both[1].0);
99
100 let evolution = contact::dates(&card("X-ANNIVERSARY:2001-06-01\n"), "k");
101 assert!(evolution[0].1.contains("๐Ÿ’"));
102
103 let apple = card(
104 "item1.X-ABDATE:2001-06-01\nitem1.X-ABLabel:_$!<Anniversary>!$_\nitem2.X-ABDATE:2010-01-01\nitem2.X-ABLabel:Other\n",
105 );
106 let dates = contact::dates(&apple, "k");
107 assert_eq!(dates.len(), 1);
108 assert!(dates[0].1.contains("๐Ÿ’ Anna Berg (2001)"));
109}
110
111#[test]
112fn non_ascii_dates_are_ignored() {
113 // Byte offsets on these would split a character.
114 for bday in ["BDAY:1980ๅนด3\n", "BDAY:--0ๅนด\n", "BDAY:รฉ1234567\n"] {
115 assert!(contact::dates(&card(bday), "k").is_empty(), "{bday}");
116 assert_eq!(pimdav::view::card(&card(bday)).birthday, None, "{bday}");
117 }
118 // A line without a value that ends in a multi-byte character.
119 let odd = card("X-NOTE;รฉ\nBDAY:1980-03-15\n");
120 let v4 = pimdav::render::AddressData {
121 props: None,
122 version: Some(pimdav::calcard::vcard::VCardVersion::V4_0),
123 };
124 assert!(pimdav::render::address_data(&odd, &v4).contains("BEGIN:VCARD"));
125}
126
127#[test]
128fn many_apple_dates_stay_linear() {
129 let mut lines = String::new();
130 for n in 0..20_000 {
131 lines.push_str(&format!("g{n}.X-ABDATE:2000-01-01\n"));
132 }
133 lines.push_str("g7.X-ABLABEL:_$!<Anniversary>!$_\n");
134 let started = std::time::Instant::now();
135 let dates = contact::dates(&card(&lines), "k");
136 assert!(
137 started.elapsed().as_secs_f64() < 2.0,
138 "{:?}",
139 started.elapsed()
140 );
141 assert_eq!(dates.len(), 1);
142}
143
144#[test]
145fn the_card_view_reads_apple_dates() {
146 let apple = card(
147 "BDAY;X-APPLE-OMIT-YEAR=1604:1604-03-15\nitem1.X-ABDATE:2001-06-01\nitem1.X-ABLabel:_$!<Anniversary>!$_\n",
148 );
149 let view = pimdav::view::card(&apple);
150 assert_eq!(view.birthday.as_deref(), Some("--03-15"));
151 assert_eq!(view.anniversary.as_deref(), Some("2001-06-01"));
152 let plain = pimdav::view::card(&card("BDAY:19800315\n"));
153 assert_eq!(plain.birthday.as_deref(), Some("1980-03-15"));
154 let android = pimdav::view::card(&card("BDAY:0000-03-15\n"));
155 assert_eq!(android.birthday.as_deref(), Some("--03-15"));
156}
157
158#[test]
159fn many_labelled_emails_stay_linear() {
160 let mut lines = String::new();
161 for n in 0..5000 {
162 lines.push_str(&format!("item{n}.EMAIL:u{n}@x\nitem{n}.X-ABLabel:work\n"));
163 }
164 let started = std::time::Instant::now();
165 let view = pimdav::view::card(&card(&lines));
166 assert!(
167 started.elapsed().as_secs_f64() < 2.0,
168 "{:?}",
169 started.elapsed()
170 );
171 assert_eq!(view.emails.len(), 5000);
172 assert_eq!(view.emails[4999].label.as_deref(), Some("work"));
173}
174
175#[test]
176fn vcard_2_1_bare_types_label_values() {
177 let view = pimdav::view::card(&card(
178 "TEL;CELL:+49 1\nEMAIL;HOME;INTERNET:a@x\nTEL;WORK;QUOTED-PRINTABLE:+49 2\n\
179 EMAIL;OTHER;CHARSET=UTF-8;8BIT:b@x\n",
180 ));
181 assert_eq!(view.phones[0].label.as_deref(), Some("cell"));
182 assert_eq!(view.emails[0].label.as_deref(), Some("home"));
183 assert_eq!(view.phones[1].label.as_deref(), Some("work"));
184 assert_eq!(view.emails[1].label.as_deref(), Some("other"));
185}
186