1use std::collections::BTreeMap;
6
7use serde::Deserialize;
8use serde_json::{Map, Value, json};
9
10use crate::{CONTACTS, Client, Error, Result, ids_of};
11
12#[derive(Debug, Clone, Default, Deserialize)]
13pub struct NameComponent {
14 #[serde(default)]
15 pub kind: String,
16 #[serde(default)]
17 pub value: String,
18}
19
20#[derive(Debug, Clone, Default, Deserialize)]
21pub struct Name {
22 #[serde(default)]
23 pub full: Option<String>,
24 #[serde(default)]
25 pub components: Vec<NameComponent>,
26}
27
28#[derive(Debug, Clone, Deserialize)]
29pub struct EmailEntry {
30 pub address: String,
31 #[serde(default)]
33 pub contexts: Option<BTreeMap<String, bool>>,
34}
35
36#[derive(Debug, Clone, Deserialize)]
37pub struct Phone {
38 pub number: String,
39 #[serde(default)]
40 pub contexts: Option<BTreeMap<String, bool>>,
41 #[serde(default)]
43 pub features: Option<BTreeMap<String, bool>>,
44}
45
46#[derive(Debug, Clone, Deserialize)]
47pub struct Organization {
48 #[serde(default)]
49 pub name: Option<String>,
50}
51
52#[derive(Debug, Clone, Deserialize)]
53pub struct Note {
54 pub note: String,
55}
56
57#[derive(Debug, Clone, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct Card {
60 pub id: String,
61 #[serde(default)]
62 pub address_book_ids: BTreeMap<String, bool>,
63 #[serde(default)]
64 pub name: Option<Name>,
65 #[serde(default)]
66 pub emails: Option<BTreeMap<String, EmailEntry>>,
67 #[serde(default)]
68 pub phones: Option<BTreeMap<String, Phone>>,
69 #[serde(default)]
70 pub organizations: Option<BTreeMap<String, Organization>>,
71 #[serde(default)]
72 pub notes: Option<BTreeMap<String, Note>>,
73 #[serde(skip)]
74 pub account: String,
75}
76
77impl Card {
78 pub fn full_name(&self) -> String {
80 if let Some(full) = self.name.as_ref().and_then(|name| name.full.clone()).filter(|full| !full.is_empty()) {
81 return full;
82 }
83 let parts: Vec<&str> = self
84 .name
85 .iter()
86 .flat_map(|name| name.components.iter())
87 .filter(|part| !part.value.is_empty() && part.kind != "separator")
88 .map(|part| part.value.as_str())
89 .collect();
90 if !parts.is_empty() {
91 return parts.join(" ");
92 }
93 self.emails().into_iter().next().unwrap_or_default()
94 }
95
96 pub fn emails(&self) -> Vec<String> {
97 self.emails.iter().flat_map(|map| map.values()).map(|entry| entry.address.clone()).collect()
98 }
99
100 pub fn phones(&self) -> Vec<String> {
101 self.phones.iter().flat_map(|map| map.values()).map(|phone| phone.number.clone()).collect()
102 }
103
104 pub fn organization(&self) -> Option<String> {
105 let names: Vec<_> =
106 self.organizations.iter().flat_map(|map| map.values()).filter_map(|o| o.name.clone()).collect();
107 (!names.is_empty()).then(|| names.join(", "))
108 }
109
110 pub fn notes(&self) -> Vec<String> {
111 self.notes.iter().flat_map(|map| map.values()).map(|note| note.note.clone()).collect()
112 }
113}
114
115#[derive(Deserialize)]
116struct List<T> {
117 list: Vec<T>,
118}
119
120pub async fn cards(client: &Client, text: &str) -> Result<Vec<Card>> {
122 let mut out = Vec::new();
123 for (account, _) in client.session.accounts_with(CONTACTS) {
124 let mut filter = Map::new();
125 if !text.trim().is_empty() {
126 filter.insert("text".into(), text.trim().into());
127 }
128 let responses = client
129 .request(
130 &[CONTACTS],
131 vec![
132 ("ContactCard/query", json!({ "accountId": account, "filter": filter }), "q"),
133 ("ContactCard/get", json!({ "accountId": account, "#ids": ids_of("q", "ContactCard/query") }), "g"),
134 ],
135 )
136 .await?;
137 let value = responses.get("g").cloned().unwrap_or(json!({ "list": [] }));
138 let cards = serde_json::from_value::<List<Card>>(value).map_err(|error| Error::Protocol(error.to_string()))?;
139 out.extend(cards.list.into_iter().map(|mut card| {
140 card.account = account.to_owned();
141 card
142 }));
143 }
144 out.sort_by_key(|card| card.full_name().to_lowercase());
145 Ok(out)
146}
147
148#[derive(Debug, Clone, Copy, Default, Deserialize)]
149#[serde(rename_all = "camelCase", default)]
150pub struct BookRights {
151 pub may_write: bool,
152}
153
154#[derive(Debug, Clone, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct AddressBook {
157 pub id: String,
158 pub name: String,
159 #[serde(default)]
160 pub is_default: bool,
161 #[serde(default)]
162 pub my_rights: Option<BookRights>,
163 #[serde(skip)]
164 pub account: String,
165 #[serde(skip)]
167 pub shared: Option<String>,
168}
169
170impl AddressBook {
171 pub fn writable(&self) -> bool {
172 self.my_rights.is_none_or(|rights| rights.may_write)
173 }
174}
175
176pub async fn address_books(client: &Client) -> Result<Vec<AddressBook>> {
178 let mut out = Vec::new();
179 for (id, account) in client.session.accounts_with(CONTACTS) {
180 let result = client.call(&[CONTACTS], "AddressBook/get", json!({ "accountId": id })).await?;
181 let books =
182 serde_json::from_value::<List<AddressBook>>(result).map_err(|error| Error::Protocol(error.to_string()))?;
183 out.extend(books.list.into_iter().map(|mut book| {
184 book.account = id.to_owned();
185 book.shared = (!account.is_personal).then(|| account.name.clone());
186 book
187 }));
188 }
189 out.sort_by_key(|book| (book.shared.is_some(), !book.is_default, book.name.to_lowercase()));
190 Ok(out)
191}
192
193pub const CONTEXTS: [&str; 2] = ["work", "private"];
195pub const FEATURES: [&str; 7] = ["mobile", "voice", "text", "video", "fax", "pager", "textphone"];
197
198#[derive(Debug, Clone, Default, PartialEq, Eq)]
200pub struct Labelled {
201 pub value: String,
202 pub labels: Vec<String>,
203}
204
205impl Labelled {
206 fn of(value: &str, maps: &[&Option<BTreeMap<String, bool>>]) -> Self {
207 let labels = maps
208 .iter()
209 .flat_map(|map| map.iter().flat_map(|map| map.iter()))
210 .filter(|(_, on)| **on)
211 .map(|(label, _)| label.clone())
212 .collect();
213 Self { value: value.to_owned(), labels }
214 }
215
216 pub fn render(&self) -> String {
218 if self.labels.is_empty() { self.value.clone() } else { format!("{} ({})", self.value, self.labels.join(", ")) }
219 }
220
221 pub fn parse(text: &str, allowed: &[&str]) -> std::result::Result<Self, String> {
223 let text = text.trim();
224 let (value, labels) = match text.strip_suffix(')').and_then(|rest| rest.rsplit_once('(')) {
225 Some((value, labels)) => {
226 (value.trim(), labels.split(',').map(|label| label.trim().to_lowercase()).collect())
227 }
228 None => (text, Vec::new()),
229 };
230 let labels: Vec<String> = labels.into_iter().filter(|label: &String| !label.is_empty()).collect();
231 if let Some(unknown) = labels.iter().find(|label| !allowed.contains(&label.as_str())) {
232 return Err(format!("unknown label {unknown} – one of {}", allowed.join(", ")));
233 }
234 Ok(Self { value: value.to_owned(), labels })
235 }
236
237 fn set(&self, among: &[&str]) -> Value {
238 let on: Map<String, Value> = self
239 .labels
240 .iter()
241 .filter(|label| among.contains(&label.as_str()))
242 .map(|label| (label.clone(), json!(true)))
243 .collect();
244 if on.is_empty() { Value::Null } else { Value::Object(on) }
245 }
246}
247
248#[derive(Debug, Clone, Default, PartialEq, Eq)]
250pub struct CardForm {
251 pub name: String,
252 pub emails: Vec<Labelled>,
253 pub phones: Vec<Labelled>,
254 pub organizations: Vec<String>,
255 pub note: String,
256}
257
258impl CardForm {
259 pub fn of(card: &Card) -> Self {
260 Self {
261 name: card.full_name(),
262 emails: card
263 .emails
264 .iter()
265 .flat_map(|map| map.values())
266 .map(|entry| Labelled::of(&entry.address, &[&entry.contexts]))
267 .collect(),
268 phones: card
269 .phones
270 .iter()
271 .flat_map(|map| map.values())
272 .map(|phone| Labelled::of(&phone.number, &[&phone.contexts, &phone.features]))
273 .collect(),
274 organizations: card
275 .organizations
276 .iter()
277 .flat_map(|map| map.values())
278 .filter_map(|organization| organization.name.clone())
279 .collect(),
280 note: card.notes().join("\n"),
281 }
282 }
283
284 pub fn object(&self) -> Value {
286 let map = |prefix: &str, values: Vec<Value>| -> Value {
287 if values.is_empty() {
288 return Value::Null;
289 }
290 Value::Object(
291 values.into_iter().enumerate().map(|(n, value)| (format!("{prefix}{}", n + 1), value)).collect(),
292 )
293 };
294 let emails = self
295 .emails
296 .iter()
297 .map(|email| json!({ "@type": "EmailAddress", "address": email.value, "contexts": email.set(&CONTEXTS) }))
298 .collect();
299 let phones = self
300 .phones
301 .iter()
302 .map(|phone| {
303 json!({ "@type": "Phone", "number": phone.value, "contexts": phone.set(&CONTEXTS),
304 "features": phone.set(&FEATURES) })
305 })
306 .collect();
307 let organizations =
308 self.organizations.iter().map(|name| json!({ "@type": "Organization", "name": name })).collect();
309 json!({
310 "name": { "full": self.name },
311 "emails": map("e", emails),
312 "phones": map("p", phones),
313 "organizations": map("o", organizations),
314 "notes": if self.note.is_empty() { Value::Null } else { json!({ "n1": { "@type": "Note", "note": self.note } }) },
315 })
316 }
317}
318
319pub async fn create(client: &Client, book: &AddressBook, form: &CardForm) -> Result<()> {
320 let mut object = form.object();
321 object["@type"] = "Card".into();
322 object["version"] = "1.0".into();
323 object["addressBookIds"] = json!({ book.id.as_str(): true });
324 client.set(&[CONTACTS], "ContactCard", &book.account, json!({ "create": { "c": object } }), json!({})).await?;
325 Ok(())
326}
327
328pub async fn update(client: &Client, book: &AddressBook, id: &str, form: &CardForm) -> Result<()> {
330 let mut object = form.object();
331 object["addressBookIds"] = json!({ book.id.as_str(): true });
332 client.set(&[CONTACTS], "ContactCard", &book.account, json!({ "update": { id: object } }), json!({})).await?;
333 Ok(())
334}
335
336pub async fn raw_cards(client: &Client, account: &str, ids: &[String]) -> Result<Vec<Map<String, Value>>> {
338 let result = client.call(&[CONTACTS], "ContactCard/get", json!({ "accountId": account, "ids": ids })).await?;
339 Ok(result
340 .get("list")
341 .and_then(Value::as_array)
342 .map(|list| list.iter().filter_map(|card| card.as_object().cloned()).collect())
343 .unwrap_or_default())
344}
345
346pub async fn import(
348 client: &Client,
349 book: &AddressBook,
350 cards: Vec<Map<String, Value>>,
351) -> Result<(usize, Vec<String>)> {
352 let create: Map<String, Value> = cards
353 .into_iter()
354 .enumerate()
355 .map(|(n, mut card)| {
356 card.insert("addressBookIds".into(), json!({ book.id.as_str(): true }));
357 (format!("i{n}"), Value::Object(card))
358 })
359 .collect();
360 let result =
361 client.call(&[CONTACTS], "ContactCard/set", json!({ "accountId": book.account, "create": create })).await?;
362 Ok(crate::calendars::made_and_refused(&result))
363}
364
365pub async fn destroy(client: &Client, account: &str, id: &str) -> Result<()> {
366 client.set(&[CONTACTS], "ContactCard", account, json!({ "destroy": [id] }), json!({})).await?;
367 Ok(())
368}