principal.rs
⎇
Raw
1//! Principals: attendee search (RFC 3744 9.4 and Apple's
2//! calendarserver-principal-search) and the system address book.
3
4use xmltree::Element;
5
6use crate::filter::{Collation, MatchType, TextMatch};
7use crate::report::{Refused, limit};
8use crate::xml::{CALDAV, CALSERVER, DAV, Name, Propfind, child, elements, text};
9
10/// The `calendar-user-type` of a principal.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum UserType {
13 Individual,
14 Room,
15 Resource,
16}
17
18impl UserType {
19 pub fn as_str(self) -> &'static str {
20 match self {
21 UserType::Individual => "INDIVIDUAL",
22 UserType::Room => "ROOM",
23 UserType::Resource => "RESOURCE",
24 }
25 }
26}
27
28/// What a search can see of a principal.
29#[derive(Debug, Clone)]
30pub struct Principal<'a> {
31 /// The URL segment.
32 pub name: &'a str,
33 pub display: &'a str,
34 /// The calendar user addresses, `mailto:` ones included.
35 pub addresses: &'a [String],
36 pub kind: UserType,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct Search {
41 pub terms: Vec<Term>,
42 /// `allof`: every term must match. Otherwise one is enough.
43 pub all: bool,
44 /// Only principals of this type, from a calendarserver search context.
45 pub kind: Option<UserType>,
46 pub find: Propfind,
47 pub limit: Option<usize>,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct Term {
52 /// The properties the text is looked for in. Empty means all searchable
53 /// ones and the name.
54 pub props: Vec<Name>,
55 pub text: TextMatch,
56}
57
58/// The properties `principal-search-property-set` offers.
59pub const SEARCHABLE: [(&str, &str, &str); 3] = [
60 (DAV, "displayname", "Display name"),
61 (CALDAV, "calendar-user-address-set", "Calendar user address"),
62 (CALDAV, "calendar-user-type", "Calendar user type"),
63];
64
65impl Search {
66 pub fn matches(&self, p: &Principal) -> bool {
67 if self.kind.is_some_and(|k| k != p.kind) {
68 return false;
69 }
70 let hit = |t: &Term| {
71 let any = t.props.is_empty();
72 let wants = |ns: &str, local: &str| any || t.props.iter().any(|n| n.is(ns, local));
73 (any && t.text.matches(p.name))
74 || (wants(DAV, "displayname") && t.text.matches(p.display))
75 || (wants(CALDAV, "calendar-user-type") && t.text.matches(p.kind.as_str()))
76 || ((wants(CALDAV, "calendar-user-address-set")
77 || wants(CALSERVER, "email-address-set"))
78 && p.addresses.iter().any(|a| {
79 t.text.matches(a)
80 || a.strip_prefix("mailto:").is_some_and(|m| t.text.matches(m))
81 }))
82 };
83 // No terms lists every principal.
84 match self.all || self.terms.is_empty() {
85 true => self.terms.iter().all(hit),
86 false => self.terms.iter().any(hit),
87 }
88 }
89}
90
91fn term_text(e: &Element) -> Result<TextMatch, Refused> {
92 let match_type = match e.attributes.get("match-type").map(String::as_str) {
93 None | Some("contains") => MatchType::Contains,
94 Some("starts-with") => MatchType::StartsWith,
95 Some("ends-with") => MatchType::EndsWith,
96 Some("equals") => MatchType::Equals,
97 Some(_) => return Err(Refused::Invalid),
98 };
99 Ok(TextMatch {
100 text: text(e),
101 collation: Collation::UnicodeCasemap,
102 match_type,
103 negate: false,
104 })
105}
106
107/// The properties in `<prop>`, and any stray property element beside it:
108/// python-caldav puts them there.
109fn prop_names(root: &Element) -> Propfind {
110 let mut names: Vec<Name> = child(root, DAV, "prop")
111 .map(|p| elements(p).map(Name::of).collect())
112 .unwrap_or_default();
113 let structure = [
114 (DAV, "prop"),
115 (DAV, "property-search"),
116 (DAV, "apply-to-principal-collection-set"),
117 (DAV, "limit"),
118 (CALSERVER, "search-token"),
119 (CALSERVER, "limit"),
120 ];
121 names.extend(
122 elements(root)
123 .map(Name::of)
124 .filter(|n| !structure.iter().any(|(ns, l)| n.is(ns, l))),
125 );
126 Propfind::Prop(names)
127}
128
129/// A `DAV:principal-property-search` body.
130pub fn property_search(root: &Element) -> Result<Search, Refused> {
131 let mut terms = Vec::new();
132 for s in elements(root).filter(|e| Name::of(e).is(DAV, "property-search")) {
133 let props = child(s, DAV, "prop")
134 .map(|p| elements(p).map(Name::of).collect())
135 .unwrap_or_default();
136 let text = term_text(child(s, DAV, "match").ok_or(Refused::Invalid)?)?;
137 terms.push(Term { props, text });
138 }
139 Ok(Search {
140 terms,
141 all: root.attributes.get("test").map(String::as_str) != Some("anyof"),
142 kind: None,
143 find: prop_names(root),
144 limit: limit(root, DAV)?,
145 })
146}
147
148/// A `calendarserver-principal-search` body. Every token must match one of
149/// the name, the display name or an address.
150pub fn calendarserver_search(root: &Element) -> Result<Search, Refused> {
151 let terms: Vec<Term> = elements(root)
152 .filter(|e| Name::of(e).is(CALSERVER, "search-token"))
153 .map(|e| {
154 term_text(e).map(|text| Term {
155 props: Vec::new(),
156 text,
157 })
158 })
159 .collect::<Result<_, _>>()?;
160 let kind = match root.attributes.get("context").map(String::as_str) {
161 Some("location") => Some(UserType::Room),
162 Some("resource") => Some(UserType::Resource),
163 Some("user") => Some(UserType::Individual),
164 _ => None,
165 };
166 Ok(Search {
167 terms,
168 all: root.attributes.get("test").map(String::as_str) != Some("anyof"),
169 kind,
170 find: prop_names(root),
171 limit: limit(root, CALSERVER)?,
172 })
173}
174
175/// The system address book's vCard of a principal.
176pub fn card(uid: &str, p: &Principal, email: &str) -> String {
177 let kind = match p.kind {
178 UserType::Individual => "individual",
179 UserType::Room => "location",
180 // RFC 6869.
181 UserType::Resource => "device",
182 };
183 let fn_ = escape(p.display);
184 format!(
185 "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:{uid}\r\nFN:{fn_}\r\nN:{fn_};;;;\r\nEMAIL;TYPE=INTERNET:{}\r\nKIND:{kind}\r\nEND:VCARD\r\n",
186 escape(email)
187 )
188}
189
190fn escape(s: &str) -> String {
191 let mut out = String::with_capacity(s.len());
192 for c in s.chars() {
193 match c {
194 '\\' | ',' | ';' => {
195 out.push('\\');
196 out.push(c);
197 }
198 '\n' => out.push_str("\\n"),
199 '\r' => {}
200 _ => out.push(c),
201 }
202 }
203 out
204}
205