render.rs
⎇
Raw
1//! The `calendar-data` and `address-data` a REPORT returns: the whole
2//! object, a part of it, or its recurrence set expanded (RFC 4791, 9.6;
3//! RFC 6352, 10.4).
4
5use std::collections::HashSet;
6
7use calcard::common::PartialDateTime;
8use calcard::icalendar::{
9 ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarParameter,
10 ICalendarParameterName, ICalendarParameterValue, ICalendarProperty, ICalendarValue,
11 ICalendarValueType,
12};
13use calcard::vcard::{VCard, VCardVersion};
14use chrono::{DateTime, Utc};
15use xmltree::Element;
16
17use crate::expand::expand;
18use crate::filter::{TimeRange, time_range};
19use crate::freebusy::period;
20use crate::report::Refused;
21use crate::xml::{CALDAV, CARDDAV, Name, elements};
22use crate::zone::{Zone, Zones};
23
24/// Instances one object may expand into in one response.
25const MAX_EXPANDED: usize = 10_000;
26
27#[derive(Debug, Clone, Default, PartialEq)]
28pub struct CalendarData {
29 /// `None` returns every component and property.
30 pub comp: Option<CompSelect>,
31 pub expand: Option<TimeRange>,
32 pub limit_recurrence: Option<TimeRange>,
33 pub limit_freebusy: Option<TimeRange>,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct CompSelect {
38 /// Upper case.
39 pub name: String,
40 /// `None` for `allprop`.
41 pub props: Option<Vec<PropSelect>>,
42 /// `None` for `allcomp`.
43 pub comps: Option<Vec<CompSelect>>,
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct PropSelect {
48 pub name: String,
49 pub novalue: bool,
50}
51
52#[derive(Debug, Clone, Default, PartialEq)]
53pub struct AddressData {
54 /// `None` for `allprop`.
55 pub props: Option<Vec<PropSelect>>,
56 pub version: Option<VCardVersion>,
57}
58
59/// The expansion would exceed [`MAX_EXPANDED`] instances.
60#[derive(Debug, PartialEq, Eq)]
61pub struct TooManyInstances;
62
63pub fn calendar_request(e: &Element) -> Result<CalendarData, Refused> {
64 let content_type = e.attributes.get("content-type").map(String::as_str);
65 let version = e.attributes.get("version").map(String::as_str);
66 if content_type.is_some_and(|t| t != "text/calendar") || version.is_some_and(|v| v != "2.0") {
67 return Err(Refused::Condition(Name::new(
68 CALDAV,
69 "supported-calendar-data",
70 )));
71 }
72 let mut d = CalendarData::default();
73 for c in elements(e).filter(|c| Name::of(c).ns == CALDAV) {
74 match c.name.as_str() {
75 "comp" => d.comp = Some(comp_select(c)?),
76 "expand" => d.expand = Some(time_range(c)?),
77 "limit-recurrence-set" => d.limit_recurrence = Some(time_range(c)?),
78 "limit-freebusy-set" => d.limit_freebusy = Some(time_range(c)?),
79 _ => {}
80 }
81 }
82 Ok(d)
83}
84
85fn comp_select(e: &Element) -> Result<CompSelect, Refused> {
86 let name = e.attributes.get("name").ok_or(Refused::Invalid)?;
87 let has = |local: &str| elements(e).any(|c| Name::of(c).is(CALDAV, local));
88 let props = (!has("allprop")).then(|| prop_selects(e, CALDAV));
89 let comps = match has("allcomp") {
90 true => None,
91 false => Some(
92 elements(e)
93 .filter(|c| Name::of(c).is(CALDAV, "comp"))
94 .map(comp_select)
95 .collect::<Result<_, _>>()?,
96 ),
97 };
98 Ok(CompSelect {
99 name: name.to_ascii_uppercase(),
100 props,
101 comps,
102 })
103}
104
105fn prop_selects(e: &Element, ns: &str) -> Vec<PropSelect> {
106 elements(e)
107 .filter(|c| Name::of(c).is(ns, "prop"))
108 .filter_map(|c| {
109 Some(PropSelect {
110 name: c.attributes.get("name")?.clone(),
111 novalue: c.attributes.get("novalue").map(String::as_str) == Some("yes"),
112 })
113 })
114 .collect()
115}
116
117pub fn address_request(e: &Element) -> Result<AddressData, Refused> {
118 let unsupported = || Refused::Condition(Name::new(CARDDAV, "supported-address-data"));
119 if e.attributes
120 .get("content-type")
121 .is_some_and(|t| t != "text/vcard")
122 {
123 return Err(unsupported());
124 }
125 // RFC 6352, 10.4: without a version, 3.0.
126 let version = match e.attributes.get("version").map(String::as_str) {
127 None | Some("3.0") => Some(VCardVersion::V3_0),
128 Some("4.0") => Some(VCardVersion::V4_0),
129 Some(_) => return Err(unsupported()),
130 };
131 let all = elements(e).any(|c| Name::of(c).is(CARDDAV, "allprop"));
132 let props = prop_selects(e, CARDDAV);
133 Ok(AddressData {
134 props: (!all && !props.is_empty()).then_some(props),
135 version,
136 })
137}
138
139/// The calendar-data of one object, and how many instances an `expand`
140/// produced. The stored text is returned unchanged unless the request
141/// narrows or expands it.
142pub fn calendar_data(
143 raw: &str,
144 req: &CalendarData,
145 floating: &Zone,
146) -> Result<(String, usize), TooManyInstances> {
147 if *req == CalendarData::default() {
148 return Ok((raw.to_string(), 0));
149 }
150 let Ok(mut cal) = ICalendar::parse(raw) else {
151 return Ok((raw.to_string(), 0));
152 };
153 let mut instances = 0;
154 if let Some(r) = &req.expand {
155 (cal, instances) = expanded(&cal, r, floating)?;
156 }
157 if let Some(r) = &req.limit_recurrence {
158 cal = limit_recurrence(&cal, r, floating);
159 }
160 if let Some(r) = &req.limit_freebusy {
161 limit_freebusy(&mut cal, r, floating);
162 }
163 if let Some(sel) = &req.comp {
164 let mut out = Vec::new();
165 copy(&cal, 0, Some(sel), &mut out);
166 cal = ICalendar { components: out };
167 }
168 Ok((cal.to_string(), instances))
169}
170
171/// One component per instance in `range`, in UTC, with a RECURRENCE-ID and
172/// without recurrence rules (RFC 4791, 9.6.5). Dates stay dates.
173fn expanded(
174 cal: &ICalendar,
175 range: &TimeRange,
176 floating: &Zone,
177) -> Result<(ICalendar, usize), TooManyInstances> {
178 let exp = expand(cal, range.clone(), floating.clone());
179 let count = exp.instances.len();
180 if exp.truncated || count > MAX_EXPANDED {
181 return Err(TooManyInstances);
182 }
183 let mut out = vec![root(cal)];
184 for x in exp.instances {
185 let src = &cal.components[x.component];
186 let date = src
187 .property(&ICalendarProperty::Dtstart)
188 .and_then(|e| e.values.first()?.as_partial_date_time())
189 .is_some_and(|p| p.hour.is_none());
190 let at = copy(cal, x.component, None, &mut out);
191 let c = &mut out[at as usize];
192 c.entries.retain(|e| {
193 !matches!(
194 e.name,
195 ICalendarProperty::Rrule
196 | ICalendarProperty::Rdate
197 | ICalendarProperty::Exdate
198 | ICalendarProperty::Exrule
199 | ICalendarProperty::Dtstart
200 | ICalendarProperty::Dtend
201 | ICalendarProperty::Due
202 | ICalendarProperty::RecurrenceId
203 )
204 });
205 let when = |name: ICalendarProperty, t: DateTime<Utc>| {
206 let (value, params) = if date {
207 let day = floating.to_local(t).and_utc().timestamp();
208 let param = ICalendarParameter::new(
209 ICalendarParameterName::Value,
210 ICalendarParameterValue::Value(ICalendarValueType::Date),
211 );
212 (PartialDateTime::from_date_timestamp(day), vec![param])
213 } else {
214 (PartialDateTime::from_utc_timestamp(t.timestamp()), vec![])
215 };
216 ICalendarEntry {
217 name,
218 params,
219 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
220 }
221 };
222 c.entries.push(when(ICalendarProperty::Dtstart, x.start));
223 if src.has_property(&ICalendarProperty::Dtend) {
224 c.entries.push(when(ICalendarProperty::Dtend, x.end));
225 }
226 if src.has_property(&ICalendarProperty::Due) {
227 c.entries.push(when(ICalendarProperty::Due, x.end));
228 }
229 if let Some(rid) = x.recurrence_id {
230 c.entries.push(when(ICalendarProperty::RecurrenceId, rid));
231 }
232 out[0].component_ids.push(at);
233 }
234 Ok((ICalendar { components: out }, count))
235}
236
237/// The masters, and only the overrides that affect `range` (RFC 4791,
238/// 9.6.6).
239fn limit_recurrence(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> ICalendar {
240 let exp = expand(cal, range.clone(), floating.clone());
241 let used: HashSet<usize> = exp.instances.iter().map(|x| x.component).collect();
242 let mut out = vec![root(cal)];
243 for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) {
244 let c = &cal.components[i as usize];
245 let keep = match c.property(&ICalendarProperty::RecurrenceId) {
246 None => true,
247 Some(rid) => {
248 exp.truncated
249 || used.contains(&(i as usize))
250 || rid.parameter(&ICalendarParameterName::Range).is_some()
251 }
252 };
253 if keep {
254 let at = copy(cal, i as usize, None, &mut out);
255 out[0].component_ids.push(at);
256 }
257 }
258 ICalendar { components: out }
259}
260
261/// Drops the FREEBUSY periods outside `range` (RFC 4791, 9.6.7).
262fn limit_freebusy(cal: &mut ICalendar, range: &TimeRange, floating: &Zone) {
263 let zones = Zones::new(cal, floating.clone());
264 for c in cal
265 .components
266 .iter_mut()
267 .filter(|c| c.component_type == ICalendarComponentType::VFreebusy)
268 {
269 for e in c
270 .entries
271 .iter_mut()
272 .filter(|e| e.name == ICalendarProperty::Freebusy)
273 {
274 let tzid = e.tz_id().map(str::to_string);
275 e.values.retain(|v| {
276 period(&zones, tzid.as_deref(), v)
277 .is_some_and(|(s, e)| s < range.end && e > range.start)
278 });
279 }
280 c.entries
281 .retain(|e| e.name != ICalendarProperty::Freebusy || !e.values.is_empty());
282 }
283}
284
285/// The VCALENDAR with its properties and no components.
286fn root(cal: &ICalendar) -> ICalendarComponent {
287 ICalendarComponent {
288 component_type: ICalendarComponentType::VCalendar,
289 entries: cal
290 .components
291 .first()
292 .map(|c| c.entries.clone())
293 .unwrap_or_default(),
294 component_ids: Vec::new(),
295 }
296}
297
298/// Appends component `i` of `src` and its sub-components, narrowed by `sel`,
299/// to `out`. Returns the new index.
300fn copy(
301 src: &ICalendar,
302 i: usize,
303 sel: Option<&CompSelect>,
304 out: &mut Vec<ICalendarComponent>,
305) -> u32 {
306 let c = &src.components[i];
307 let entries = match sel.and_then(|s| s.props.as_ref()) {
308 None => c.entries.clone(),
309 Some(props) => c
310 .entries
311 .iter()
312 .filter_map(|e| {
313 let p = props
314 .iter()
315 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
316 let mut e = e.clone();
317 if p.novalue {
318 e.values.clear();
319 }
320 Some(e)
321 })
322 .collect(),
323 };
324 let at = out.len();
325 out.push(ICalendarComponent {
326 component_type: c.component_type.clone(),
327 entries,
328 component_ids: Vec::new(),
329 });
330 for &child in &c.component_ids {
331 let child = child as usize;
332 let child_sel = match sel.map(|s| &s.comps) {
333 None | Some(None) => None,
334 Some(Some(list)) => {
335 let name = src.components[child].component_type.as_str();
336 match list.iter().find(|s| s.name.eq_ignore_ascii_case(name)) {
337 Some(s) => Some(s),
338 None => continue,
339 }
340 }
341 };
342 let id = copy(src, child, child_sel, out);
343 out[at].component_ids.push(id);
344 }
345 at as u32
346}
347
348/// The address-data of one card. The stored text is returned unchanged
349/// unless the request narrows it or asks for another version.
350pub fn address_data(raw: &str, req: &AddressData) -> String {
351 if *req == AddressData::default() {
352 return raw.to_string();
353 }
354 let Ok(card) = VCard::parse(raw) else {
355 return raw.to_string();
356 };
357 let version = req.version.or(card.version()).unwrap_or_default();
358 if req.props.is_none() && Some(version) == card.version() {
359 return raw.to_string();
360 }
361 // calcard's 4.0 writer drops a `pref` type, so map before parsing.
362 let card = match version {
363 VCardVersion::V4_0 => VCard::parse(apple_forms(raw, version)).unwrap_or(card),
364 _ => card,
365 };
366 let entries = match &req.props {
367 None => card.entries,
368 Some(props) => card
369 .entries
370 .into_iter()
371 .filter_map(|mut e| {
372 let p = props
373 .iter()
374 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
375 if p.novalue {
376 e.values.clear();
377 }
378 Some(e)
379 })
380 .collect(),
381 };
382 let mut out = String::new();
383 let _ = VCard { entries }.write_to(&mut out, version);
384 match version {
385 VCardVersion::V4_0 => out,
386 _ => apple_forms(&out, version),
387 }
388}
389
390/// The version a GET asks for in its `Accept` header: 4.0 when named,
391/// else 3.0, which RFC 6352 makes the default and Apple Contacts needs.
392pub fn accepted_version(accept: Option<&str>) -> VCardVersion {
393 match accept.is_some_and(|a| a.contains("version=4.0")) {
394 true => VCardVersion::V4_0,
395 false => VCardVersion::V3_0,
396 }
397}
398
399/// vCard 4.0 groups and companies in the vCard 3.0 extensions Apple
400/// Contacts reads, and back: `KIND:group` and `MEMBER` as
401/// `X-ADDRESSBOOKSERVER-KIND` and `X-ADDRESSBOOKSERVER-MEMBER`, `KIND:org`
402/// as `X-ABSHOWAS:COMPANY`, and `PREF=1` as `TYPE=pref`. sabre/vobject
403/// converts the same way.
404fn apple_forms(text: &str, version: VCardVersion) -> String {
405 let v3 = version == VCardVersion::V3_0;
406 let eol = if text.contains("\r\n") { "\r\n" } else { "\n" };
407 let mut out = String::with_capacity(text.len());
408 for raw in crate::text::logical_lines(text) {
409 let line = crate::text::unfold(raw);
410 let start = crate::text::value_start(&line);
411 let head = crate::text::head(&line);
412 let head_name = head.split(';').next().unwrap_or_default();
413 let (group, name) = match head_name.rsplit_once('.') {
414 Some((g, n)) => (&head_name[..=g.len()], n.to_ascii_uppercase()),
415 None => ("", head_name.to_ascii_uppercase()),
416 };
417 let value = &line[start..];
418 let lower = value.trim().to_ascii_lowercase();
419 let renamed = match (v3, name.as_str(), lower.as_str()) {
420 (true, "KIND", "group") => Some(("X-ADDRESSBOOKSERVER-KIND", "group")),
421 (true, "KIND", "org") => Some(("X-ABSHOWAS", "COMPANY")),
422 (true, "KIND", "individual") => continue,
423 (true, "MEMBER", _) => Some(("X-ADDRESSBOOKSERVER-MEMBER", value)),
424 (false, "X-ADDRESSBOOKSERVER-KIND", "group") => Some(("KIND", "group")),
425 (false, "X-ABSHOWAS", "company") => Some(("KIND", "org")),
426 (false, "X-ADDRESSBOOKSERVER-MEMBER", _) => Some(("MEMBER", value)),
427 _ => None,
428 };
429 let params = pref_params(crate::text::param_parts(&line), v3);
430 let (name, value) = renamed.unwrap_or((&name, value));
431 let mut new = format!("{group}{name}");
432 for p in &params {
433 new.push(';');
434 new.push_str(p);
435 }
436 new.push(':');
437 new.push_str(value);
438 if new == line {
439 out.push_str(raw);
440 } else {
441 out.push_str(&crate::text::fold(&new, eol));
442 }
443 }
444 out
445}
446
447/// `PREF=1` as a `pref` type for vCard 3.0, and back for 4.0.
448fn pref_params(params: Vec<&str>, v3: bool) -> Vec<String> {
449 let mut out = Vec::with_capacity(params.len());
450 let mut pref = false;
451 for p in params {
452 let (key, value) = p.split_once('=').unwrap_or((p, ""));
453 match (v3, key.to_ascii_uppercase().as_str()) {
454 (true, "PREF") => pref |= value.trim() == "1",
455 (false, "TYPE") => {
456 let (prefs, rest): (Vec<&str>, Vec<&str>) = value
457 .trim_matches('"')
458 .split(',')
459 .partition(|t| t.eq_ignore_ascii_case("pref"));
460 pref |= !prefs.is_empty();
461 if !rest.is_empty() {
462 out.push(format!("{key}={}", rest.join(",")));
463 }
464 }
465 _ => out.push(p.to_string()),
466 }
467 }
468 if pref {
469 out.push(if v3 { "TYPE=pref" } else { "PREF=1" }.to_string());
470 }
471 out
472}
473