1 /* jmap_contact.c -- Routines for handling JMAP contact messages
2 *
3 * Copyright (c) 1994-2014 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 *
42 */
43
44 #include <config.h>
45
46 #ifdef HAVE_UNISTD_H
47 #include <unistd.h>
48 #endif
49 #include <ctype.h>
50 #include <string.h>
51 #include <syslog.h>
52 #include <assert.h>
53 #include <errno.h>
54
55 #include "annotate.h"
56 #include "carddav_db.h"
57 #include "cyr_qsort_r.h"
58 #include "global.h"
59 #include "hash.h"
60 #include "http_carddav.h"
61 #include "http_dav.h"
62 #include "http_jmap.h"
63 #include "json_support.h"
64 #include "mailbox.h"
65 #include "mboxname.h"
66 #include "stristr.h"
67 #include "times.h"
68 #include "util.h"
69 #include "vcard_support.h"
70 #include "xapian_wrap.h"
71 #include "xmalloc.h"
72
73 /* generated headers are not necessarily in current directory */
74 #include "imap/http_err.h"
75 #include "imap/imap_err.h"
76
77 static int jmap_contactgroup_get(struct jmap_req *req);
78 static int jmap_contactgroup_changes(struct jmap_req *req);
79 static int jmap_contactgroup_set(struct jmap_req *req);
80 static int jmap_contactgroup_query(struct jmap_req *req);
81 static int jmap_contact_get(struct jmap_req *req);
82 static int jmap_contact_changes(struct jmap_req *req);
83 static int jmap_contact_query(struct jmap_req *req);
84 static int jmap_contact_set(struct jmap_req *req);
85 static int jmap_contact_copy(struct jmap_req *req);
86
87 static int _contact_set_create(jmap_req_t *req, unsigned kind,
88 json_t *jcard, struct carddav_data *cdata,
89 struct mailbox **mailbox, json_t *item,
90 json_t *invalid);
91 static int required_set_rights(json_t *props);
92 static int _json_to_card(struct jmap_req *req,
93 struct carddav_data *cdata,
94 struct vparse_card *card,
95 json_t *arg, strarray_t *flags,
96 struct entryattlist **annotsp,
97 json_t *invalid);
98
99 static json_t *jmap_contact_from_vcard(struct vparse_card *card,
100 struct mailbox *mailbox,
101 struct index_record *record);
102
103 static int jmap_contact_getblob(jmap_req_t *req, const char *blobid,
104 const char *accept);
105
106 #define JMAPCACHE_CONTACTVERSION 1
107
108 jmap_method_t jmap_contact_methods_standard[] = {
109 { NULL, NULL, NULL, 0}
110 };
111
112 jmap_method_t jmap_contact_methods_nonstandard[] = {
113 {
114 "ContactGroup/get",
115 JMAP_CONTACTS_EXTENSION,
116 &jmap_contactgroup_get,
117 JMAP_NEED_CSTATE
118 },
119 {
120 "ContactGroup/changes",
121 JMAP_CONTACTS_EXTENSION,
122 &jmap_contactgroup_changes,
123 JMAP_NEED_CSTATE
124 },
125 {
126 "ContactGroup/set",
127 JMAP_CONTACTS_EXTENSION,
128 &jmap_contactgroup_set,
129 JMAP_NEED_CSTATE | JMAP_READ_WRITE
130 },
131 {
132 "ContactGroup/query",
133 JMAP_CONTACTS_EXTENSION,
134 &jmap_contactgroup_query,
135 JMAP_NEED_CSTATE
136 },
137 {
138 "Contact/get",
139 JMAP_CONTACTS_EXTENSION,
140 &jmap_contact_get,
141 JMAP_NEED_CSTATE
142 },
143 {
144 "Contact/changes",
145 JMAP_CONTACTS_EXTENSION,
146 &jmap_contact_changes,
147 JMAP_NEED_CSTATE
148 },
149 {
150 "Contact/query",
151 JMAP_CONTACTS_EXTENSION,
152 &jmap_contact_query,
153 JMAP_NEED_CSTATE
154 },
155 {
156 "Contact/set",
157 JMAP_CONTACTS_EXTENSION,
158 &jmap_contact_set,
159 JMAP_NEED_CSTATE | JMAP_READ_WRITE
160 },
161 {
162 "Contact/copy",
163 JMAP_CONTACTS_EXTENSION,
164 &jmap_contact_copy,
165 JMAP_NEED_CSTATE | JMAP_READ_WRITE
166 },
167 { NULL, NULL, NULL, 0}
168 };
169
170 static char *_prodid = NULL;
171
jmap_contact_init(jmap_settings_t * settings)172 HIDDEN void jmap_contact_init(jmap_settings_t *settings)
173 {
174 jmap_method_t *mp;
175 for (mp = jmap_contact_methods_standard; mp->name; mp++) {
176 hash_insert(mp->name, mp, &settings->methods);
177 }
178
179 if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
180 json_object_set_new(settings->server_capabilities,
181 JMAP_CONTACTS_EXTENSION, json_object());
182
183 for (mp = jmap_contact_methods_nonstandard; mp->name; mp++) {
184 hash_insert(mp->name, mp, &settings->methods);
185 }
186 }
187
188 ptrarray_append(&settings->getblob_handlers, jmap_contact_getblob);
189
190 /* Initialize PRODID value
191 *
192 * XXX - OS X 10.11.6 Contacts is not unfolding PRODID lines, so make
193 * sure that PRODID never exceeds the 75 octet limit without CRLF */
194 struct buf prodidbuf = BUF_INITIALIZER;
195 size_t max_len = 68; /* 75 - strlen("PRODID:") */
196 buf_printf(&prodidbuf, "-//CyrusIMAP.org//Cyrus %s//EN", CYRUS_VERSION);
197 if (buf_len(&prodidbuf) > max_len) {
198 buf_truncate(&prodidbuf, max_len - 6);
199 buf_appendcstr(&prodidbuf, "..//EN");
200 }
201 _prodid = buf_release(&prodidbuf);
202 }
203
jmap_contact_capabilities(json_t * account_capabilities)204 HIDDEN void jmap_contact_capabilities(json_t *account_capabilities)
205 {
206 if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
207 json_object_set_new(account_capabilities, JMAP_CONTACTS_EXTENSION, json_object());
208 }
209 }
210
211 struct changes_rock {
212 jmap_req_t *req;
213 struct jmap_changes *changes;
214 size_t seen_records;
215 modseq_t highestmodseq;
216 };
217
strip_spurious_deletes(struct changes_rock * urock)218 static void strip_spurious_deletes(struct changes_rock *urock)
219 {
220 /* if something is mentioned in both DELETEs and UPDATEs, it's probably
221 * a move. O(N*M) algorithm, but there are rarely many, and the alternative
222 * of a hash will cost more */
223 unsigned i, j;
224
225 for (i = 0; i < json_array_size(urock->changes->destroyed); i++) {
226 const char *del =
227 json_string_value(json_array_get(urock->changes->destroyed, i));
228
229 for (j = 0; j < json_array_size(urock->changes->updated); j++) {
230 const char *up =
231 json_string_value(json_array_get(urock->changes->updated, j));
232 if (!strcmpsafe(del, up)) {
233 json_array_remove(urock->changes->destroyed, i--);
234 break;
235 }
236 }
237 }
238 }
239
jmap_utf8string(const char * s)240 static json_t *jmap_utf8string(const char *s)
241 {
242 char *freeme = NULL;
243 json_t *jval = json_string(jmap_decode_to_utf8("utf-8", ENCODING_NONE,
244 s, strlen(s), 1.0, &freeme, NULL));
245 free(freeme);
246 return jval;
247 }
248
249 /*****************************************************************************
250 * JMAP Contacts API
251 ****************************************************************************/
252
253 struct cards_rock {
254 struct carddav_db *db;
255 struct jmap_req *req;
256 struct jmap_get *get;
257 struct mailbox *mailbox;
258 hashu64_table jmapcache;
259 int rows;
260 };
261
jmap_group_from_vcard(struct vparse_card * vcard)262 static json_t *jmap_group_from_vcard(struct vparse_card *vcard)
263 {
264 struct vparse_entry *ventry = NULL;
265 json_t *obj = json_pack("{}");
266
267 json_t *contactids = json_pack("[]");
268 json_t *otherids = json_pack("{}");
269
270 for (ventry = vcard->properties; ventry; ventry = ventry->next) {
271 const char *name = ventry->name;
272 const char *propval = ventry->v.value;
273
274 if (!name) continue;
275 if (!propval) continue;
276
277 if (!strcasecmp(name, "fn")) {
278 json_object_set_new(obj, "name", jmap_utf8string(propval));
279 }
280
281 else if (!strcasecmp(name, "x-addressbookserver-member")) {
282 if (strncmp(propval, "urn:uuid:", 9)) continue;
283 json_array_append_new(contactids, json_string(propval+9));
284 }
285
286 else if (!strcasecmp(name, "x-fm-otheraccount-member")) {
287 if (strncmp(propval, "urn:uuid:", 9)) continue;
288 struct vparse_param *param = vparse_get_param(ventry, "userid");
289 if (!param) continue;
290 json_t *object = json_object_get(otherids, param->value);
291 if (!object) {
292 object = json_array();
293 json_object_set_new(otherids, param->value, object);
294 }
295 json_array_append_new(object, json_string(propval+9));
296 }
297 }
298
299 json_object_set_new(obj, "contactIds", contactids);
300 json_object_set_new(obj, "otherAccountContactIds", otherids);
301
302 return obj;
303 }
304
getgroups_cb(void * rock,struct carddav_data * cdata)305 static int getgroups_cb(void *rock, struct carddav_data *cdata)
306 {
307 struct cards_rock *crock = (struct cards_rock *) rock;
308 struct index_record record;
309 jmap_req_t *req = crock->req;
310 json_t *obj = NULL;
311 char *xhref;
312 int r;
313
314 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS))
315 return 0;
316
317 if (cdata->jmapversion == JMAPCACHE_CONTACTVERSION) {
318 json_error_t jerr;
319 obj = json_loads(cdata->jmapdata, 0, &jerr);
320 if (obj) goto gotvalue;
321 }
322
323 if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
324 mailbox_close(&crock->mailbox);
325 r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
326 if (r) return r;
327 }
328
329 r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
330 if (r) return r;
331
332 /* Load message containing the resource and parse vcard data */
333 struct vparse_card *vcard = record_to_vcard(crock->mailbox, &record);
334 if (!vcard || !vcard->objects) {
335 syslog(LOG_ERR, "record_to_vcard failed for record %u:%s",
336 cdata->dav.imap_uid, crock->mailbox->name);
337 vparse_free_card(vcard);
338 return IMAP_INTERNAL;
339 }
340
341 obj = jmap_group_from_vcard(vcard->objects);
342
343 vparse_free_card(vcard);
344
345 hashu64_insert(cdata->dav.rowid, json_dumps(obj, 0), &crock->jmapcache);
346
347 gotvalue:
348
349 json_object_set_new(obj, "id", json_string(cdata->vcard_uid));
350 json_object_set_new(obj, "uid", json_string(cdata->vcard_uid));
351
352 json_object_set_new(obj, "addressbookId",
353 json_string(strrchr(cdata->dav.mailbox, '.')+1));
354
355 xhref = jmap_xhref(cdata->dav.mailbox, cdata->dav.resource);
356 json_object_set_new(obj, "x-href", json_string(xhref));
357 free(xhref);
358
359 json_array_append_new(crock->get->list, obj);
360
361 crock->rows++;
362
363 return 0;
364 }
365
366 static const jmap_property_t contact_props[] = {
367 {
368 "id",
369 NULL,
370 JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
371 },
372 {
373 "uid",
374 NULL,
375 JMAP_PROP_IMMUTABLE
376 },
377 {
378 "isFlagged",
379 NULL,
380 0
381 },
382 {
383 "avatar",
384 NULL,
385 0
386 },
387 {
388 "prefix",
389 NULL,
390 0
391 },
392 {
393 "firstName",
394 NULL,
395 0
396 },
397 {
398 "lastName",
399 NULL,
400 0
401 },
402 {
403 "suffix",
404 NULL,
405 0
406 },
407 {
408 "nickname",
409 NULL,
410 0
411 },
412 {
413 "birthday",
414 NULL,
415 0
416 },
417 {
418 "anniversary",
419 NULL,
420 0
421 },
422 {
423 "company",
424 NULL,
425 0
426 },
427 {
428 "department",
429 NULL,
430 0
431 },
432 {
433 "jobTitle",
434 NULL,
435 0
436 },
437 {
438 "emails",
439 NULL,
440 0
441 },
442 {
443 "phones",
444 NULL,
445 0
446 },
447 {
448 "online",
449 NULL,
450 0
451 },
452 {
453 "addresses",
454 NULL,
455 0
456 },
457 {
458 "notes",
459 NULL,
460 0
461 },
462
463 /* FM extensions */
464 {
465 "addressbookId",
466 JMAP_CONTACTS_EXTENSION,
467 0
468 },
469 {
470 "x-href",
471 JMAP_CONTACTS_EXTENSION,
472 JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
473 }, // AJAXUI only
474 {
475 "x-hasPhoto",
476 JMAP_CONTACTS_EXTENSION,
477 JMAP_PROP_SERVER_SET
478 }, // AJAXUI only
479 {
480 "importance",
481 JMAP_CONTACTS_EXTENSION,
482 0
483 }, // JMAPUI only
484 {
485 "blobId",
486 JMAP_CONTACTS_EXTENSION,
487 JMAP_PROP_SERVER_SET | JMAP_PROP_SKIP_GET
488 },
489
490 { NULL, NULL, 0 }
491 };
492
493 static const jmap_property_t group_props[] = {
494 {
495 "id",
496 NULL,
497 JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
498 },
499 {
500 "uid",
501 NULL,
502 JMAP_PROP_IMMUTABLE
503 },
504 {
505 "name",
506 NULL,
507 0
508 },
509 {
510 "contactIds",
511 NULL,
512 0
513 },
514
515 // FM extensions */
516 {
517 "addressbookId",
518 JMAP_CONTACTS_EXTENSION,
519 0
520 },
521 {
522 "x-href",
523 JMAP_CONTACTS_EXTENSION,
524 JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
525 }, // AJAXUI only
526 {
527 "otherAccountContactIds",
528 JMAP_CONTACTS_EXTENSION,
529 0
530 }, // Both AJAXUI and JMAPUI
531
532 { NULL, NULL, 0 }
533 };
534
_contact_getargs_parse(jmap_req_t * req,struct jmap_parser * parser,const char * key,json_t * arg,void * rock)535 static int _contact_getargs_parse(jmap_req_t *req __attribute__((unused)),
536 struct jmap_parser *parser __attribute__((unused)),
537 const char *key,
538 json_t *arg,
539 void *rock)
540 {
541 const char **addressbookId = (const char **) rock;
542 int r = 1;
543
544 /* Non-JMAP spec addressbookId argument */
545 if (!strcmp(key, "addressbookId") && json_is_string(arg)) {
546 *addressbookId = json_string_value(arg);
547 }
548
549 else r = 0;
550
551 return r;
552 }
553
cachecards_cb(uint64_t rowid,void * payload,void * vrock)554 static void cachecards_cb(uint64_t rowid, void *payload, void *vrock)
555 {
556 const char *eventrep = payload;
557 struct cards_rock *rock = vrock;
558
559 // there's no way to return errors, but luckily it doesn't matter if we
560 // fail to cache
561 carddav_write_jmapcache(rock->db, rowid,
562 JMAPCACHE_CONTACTVERSION, eventrep);
563 }
564
has_addressbooks_cb(const mbentry_t * mbentry,void * rock)565 static int has_addressbooks_cb(const mbentry_t *mbentry, void *rock)
566 {
567 jmap_req_t *req = rock;
568 if (mbentry->mbtype == MBTYPE_ADDRESSBOOK &&
569 jmap_hasrights_mbentry(req, mbentry, JACL_READITEMS)) {
570 return CYRUSDB_DONE;
571 }
572 return 0;
573 }
574
has_addressbooks(jmap_req_t * req)575 static int has_addressbooks(jmap_req_t *req)
576 {
577 mbname_t *mbname = mbname_from_userid(req->accountid);
578 mbname_push_boxes(mbname, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX));
579 int r = mboxlist_mboxtree(mbname_intname(mbname), has_addressbooks_cb,
580 req, MBOXTREE_SKIP_ROOT);
581 mbname_free(&mbname);
582 return r == CYRUSDB_DONE;
583 }
584
_contacts_get(struct jmap_req * req,carddav_cb_t * cb,int kind)585 static int _contacts_get(struct jmap_req *req, carddav_cb_t *cb, int kind)
586 {
587 if (!has_addressbooks(req)) {
588 jmap_error(req, json_pack("{s:s}", "type", "accountNoAddressbooks"));
589 return 0;
590 }
591
592 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
593 struct jmap_get get;
594 json_t *err = NULL;
595 struct carddav_db *db = NULL;
596 char *mboxname = NULL;
597 int r = 0;
598
599 /* Build callback data */
600 struct cards_rock rock = { NULL, req, &get, NULL /*mailbox*/,
601 HASHU64_TABLE_INITIALIZER, 0 /*rows */ };
602
603 construct_hashu64_table(&rock.jmapcache, 512, 0);
604
605 /* Parse request */
606 const char *addressbookId = NULL;
607 jmap_get_parse(req, &parser,
608 kind == CARDDAV_KIND_GROUP ? group_props : contact_props,
609 /* allow_null_ids */ 1,
610 &_contact_getargs_parse, &addressbookId, &get, &err);
611 if (err) {
612 jmap_error(req, err);
613 goto done;
614 }
615 mboxname = addressbookId ?
616 carddav_mboxname(req->accountid, addressbookId) : NULL;
617
618 /* Does the client request specific events? */
619 rock.db = db = carddav_open_userid(req->accountid);
620 if (!db) {
621 syslog(LOG_ERR,
622 "carddav_open_mailbox failed for user %s", req->accountid);
623 r = IMAP_INTERNAL;
624 goto done;
625 }
626 if (JNOTNULL(get.ids)) {
627 size_t i;
628 json_t *jval;
629 json_array_foreach(get.ids, i, jval) {
630 rock.rows = 0;
631 const char *id = json_string_value(jval);
632
633 r = carddav_get_cards(db, mboxname, id, kind, cb, &rock);
634 if (r || !rock.rows) {
635 json_array_append(get.not_found, jval);
636 }
637 r = 0; // we don't ever fail the whole request from this
638 }
639 }
640 else {
641 rock.rows = 0;
642 r = carddav_get_cards(db, mboxname, NULL, kind, cb, &rock);
643 if (r) goto done;
644 }
645
646 if (hashu64_count(&rock.jmapcache)) {
647 r = carddav_begin(db);
648 if (!r) hashu64_enumerate(&rock.jmapcache, cachecards_cb, &rock);
649 if (r) carddav_abort(db);
650 else r = carddav_commit(db);
651 if (r) goto done;
652 }
653
654 /* Build response */
655 json_t *jstate = jmap_getstate(req, MBTYPE_ADDRESSBOOK, /*refresh*/0);
656 get.state = xstrdup(json_string_value(jstate));
657 json_decref(jstate);
658 jmap_ok(req, jmap_get_reply(&get));
659
660 done:
661 jmap_parser_fini(&parser);
662 jmap_get_fini(&get);
663 free(mboxname);
664 mailbox_close(&rock.mailbox);
665 free_hashu64_table(&rock.jmapcache, free);
666 if (db) carddav_close(db);
667 return r;
668 }
669
jmap_contactgroup_get(struct jmap_req * req)670 static int jmap_contactgroup_get(struct jmap_req *req)
671 {
672 return _contacts_get(req, &getgroups_cb, CARDDAV_KIND_GROUP);
673 }
674
_json_array_get_string(const json_t * obj,size_t index)675 static const char *_json_array_get_string(const json_t *obj, size_t index)
676 {
677 const json_t *jval = json_array_get(obj, index);
678 if (!jval) return NULL;
679 const char *val = json_string_value(jval);
680 return val;
681 }
682
683
getchanges_cb(void * rock,struct carddav_data * cdata)684 static int getchanges_cb(void *rock, struct carddav_data *cdata)
685 {
686 struct changes_rock *urock = (struct changes_rock *) rock;
687 struct dav_data dav = cdata->dav;
688 const char *uid = cdata->vcard_uid;
689
690 if (!jmap_hasrights(urock->req, dav.mailbox, JACL_READITEMS))
691 return 0;
692
693 /* Count, but don't process items that exceed the maximum record count. */
694 if (urock->changes->max_changes &&
695 ++(urock->seen_records) > urock->changes->max_changes) {
696 urock->changes->has_more_changes = 1;
697 return 0;
698 }
699
700 /* Report item as updated or destroyed. */
701 if (dav.alive) {
702 if (dav.createdmodseq <= urock->changes->since_modseq)
703 json_array_append_new(urock->changes->updated, json_string(uid));
704 else
705 json_array_append_new(urock->changes->created, json_string(uid));
706 } else {
707 if (dav.createdmodseq <= urock->changes->since_modseq)
708 json_array_append_new(urock->changes->destroyed, json_string(uid));
709 }
710
711 /* Fetch record to determine modseq. */
712 if (dav.modseq > urock->highestmodseq) {
713 urock->highestmodseq = dav.modseq;
714 }
715
716 return 0;
717 }
718
_contacts_changes(struct jmap_req * req,int kind)719 static int _contacts_changes(struct jmap_req *req, int kind)
720 {
721 if (!has_addressbooks(req)) {
722 jmap_error(req, json_pack("{s:s}", "type", "accountNoAddressbooks"));
723 return 0;
724 }
725
726 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
727 struct jmap_changes changes;
728 json_t *err = NULL;
729 struct carddav_db *db = NULL;
730 char *mboxname = NULL;
731 int r = 0;
732
733 /* Parse request */
734 const char *addressbookId = NULL;
735 jmap_changes_parse(req, &parser, req->counters.carddavdeletedmodseq,
736 &_contact_getargs_parse, &addressbookId, &changes, &err);
737 if (err) {
738 jmap_error(req, err);
739 goto done;
740 }
741 mboxname = addressbookId ?
742 carddav_mboxname(req->accountid, addressbookId) : NULL;
743
744 /* Lookup updates. */
745 db = carddav_open_userid(req->accountid);
746 if (!db) {
747 syslog(LOG_ERR,
748 "carddav_open_userid failed for user %s", req->accountid);
749 r = IMAP_INTERNAL;
750 goto done;
751 }
752 struct changes_rock rock = { req, &changes, 0 /*seen_records*/, 0 /*highestmodseq*/};
753 r = carddav_get_updates(db, changes.since_modseq, mboxname, kind,
754 -1 /*max_records*/, &getchanges_cb, &rock);
755 if (r) goto done;
756
757 strip_spurious_deletes(&rock);
758
759 /* Determine new state. */
760 changes.new_modseq = changes.has_more_changes ?
761 rock.highestmodseq : jmap_highestmodseq(req, MBTYPE_ADDRESSBOOK);
762
763 /* Build response */
764 jmap_ok(req, jmap_changes_reply(&changes));
765
766 done:
767 if (r) jmap_error(req, jmap_server_error(r));
768 jmap_changes_fini(&changes);
769 jmap_parser_fini(&parser);
770 carddav_close(db);
771 free(mboxname);
772 return 0;
773 }
774
jmap_contactgroup_changes(struct jmap_req * req)775 static int jmap_contactgroup_changes(struct jmap_req *req)
776 {
777 return _contacts_changes(req, CARDDAV_KIND_GROUP);
778 }
779
_resolve_contactid(struct jmap_req * req,const char * id)780 static const char *_resolve_contactid(struct jmap_req *req, const char *id)
781 {
782 if (id && *id == '#') {
783 return jmap_lookup_id(req, id + 1);
784 }
785 return id;
786 }
787
_add_group_entries(struct jmap_req * req,struct vparse_card * card,json_t * members,json_t * invalid)788 static int _add_group_entries(struct jmap_req *req,
789 struct vparse_card *card, json_t *members,
790 json_t *invalid)
791 {
792 vparse_delete_entries(card, NULL, "X-ADDRESSBOOKSERVER-MEMBER");
793 int r = 0;
794 size_t index;
795 struct buf buf = BUF_INITIALIZER;
796
797 for (index = 0; index < json_array_size(members); index++) {
798 const char *item = _json_array_get_string(members, index);
799 const char *uid = _resolve_contactid(req, item);
800 if (!item || !uid) {
801 buf_printf(&buf, "contactIds[%zu]", index);
802 json_array_append_new(invalid, json_string(buf_cstring(&buf)));
803 buf_reset(&buf);
804 continue;
805 }
806 buf_setcstr(&buf, "urn:uuid:");
807 buf_appendcstr(&buf, uid);
808 vparse_add_entry(card, NULL,
809 "X-ADDRESSBOOKSERVER-MEMBER", buf_cstring(&buf));
810 buf_reset(&buf);
811 }
812
813 buf_free(&buf);
814 return r;
815 }
816
_add_othergroup_entries(struct jmap_req * req,struct vparse_card * card,json_t * members,json_t * invalid)817 static int _add_othergroup_entries(struct jmap_req *req,
818 struct vparse_card *card, json_t *members,
819 json_t *invalid)
820 {
821 vparse_delete_entries(card, NULL, "X-FM-OTHERACCOUNT-MEMBER");
822 int r = 0;
823 struct buf buf = BUF_INITIALIZER;
824 const char *key;
825 json_t *arg;
826 json_object_foreach(members, key, arg) {
827 unsigned i;
828 for (i = 0; i < json_array_size(arg); i++) {
829 const char *item = json_string_value(json_array_get(arg, i));
830 const char *uid = _resolve_contactid(req, item);
831 if (!item || !uid) {
832 buf_printf(&buf, "otherAccountContactIds[%s]", key);
833 json_array_append_new(invalid, json_string(buf_cstring(&buf)));
834 buf_reset(&buf);
835 continue;
836 }
837 buf_setcstr(&buf, "urn:uuid:");
838 buf_appendcstr(&buf, uid);
839 struct vparse_entry *entry =
840 vparse_add_entry(card, NULL,
841 "X-FM-OTHERACCOUNT-MEMBER", buf_cstring(&buf));
842 vparse_add_param(entry, "USERID", key);
843 buf_reset(&buf);
844 }
845 }
846 buf_free(&buf);
847 return r;
848 }
849
_contacts_set(struct jmap_req * req,unsigned kind)850 static void _contacts_set(struct jmap_req *req, unsigned kind)
851 {
852 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
853 struct jmap_set set;
854 json_t *err = NULL;
855 int r = 0;
856
857 struct mailbox *mailbox = NULL;
858 struct mailbox *newmailbox = NULL;
859
860 struct carddav_db *db = carddav_open_userid(req->accountid);
861 if (!db) {
862 r = IMAP_INTERNAL;
863 goto done;
864 }
865
866 /* Parse arguments */
867 jmap_set_parse(req, &parser,
868 kind == CARDDAV_KIND_GROUP ? group_props : contact_props,
869 NULL, NULL, &set, &err);
870 if (err) {
871 jmap_error(req, err);
872 goto done;
873 }
874
875 if (set.if_in_state) {
876 /* TODO rewrite state function to use char* not json_t* */
877 json_t *jstate = json_string(set.if_in_state);
878 if (jmap_cmpstate(req, jstate, MBTYPE_ADDRESSBOOK)) {
879 jmap_error(req, json_pack("{s:s}", "type", "stateMismatch"));
880 json_decref(jstate);
881 goto done;
882 }
883 json_decref(jstate);
884 set.old_state = xstrdup(set.if_in_state);
885 }
886 else {
887 json_t *jstate = jmap_getstate(req, MBTYPE_ADDRESSBOOK, /*refresh*/0);
888 set.old_state = xstrdup(json_string_value(jstate));
889 json_decref(jstate);
890 }
891
892 r = carddav_create_defaultaddressbook(req->accountid);
893 if (r) goto done;
894
895 /* create */
896 const char *key;
897 json_t *arg;
898 json_object_foreach(set.create, key, arg) {
899 json_t *invalid = json_pack("[]");
900 json_t *item = json_pack("{}");
901 r = _contact_set_create(req, kind, arg,
902 NULL, &mailbox, item, invalid);
903 if (r) {
904 json_t *err;
905 switch (r) {
906 case HTTP_FORBIDDEN:
907 case IMAP_PERMISSION_DENIED:
908 err = json_pack("{s:s}", "type", "forbidden");
909 break;
910 case IMAP_QUOTA_EXCEEDED:
911 err = json_pack("{s:s}", "type", "overQuota");
912 break;
913 default:
914 err = jmap_server_error(r);
915 }
916 json_object_set_new(set.not_created, key, err);
917 r = 0;
918 json_decref(item);
919 json_decref(invalid);
920 continue;
921 }
922 if (json_array_size(invalid)) {
923 json_t *err = json_pack("{s:s s:o}",
924 "type", "invalidProperties",
925 "properties", invalid);
926 json_object_set_new(set.not_created, key, err);
927 json_decref(item);
928 continue;
929 }
930 json_decref(invalid);
931
932 /* Report contact as created. */
933 json_object_set_new(set.created, key, item);
934
935 /* Register creation id */
936 jmap_add_id(req, key, json_string_value(json_object_get(item, "id")));
937 }
938
939 /* update */
940 const char *uid;
941 json_object_foreach(set.update, uid, arg) {
942 struct carddav_data *cdata = NULL;
943 r = carddav_lookup_uid(db, uid, &cdata);
944 uint32_t olduid;
945 char *resource = NULL;
946 int do_move = 0;
947 json_t *jupdated = NULL;
948
949 /* is it a valid contact? */
950 if (r || !cdata || !cdata->dav.imap_uid || cdata->kind != kind) {
951 r = 0;
952 json_t *err = json_pack("{s:s}", "type", "notFound");
953 json_object_set_new(set.not_updated, uid, err);
954 continue;
955 }
956
957 json_t *abookid = json_object_get(arg, "addressbookId");
958 if (abookid && json_string_value(abookid)) {
959 const char *mboxname =
960 mboxname_abook(req->accountid, json_string_value(abookid));
961 if (strcmp(mboxname, cdata->dav.mailbox)) {
962 /* move */
963 if (!jmap_hasrights(req, mboxname, JACL_ADDITEMS)) {
964 json_t *err = json_pack("{s:s s:[s]}",
965 "type", "invalidProperties",
966 "properties", "addressbookId");
967 json_object_set_new(set.not_updated, uid, err);
968 continue;
969 }
970 r = jmap_openmbox(req, mboxname, &newmailbox, 1);
971 if (r) {
972 syslog(LOG_ERR, "IOERROR: failed to open %s", mboxname);
973 goto done;
974 }
975 do_move = 1;
976 }
977 json_object_del(arg, "addressbookId");
978 }
979
980 int needrights = do_move ? JACL_UPDATEITEMS : required_set_rights(arg);
981
982 if (!jmap_hasrights(req, cdata->dav.mailbox, needrights)) {
983 int rights = jmap_myrights(req, cdata->dav.mailbox);
984 json_t *err = json_pack("{s:s}", "type",
985 rights & JACL_READITEMS ?
986 "accountReadOnly" : "notFound");
987 json_object_set_new(set.not_updated, uid, err);
988 continue;
989 }
990
991 if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
992 jmap_closembox(req, &mailbox);
993 r = jmap_openmbox(req, cdata->dav.mailbox, &mailbox, 1);
994 if (r) {
995 syslog(LOG_ERR, "IOERROR: failed to open %s",
996 cdata->dav.mailbox);
997 goto done;
998 }
999 }
1000
1001 struct index_record record;
1002
1003 r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record);
1004 if (r) goto done;
1005
1006 olduid = cdata->dav.imap_uid;
1007 resource = xstrdup(cdata->dav.resource);
1008
1009 struct entryattlist *annots = NULL;
1010 strarray_t *flags = NULL;
1011
1012 json_t *invalid = json_pack("[]");
1013
1014 /* Load message containing the resource and parse vcard data */
1015 struct vparse_card *vcard = record_to_vcard(mailbox, &record);
1016 if (!vcard || !vcard->objects) {
1017 syslog(LOG_ERR, "record_to_vcard failed for record %u:%s",
1018 cdata->dav.imap_uid, mailbox->name);
1019 r = 0;
1020 json_t *err = json_pack("{s:s}", "type", "parseError");
1021 json_object_set_new(set.not_updated, uid, err);
1022 goto finish;
1023 }
1024
1025 struct vparse_card *card = vcard->objects;
1026 vparse_replace_entry(card, NULL, "VERSION", "3.0");
1027 vparse_replace_entry(card, NULL, "PRODID", _prodid);
1028
1029 if (kind == CARDDAV_KIND_GROUP) {
1030 json_t *namep = NULL;
1031 json_t *members = NULL;
1032 json_t *others = NULL;
1033 json_t *jval;
1034 const char *key;
1035
1036 json_object_foreach(arg, key, jval) {
1037 if (!strcmp(key, "name")) {
1038 if (json_is_string(jval))
1039 namep = jval;
1040 else json_array_append_new(invalid, json_string("name"));
1041 }
1042 else if (!strcmp(key, "contactIds")) {
1043 members = jval;
1044 }
1045 else if (!strcmp(key, "otherAccountContactIds")) {
1046 others = jval;
1047 }
1048 else if (!strncmp(key, "otherAccountContactIds/", 23)) {
1049 /* Read and apply patch to current card */
1050 json_t *jcurrent = jmap_group_from_vcard(card);
1051 if (!jcurrent) {
1052 syslog(LOG_ERR, "can't read vcard %u:%s for update",
1053 cdata->dav.imap_uid, mailbox->name);
1054 r = 0;
1055 json_t *err = json_pack("{s:s s:s}",
1056 "type", "serverError", "description", "invalid current card");
1057 json_object_set_new(set.not_updated, uid, err);
1058 goto finish;
1059 }
1060 jupdated = jmap_patchobject_apply(jcurrent, arg, NULL);
1061 json_decref(jcurrent);
1062 if (JNOTNULL(jupdated)) {
1063 json_object_del(jupdated, "addressbookId");
1064 /* Now read the updated property value */
1065 others = json_object_get(jupdated, "otherAccountContactIds");
1066 }
1067 }
1068 else if (!strcmp(key, "id") || !strcmp(key, "uid")) {
1069 if (cdata && strcmpnull(cdata->vcard_uid, json_string_value(jval))) {
1070 json_array_append_new(invalid, json_string(key));
1071 }
1072 }
1073 }
1074
1075 if (namep) {
1076 const char *name = json_string_value(namep);
1077 if (name) {
1078 vparse_replace_entry(card, NULL, "FN", name);
1079 vparse_replace_entry(card, NULL, "N", name);
1080 }
1081 }
1082 else if (!vparse_get_entry(card, NULL, "N")) {
1083 struct vparse_entry *entry = vparse_get_entry(card, NULL, "FN");
1084 if (entry) vparse_replace_entry(card, NULL, "N", entry->v.value);
1085 }
1086 if (members) {
1087 _add_group_entries(req, card, members, invalid);
1088 }
1089 if (others) {
1090 _add_othergroup_entries(req, card, others, invalid);
1091 }
1092 }
1093 else {
1094 flags = mailbox_extract_flags(mailbox, &record, req->accountid);
1095 annots = mailbox_extract_annots(mailbox, &record);
1096
1097 r = _json_to_card(req, cdata, card, arg, flags, &annots, invalid);
1098 if (r == HTTP_NO_CONTENT) {
1099 r = 0;
1100 if (!newmailbox) {
1101 /* just bump the modseq
1102 if in the same mailbox and no data change */
1103 syslog(LOG_NOTICE, "jmap: touch contact %s/%s",
1104 req->accountid, resource);
1105 if (strarray_find_case(flags, "\\Flagged", 0) >= 0)
1106 record.system_flags |= FLAG_FLAGGED;
1107 else
1108 record.system_flags &= ~FLAG_FLAGGED;
1109 annotate_state_t *state = NULL;
1110 r = mailbox_get_annotate_state(mailbox, record.uid, &state);
1111 annotate_state_set_auth(state, 0,
1112 req->accountid, req->authstate);
1113 if (!r) r = annotate_state_store(state, annots);
1114 if (!r) r = mailbox_rewrite_index_record(mailbox, &record);
1115 if (!r) json_object_set_new(set.updated, uid, json_null());
1116 goto finish;
1117 }
1118 }
1119 }
1120
1121 if (!r && !json_array_size(invalid)) {
1122 syslog(LOG_NOTICE, "jmap: update %s %s/%s",
1123 kind == CARDDAV_KIND_GROUP ? "group" : "contact",
1124 req->accountid, resource);
1125 r = carddav_store(newmailbox ? newmailbox : mailbox, card, resource,
1126 record.createdmodseq, flags, &annots, req->accountid,
1127 req->authstate, ignorequota);
1128 if (!r)
1129 r = carddav_remove(mailbox, olduid,
1130 /*isreplace*/!newmailbox, req->accountid);
1131 }
1132
1133 if (json_array_size(invalid)) {
1134 json_t *err = json_pack("{s:s s:O}",
1135 "type", "invalidProperties",
1136 "properties", invalid);
1137 json_object_set_new(set.not_updated, uid, err);
1138 goto finish;
1139 }
1140 else if (r) {
1141 json_t *err = NULL;
1142 switch (r) {
1143 case HTTP_FORBIDDEN:
1144 case IMAP_PERMISSION_DENIED:
1145 err = json_pack("{s:s}", "type", "forbidden");
1146 break;
1147 case IMAP_QUOTA_EXCEEDED:
1148 err = json_pack("{s:s}", "type", "overQuota");
1149 break;
1150 default:
1151 err = jmap_server_error(r);
1152 }
1153 json_object_set_new(set.not_updated, uid, err);
1154 goto finish;
1155 }
1156 else json_object_set_new(set.updated, uid, json_null());
1157
1158 finish:
1159 strarray_free(flags);
1160 freeentryatts(annots);
1161 jmap_closembox(req, &newmailbox);
1162 vparse_free_card(vcard);
1163 free(resource);
1164 json_decref(jupdated);
1165 json_decref(invalid);
1166 r = 0;
1167 }
1168
1169
1170 /* destroy */
1171 size_t index;
1172 for (index = 0; index < json_array_size(set.destroy); index++) {
1173 const char *uid = _json_array_get_string(set.destroy, index);
1174 if (!uid) {
1175 json_t *err = json_pack("{s:s}", "type", "invalidArguments");
1176 json_object_set_new(set.not_destroyed, uid, err);
1177 continue;
1178 }
1179 struct carddav_data *cdata = NULL;
1180 uint32_t olduid;
1181 r = carddav_lookup_uid(db, uid, &cdata);
1182
1183 /* is it a valid contact? */
1184 if (r || !cdata || !cdata->dav.imap_uid || cdata->kind != kind) {
1185 r = 0;
1186 json_t *err = json_pack("{s:s}", "type", "notFound");
1187 json_object_set_new(set.not_destroyed, uid, err);
1188 continue;
1189 }
1190 olduid = cdata->dav.imap_uid;
1191
1192 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_REMOVEITEMS)) {
1193 int rights = jmap_myrights(req, cdata->dav.mailbox);
1194 json_t *err = json_pack("{s:s}", "type",
1195 rights & JACL_READITEMS ?
1196 "accountReadOnly" : "notFound");
1197 json_object_set_new(set.not_destroyed, uid, err);
1198 continue;
1199 }
1200
1201 if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
1202 jmap_closembox(req, &mailbox);
1203 r = jmap_openmbox(req, cdata->dav.mailbox, &mailbox, 1);
1204 if (r) goto done;
1205 }
1206
1207 syslog(LOG_NOTICE,
1208 "jmap: remove %s %s/%s",
1209 kind == CARDDAV_KIND_GROUP ? "group" : "contact",
1210 req->accountid, uid);
1211 r = carddav_remove(mailbox, olduid, /*isreplace*/0, req->accountid);
1212 if (r) {
1213 xsyslog(LOG_ERR, "IOERROR: carddav remove failed",
1214 "kind=<%s> mailbox=<%s> olduid=<%u>",
1215 kind == CARDDAV_KIND_GROUP ? "group" : "contact",
1216 mailbox->name, olduid);
1217 goto done;
1218 }
1219
1220 json_array_append_new(set.destroyed, json_string(uid));
1221 }
1222
1223 /* force modseq to stable */
1224 if (mailbox) mailbox_unlock_index(mailbox, NULL);
1225
1226 // TODO refactor jmap_getstate to return a string, once
1227 // all code has been migrated to the new JMAP parser.
1228 json_t *jstate = jmap_getstate(req, MBTYPE_ADDRESSBOOK, /*refresh*/1);
1229 set.new_state = xstrdup(json_string_value(jstate));
1230 json_decref(jstate);
1231
1232 jmap_ok(req, jmap_set_reply(&set));
1233 r = 0;
1234
1235 done:
1236 if (r) jmap_error(req, jmap_server_error(r));
1237 jmap_parser_fini(&parser);
1238 jmap_set_fini(&set);
1239 jmap_closembox(req, &newmailbox);
1240 jmap_closembox(req, &mailbox);
1241
1242 carddav_close(db);
1243 }
1244
jmap_contactgroup_set(struct jmap_req * req)1245 static int jmap_contactgroup_set(struct jmap_req *req)
1246 {
1247 _contacts_set(req, CARDDAV_KIND_GROUP);
1248 return 0;
1249 }
1250
1251 /* Extract separate y,m,d from YYYY-MM-DD or (with ignore_hyphens) YYYYMMDD
1252 *
1253 * This handles birthday/anniversary and BDAY/ANNIVERSARY for JMAP and vCard
1254 *
1255 * JMAP dates are _always_ YYYY-MM-DD, so use require_hyphens = 1
1256 *
1257 * For vCard, this handles "date-value" from RFC 2426 (which is "date" from
1258 * RFC 2425), used by BDAY (ANNIVERSARY isn't in vCard 3). vCard 4 says BDAY and
1259 * ANNIVERSARY is date-and-or-time, which is far more complicated. I haven't
1260 * seen that in the wild yet and hope I never do.
1261 */
_parse_date(const char * date,unsigned * y,unsigned * m,unsigned * d,int require_hyphens)1262 static int _parse_date(const char *date, unsigned *y,
1263 unsigned *m, unsigned *d, int require_hyphens)
1264 {
1265 /* there isn't a convenient libc function that will let us convert parts of
1266 * a string to integer and only take digit characters, so we just pull it
1267 * apart ourselves */
1268
1269 const char *yp = NULL, *mp = NULL, *dp = NULL;
1270
1271 /* getting pointers to the ymd components, skipping hyphens if necessary.
1272 * format checking as we go. no need to strlen() beforehand, it will fall
1273 * out of the range checks. */
1274 yp = date;
1275 if (yp[0] < '0' || yp[0] > '9' ||
1276 yp[1] < '0' || yp[1] > '9' ||
1277 yp[2] < '0' || yp[2] > '9' ||
1278 yp[3] < '0' || yp[3] > '9') return -1;
1279
1280 mp = &yp[4];
1281
1282 if (*mp == '-') mp++;
1283 else if (require_hyphens) return -1;
1284
1285 if (mp[0] < '0' || mp[0] > '9' ||
1286 mp[1] < '0' || mp[1] > '9') return -1;
1287
1288 dp = &mp[2];
1289
1290 if (*dp == '-') dp++;
1291 else if (require_hyphens) return -1;
1292
1293 if (dp[0] < '0' || dp[0] > '9' ||
1294 dp[1] < '0' || dp[1] > '9') return -1;
1295
1296 if (dp[2] != '\0') return -1;
1297
1298 /* convert to integer. ascii digits are 0x30-0x37, so we can take bottom
1299 * four bits and multiply */
1300 *y =
1301 (yp[0] & 0xf) * 1000 +
1302 (yp[1] & 0xf) * 100 +
1303 (yp[2] & 0xf) * 10 +
1304 (yp[3] & 0xf);
1305
1306 *m =
1307 (mp[0] & 0xf) * 10 +
1308 (mp[1] & 0xf);
1309
1310 *d =
1311 (dp[0] & 0xf) * 10 +
1312 (dp[1] & 0xf);
1313
1314 return 0;
1315 }
1316
_date_to_jmap(struct vparse_entry * entry,struct buf * buf)1317 static void _date_to_jmap(struct vparse_entry *entry, struct buf *buf)
1318 {
1319 if (!entry)
1320 goto no_date;
1321
1322 unsigned y, m, d;
1323 if (_parse_date(entry->v.value, &y, &m, &d, 0))
1324 goto no_date;
1325
1326 if (y < 1604 || m > 12 || d > 31)
1327 goto no_date;
1328
1329 const struct vparse_param *param;
1330 for (param = entry->params; param; param = param->next) {
1331 if (!strcasecmp(param->name, "x-apple-omit-year"))
1332 /* XXX compare value with actual year? */
1333 y = 0;
1334 if (!strcasecmp(param->name, "x-fm-no-month"))
1335 m = 0;
1336 if (!strcasecmp(param->name, "x-fm-no-day"))
1337 d = 0;
1338 }
1339
1340 /* sigh, magic year 1604 has been seen without X-APPLE-OMIT-YEAR, making
1341 * me wonder what the bloody point is */
1342 if (y == 1604)
1343 y = 0;
1344
1345 buf_reset(buf);
1346 buf_printf(buf, "%04d-%02d-%02d", y, m, d);
1347 return;
1348
1349 no_date:
1350 buf_setcstr(buf, "0000-00-00");
1351 }
1352
_servicetype(const char * type)1353 static const char *_servicetype(const char *type)
1354 {
1355 /* add new services here */
1356 if (!strcasecmp(type, "aim")) return "AIM";
1357 if (!strcasecmp(type, "facebook")) return "Facebook";
1358 if (!strcasecmp(type, "flickr")) return "Flickr";
1359 if (!strcasecmp(type, "gadugadu")) return "GaduGadu";
1360 if (!strcasecmp(type, "github")) return "GitHub";
1361 if (!strcasecmp(type, "googletalk")) return "GoogleTalk";
1362 if (!strcasecmp(type, "icq")) return "ICQ";
1363 if (!strcasecmp(type, "jabber")) return "Jabber";
1364 if (!strcasecmp(type, "linkedin")) return "LinkedIn";
1365 if (!strcasecmp(type, "msn")) return "MSN";
1366 if (!strcasecmp(type, "myspace")) return "MySpace";
1367 if (!strcasecmp(type, "qq")) return "QQ";
1368 if (!strcasecmp(type, "skype")) return "Skype";
1369 if (!strcasecmp(type, "twitter")) return "Twitter";
1370 if (!strcasecmp(type, "yahoo")) return "Yahoo";
1371
1372 syslog(LOG_NOTICE, "unknown service type %s", type);
1373 return type;
1374 }
1375
1376 /* Convert the VCARD card to jmap properties */
jmap_contact_from_vcard(struct vparse_card * card,struct mailbox * mailbox,struct index_record * record)1377 static json_t *jmap_contact_from_vcard(struct vparse_card *card,
1378 struct mailbox *mailbox,
1379 struct index_record *record)
1380 {
1381 strarray_t *empty = NULL;
1382 json_t *obj = json_pack("{}");
1383 struct buf buf = BUF_INITIALIZER;
1384 struct vparse_entry *entry;
1385
1386 const strarray_t *n = vparse_multival(card, "n");
1387 const strarray_t *org = vparse_multival(card, "org");
1388 if (!n) n = empty ? empty : (empty = strarray_new());
1389 if (!org) org = empty ? empty : (empty = strarray_new());
1390
1391 /* name fields: Family; Given; Middle; Prefix; Suffix. */
1392
1393 const char *family = strarray_safenth(n, 0);
1394 json_object_set_new(obj, "lastName", jmap_utf8string(family));
1395
1396 /* JMAP doesn't have a separate field for Middle (aka "Additional
1397 * Names"), so we just mash them into firstName. See reverse of this in
1398 * _json_to_card */
1399 const char *given = strarray_safenth(n, 1);
1400 const char *middle = strarray_safenth(n, 2);
1401 buf_setcstr(&buf, given);
1402 if (*middle) {
1403 buf_putc(&buf, ' ');
1404 buf_appendcstr(&buf, middle);
1405 }
1406 json_object_set_new(obj, "firstName", jmap_utf8string(buf_cstring(&buf)));
1407
1408 const char *prefix = strarray_safenth(n, 3);
1409 json_object_set_new(obj, "prefix",
1410 jmap_utf8string(prefix)); /* just prefix */
1411
1412 const char *suffix = strarray_safenth(n, 4);
1413 json_object_set_new(obj, "suffix",
1414 jmap_utf8string(suffix)); /* just suffix */
1415
1416 json_object_set_new(obj, "company",
1417 jmap_utf8string(strarray_safenth(org, 0)));
1418 json_object_set_new(obj, "department",
1419 jmap_utf8string(strarray_safenth(org, 1)));
1420
1421 /* we used to store jobTitle in ORG[2] instead of TITLE, which confused
1422 * CardDAV clients. that's fixed, but there's now lots of cards with it
1423 * stored in the wrong place, so check both */
1424 const char *item = vparse_stringval(card, "title");
1425 if (!item)
1426 item = strarray_safenth(org, 2);
1427 json_object_set_new(obj, "jobTitle", jmap_utf8string(item));
1428
1429 json_t *adr = json_array();
1430
1431 for (entry = card->properties; entry; entry = entry->next) {
1432 if (strcasecmp(entry->name, "adr")) continue;
1433 json_t *item = json_pack("{}");
1434
1435 /* XXX - type and label */
1436 const strarray_t *a = entry->v.values;
1437
1438 const struct vparse_param *param;
1439 const char *type = "other";
1440 const char *label = NULL;
1441 for (param = entry->params; param; param = param->next) {
1442 if (!strcasecmp(param->name, "type")) {
1443 if (!strcasecmp(param->value, "home")) {
1444 type = "home";
1445 }
1446 else if (!strcasecmp(param->value, "work")) {
1447 type = "work";
1448 }
1449 else if (!strcasecmp(param->value, "billing")) {
1450 type = "billing";
1451 }
1452 else if (!strcasecmp(param->value, "postal")) {
1453 type = "postal";
1454 }
1455 }
1456 else if (!strcasecmp(param->name, "label")) {
1457 label = param->value;
1458 }
1459 }
1460 json_object_set_new(item, "type", json_string(type));
1461 json_object_set_new(item, "label", label ? json_string(label) : json_null());
1462
1463 const char *pobox = strarray_safenth(a, 0);
1464 const char *extended = strarray_safenth(a, 1);
1465 const char *street = strarray_safenth(a, 2);
1466 buf_reset(&buf);
1467 if (*pobox) {
1468 buf_appendcstr(&buf, pobox);
1469 if (extended || street) buf_putc(&buf, '\n');
1470 }
1471 if (*extended) {
1472 buf_appendcstr(&buf, extended);
1473 if (street) buf_putc(&buf, '\n');
1474 }
1475 if (*street) {
1476 buf_appendcstr(&buf, street);
1477 }
1478
1479 json_object_set_new(item, "street",
1480 jmap_utf8string(buf_cstring(&buf)));
1481 json_object_set_new(item, "locality",
1482 jmap_utf8string(strarray_safenth(a, 3)));
1483 json_object_set_new(item, "region",
1484 jmap_utf8string(strarray_safenth(a, 4)));
1485 json_object_set_new(item, "postcode",
1486 jmap_utf8string(strarray_safenth(a, 5)));
1487 json_object_set_new(item, "country",
1488 jmap_utf8string(strarray_safenth(a, 6)));
1489
1490 json_array_append_new(adr, item);
1491 }
1492
1493 json_object_set_new(obj, "addresses", adr);
1494
1495 /* emails - we need to open code this, because it's repeated */
1496 json_t *emails = json_array();
1497
1498 int defaultIndex = -1;
1499 int i = 0;
1500 for (entry = card->properties; entry; entry = entry->next) {
1501 if (strcasecmp(entry->name, "email")) continue;
1502 json_t *item = json_pack("{}");
1503 const struct vparse_param *param;
1504 const char *type = "other";
1505 const char *label = NULL;
1506 for (param = entry->params; param; param = param->next) {
1507 if (!strcasecmp(param->name, "type")) {
1508 if (!strcasecmp(param->value, "home")) {
1509 type = "personal";
1510 }
1511 else if (!strcasecmp(param->value, "work")) {
1512 type = "work";
1513 }
1514 else if (!strcasecmp(param->value, "pref")) {
1515 if (defaultIndex < 0)
1516 defaultIndex = i;
1517 }
1518 }
1519 else if (!strcasecmp(param->name, "label")) {
1520 label = param->value;
1521 }
1522 }
1523 json_object_set_new(item, "type", json_string(type));
1524 if (label) json_object_set_new(item, "label", json_string(label));
1525
1526 json_object_set_new(item, "value", jmap_utf8string(entry->v.value));
1527
1528 json_array_append_new(emails, item);
1529 i++;
1530 }
1531
1532 if (defaultIndex < 0)
1533 defaultIndex = 0;
1534 int size = json_array_size(emails);
1535 for (i = 0; i < size; i++) {
1536 json_t *item = json_array_get(emails, i);
1537 json_object_set_new(item, "isDefault",
1538 i == defaultIndex ? json_true() : json_false());
1539 }
1540
1541 json_object_set_new(obj, "emails", emails);
1542
1543 /* address - we need to open code this, because it's repeated */
1544 json_t *phones = json_array();
1545
1546 for (entry = card->properties; entry; entry = entry->next) {
1547 if (strcasecmp(entry->name, "tel")) continue;
1548 json_t *item = json_pack("{}");
1549 const struct vparse_param *param;
1550 const char *type = "other";
1551 const char *label = NULL;
1552 for (param = entry->params; param; param = param->next) {
1553 if (!strcasecmp(param->name, "type")) {
1554 if (!strcasecmp(param->value, "home")) {
1555 type = "home";
1556 }
1557 else if (!strcasecmp(param->value, "work")) {
1558 type = "work";
1559 }
1560 else if (!strcasecmp(param->value, "cell")) {
1561 type = "mobile";
1562 }
1563 else if (!strcasecmp(param->value, "mobile")) {
1564 type = "mobile";
1565 }
1566 else if (!strcasecmp(param->value, "fax")) {
1567 type = "fax";
1568 }
1569 else if (!strcasecmp(param->value, "pager")) {
1570 type = "pager";
1571 }
1572 }
1573 else if (!strcasecmp(param->name, "label")) {
1574 label = param->value;
1575 }
1576 }
1577 json_object_set_new(item, "type", json_string(type));
1578 if (label) json_object_set_new(item, "label", json_string(label));
1579
1580 json_object_set_new(item, "value", jmap_utf8string(entry->v.value));
1581
1582 json_array_append_new(phones, item);
1583 }
1584
1585 json_object_set_new(obj, "phones", phones);
1586
1587 /* address - we need to open code this, because it's repeated */
1588 json_t *online = json_array();
1589
1590 for (entry = card->properties; entry; entry = entry->next) {
1591 if (!strcasecmp(entry->name, "url")) {
1592 json_t *item = json_pack("{}");
1593 const struct vparse_param *param;
1594 const char *label = NULL;
1595 for (param = entry->params; param; param = param->next) {
1596 if (!strcasecmp(param->name, "label")) {
1597 label = param->value;
1598 }
1599 }
1600 json_object_set_new(item, "type", json_string("uri"));
1601 if (label) json_object_set_new(item, "label", json_string(label));
1602 json_object_set_new(item, "value", json_string(entry->v.value));
1603 json_array_append_new(online, item);
1604 }
1605 if (!strcasecmp(entry->name, "impp")) {
1606 json_t *item = json_pack("{}");
1607 const struct vparse_param *param;
1608 const char *label = NULL;
1609 for (param = entry->params; param; param = param->next) {
1610 if (!strcasecmp(param->name, "x-service-type")) {
1611 label = _servicetype(param->value);
1612 }
1613 }
1614 json_object_set_new(item, "type", json_string("username"));
1615 if (label) json_object_set_new(item, "label", json_string(label));
1616 json_object_set_new(item, "value", jmap_utf8string(entry->v.value));
1617 json_array_append_new(online, item);
1618 }
1619 if (!strcasecmp(entry->name, "x-social-profile")) {
1620 json_t *item = json_pack("{}");
1621 const struct vparse_param *param;
1622 const char *label = NULL;
1623 const char *value = NULL;
1624 for (param = entry->params; param; param = param->next) {
1625 if (!strcasecmp(param->name, "type")) {
1626 label = _servicetype(param->value);
1627 }
1628 if (!strcasecmp(param->name, "x-user")) {
1629 value = param->value;
1630 }
1631 }
1632 json_object_set_new(item, "type", json_string("username"));
1633 if (label) json_object_set_new(item, "label", json_string(label));
1634 json_object_set_new(item, "value",
1635 jmap_utf8string(value ? value : entry->v.value));
1636 json_array_append_new(online, item);
1637 }
1638 if (!strcasecmp(entry->name, "x-fm-online-other")) {
1639 json_t *item = json_pack("{}");
1640 const struct vparse_param *param;
1641 const char *label = NULL;
1642 for (param = entry->params; param; param = param->next) {
1643 if (!strcasecmp(param->name, "label")) {
1644 label = param->value;
1645 }
1646 }
1647 json_object_set_new(item, "type", json_string("other"));
1648 if (label) json_object_set_new(item, "label", json_string(label));
1649 json_object_set_new(item, "value", jmap_utf8string(entry->v.value));
1650 json_array_append_new(online, item);
1651 }
1652 }
1653
1654 json_object_set_new(obj, "online", online);
1655
1656 item = vparse_stringval(card, "nickname");
1657 json_object_set_new(obj, "nickname", jmap_utf8string(item ? item : ""));
1658
1659 entry = vparse_get_entry(card, NULL, "anniversary");
1660 _date_to_jmap(entry, &buf);
1661 json_object_set_new(obj, "anniversary", jmap_utf8string(buf_cstring(&buf)));
1662
1663 entry = vparse_get_entry(card, NULL, "bday");
1664 _date_to_jmap(entry, &buf);
1665 json_object_set_new(obj, "birthday", jmap_utf8string(buf_cstring(&buf)));
1666
1667 item = vparse_stringval(card, "note");
1668 json_object_set_new(obj, "notes", jmap_utf8string(item ? item : ""));
1669
1670 item = vparse_stringval(card, "photo");
1671 json_object_set_new(obj, "x-hasPhoto",
1672 item ? json_true() : json_false());
1673
1674 struct vparse_entry *photo = vparse_get_entry(card, NULL, "photo");
1675 struct message_guid guid;
1676 char *type = NULL;
1677 json_t *file;
1678
1679 if (photo &&
1680 (size = vcard_prop_decode_value(photo, NULL, &type, &guid))) {
1681 char blob_id[JMAP_BLOBID_SIZE];
1682 jmap_set_blobid(&guid, blob_id);
1683
1684 file = json_pack("{s:s s:i s:s? s:n}",
1685 "blobId", blob_id, "size", size,
1686 "type", type, "name");
1687 }
1688 else file = json_null();
1689
1690 json_object_set_new(obj, "avatar", file);
1691 free(type);
1692
1693 // record properties
1694
1695 json_object_set_new(obj, "isFlagged",
1696 record->system_flags & FLAG_FLAGGED ? json_true() :
1697 json_false());
1698
1699 const char *annot = DAV_ANNOT_NS "<" XML_NS_CYRUS ">importance";
1700 // NOTE: using buf_free here because annotatemore_msg_lookup uses
1701 // buf_init_ro on the buffer, which blats the base pointer.
1702 buf_free(&buf);
1703 annotatemore_msg_lookup(mailbox->name, record->uid, annot, "", &buf);
1704 double val = 0;
1705 if (buf.len) val = strtod(buf_cstring(&buf), NULL);
1706
1707 // need to keep the x- version while AJAXUI is around
1708 json_object_set_new(obj, "importance", json_real(val));
1709
1710 /* XXX - other fields */
1711
1712 buf_free(&buf);
1713 if (empty) strarray_free(empty);
1714
1715 return obj;
1716 }
1717
_encode_contact_blobid(struct carddav_data * cdata,struct buf * dst)1718 static const char *_encode_contact_blobid(struct carddav_data *cdata,
1719 struct buf *dst)
1720 {
1721 /* Set vCard smart blob prefix */
1722 buf_putc(dst, 'V');
1723
1724 /* Encode vCard UID */
1725 char *b64uid =
1726 jmap_encode_base64_nopad(cdata->vcard_uid, strlen(cdata->vcard_uid));
1727 if (!b64uid) {
1728 buf_reset(dst);
1729 return NULL;
1730 }
1731 buf_appendcstr(dst, b64uid);
1732 free(b64uid);
1733
1734 /* Encode modseq */
1735 buf_printf(dst, "-" MODSEQ_FMT, cdata->dav.modseq);
1736
1737 return buf_cstring(dst);
1738 }
1739
_decode_contact_blobid(const char * blobid,char ** uidptr,modseq_t * modseqptr)1740 static int _decode_contact_blobid(const char *blobid,
1741 char **uidptr,
1742 modseq_t *modseqptr)
1743 {
1744 char *uid = NULL;
1745 modseq_t modseq = 0;
1746 int is_valid = 0;
1747
1748 /* Decode vCard UID */
1749 const char *base = blobid+1;
1750 const char *p = strchr(base, '-');
1751 if (!p) goto done;
1752 uid = jmap_decode_base64_nopad(base, p-base);
1753 if (!uid) goto done;
1754 base = p + 1;
1755
1756 /* Decode modseq */
1757 if (*base == '\0') goto done;
1758 char *endptr = NULL;
1759 errno = 0;
1760 modseq = strtoull(base, &endptr, 10);
1761 if (errno == ERANGE || (*endptr && *endptr != '-')) {
1762 goto done;
1763 }
1764 base = endptr;
1765
1766 /* All done */
1767 *uidptr = uid;
1768 *modseqptr = modseq;
1769 is_valid = 1;
1770
1771 done:
1772 if (!is_valid) {
1773 free(uid);
1774 }
1775 return is_valid;
1776 }
1777
jmap_contact_getblob(jmap_req_t * req,const char * blobid,const char * accept_mime)1778 static int jmap_contact_getblob(jmap_req_t *req,
1779 const char *blobid,
1780 const char *accept_mime)
1781 {
1782 struct carddav_db *db = NULL;
1783 struct carddav_data *cdata = NULL;
1784 struct mailbox *mailbox = NULL;
1785 struct vparse_card *vcard = NULL;
1786 char *uid = NULL;
1787 modseq_t modseq;
1788 struct buf buf = BUF_INITIALIZER;
1789 int res = 0;
1790 int r;
1791
1792 if (*blobid != 'V') return 0;
1793
1794 if (!_decode_contact_blobid(blobid, &uid, &modseq)) {
1795 res = HTTP_BAD_REQUEST;
1796 goto done;
1797 }
1798
1799 /* Lookup uid in CarddavDB */
1800 db = carddav_open_userid(req->accountid);
1801 if (!db) {
1802 req->txn->error.desc = "no addressbook db";
1803 res = HTTP_SERVER_ERROR;
1804 goto done;
1805 }
1806 if (carddav_lookup_uid(db, uid, &cdata)) {
1807 res = HTTP_NOT_FOUND;
1808 goto done;
1809 }
1810 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS)) {
1811 res = HTTP_NOT_FOUND;
1812 goto done;
1813 }
1814
1815 /* Validate modseq */
1816 if (modseq != cdata->dav.modseq) {
1817 res = HTTP_NOT_FOUND;
1818 goto done;
1819 }
1820
1821 /* Open mailbox, we need it now */
1822 if ((r = jmap_openmbox(req, cdata->dav.mailbox, &mailbox, 0))) {
1823 req->txn->error.desc = error_message(r);
1824 res = HTTP_SERVER_ERROR;
1825 goto done;
1826 }
1827
1828 /* Make sure client can handle blob type. */
1829 if (accept_mime) {
1830 if (strcmp(accept_mime, "application/octet-stream") &&
1831 strcmp(accept_mime, "text/vcard")) {
1832 res = HTTP_NOT_ACCEPTABLE;
1833 goto done;
1834 }
1835 }
1836
1837 /* Load vCard data */
1838 struct index_record record;
1839 if (!mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record)) {
1840 vcard = record_to_vcard(mailbox, &record);
1841 }
1842 if (!vcard) {
1843 req->txn->error.desc = "failed to load record";
1844 res = HTTP_SERVER_ERROR;
1845 goto done;
1846 }
1847
1848 /* Write blob to socket */
1849 char *content_type = NULL;
1850 if (!accept_mime || !strcmp(accept_mime, "text/vcard")) {
1851 struct vparse_entry *entry =
1852 vparse_get_entry(vcard->objects, NULL, "VERSION");
1853 if (entry) {
1854 content_type =
1855 strconcat("text/vcard; version=", entry->v.value, NULL);
1856 req->txn->resp_body.type = content_type;
1857 }
1858 }
1859 if (!req->txn->resp_body.type) {
1860 req->txn->resp_body.type = accept_mime;
1861 }
1862
1863 /* Write body */
1864 vparse_tobuf(vcard, &buf);
1865 req->txn->resp_body.len = buf_len(&buf);
1866 write_body(HTTP_OK, req->txn, buf_base(&buf), buf_len(&buf));
1867 free(content_type);
1868 res = HTTP_OK;
1869
1870 done:
1871 if (res != HTTP_OK && !req->txn->error.desc) {
1872 const char *desc = NULL;
1873 switch (res) {
1874 case HTTP_BAD_REQUEST:
1875 desc = "invalid contact blobid";
1876 break;
1877 case HTTP_NOT_FOUND:
1878 desc = "failed to find blob by contact blobid";
1879 break;
1880 default:
1881 desc = error_message(res);
1882 }
1883 req->txn->error.desc = desc;
1884 }
1885 if (vcard) vparse_free_card(vcard);
1886 if (mailbox) jmap_closembox(req, &mailbox);
1887 if (db) carddav_close(db);
1888 buf_free(&buf);
1889 free(uid);
1890 return res;
1891 }
1892
getcontacts_cb(void * rock,struct carddav_data * cdata)1893 static int getcontacts_cb(void *rock, struct carddav_data *cdata)
1894 {
1895 struct cards_rock *crock = (struct cards_rock *) rock;
1896 struct index_record record;
1897 json_t *obj = NULL;
1898 int r = 0;
1899
1900 if (!jmap_hasrights(crock->req, cdata->dav.mailbox, JACL_READITEMS))
1901 return 0;
1902
1903 if (cdata->jmapversion == JMAPCACHE_CONTACTVERSION) {
1904 json_error_t jerr;
1905 obj = json_loads(cdata->jmapdata, 0, &jerr);
1906 goto gotvalue;
1907 }
1908
1909 if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
1910 mailbox_close(&crock->mailbox);
1911 r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
1912 if (r) return r;
1913 }
1914
1915 r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
1916 if (r) return r;
1917
1918 /* Load message containing the resource and parse vcard data */
1919 struct vparse_card *vcard = record_to_vcard(crock->mailbox, &record);
1920 if (!vcard || !vcard->objects) {
1921 syslog(LOG_ERR, "record_to_vcard failed for record %u:%s",
1922 cdata->dav.imap_uid, crock->mailbox->name);
1923 vparse_free_card(vcard);
1924 return IMAP_INTERNAL;
1925 }
1926
1927 /* Convert the VCARD to a JMAP contact. */
1928 obj = jmap_contact_from_vcard(vcard->objects, crock->mailbox, &record);
1929 vparse_free_card(vcard);
1930
1931 gotvalue:
1932 jmap_filterprops(obj, crock->get->props);
1933
1934 if (jmap_wantprop(crock->get->props, "x-href")) {
1935 char *xhref = jmap_xhref(cdata->dav.mailbox, cdata->dav.resource);
1936 json_object_set_new(obj, "x-href", json_string(xhref));
1937 free(xhref);
1938 }
1939 if (jmap_wantprop(crock->get->props, "blobId")) {
1940 json_t *jblobid = json_null();
1941 struct buf blobid = BUF_INITIALIZER;
1942 if (_encode_contact_blobid(cdata, &blobid)) {
1943 jblobid = json_string(buf_cstring(&blobid));
1944 }
1945 buf_free(&blobid);
1946 json_object_set_new(obj, "blobId", jblobid);
1947 }
1948
1949 json_object_set_new(obj, "id", json_string(cdata->vcard_uid));
1950 json_object_set_new(obj, "uid", json_string(cdata->vcard_uid));
1951
1952 json_object_set_new(obj, "addressbookId",
1953 json_string(strrchr(cdata->dav.mailbox, '.')+1));
1954
1955 json_array_append_new(crock->get->list, obj);
1956 crock->rows++;
1957
1958 return 0;
1959 }
1960
jmap_contact_get(struct jmap_req * req)1961 static int jmap_contact_get(struct jmap_req *req)
1962 {
1963 return _contacts_get(req, &getcontacts_cb, CARDDAV_KIND_CONTACT);
1964 }
1965
jmap_contact_changes(struct jmap_req * req)1966 static int jmap_contact_changes(struct jmap_req *req)
1967 {
1968 return _contacts_changes(req, CARDDAV_KIND_CONTACT);
1969 }
1970
1971 struct contact_textfilter {
1972 strarray_t terms;
1973 bitvector_t matched_terms;
1974 strarray_t phrases;
1975 bitvector_t matched_phrases;
1976 int is_any;
1977 };
1978
contact_textfilter_add_to_termset(const char * term,void * termset)1979 static int contact_textfilter_add_to_termset(const char *term, void *termset)
1980 {
1981 hash_insert(term, (void*)1, (hash_table*)termset);
1982 return 0;
1983 }
1984
contact_textfilter_add_to_strarray(const char * term,void * sa)1985 static int contact_textfilter_add_to_strarray(const char *term, void *sa)
1986 {
1987 strarray_append((strarray_t*)sa, term);
1988 return 0;
1989 }
1990
contact_textfilter_new(const char * query)1991 static struct contact_textfilter *contact_textfilter_new(const char *query)
1992 {
1993 struct contact_textfilter *f = xzmalloc(sizeof(struct contact_textfilter));
1994 struct buf buf = BUF_INITIALIZER;
1995 const char *p, *q;
1996 int in_phrase = 0;
1997 xapian_doc_t *doc = xapian_doc_new();
1998
1999 /* Parse query string into loose terms and phrases */
2000 for (p = query, q = query; *p; p++) {
2001 if (in_phrase) {
2002 if (*p == '\'' || *(p+1) == '\0') {
2003 // end of phrase
2004 if (*p != '\'') {
2005 buf_putc(&buf, *p);
2006 }
2007 if (buf_len(&buf)) {
2008 strarray_append(&f->phrases, buf_cstring(&buf));
2009 buf_reset(&buf);
2010 }
2011 in_phrase = 0;
2012 }
2013 else if (*p == '\\') {
2014 // escape character within phrase
2015 switch (*(p+1)) {
2016 case '"':
2017 case '\'':
2018 case '\\':
2019 buf_putc(&buf, *(p+1));
2020 p++;
2021 break;
2022 default:
2023 buf_putc(&buf, *p);
2024 }
2025 }
2026 else buf_putc(&buf, *p);
2027 }
2028 else {
2029 if (*p == '\'' || *(p+1) == '\0') {
2030 // end of loose terms
2031 if (q) {
2032 const char *end = *p == '\'' ? p : p + 1;
2033 xapian_doc_index_text(doc, q, end - q);
2034 }
2035 if (*p == '\'') {
2036 //start of phrase
2037 in_phrase = 1;
2038 }
2039 q = NULL;
2040 }
2041 else if (!q) {
2042 // start of loose terms
2043 q = p;
2044 }
2045 }
2046 }
2047
2048 /* Add loose terms to matcher */
2049 xapian_doc_foreach_term(doc, contact_textfilter_add_to_strarray, &f->terms);
2050
2051 /* Initialize state */
2052 bv_init(&f->matched_phrases);
2053 bv_init(&f->matched_terms);
2054 bv_setsize(&f->matched_phrases, (unsigned) strarray_size(&f->phrases));
2055 bv_setsize(&f->matched_terms, (unsigned) strarray_size(&f->terms));
2056
2057 xapian_doc_close(doc);
2058 buf_free(&buf);
2059 return f;
2060 }
2061
contact_textfilter_match(struct contact_textfilter * f,const char * text,hash_table * termset)2062 static int contact_textfilter_match(struct contact_textfilter *f, const char *text, hash_table *termset)
2063 {
2064 int matches = 0;
2065
2066 if (!f->is_any) {
2067 bv_clearall(&f->matched_phrases);
2068 bv_clearall(&f->matched_terms);
2069 }
2070
2071 /* Validate phrase search */
2072 int i;
2073 for (i = 0; i < strarray_size(&f->phrases); i++) {
2074 const char *phrase = strarray_nth(&f->phrases, i);
2075 if (stristr(text, phrase)) {
2076 bv_set(&f->matched_phrases, i);
2077 if (f->is_any) {
2078 matches = 1;
2079 goto done;
2080 }
2081 }
2082 else if (!f->is_any) goto done;
2083 }
2084
2085 /* Validate loose term search */
2086 if (!termset->size) {
2087 /* Extract terms from text and store result in termset */
2088 xapian_doc_t *doc = xapian_doc_new();
2089 xapian_doc_index_text(doc, text, strlen(text));
2090 if (!xapian_doc_termcount(doc)) {
2091 xapian_doc_close(doc);
2092 goto done;
2093 }
2094 construct_hash_table(termset, xapian_doc_termcount(doc), 0);
2095 xapian_doc_foreach_term(doc, contact_textfilter_add_to_termset, termset);
2096 xapian_doc_close(doc);
2097 }
2098 for (i = 0; i < strarray_size(&f->terms); i++) {
2099 const char *term = strarray_nth(&f->terms, i);
2100 if (hash_lookup(term, termset)) {
2101 bv_set(&f->matched_terms, i);
2102 if (f->is_any) {
2103 matches = 1;
2104 goto done;
2105 }
2106 }
2107 else if (!f->is_any) goto done;
2108 }
2109
2110 /* All loose terms and phrases matched */
2111 matches = 1;
2112
2113 done:
2114 return matches;
2115 }
2116
contact_textfilter_reset(struct contact_textfilter * f)2117 static void contact_textfilter_reset(struct contact_textfilter *f)
2118 {
2119 bv_clearall(&f->matched_phrases);
2120 bv_clearall(&f->matched_terms);
2121 }
2122
2123
contact_textfilter_free(struct contact_textfilter * f)2124 static void contact_textfilter_free(struct contact_textfilter *f)
2125 {
2126 if (f) {
2127 strarray_fini(&f->terms);
2128 bv_fini(&f->matched_terms);
2129 strarray_fini(&f->phrases);
2130 bv_fini(&f->matched_phrases);
2131 free(f);
2132 }
2133 }
2134
contact_textfilter_matched_all(struct contact_textfilter * f)2135 static int contact_textfilter_matched_all(struct contact_textfilter *f)
2136 {
2137 return bv_count(&f->matched_terms) == (unsigned) strarray_size(&f->terms) &&
2138 bv_count(&f->matched_phrases) == (unsigned) strarray_size(&f->phrases);
2139 }
2140
2141 struct named_termset {
2142 const char *propname;
2143 hash_table termset;
2144 };
2145
2146 struct contactsquery_filter_rock {
2147 struct carddav_db *carddavdb;
2148 struct carddav_data *cdata;
2149 json_t *entry;
2150 ptrarray_t cached_termsets; // list of named_termset
2151 };
2152
2153 struct contact_filter {
2154 hash_table *inContactGroup;
2155 json_t *isFlagged;
2156 const char *uid;
2157 struct contact_textfilter *prefix;
2158 struct contact_textfilter *firstName;
2159 struct contact_textfilter *lastName;
2160 struct contact_textfilter *suffix;
2161 struct contact_textfilter *nickname;
2162 struct contact_textfilter *company;
2163 struct contact_textfilter *department;
2164 struct contact_textfilter *jobTitle;
2165 struct contact_textfilter *email;
2166 struct contact_textfilter *phone;
2167 struct contact_textfilter *online;
2168 struct contact_textfilter *address;
2169 struct contact_textfilter *notes;
2170 struct contact_textfilter *text;
2171 };
2172
contact_filter_match_textval(const char * val,struct contact_textfilter * propfilter,struct contact_textfilter * textfilter,hash_table * termset)2173 static int contact_filter_match_textval(const char *val,
2174 struct contact_textfilter *propfilter,
2175 struct contact_textfilter *textfilter,
2176 hash_table *termset)
2177 {
2178 if (propfilter) {
2179 /* Fail early if propfilter does not match */
2180 if (val && !contact_textfilter_match(propfilter, val, termset)) {
2181 return 0;
2182 }
2183 }
2184 if (textfilter) {
2185 /* Don't care if textfilter matches */
2186 if (val && !contact_textfilter_matched_all(textfilter)) {
2187 contact_textfilter_match(textfilter, val, termset);
2188 }
2189 }
2190
2191 return 1;
2192 }
2193
getorset_termset(ptrarray_t * cached_termsets,const char * propname)2194 static hash_table *getorset_termset(ptrarray_t *cached_termsets, const char *propname)
2195 {
2196 int i;
2197 for (i = 0; i < ptrarray_size(cached_termsets); i++) {
2198 struct named_termset *nts = ptrarray_nth(cached_termsets, i);
2199 if (!strcmp(nts->propname, propname)) return &nts->termset;
2200 }
2201 struct named_termset *nts = xzmalloc(sizeof(struct named_termset));
2202 nts->propname = propname;
2203 ptrarray_append(cached_termsets, nts);
2204 return &nts->termset;
2205 }
2206
contact_filter_match_textprop(json_t * jentry,const char * propname,struct contact_textfilter * propfilter,struct contact_textfilter * textfilter,ptrarray_t * cached_termsets)2207 static int contact_filter_match_textprop(json_t *jentry, const char *propname,
2208 struct contact_textfilter *propfilter,
2209 struct contact_textfilter *textfilter,
2210 ptrarray_t *cached_termsets)
2211 {
2212
2213 /* Skip matching if possible */
2214 if (!propfilter && (!textfilter || contact_textfilter_matched_all(textfilter)))
2215 return 1;
2216
2217 /* Evaluate search on text value */
2218 hash_table *termset = getorset_termset(cached_termsets, propname);
2219 const char *val = json_string_value(json_object_get(jentry, propname));
2220 return contact_filter_match_textval(val, propfilter, textfilter, termset);
2221 }
2222
contact_filter_match_contactinfo(json_t * jentry,const char * propname,struct contact_textfilter * propfilter,struct contact_textfilter * textfilter,ptrarray_t * cached_termsets)2223 static int contact_filter_match_contactinfo(json_t *jentry, const char *propname,
2224 struct contact_textfilter *propfilter,
2225 struct contact_textfilter *textfilter,
2226 ptrarray_t *cached_termsets)
2227 {
2228 /* Skip matching if possible */
2229 if (!propfilter && (!textfilter || contact_textfilter_matched_all(textfilter)))
2230 return 1;
2231
2232 /* Combine values into text buffer */
2233 json_t *jlist = json_object_get(jentry, propname);
2234 struct buf buf = BUF_INITIALIZER;
2235 json_t *jinfo;
2236 size_t i;
2237 json_array_foreach(jlist, i, jinfo) {
2238 const char *val = json_string_value(json_object_get(jinfo, "value"));
2239 if (!val) continue;
2240 if (i) buf_putc(&buf, ' ');
2241 buf_appendcstr(&buf, val);
2242 }
2243 if (propfilter && !buf_len(&buf))
2244 return 0;
2245
2246 /* Evaluate search on text buffer */
2247 hash_table *termset = getorset_termset(cached_termsets, propname);
2248 int ret = contact_filter_match_textval(buf_cstring(&buf), propfilter, textfilter, termset);
2249 buf_free(&buf);
2250 return ret;
2251 }
2252
contact_filter_match_addresses(json_t * jentry,const char * propname,struct contact_textfilter * propfilter,struct contact_textfilter * textfilter,ptrarray_t * cached_termsets)2253 static int contact_filter_match_addresses(json_t *jentry, const char *propname,
2254 struct contact_textfilter *propfilter,
2255 struct contact_textfilter *textfilter,
2256 ptrarray_t *cached_termsets)
2257 {
2258 /* Skip matching if possible */
2259 if (!propfilter && (!textfilter || contact_textfilter_matched_all(textfilter)))
2260 return 1;
2261
2262 /* Combine values into text buffer */
2263 json_t *jlist = json_object_get(jentry, propname);
2264 struct buf buf = BUF_INITIALIZER;
2265 json_t *jaddr;
2266 size_t i;
2267 json_array_foreach(jlist, i, jaddr) {
2268 const char *val;
2269 if ((val = json_string_value(json_object_get(jaddr, "street")))) {
2270 buf_putc(&buf, ' ');
2271 buf_appendcstr(&buf, val);
2272 }
2273 if ((val = json_string_value(json_object_get(jaddr, "locality")))) {
2274 buf_putc(&buf, ' ');
2275 buf_appendcstr(&buf, val);
2276 }
2277 if ((val = json_string_value(json_object_get(jaddr, "region")))) {
2278 buf_putc(&buf, ' ');
2279 buf_appendcstr(&buf, val);
2280 }
2281 if ((val = json_string_value(json_object_get(jaddr, "postcode")))) {
2282 buf_putc(&buf, ' ');
2283 buf_appendcstr(&buf, val);
2284 }
2285 if ((val = json_string_value(json_object_get(jaddr, "country")))) {
2286 buf_putc(&buf, ' ');
2287 buf_appendcstr(&buf, val);
2288 }
2289 }
2290 if (propfilter && !buf_len(&buf))
2291 return 0;
2292
2293 /* Evaluate search on text buffer */
2294 hash_table *termset = getorset_termset(cached_termsets, propname);
2295 int ret = contact_filter_match_textval(buf_cstring(&buf), propfilter, textfilter, termset);
2296 buf_free(&buf);
2297 return ret;
2298 }
2299
2300 /* Match the contact in rock against filter. */
contact_filter_match(void * vf,void * rock)2301 static int contact_filter_match(void *vf, void *rock)
2302 {
2303 struct contact_filter *f = (struct contact_filter *) vf;
2304 struct contactsquery_filter_rock *cfrock = (struct contactsquery_filter_rock*) rock;
2305 json_t *contact = cfrock->entry;
2306 struct carddav_data *cdata = cfrock->cdata;
2307 struct carddav_db *db = cfrock->carddavdb;
2308
2309 /* uid */
2310 if (f->uid && strcmpsafe(cdata->vcard_uid, f->uid)) {
2311 return 0;
2312 }
2313
2314 /* isFlagged */
2315 if (JNOTNULL(f->isFlagged)) {
2316 json_t *isFlagged = json_object_get(contact, "isFlagged");
2317 if (f->isFlagged != isFlagged) {
2318 return 0;
2319 }
2320 }
2321
2322 /* Match text filters */
2323 if (f->text) contact_textfilter_reset(f->text);
2324 if (!contact_filter_match_textprop(contact, "prefix", f->prefix, f->text, &cfrock->cached_termsets) ||
2325 !contact_filter_match_textprop(contact, "firstName", f->firstName, f->text, &cfrock->cached_termsets) ||
2326 !contact_filter_match_textprop(contact, "lastName", f->lastName, f->text, &cfrock->cached_termsets) ||
2327 !contact_filter_match_textprop(contact, "suffix", f->suffix, f->text, &cfrock->cached_termsets) ||
2328 !contact_filter_match_textprop(contact, "nickname", f->nickname, f->text, &cfrock->cached_termsets) ||
2329 !contact_filter_match_textprop(contact, "company", f->company, f->text, &cfrock->cached_termsets) ||
2330 !contact_filter_match_textprop(contact, "department", f->department, f->text, &cfrock->cached_termsets) ||
2331 !contact_filter_match_textprop(contact, "jobTitle", f->jobTitle, f->text, &cfrock->cached_termsets) ||
2332 !contact_filter_match_textprop(contact, "notes", f->notes, f->text, &cfrock->cached_termsets) ||
2333 !contact_filter_match_contactinfo(contact, "emails", f->email, f->text, &cfrock->cached_termsets) ||
2334 !contact_filter_match_contactinfo(contact, "phones", f->phone, f->text, &cfrock->cached_termsets) ||
2335 !contact_filter_match_contactinfo(contact, "online", f->online, f->text, &cfrock->cached_termsets) ||
2336 !contact_filter_match_addresses(contact, "addresses", f->address, f->text, &cfrock->cached_termsets)) {
2337 return 0;
2338 }
2339
2340 if (f->text && !contact_textfilter_matched_all(f->text)) return 0;
2341
2342 /* inContactGroup */
2343 if (f->inContactGroup) {
2344 /* XXX Calling carddav_db for every contact isn't really efficient. If
2345 * this turns out to be a performance issue, the carddav_db API might
2346 * support lookup contacts by group ids. */
2347 strarray_t *gids = carddav_getuid_groups(db, cdata->vcard_uid);
2348 if (!gids) {
2349 syslog(LOG_INFO,
2350 "carddav_getuid_groups(%s) returned NULL group array",
2351 cdata->vcard_uid);
2352 return 0;
2353 }
2354 int i, m = 0;
2355 for (i = 0; i < gids->count; i++) {
2356 if (hash_lookup(strarray_nth(gids, i), f->inContactGroup)) {
2357 m = 1;
2358 break;
2359 }
2360 }
2361 strarray_free(gids);
2362 if (!m) return 0;
2363 }
2364
2365 /* All matched. */
2366 return 1;
2367 }
2368
2369 /* Free the memory allocated by this contact filter. */
contact_filter_free(void * vf)2370 static void contact_filter_free(void *vf)
2371 {
2372 struct contact_filter *f = (struct contact_filter*) vf;
2373 if (f->inContactGroup) {
2374 free_hash_table(f->inContactGroup, NULL);
2375 free(f->inContactGroup);
2376 }
2377 contact_textfilter_free(f->prefix);
2378 contact_textfilter_free(f->firstName);
2379 contact_textfilter_free(f->lastName);
2380 contact_textfilter_free(f->suffix);
2381 contact_textfilter_free(f->nickname);
2382 contact_textfilter_free(f->company);
2383 contact_textfilter_free(f->department);
2384 contact_textfilter_free(f->jobTitle);
2385 contact_textfilter_free(f->email);
2386 contact_textfilter_free(f->phone);
2387 contact_textfilter_free(f->online);
2388 contact_textfilter_free(f->address);
2389 contact_textfilter_free(f->notes);
2390 contact_textfilter_free(f->text);
2391 free(f);
2392 }
2393
2394 /* Parse the JMAP Contact FilterCondition in arg.
2395 * Report any invalid properties in invalid, prefixed by prefix.
2396 * Return NULL on error. */
contact_filter_parse(json_t * arg)2397 static void *contact_filter_parse(json_t *arg)
2398 {
2399 struct contact_filter *f =
2400 (struct contact_filter *) xzmalloc(sizeof(struct contact_filter));
2401
2402 /* inContactGroup */
2403 json_t *inContactGroup = json_object_get(arg, "inContactGroup");
2404 if (inContactGroup) {
2405 f->inContactGroup = xmalloc(sizeof(struct hash_table));
2406 construct_hash_table(f->inContactGroup,
2407 json_array_size(inContactGroup)+1, 0);
2408 size_t i;
2409 json_t *val;
2410 json_array_foreach(inContactGroup, i, val) {
2411 const char *id;
2412 if (json_unpack(val, "s", &id) != -1) {
2413 hash_insert(id, (void*)1, f->inContactGroup);
2414 }
2415 }
2416 }
2417
2418 /* isFlagged */
2419 f->isFlagged = json_object_get(arg, "isFlagged");
2420
2421 /* prefix */
2422 if (JNOTNULL(json_object_get(arg, "prefix"))) {
2423 const char *s = NULL;
2424 if (jmap_readprop(arg, "prefix", 0, NULL, "s", &s) > 0) {
2425 f->prefix = contact_textfilter_new(s);
2426 }
2427 }
2428 /* firstName */
2429 if (JNOTNULL(json_object_get(arg, "firstName"))) {
2430 const char *s = NULL;
2431 if (jmap_readprop(arg, "firstName", 0, NULL, "s", &s) > 0) {
2432 f->firstName = contact_textfilter_new(s);
2433 }
2434 }
2435 /* lastName */
2436 if (JNOTNULL(json_object_get(arg, "lastName"))) {
2437 const char *s = NULL;
2438 if (jmap_readprop(arg, "lastName", 0, NULL, "s", &s) > 0) {
2439 f->lastName = contact_textfilter_new(s);
2440 }
2441 }
2442 /* suffix */
2443 if (JNOTNULL(json_object_get(arg, "suffix"))) {
2444 const char *s = NULL;
2445 if (jmap_readprop(arg, "suffix", 0, NULL, "s", &s) > 0) {
2446 f->suffix = contact_textfilter_new(s);
2447 }
2448 }
2449 /* nickname */
2450 if (JNOTNULL(json_object_get(arg, "nickname"))) {
2451 const char *s = NULL;
2452 if (jmap_readprop(arg, "nickname", 0, NULL, "s", &s) > 0) {
2453 f->nickname = contact_textfilter_new(s);
2454 }
2455 }
2456 /* company */
2457 if (JNOTNULL(json_object_get(arg, "company"))) {
2458 const char *s = NULL;
2459 if (jmap_readprop(arg, "company", 0, NULL, "s", &s) > 0) {
2460 f->company = contact_textfilter_new(s);
2461 }
2462 }
2463 /* department */
2464 if (JNOTNULL(json_object_get(arg, "department"))) {
2465 const char *s = NULL;
2466 if (jmap_readprop(arg, "department", 0, NULL, "s", &s) > 0) {
2467 f->department = contact_textfilter_new(s);
2468 }
2469 }
2470 /* jobTitle */
2471 if (JNOTNULL(json_object_get(arg, "jobTitle"))) {
2472 const char *s = NULL;
2473 if (jmap_readprop(arg, "jobTitle", 0, NULL, "s", &s) > 0) {
2474 f->jobTitle = contact_textfilter_new(s);
2475 }
2476 }
2477 /* email */
2478 if (JNOTNULL(json_object_get(arg, "email"))) {
2479 const char *s = NULL;
2480 if (jmap_readprop(arg, "email", 0, NULL, "s", &s) > 0) {
2481 f->email = contact_textfilter_new(s);
2482 }
2483 }
2484 /* phone */
2485 if (JNOTNULL(json_object_get(arg, "phone"))) {
2486 const char *s = NULL;
2487 if (jmap_readprop(arg, "phone", 0, NULL, "s", &s) > 0) {
2488 f->phone = contact_textfilter_new(s);
2489 }
2490 }
2491 /* online */
2492 if (JNOTNULL(json_object_get(arg, "online"))) {
2493 const char *s = NULL;
2494 if (jmap_readprop(arg, "online", 0, NULL, "s", &s) > 0) {
2495 f->online = contact_textfilter_new(s);
2496 }
2497 }
2498 /* address */
2499 if (JNOTNULL(json_object_get(arg, "address"))) {
2500 const char *s = NULL;
2501 if (jmap_readprop(arg, "address", 0, NULL, "s", &s) > 0) {
2502 f->address = contact_textfilter_new(s);
2503 }
2504 }
2505 /* notes */
2506 if (JNOTNULL(json_object_get(arg, "notes"))) {
2507 const char *s = NULL;
2508 if (jmap_readprop(arg, "notes", 0, NULL, "s", &s) > 0) {
2509 f->notes = contact_textfilter_new(s);
2510 }
2511 }
2512 /* text */
2513 if (JNOTNULL(json_object_get(arg, "text"))) {
2514 const char *s = NULL;
2515 if (jmap_readprop(arg, "text", 0, NULL, "s", &s) > 0) {
2516 f->text = contact_textfilter_new(s);
2517 }
2518 }
2519 /* uid */
2520 if (JNOTNULL(json_object_get(arg, "uid"))) {
2521 jmap_readprop(arg, "uid", 0, NULL, "s", &f->uid);
2522 }
2523
2524 return f;
2525 }
2526
contact_filter_validate(jmap_req_t * req,struct jmap_parser * parser,json_t * filter,json_t * unsupported,void * rock,json_t ** err)2527 static void contact_filter_validate(jmap_req_t *req __attribute__((unused)),
2528 struct jmap_parser *parser,
2529 json_t *filter,
2530 json_t *unsupported __attribute__((unused)),
2531 void *rock __attribute__((unused)),
2532 json_t **err __attribute__((unused)))
2533 {
2534 const char *field;
2535 json_t *arg;
2536
2537 json_object_foreach(filter, field, arg) {
2538 if (!strcmp(field, "inContactGroup")) {
2539 if (!json_is_array(arg)) {
2540 jmap_parser_invalid(parser, field);
2541 }
2542 else {
2543 jmap_parse_strings(arg, parser, field);
2544 }
2545 }
2546 else if (!strcmp(field, "isFlagged")) {
2547 if (!json_is_boolean(arg)) {
2548 jmap_parser_invalid(parser, field);
2549 }
2550 }
2551 else if (!strcmp(field, "text") ||
2552 !strcmp(field, "prefix") ||
2553 !strcmp(field, "firstName") ||
2554 !strcmp(field, "lastName") ||
2555 !strcmp(field, "suffix") ||
2556 !strcmp(field, "nickname") ||
2557 !strcmp(field, "company") ||
2558 !strcmp(field, "department") ||
2559 !strcmp(field, "jobTitle") ||
2560 !strcmp(field, "email") ||
2561 !strcmp(field, "phone") ||
2562 !strcmp(field, "online") ||
2563 !strcmp(field, "address") ||
2564 !strcmp(field, "uid") ||
2565 !strcmp(field, "notes")) {
2566 if (!json_is_string(arg)) {
2567 jmap_parser_invalid(parser, field);
2568 }
2569 }
2570 else {
2571 jmap_parser_invalid(parser, field);
2572 }
2573 }
2574 }
2575
contact_comparator_validate(jmap_req_t * req,struct jmap_comparator * comp,void * rock,json_t ** err)2576 static int contact_comparator_validate(jmap_req_t *req __attribute__((unused)),
2577 struct jmap_comparator *comp,
2578 void *rock __attribute__((unused)),
2579 json_t **err __attribute__((unused)))
2580 {
2581 /* Reject any collation */
2582 if (comp->collation) {
2583 return 0;
2584 }
2585 if (!strcmp(comp->property, "isFlagged") ||
2586 !strcmp(comp->property, "firstName") ||
2587 !strcmp(comp->property, "lastName") ||
2588 !strcmp(comp->property, "nickname") ||
2589 !strcmp(comp->property, "company") ||
2590 !strcmp(comp->property, "uid")) {
2591 return 1;
2592 }
2593 return 0;
2594 }
2595
2596 struct contactgroup_filter {
2597 const char *uid;
2598 struct contact_textfilter *name;
2599 struct contact_textfilter *text;
2600 };
2601
2602 /* Match the contact in rock against filter. */
contactgroup_filter_match(void * vf,void * rock)2603 static int contactgroup_filter_match(void *vf, void *rock)
2604 {
2605 struct contactgroup_filter *f = (struct contactgroup_filter *) vf;
2606 struct contactsquery_filter_rock *cfrock = (struct contactsquery_filter_rock*) rock;
2607 struct carddav_data *cdata = cfrock->cdata;
2608 json_t *group = cfrock->entry;
2609
2610 /* uid */
2611 if (f->uid && strcmpsafe(cdata->vcard_uid, f->uid)) {
2612 return 0;
2613 }
2614 /* Match text filters */
2615 if (f->text) {
2616 contact_textfilter_reset(f->text);
2617 }
2618 if (!contact_filter_match_textprop(group, "name", f->name, f->text, &cfrock->cached_termsets)) {
2619 return 0;
2620 }
2621 if (f->text && !contact_textfilter_matched_all(f->text)) {
2622 return 0;
2623 }
2624
2625 /* All matched. */
2626 return 1;
2627 }
2628
2629 /* Free the memory allocated by this contact filter. */
contactgroup_filter_free(void * vf)2630 static void contactgroup_filter_free(void *vf)
2631 {
2632 struct contactgroup_filter *f = vf;
2633 contact_textfilter_free(f->name);
2634 contact_textfilter_free(f->text);
2635 free(vf);
2636 }
2637
contactgroup_filter_parse(json_t * arg)2638 static void *contactgroup_filter_parse(json_t *arg)
2639 {
2640 struct contactgroup_filter *f =
2641 (struct contactgroup_filter *) xzmalloc(sizeof(struct contactgroup_filter));
2642
2643 /* uid */
2644 if (JNOTNULL(json_object_get(arg, "uid"))) {
2645 jmap_readprop(arg, "uid", 0, NULL, "s", &f->uid);
2646 }
2647 /* text */
2648 if (JNOTNULL(json_object_get(arg, "text"))) {
2649 const char *s = NULL;
2650 if (jmap_readprop(arg, "text", 0, NULL, "s", &s) > 0) {
2651 f->text = contact_textfilter_new(s);
2652 }
2653 }
2654 /* name */
2655 if (JNOTNULL(json_object_get(arg, "name"))) {
2656 const char *s = NULL;
2657 if (jmap_readprop(arg, "name", 0, NULL, "s", &s) > 0) {
2658 f->name = contact_textfilter_new(s);
2659 }
2660 }
2661
2662 return f;
2663 }
2664
contactgroup_filter_validate(jmap_req_t * req,struct jmap_parser * parser,json_t * filter,json_t * unsupported,void * rock,json_t ** err)2665 static void contactgroup_filter_validate(jmap_req_t *req __attribute__((unused)),
2666 struct jmap_parser *parser,
2667 json_t *filter,
2668 json_t *unsupported __attribute__((unused)),
2669 void *rock __attribute__((unused)),
2670 json_t **err __attribute__((unused)))
2671 {
2672 const char *field;
2673 json_t *arg;
2674
2675 json_object_foreach(filter, field, arg) {
2676 if (!strcmp(field, "uid") ||
2677 !strcmp(field, "text") ||
2678 !strcmp(field, "name")) {
2679 if (!json_is_string(arg)) {
2680 jmap_parser_invalid(parser, field);
2681 }
2682 }
2683 else {
2684 jmap_parser_invalid(parser, field);
2685 }
2686 }
2687 }
2688
contactgroup_comparator_validate(jmap_req_t * req,struct jmap_comparator * comp,void * rock,json_t ** err)2689 static int contactgroup_comparator_validate(jmap_req_t *req __attribute__((unused)),
2690 struct jmap_comparator *comp,
2691 void *rock __attribute__((unused)),
2692 json_t **err __attribute__((unused)))
2693 {
2694 /* Reject any collation */
2695 if (comp->collation) {
2696 return 0;
2697 }
2698 if (!strcmp(comp->property, "uid")) {
2699 return 1;
2700 }
2701 return 0;
2702 }
2703
2704 struct contactsquery_rock {
2705 jmap_req_t *req;
2706 struct jmap_query *query;
2707 jmap_filter *filter;
2708
2709 struct mailbox *mailbox;
2710 struct carddav_db *carddavdb;
2711 unsigned kind;
2712 int build_response;
2713 ptrarray_t entries;
2714 };
2715
_contactsquery_cb(void * rock,struct carddav_data * cdata)2716 static int _contactsquery_cb(void *rock, struct carddav_data *cdata)
2717 {
2718 struct contactsquery_rock *crock = (struct contactsquery_rock*) rock;
2719 struct index_record record;
2720 json_t *entry = NULL;
2721 int r = 0;
2722
2723 if (!cdata->dav.alive || !cdata->dav.rowid || !cdata->dav.imap_uid) {
2724 return 0;
2725 }
2726
2727 /* Ignore anything but the requested kind. */
2728 if (cdata->kind != crock->kind) {
2729 return 0;
2730 }
2731
2732 if (!jmap_hasrights(crock->req, cdata->dav.mailbox, JACL_READITEMS))
2733 return 0;
2734
2735 if (cdata->jmapversion == JMAPCACHE_CONTACTVERSION) {
2736 json_error_t jerr;
2737 entry = json_loads(cdata->jmapdata, 0, &jerr);
2738 if (entry) goto gotvalue;
2739 }
2740
2741 /* Open mailbox. */
2742 if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
2743 mailbox_close(&crock->mailbox);
2744 r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
2745 if (r) goto done;
2746 }
2747
2748 /* Load record. */
2749 r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
2750 if (r) goto done;
2751
2752 /* Load contact from record. */
2753 struct vparse_card *vcard = record_to_vcard(crock->mailbox, &record);
2754 if (!vcard || !vcard->objects) {
2755 syslog(LOG_ERR, "record_to_vcard failed for record %u:%s",
2756 cdata->dav.imap_uid, crock->mailbox->name);
2757 vparse_free_card(vcard);
2758 r = IMAP_INTERNAL;
2759 goto done;
2760 }
2761
2762 /* Convert the VCARD to a JMAP contact. */
2763 /* XXX If this conversion turns out to waste too many cycles, then first
2764 * initialize props with any non-NULL field in filter f or its subconditions.
2765 */
2766 entry = crock->kind == CARDDAV_KIND_GROUP ?
2767 jmap_group_from_vcard(vcard->objects) :
2768 jmap_contact_from_vcard(vcard->objects, crock->mailbox, &record);
2769 vparse_free_card(vcard);
2770
2771 gotvalue:
2772
2773
2774 if (crock->filter) {
2775 /* Match the contact against the filter */
2776 struct contactsquery_filter_rock cfrock = {
2777 crock->carddavdb, cdata, entry, PTRARRAY_INITIALIZER
2778 };
2779 /* Match filter */
2780 jmap_filtermatch_cb *matcher = crock->kind == CARDDAV_KIND_GROUP ?
2781 contactgroup_filter_match : contact_filter_match;
2782 int matches = jmap_filter_match(crock->filter, matcher, &cfrock);
2783 /* Free text search cached_termsets */
2784 int i;
2785 for (i = 0; i < ptrarray_size(&cfrock.cached_termsets); i++) {
2786 struct named_termset *nts = ptrarray_nth(&cfrock.cached_termsets, i);
2787 free_hash_table(&nts->termset, NULL);
2788 free(nts);
2789 }
2790 ptrarray_fini(&cfrock.cached_termsets);
2791 /* Skip non-matching entries */
2792 if (!matches) goto done;
2793 }
2794
2795 /* Update statistics */
2796 crock->query->total++;
2797
2798 if (crock->build_response) {
2799 struct jmap_query *query = crock->query;
2800 /* Apply windowing and build response ids */
2801 if (query->position > 0 && query->position > (ssize_t) query->total - 1) {
2802 goto done;
2803 }
2804 if (query->limit && json_array_size(query->ids) >= query->limit) {
2805 goto done;
2806 }
2807 if (!json_array_size(query->ids)) {
2808 query->result_position = query->total - 1;
2809 }
2810 json_array_append_new(query->ids, json_string(cdata->vcard_uid));
2811 }
2812 else {
2813 /* Keep matching entries for post-processing */
2814 json_object_set_new(entry, "id", json_string(cdata->vcard_uid));
2815 json_object_set_new(entry, "uid", json_string(cdata->vcard_uid));
2816 ptrarray_append(&crock->entries, entry);
2817 entry = NULL;
2818 }
2819
2820 done:
2821 if (entry) json_decref(entry);
2822 return r;
2823 }
2824
2825 enum contactsquery_sort {
2826 CONTACTS_SORT_NONE = 0,
2827 CONTACTS_SORT_UID,
2828 /* Comparators for Contact */
2829 CONTACTS_SORT_ISFLAGGED,
2830 CONTACTS_SORT_FIRSTNAME,
2831 CONTACTS_SORT_LASTNAME,
2832 CONTACTS_SORT_NICKNAME,
2833 CONTACTS_SORT_COMPANY,
2834 /* Comparators for ContactGroup */
2835 CONTACTS_SORT_NAME,
2836 /* Flag for descencding sort */
2837 CONTACTS_SORT_DESC = 0x80,
2838 };
2839
buildsort(json_t * jsort)2840 enum contactsquery_sort *buildsort(json_t *jsort)
2841 {
2842 enum contactsquery_sort *sort =
2843 xzmalloc((json_array_size(jsort) + 1) * sizeof(enum contactsquery_sort));
2844
2845 size_t i;
2846 json_t *jcomp;
2847 json_array_foreach(jsort, i, jcomp) {
2848 const char *prop = json_string_value(json_object_get(jcomp, "property"));
2849 if (!strcmp(prop, "uid"))
2850 sort[i] = CONTACTS_SORT_UID;
2851 /* Comparators for Contact */
2852 else if (!strcmp(prop, "isFlagged"))
2853 sort[i] = CONTACTS_SORT_ISFLAGGED;
2854 else if (!strcmp(prop, "firstName"))
2855 sort[i] = CONTACTS_SORT_FIRSTNAME;
2856 else if (!strcmp(prop, "lastName"))
2857 sort[i] = CONTACTS_SORT_LASTNAME;
2858 else if (!strcmp(prop, "nickname"))
2859 sort[i] = CONTACTS_SORT_NICKNAME;
2860 else if (!strcmp(prop, "company"))
2861 sort[i] = CONTACTS_SORT_COMPANY;
2862 /* Comparators for ContactGroup */
2863 else if (!strcmp(prop, "name"))
2864 sort[i] = CONTACTS_SORT_NAME;
2865
2866 if (json_object_get(jcomp, "isAscending") == json_false())
2867 sort[i] |= CONTACTS_SORT_DESC;
2868 }
2869
2870 return sort;
2871 }
2872
QSORT_R_COMPAR_ARGS(const void * va,const void * vb,void * rock)2873 static int _contactsquery_cmp QSORT_R_COMPAR_ARGS(const void *va,
2874 const void *vb,
2875 void *rock)
2876 {
2877 enum contactsquery_sort *sort = rock;
2878 json_t *ja = (json_t*) *(void**)va;
2879 json_t *jb = (json_t*) *(void**)vb;
2880
2881 enum contactsquery_sort *comp;
2882 for (comp = sort; *comp != CONTACTS_SORT_NONE; comp++) {
2883 int ret = 0;
2884 switch (*comp & ~CONTACTS_SORT_DESC) {
2885 case CONTACTS_SORT_UID:
2886 ret = strcmpsafe(json_string_value(json_object_get(ja, "uid")),
2887 json_string_value(json_object_get(jb, "uid")));
2888 break;
2889 case CONTACTS_SORT_FIRSTNAME:
2890 ret = strcmpsafe(json_string_value(json_object_get(ja, "firstName")),
2891 json_string_value(json_object_get(jb, "firstName")));
2892 break;
2893 case CONTACTS_SORT_LASTNAME:
2894 ret = strcmpsafe(json_string_value(json_object_get(ja, "lastName")),
2895 json_string_value(json_object_get(jb, "lastName")));
2896 break;
2897 case CONTACTS_SORT_NICKNAME:
2898 ret = strcmpsafe(json_string_value(json_object_get(ja, "nickname")),
2899 json_string_value(json_object_get(jb, "nickname")));
2900 break;
2901 case CONTACTS_SORT_COMPANY:
2902 ret = strcmpsafe(json_string_value(json_object_get(ja, "company")),
2903 json_string_value(json_object_get(jb, "company")));
2904 break;
2905 case CONTACTS_SORT_NAME:
2906 ret = strcmpsafe(json_string_value(json_object_get(ja, "name")),
2907 json_string_value(json_object_get(jb, "name")));
2908 break;
2909 case CONTACTS_SORT_ISFLAGGED:
2910 ret = json_boolean_value(json_object_get(ja, "isFlagged")) -
2911 json_boolean_value(json_object_get(jb, "isFlagged"));
2912 break;
2913 }
2914 if (ret)
2915 return (*comp & CONTACTS_SORT_DESC) ? -ret : ret;
2916 }
2917
2918 return 0;
2919 }
2920
_contactsquery(struct jmap_req * req,unsigned kind)2921 static int _contactsquery(struct jmap_req *req, unsigned kind)
2922 {
2923 if (!has_addressbooks(req)) {
2924 jmap_error(req, json_pack("{s:s}", "type", "accountNoAddressbooks"));
2925 return 0;
2926 }
2927
2928 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
2929 struct jmap_query query;
2930 struct carddav_db *db;
2931 jmap_filter *parsed_filter = NULL;
2932 int r = 0;
2933
2934 db = carddav_open_userid(req->accountid);
2935 if (!db) {
2936 syslog(LOG_ERR,
2937 "carddav_open_userid failed for user %s", req->accountid);
2938 r = IMAP_INTERNAL;
2939 goto done;
2940 }
2941
2942 /* Parse request */
2943 json_t *err = NULL;
2944 jmap_query_parse(req, &parser, NULL, NULL,
2945 kind == CARDDAV_KIND_GROUP ?
2946 contactgroup_filter_validate :
2947 contact_filter_validate,
2948 NULL,
2949 kind == CARDDAV_KIND_GROUP ?
2950 contactgroup_comparator_validate :
2951 contact_comparator_validate,
2952 NULL,
2953 &query, &err);
2954 if (err) {
2955 jmap_error(req, err);
2956 goto done;
2957 }
2958 if (json_array_size(parser.invalid)) {
2959 err = json_pack("{s:s}", "type", "invalidArguments");
2960 json_object_set(err, "arguments", parser.invalid);
2961 jmap_error(req, err);
2962 goto done;
2963 }
2964
2965 /* Build filter */
2966 json_t *filter = json_object_get(req->args, "filter");
2967 const char *wantuid = NULL;
2968 if (JNOTNULL(filter)) {
2969 parsed_filter = jmap_buildfilter(filter,
2970 kind == CARDDAV_KIND_GROUP ?
2971 contactgroup_filter_parse : contact_filter_parse);
2972 wantuid = json_string_value(json_object_get(filter, "uid"));
2973 }
2974
2975 /* Does this query have a complex sort? */
2976 int is_complexsort;
2977 if (json_array_size(query.sort) == 1) {
2978 json_t *jcomp = json_array_get(query.sort, 0);
2979 const char *prop = json_string_value(json_object_get(jcomp, "property"));
2980 is_complexsort = strcmpsafe("uid", prop);
2981 }
2982 else is_complexsort = json_array_size(query.sort) > 0;
2983
2984 /* Inspect every entry in this accounts addressbook mailboxes. */
2985 struct contactsquery_rock rock = {
2986 req,
2987 &query,
2988 parsed_filter,
2989 NULL /*mailbox*/,
2990 db,
2991 kind,
2992 1 /*build_result*/,
2993 PTRARRAY_INITIALIZER
2994 };
2995 if (wantuid) {
2996 /* Fast-path single filter condition by UID */
2997 struct carddav_data *cdata = NULL;
2998 r = carddav_lookup_uid(db, wantuid, &cdata);
2999 if (!r) _contactsquery_cb(&rock, cdata);
3000 if (r == CYRUSDB_NOTFOUND) r = 0;
3001 }
3002 else if (!is_complexsort && query.position >= 0 && !query.anchor) {
3003 /* Fast-path simple query with carddav db */
3004 enum carddav_sort sort = CARD_SORT_UID; /* ignored if nsort == 0 */
3005 size_t nsort = 0;
3006 if (json_array_size(query.sort)) {
3007 json_t *jcomp = json_array_get(query.sort, 0);
3008 if (json_object_get(jcomp, "isAscending") == json_false()) {
3009 sort |= CARD_SORT_DESC;
3010 }
3011 nsort = 1;
3012 }
3013 r = carddav_foreach_sort(db, NULL, &sort, nsort, _contactsquery_cb, &rock);
3014 }
3015 else {
3016 /* Run carddav db query and apply custom sort */
3017 rock.build_response = 0;
3018 r = carddav_foreach(db, NULL, _contactsquery_cb, &rock);
3019 if (!r) {
3020 /* Sort entries */
3021 enum contactsquery_sort *sort = buildsort(query.sort);
3022 cyr_qsort_r(rock.entries.data, rock.entries.count, sizeof(void*),
3023 _contactsquery_cmp, sort);
3024 free(sort);
3025 /* Build result ids */
3026 int i;
3027 for (i = 0; i < ptrarray_size(&rock.entries); i++) {
3028 json_t *entry = ptrarray_nth(&rock.entries, i);
3029 json_array_append(query.ids, json_object_get(entry, "uid"));
3030 json_decref(entry);
3031 }
3032 /* Determine start position of result window */
3033 size_t startpos = 0;
3034 if (query.anchor) {
3035 /* Look for anchor in result ids */
3036 size_t anchor_pos = 0;
3037 for ( ; anchor_pos < json_array_size(query.ids); anchor_pos++) {
3038 json_t *jid = json_array_get(query.ids, anchor_pos);
3039 if (!strcmpsafe(query.anchor, json_string_value(jid))) break;
3040 }
3041 /* Determine start of windowed result ids */
3042 if (anchor_pos < json_array_size(query.ids)) {
3043 if (query.anchor_offset < 0) {
3044 startpos = (size_t) -query.anchor_offset > anchor_pos ?
3045 0 : anchor_pos + query.anchor_offset;
3046 }
3047 else {
3048 startpos = anchor_pos + query.anchor_offset;
3049 }
3050 }
3051 else err = json_pack("{s:s}", "type", "anchorNotFound");
3052 }
3053 else if (query.position < 0) {
3054 startpos = (size_t) -query.position > json_array_size(query.ids) ?
3055 0 : json_array_size(query.ids) + query.position;
3056 }
3057 else startpos = query.position;
3058 /* Apply window to result list */
3059 if (startpos < json_array_size(query.ids)) {
3060 json_t *windowed_ids = json_array();
3061 size_t j;
3062 for (j = startpos; j < json_array_size(query.ids); j++) {
3063 if (!query.limit || json_array_size(windowed_ids) < query.limit) {
3064 json_array_append(windowed_ids, json_array_get(query.ids, j));
3065 }
3066 else break;
3067 }
3068 json_decref(query.ids);
3069 query.ids = windowed_ids;
3070 query.result_position = startpos;
3071 }
3072 else {
3073 json_decref(query.ids);
3074 query.ids = json_array();
3075 }
3076 ptrarray_fini(&rock.entries);
3077 }
3078 }
3079 /* Clean up callback state */
3080 if (rock.mailbox) mailbox_close(&rock.mailbox);
3081 /* Handle callback errors */
3082 if (r || err) {
3083 if (!err) err = jmap_server_error(r);
3084 jmap_error(req, err);
3085 goto done;
3086 }
3087
3088 /* Build response */
3089 json_t *jstate = jmap_getstate(req, MBTYPE_ADDRESSBOOK, /*refresh*/0);
3090 query.query_state = xstrdup(json_string_value(jstate));
3091 json_decref(jstate);
3092
3093 json_t *res = jmap_query_reply(&query);
3094 jmap_ok(req, res);
3095
3096 done:
3097 jmap_query_fini(&query);
3098 jmap_parser_fini(&parser);
3099 if (parsed_filter) {
3100 jmap_filter_free(parsed_filter, kind == CARDDAV_KIND_GROUP ?
3101 contactgroup_filter_free : contact_filter_free);
3102 }
3103 if (db) carddav_close(db);
3104 return 0;
3105 }
3106
jmap_contact_query(struct jmap_req * req)3107 static int jmap_contact_query(struct jmap_req *req)
3108 {
3109 return _contactsquery(req, CARDDAV_KIND_CONTACT);
3110 }
3111
jmap_contactgroup_query(struct jmap_req * req)3112 static int jmap_contactgroup_query(struct jmap_req *req)
3113 {
3114 return _contactsquery(req, CARDDAV_KIND_GROUP);
3115 }
3116
_card_multi(struct vparse_card * card,const char * name,char sepchar)3117 static struct vparse_entry *_card_multi(struct vparse_card *card,
3118 const char *name, char sepchar)
3119 {
3120 struct vparse_entry *res = vparse_get_entry(card, NULL, name);
3121 if (!res) {
3122 res = vparse_add_entry(card, NULL, name, NULL);
3123 res->multivaluesep = sepchar;
3124 res->v.values = strarray_new();
3125 }
3126 return res;
3127 }
3128
_emails_to_card(struct vparse_card * card,json_t * arg,json_t * invalid)3129 static int _emails_to_card(struct vparse_card *card,
3130 json_t *arg, json_t *invalid)
3131 {
3132 vparse_delete_entries(card, NULL, "email");
3133
3134 int i;
3135 int size = json_array_size(arg);
3136 struct buf buf = BUF_INITIALIZER;
3137 for (i = 0; i < size; i++) {
3138 json_t *item = json_array_get(arg, i);
3139
3140 buf_printf(&buf, "emails[%d]", i);
3141 const char *prefix = buf_cstring(&buf);
3142
3143 /* Parse properties. */
3144 const char *type = NULL;
3145 const char *label = NULL;
3146 const char *value = NULL;
3147
3148 jmap_readprop_full(item, prefix, "type", 1, invalid, "s", &type);
3149 if (type) {
3150 if (strcmp(type, "personal") && strcmp(type, "work") && strcmp(type, "other")) {
3151 char *tmp = strconcat(prefix, ".type", NULL);
3152 json_array_append_new(invalid, json_string(tmp));
3153 free(tmp);
3154 }
3155 }
3156 jmap_readprop_full(item, prefix, "value", 1, invalid, "s", &value);
3157 if (JNOTNULL(json_object_get(item, "label"))) {
3158 jmap_readprop_full(item, prefix, "label", 1, invalid, "s", &label);
3159 }
3160 json_t *jisDefault = json_object_get(item, "isDefault");
3161
3162 /* Bail out for any property errors. */
3163 if (!type || !value || json_array_size(invalid)) {
3164 buf_free(&buf);
3165 return -1;
3166 }
3167
3168 /* Update card. */
3169 struct vparse_entry *entry =
3170 vparse_add_entry(card, NULL, "EMAIL", value);
3171
3172 if (!strcmpsafe(type, "personal"))
3173 type = "home";
3174 if (strcmpsafe(type, "other"))
3175 vparse_add_param(entry, "TYPE", type);
3176
3177 if (label)
3178 vparse_add_param(entry, "LABEL", label);
3179
3180 if (jisDefault && json_is_true(jisDefault))
3181 vparse_add_param(entry, "TYPE", "pref");
3182
3183 buf_reset(&buf);
3184 }
3185 buf_free(&buf);
3186 return 0;
3187 }
3188
_phones_to_card(struct vparse_card * card,json_t * arg,json_t * invalid)3189 static int _phones_to_card(struct vparse_card *card,
3190 json_t *arg, json_t *invalid)
3191 {
3192 vparse_delete_entries(card, NULL, "tel");
3193
3194 int i;
3195 int size = json_array_size(arg);
3196 struct buf buf = BUF_INITIALIZER;
3197 for (i = 0; i < size; i++) {
3198 json_t *item = json_array_get(arg, i);
3199
3200 buf_printf(&buf, "phones[%d]", i);
3201 const char *prefix = buf_cstring(&buf);
3202
3203 /* Parse properties. */
3204 const char *type = NULL;
3205 const char *label = NULL;
3206 const char *value = NULL;
3207
3208 jmap_readprop_full(item, prefix, "type", 1, invalid, "s", &type);
3209 if (type) {
3210 if (strcmp(type, "home") && strcmp(type, "work") && strcmp(type, "mobile") &&
3211 strcmp(type, "fax") && strcmp(type, "pager") && strcmp(type, "other")) {
3212 char *tmp = strconcat(prefix, ".type", NULL);
3213 json_array_append_new(invalid, json_string(tmp));
3214 free(tmp);
3215 }
3216 }
3217 jmap_readprop_full(item, prefix, "value", 1, invalid, "s", &value);
3218 if (JNOTNULL(json_object_get(item, "label"))) {
3219 jmap_readprop_full(item, prefix, "label", 1, invalid, "s", &label);
3220 }
3221
3222 /* Bail out for any property errors. */
3223 if (!type || !value || json_array_size(invalid)) {
3224 buf_free(&buf);
3225 return -1;
3226 }
3227
3228 /* Update card. */
3229 struct vparse_entry *entry = vparse_add_entry(card, NULL, "TEL", value);
3230
3231 if (!strcmp(type, "mobile"))
3232 vparse_add_param(entry, "TYPE", "CELL");
3233 else if (strcmp(type, "other"))
3234 vparse_add_param(entry, "TYPE", type);
3235
3236 if (label)
3237 vparse_add_param(entry, "LABEL", label);
3238
3239 buf_reset(&buf);
3240 }
3241 buf_free(&buf);
3242 return 0;
3243 }
3244
_is_im(const char * type)3245 static int _is_im(const char *type)
3246 {
3247 /* add new services here */
3248 if (!strcasecmp(type, "aim")) return 1;
3249 if (!strcasecmp(type, "facebook")) return 1;
3250 if (!strcasecmp(type, "gadugadu")) return 1;
3251 if (!strcasecmp(type, "googletalk")) return 1;
3252 if (!strcasecmp(type, "icq")) return 1;
3253 if (!strcasecmp(type, "jabber")) return 1;
3254 if (!strcasecmp(type, "msn")) return 1;
3255 if (!strcasecmp(type, "qq")) return 1;
3256 if (!strcasecmp(type, "skype")) return 1;
3257 if (!strcasecmp(type, "twitter")) return 1;
3258 if (!strcasecmp(type, "yahoo")) return 1;
3259
3260 return 0;
3261 }
3262
_online_to_card(struct vparse_card * card,json_t * arg,json_t * invalid)3263 static int _online_to_card(struct vparse_card *card,
3264 json_t *arg, json_t *invalid)
3265 {
3266 vparse_delete_entries(card, NULL, "url");
3267 vparse_delete_entries(card, NULL, "impp");
3268 vparse_delete_entries(card, NULL, "x-social-profile");
3269 vparse_delete_entries(card, NULL, "x-fm-online-other");
3270
3271 int i;
3272 int size = json_array_size(arg);
3273 struct buf buf = BUF_INITIALIZER;
3274 for (i = 0; i < size; i++) {
3275 json_t *item = json_array_get(arg, i);
3276
3277 buf_printf(&buf, "online[%d]", i);
3278 const char *prefix = buf_cstring(&buf);
3279
3280 /* Parse properties. */
3281 const char *type = NULL;
3282 const char *label = NULL;
3283 const char *value = NULL;
3284
3285 jmap_readprop_full(item, prefix, "type", 1, invalid, "s", &type);
3286 if (type) {
3287 if (strcmp(type, "uri") && strcmp(type, "username") && strcmp(type, "other")) {
3288 char *tmp = strconcat(prefix, ".type", NULL);
3289 json_array_append_new(invalid, json_string(tmp));
3290 free(tmp);
3291 }
3292 }
3293 jmap_readprop_full(item, prefix, "value", 1, invalid, "s", &value);
3294 if (JNOTNULL(json_object_get(item, "label"))) {
3295 jmap_readprop_full(item, prefix, "label", 1, invalid, "s", &label);
3296 }
3297
3298 /* Bail out for any property errors. */
3299 if (!type || !value || json_array_size(invalid)) {
3300 buf_free(&buf);
3301 return -1;
3302 }
3303
3304 /* Update card. */
3305 if (!strcmp(type, "uri")) {
3306 struct vparse_entry *entry =
3307 vparse_add_entry(card, NULL, "URL", value);
3308 if (label)
3309 vparse_add_param(entry, "LABEL", label);
3310 }
3311 else if (!strcmp(type, "username")) {
3312 if (label && _is_im(label)) {
3313 struct vparse_entry *entry =
3314 vparse_add_entry(card, NULL, "IMPP", value);
3315 vparse_add_param(entry, "X-SERVICE-TYPE", label);
3316 }
3317 else {
3318 struct vparse_entry *entry =
3319 vparse_add_entry(card, NULL, "X-SOCIAL-PROFILE", ""); // XXX - URL calculated, ick
3320 if (label)
3321 vparse_add_param(entry, "TYPE", label);
3322 vparse_add_param(entry, "X-USER", value);
3323 }
3324 }
3325 else if (!strcmp(type, "other")) {
3326 struct vparse_entry *entry =
3327 vparse_add_entry(card, NULL, "X-FM-ONLINE-OTHER", value);
3328 if (label)
3329 vparse_add_param(entry, "LABEL", label);
3330 }
3331 }
3332 buf_free(&buf);
3333 return 0;
3334 }
3335
_addresses_to_card(struct vparse_card * card,json_t * arg,json_t * invalid)3336 static int _addresses_to_card(struct vparse_card *card,
3337 json_t *arg, json_t *invalid)
3338 {
3339 vparse_delete_entries(card, NULL, "adr");
3340
3341 int i;
3342 int size = json_array_size(arg);
3343 struct buf buf = BUF_INITIALIZER;
3344 for (i = 0; i < size; i++) {
3345 json_t *item = json_array_get(arg, i);
3346
3347 buf_printf(&buf, "addresses[%d]", i);
3348 const char *prefix = buf_cstring(&buf);
3349
3350 /* Parse properties. */
3351 const char *type = NULL;
3352 const char *label = NULL;
3353 const char *street = NULL;
3354 const char *locality = NULL;
3355 const char *region = NULL;
3356 const char *postcode = NULL;
3357 const char *country = NULL;
3358 int pe; /* parse error */
3359
3360 /* Mandatory */
3361 pe = jmap_readprop_full(item, prefix, "type", 1, invalid, "s", &type);
3362 if (type) {
3363 if (strcmp(type, "home") && strcmp(type, "work") && strcmp(type, "billing") &&
3364 strcmp(type, "postal") && strcmp(type, "other")) {
3365 char *tmp = strconcat(prefix, ".type", NULL);
3366 json_array_append_new(invalid, json_string(tmp));
3367 free(tmp);
3368 }
3369 }
3370 pe = jmap_readprop_full(item, prefix, "street", 1, invalid, "s", &street);
3371 pe = jmap_readprop_full(item, prefix, "locality", 1, invalid, "s", &locality);
3372 pe = jmap_readprop_full(item, prefix, "region", 1, invalid, "s", ®ion);
3373 pe = jmap_readprop_full(item, prefix, "postcode", 1, invalid, "s", &postcode);
3374 pe = jmap_readprop_full(item, prefix, "country", 1, invalid, "s", &country);
3375
3376 /* Optional */
3377 if (JNOTNULL(json_object_get(item, "label"))) {
3378 pe = jmap_readprop_full(item, prefix, "label", 0, invalid, "s", &label);
3379 }
3380
3381 /* Bail out for any property errors. */
3382 if (!type || !street || !locality ||
3383 !region || !postcode || !country || pe < 0) {
3384 buf_free(&buf);
3385 return -1;
3386 }
3387
3388 /* Update card. */
3389 struct vparse_entry *entry = vparse_add_entry(card, NULL, "ADR", NULL);
3390
3391 if (strcmpsafe(type, "other"))
3392 vparse_add_param(entry, "TYPE", type);
3393
3394 if (label)
3395 vparse_add_param(entry, "LABEL", label);
3396
3397 entry->multivaluesep = ';';
3398 entry->v.values = strarray_new();
3399 strarray_append(entry->v.values, ""); // PO Box
3400 strarray_append(entry->v.values, ""); // Extended Address
3401 strarray_append(entry->v.values, street);
3402 strarray_append(entry->v.values, locality);
3403 strarray_append(entry->v.values, region);
3404 strarray_append(entry->v.values, postcode);
3405 strarray_append(entry->v.values, country);
3406
3407 buf_reset(&buf);
3408 }
3409
3410 buf_free(&buf);
3411 return 0;
3412 }
3413
_date_to_card(struct vparse_card * card,const char * key,json_t * jval)3414 static int _date_to_card(struct vparse_card *card,
3415 const char *key, json_t *jval)
3416 {
3417 if (!jval)
3418 return -1;
3419 const char *val = json_string_value(jval);
3420 if (!val)
3421 return -1;
3422
3423 /* JMAP dates are always YYYY-MM-DD */
3424 unsigned y, m, d;
3425 if (_parse_date(val, &y, &m, &d, 1))
3426 return -1;
3427
3428 /* range checks. month and day just get basic sanity checks because we're
3429 * not carrying a full calendar implementation here. JMAP says zero is valid
3430 * so we'll allow that and deal with it later on */
3431 if (m > 12 || d > 31)
3432 return -1;
3433
3434 /* all years are valid in JMAP, but ISO8601 only allows Gregorian ie >= 1583.
3435 * moreover, iOS uses 1604 as a magic number for "unknown", so we'll say 1605
3436 * is the minimum */
3437 if (y > 0 && y < 1605)
3438 return -1;
3439
3440 /* everything in range. now comes the fun bit. vCard v3 says BDAY is
3441 * YYYY-MM-DD. It doesn't reference ISO8601 (vCard v4 does) and make no
3442 * provision for "unknown" date components, so there's no way to represent
3443 * JMAP's "unknown" values. Apple worked around this for year by using the
3444 * year 1604 and adding the parameter X-APPLE-OMIT-YEAR=1604 (value
3445 * apparently ignored). We will use a similar hack for month and day so we
3446 * can convert it back into a JMAP date */
3447
3448 int no_year = 0;
3449 if (y == 0) {
3450 no_year = 1;
3451 y = 1604;
3452 }
3453
3454 int no_month = 0;
3455 if (m == 0) {
3456 no_month = 1;
3457 m = 1;
3458 }
3459
3460 int no_day = 0;
3461 if (d == 0) {
3462 no_day = 1;
3463 d = 1;
3464 }
3465
3466 vparse_delete_entries(card, NULL, key);
3467
3468 /* no values, we're done! */
3469 if (no_year && no_month && no_day)
3470 return 0;
3471
3472 /* build the value */
3473 static char buf[11];
3474 snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, d);
3475 struct vparse_entry *entry = vparse_add_entry(card, NULL, key, buf);
3476
3477 /* set all the round-trip flags, sigh */
3478 if (no_year)
3479 vparse_add_param(entry, "X-APPLE-OMIT-YEAR", "1604");
3480 if (no_month)
3481 vparse_add_param(entry, "X-FM-NO-MONTH", "1");
3482 if (no_day)
3483 vparse_add_param(entry, "X-FM-NO-DAY", "1");
3484
3485 return 0;
3486 }
3487
_kv_to_card(struct vparse_card * card,const char * key,json_t * jval)3488 static int _kv_to_card(struct vparse_card *card, const char *key, json_t *jval)
3489 {
3490 if (!jval)
3491 return -1;
3492 const char *val = json_string_value(jval);
3493 if (!val)
3494 return -1;
3495 vparse_replace_entry(card, NULL, key, val);
3496 return 0;
3497 }
3498
_blob_to_card(struct jmap_req * req,struct vparse_card * card,const char * key,json_t * file)3499 static int _blob_to_card(struct jmap_req *req,
3500 struct vparse_card *card, const char *key, json_t *file)
3501 {
3502 struct buf blob_buf = BUF_INITIALIZER;
3503 msgrecord_t *mr = NULL;
3504 struct mailbox *mbox = NULL;
3505 struct body *body = NULL;
3506 const struct body *part = NULL;
3507 const char *blobid = NULL;
3508 const char *accountid = NULL;
3509 char *encbuf = NULL;
3510 char *decbuf = NULL;
3511 json_t *val;
3512 int r = -1;
3513 const char *base = NULL;
3514 size_t len = 0;
3515
3516 if (!file) goto done;
3517
3518 /* Extract blobId */
3519 val = json_object_get(file, "blobId");
3520 if (val) blobid = jmap_id_string_value(req, val);
3521 if (!blobid) goto done;
3522
3523 /* Find body part containing blob */
3524 accountid = json_string_value(json_object_get(file, "accountId"));
3525 r = jmap_findblob(req, accountid, blobid,
3526 &mbox, &mr, &body, &part, &blob_buf);
3527 if (r) goto done;
3528
3529 /* Fetch blob contents and decode */
3530 base = buf_base(&blob_buf);
3531 len = buf_len(&blob_buf);
3532
3533 if (part) {
3534 /* Map into body part */
3535 base += part->content_offset;
3536 len = part->content_size;
3537
3538 /* Determine encoding */
3539 int encoding = part->charset_enc & 0xff;
3540 base = charset_decode_mimebody(base, len, encoding, &decbuf, &len);
3541 }
3542
3543 /* Pre-flight base64 encoder to determine length */
3544 size_t len64 = 0;
3545 charset_encode_mimebody(NULL, len, NULL, &len64, NULL, 0 /* no wrap */);
3546
3547 /* Now encode the blob */
3548 encbuf = xmalloc(len64+1);
3549 charset_encode_mimebody(base, len, encbuf, &len64, NULL, 0 /* no wrap */);
3550 encbuf[len64] = '\0';
3551 base = encbuf;
3552
3553 /* (Re)write vCard property */
3554 vparse_delete_entries(card, NULL, key);
3555
3556 struct vparse_entry *entry = vparse_add_entry(card, NULL, key, base);
3557
3558 vparse_add_param(entry, "ENCODING", "b");
3559
3560 val = json_object_get(file, "type");
3561 if (JNOTNULL(val)) {
3562 r = -1;
3563 const char *type = json_string_value(val);
3564 if (!type) goto done;
3565 char *subtype = xstrdupnull(strchr(type, '/'));
3566 if (!subtype) goto done;
3567
3568 vparse_add_param(entry, "TYPE", ucase(subtype+1));
3569 free(subtype);
3570 }
3571
3572 r = 0;
3573
3574 done:
3575 free(decbuf);
3576 free(encbuf);
3577 if (body) {
3578 message_free_body(body);
3579 free(body);
3580 }
3581 msgrecord_unref(&mr);
3582 jmap_closembox(req, &mbox);
3583 buf_free(&blob_buf);
3584
3585 return r;
3586 }
3587
_make_fn(struct vparse_card * card)3588 static void _make_fn(struct vparse_card *card)
3589 {
3590 struct vparse_entry *n = vparse_get_entry(card, NULL, "N");
3591 strarray_t *name = strarray_new();
3592 const char *v;
3593
3594 if (n) {
3595 v = strarray_safenth(n->v.values, 3); // prefix
3596 if (*v) strarray_append(name, v);
3597
3598 v = strarray_safenth(n->v.values, 1); // first
3599 if (*v) strarray_append(name, v);
3600
3601 v = strarray_safenth(n->v.values, 2); // middle
3602 if (*v) strarray_append(name, v);
3603
3604 v = strarray_safenth(n->v.values, 0); // last
3605 if (*v) strarray_append(name, v);
3606
3607 v = strarray_safenth(n->v.values, 4); // suffix
3608 if (*v) strarray_append(name, v);
3609 }
3610
3611 if (!strarray_size(name)) {
3612 v = vparse_stringval(card, "NICKNAME");
3613 if (v && v[0]) strarray_append(name, v);
3614 }
3615
3616 char *fn = NULL;
3617 if (strarray_size(name))
3618 fn = strarray_join(name, " ");
3619 else
3620 fn = xstrdup(" ");
3621
3622 strarray_free(name);
3623 vparse_replace_entry(card, NULL, "FN", fn);
3624 free(fn);
3625 }
3626
_json_to_card(struct jmap_req * req,struct carddav_data * cdata,struct vparse_card * card,json_t * arg,strarray_t * flags,struct entryattlist ** annotsp,json_t * invalid)3627 static int _json_to_card(struct jmap_req *req,
3628 struct carddav_data *cdata,
3629 struct vparse_card *card,
3630 json_t *arg, strarray_t *flags,
3631 struct entryattlist **annotsp,
3632 json_t *invalid)
3633 {
3634 const char *key;
3635 json_t *jval;
3636 struct vparse_entry *n = vparse_get_entry(card, NULL, "N");
3637 int name_is_dirty = 0;
3638 int has_noncontent = 0;
3639 int record_is_dirty = 0;
3640
3641 /* we'll be updating you later anyway... create early so that it's
3642 * at the top of the card */
3643 if (!n) {
3644 /* _card_multi repeats some work, but we don't care */
3645 n = _card_multi(card, "N", ';');
3646 record_is_dirty = 1;
3647 }
3648
3649 if (!vparse_get_entry(card, NULL, "FN")) {
3650 /* adding first to get position near the top */
3651 vparse_add_entry(card, NULL, "FN", "No Name");
3652 name_is_dirty = 1;
3653 }
3654
3655 json_object_foreach(arg, key, jval) {
3656 if (cdata) {
3657 if (!strcmp(key, "id")) {
3658 if (strcmpnull(cdata->vcard_uid, json_string_value(jval))) {
3659 json_array_append_new(invalid, json_string("id"));
3660 }
3661 continue;
3662 }
3663 if (!strcmp(key, "uid")) {
3664 if (strcmpnull(cdata->vcard_uid, json_string_value(jval))) {
3665 json_array_append_new(invalid, json_string("uid"));
3666 }
3667 continue;
3668 }
3669 else if (!strcmp(key, "x-href")) {
3670 char *xhref = jmap_xhref(cdata->dav.mailbox, cdata->dav.resource);
3671 if (strcmpnull(json_string_value(jval), xhref)) {
3672 json_array_append_new(invalid, json_string("x-href"));
3673 }
3674 free(xhref);
3675 continue;
3676 }
3677 else if (!strcmp(key, "x-hasPhoto")) {
3678 if ((vparse_stringval(card, "photo") && !json_is_true(jval)) ||
3679 !json_is_false(jval)) {
3680 json_array_append_new(invalid, json_string("x-hasPhoto"));
3681 }
3682 continue;
3683 }
3684 }
3685
3686 if (!strcmp(key, "uid")) {
3687 if (!json_is_string(jval)) {
3688 json_array_append_new(invalid, json_string("uid"));
3689 }
3690 }
3691 else if (!strcmp(key, "isFlagged")) {
3692 has_noncontent = 1;
3693 if (json_is_true(jval)) {
3694 strarray_add_case(flags, "\\Flagged");
3695 } else if (json_is_false(jval)) {
3696 strarray_remove_all_case(flags, "\\Flagged");
3697 } else {
3698 json_array_append_new(invalid, json_string("isFlagged"));
3699 }
3700 }
3701 else if (!strcmp(key, "importance")) {
3702 has_noncontent = 1;
3703 double dval = json_number_value(jval);
3704 const char *ns = DAV_ANNOT_NS "<" XML_NS_CYRUS ">importance";
3705 const char *attrib = "value.shared";
3706 struct buf buf = BUF_INITIALIZER;
3707 if (dval) {
3708 buf_printf(&buf, "%e", dval);
3709 }
3710 setentryatt(annotsp, ns, attrib, &buf);
3711 buf_free(&buf);
3712 }
3713 else if (!strcmp(key, "avatar")) {
3714 if (!json_is_null(jval)) {
3715 int r = _blob_to_card(req, card, "PHOTO", jval);
3716 if (r) {
3717 json_array_append_new(invalid, json_string("avatar"));
3718 continue;
3719 }
3720 record_is_dirty = 1;
3721 }
3722 else if (vparse_get_entry(card, NULL, "PHOTO")) {
3723 vparse_delete_entries(card, NULL, "PHOTO");
3724 record_is_dirty = 1;
3725 }
3726 }
3727 else if (!strcmp(key, "prefix")) {
3728 const char *val = json_string_value(jval);
3729 if (!val) {
3730 json_array_append_new(invalid, json_string("prefix"));
3731 continue;
3732 }
3733
3734 name_is_dirty = 1;
3735 strarray_set(n->v.values, 3, val);
3736 }
3737 else if (!strcmp(key, "firstName")) {
3738 const char *val = json_string_value(jval);
3739 if (!val) {
3740 json_array_append_new(invalid, json_string("firstName"));
3741 continue;
3742 }
3743 name_is_dirty = 1;
3744 /* JMAP doesn't have a separate field for Middle (aka "Additional
3745 * Names"), so any extra names are probably in firstName, and we
3746 * should split them out. See reverse of this in getcontacts_cb */
3747 const char *middle = strchr(val, ' ');
3748 if (middle) {
3749 /* multiple worlds, first to First, rest to Middle */
3750 strarray_setm(n->v.values, 1, xstrndup(val, middle-val));
3751 strarray_set(n->v.values, 2, ++middle);
3752 }
3753 else {
3754 /* single word, set First, clear Middle */
3755 strarray_set(n->v.values, 1, val);
3756 strarray_set(n->v.values, 2, "");
3757 }
3758 }
3759 else if (!strcmp(key, "lastName")) {
3760 const char *val = json_string_value(jval);
3761 if (!val) {
3762 json_array_append_new(invalid, json_string("lastName"));
3763 continue;
3764 }
3765 name_is_dirty = 1;
3766 strarray_set(n->v.values, 0, val);
3767 }
3768 else if (!strcmp(key, "suffix")) {
3769 const char *val = json_string_value(jval);
3770 if (!val) {
3771 json_array_append_new(invalid, json_string("suffix"));
3772 continue;
3773 }
3774 name_is_dirty = 1;
3775 strarray_set(n->v.values, 4, val);
3776 }
3777 else if (!strcmp(key, "nickname")) {
3778 const char *val = json_string_value(jval);
3779 if (!val) {
3780 json_array_append_new(invalid, json_string("nickname"));
3781 continue;
3782 }
3783 struct vparse_entry *nick = _card_multi(card, "NICKNAME", ',');
3784 strarray_truncate(nick->v.values, 0);
3785 if (*val) strarray_set(nick->v.values, 0, val);
3786 record_is_dirty = 1;
3787 }
3788 else if (!strcmp(key, "birthday")) {
3789 int r = _date_to_card(card, "BDAY", jval);
3790 if (r) {
3791 json_array_append_new(invalid, json_string("birthday"));
3792 continue;
3793 }
3794 record_is_dirty = 1;
3795 }
3796 else if (!strcmp(key, "anniversary")) {
3797 int r = _date_to_card(card, "ANNIVERSARY", jval);
3798 if (r) {
3799 json_array_append_new(invalid, json_string("anniversary"));
3800 continue;
3801 }
3802 record_is_dirty = 1;
3803 }
3804 else if (!strcmp(key, "jobTitle")) {
3805 int r = _kv_to_card(card, "TITLE", jval);
3806 if (r) {
3807 json_array_append_new(invalid, json_string("jobTitle"));
3808 continue;
3809 }
3810 record_is_dirty = 1;
3811 }
3812 else if (!strcmp(key, "company")) {
3813 const char *val = json_string_value(jval);
3814 if (!val) {
3815 json_array_append_new(invalid, json_string("company"));
3816 continue;
3817 }
3818 struct vparse_entry *org = _card_multi(card, "ORG", ';');
3819 strarray_set(org->v.values, 0, val);
3820 record_is_dirty = 1;
3821 }
3822 else if (!strcmp(key, "department")) {
3823 const char *val = json_string_value(jval);
3824 if (!val) {
3825 json_array_append_new(invalid, json_string("department"));
3826 continue;
3827 }
3828 struct vparse_entry *org = _card_multi(card, "ORG", ';');
3829 strarray_set(org->v.values, 1, val);
3830 record_is_dirty = 1;
3831 }
3832 else if (!strcmp(key, "emails")) {
3833 int r = _emails_to_card(card, jval, invalid);
3834 if (r) continue;
3835 record_is_dirty = 1;
3836 }
3837 else if (!strcmp(key, "phones")) {
3838 int r = _phones_to_card(card, jval, invalid);
3839 if (r) continue;
3840 record_is_dirty = 1;
3841 }
3842 else if (!strcmp(key, "online")) {
3843 int r = _online_to_card(card, jval, invalid);
3844 if (r) continue;
3845 record_is_dirty = 1;
3846 }
3847 else if (!strcmp(key, "addresses")) {
3848 int r = _addresses_to_card(card, jval, invalid);
3849 if (r) continue;
3850 record_is_dirty = 1;
3851 }
3852 else if (!strcmp(key, "notes")) {
3853 int r = _kv_to_card(card, "NOTE", jval);
3854 if (r) {
3855 json_array_append_new(invalid, json_string("notes"));
3856 continue;
3857 }
3858 record_is_dirty = 1;
3859 }
3860 else {
3861 json_array_append_new(invalid, json_string(key));
3862 }
3863 }
3864
3865 if (json_array_size(invalid)) return -1;
3866
3867 if (name_is_dirty) {
3868 _make_fn(card);
3869 record_is_dirty = 1;
3870 }
3871
3872 if (!record_is_dirty && has_noncontent)
3873 return HTTP_NO_CONTENT; /* no content */
3874
3875 return 0;
3876 }
3877
required_set_rights(json_t * props)3878 static int required_set_rights(json_t *props)
3879 {
3880 int needrights = 0;
3881 const char *name;
3882 json_t *val;
3883
3884 json_object_foreach(props, name, val) {
3885 if (!strcmp(name, "id") ||
3886 !strcmp(name, "x-href") ||
3887 !strcmp(name, "x-hasPhoto") ||
3888 !strcmp(name, "addressbookId")) {
3889 /* immutable */
3890 }
3891 else if (!strcmp(name, "importance")) {
3892 /* writing shared meta-data (per RFC 5257) */
3893 needrights |= JACL_SETPROPERTIES;
3894 }
3895 else if (!strcmp(name, "isFlagged")) {
3896 /* writing private meta-data */
3897 needrights |= JACL_SETKEYWORDS;
3898 }
3899 else {
3900 /* writing vCard data */
3901 needrights |= JACL_UPDATEITEMS;
3902 }
3903 }
3904
3905 return needrights;
3906 }
3907
_contact_set_create(jmap_req_t * req,unsigned kind,json_t * jcard,struct carddav_data * cdata,struct mailbox ** mailbox,json_t * item,json_t * invalid)3908 static int _contact_set_create(jmap_req_t *req, unsigned kind,
3909 json_t *jcard, struct carddav_data *cdata,
3910 struct mailbox **mailbox, json_t *item,
3911 json_t *invalid)
3912 {
3913 struct entryattlist *annots = NULL;
3914 strarray_t *flags = NULL;
3915 struct vparse_card *card = NULL;
3916 char *uid = NULL;
3917 int r = 0;
3918 char *resourcename = NULL;
3919
3920 if ((uid = (char *) json_string_value(json_object_get(jcard, "uid")))) {
3921 /* Use custom vCard UID from request object */
3922 uid = xstrdup(uid);
3923 } else {
3924 /* Create a vCard UID */
3925 uid = xstrdup(makeuuid());
3926 json_object_set_new(item, "uid", json_string(uid));
3927 }
3928
3929 /* Determine mailbox and resource name of card.
3930 * We attempt to reuse the UID as DAV resource name; but
3931 * only if it looks like a reasonable URL path segment. */
3932 struct buf buf = BUF_INITIALIZER;
3933 const char *p;
3934 for (p = uid; *p; p++) {
3935 if ((*p >= '0' && *p <= '9') ||
3936 (*p >= 'a' && *p <= 'z') ||
3937 (*p >= 'A' && *p <= 'Z') ||
3938 (p > uid &&
3939 (*p == '@' || *p == '.' ||
3940 *p == '_' || *p == '-'))) {
3941 continue;
3942 }
3943 break;
3944 }
3945 if (*p == '\0' && p - uid >= 16 && p - uid <= 200) {
3946 buf_setcstr(&buf, uid);
3947 } else {
3948 buf_setcstr(&buf, makeuuid());
3949 }
3950 buf_appendcstr(&buf, ".vcf");
3951 resourcename = buf_newcstring(&buf);
3952 buf_free(&buf);
3953
3954 const char *addressbookId = "Default";
3955 json_t *abookid = json_object_get(jcard, "addressbookId");
3956 if (abookid && json_string_value(abookid)) {
3957 /* XXX - invalid arguments */
3958 addressbookId = json_string_value(abookid);
3959 }
3960 else {
3961 json_object_set_new(item, "addressbookId", json_string(addressbookId));
3962 }
3963 char *mboxname = mboxname_abook(req->accountid, addressbookId);
3964 json_object_del(jcard, "addressbookId");
3965 addressbookId = NULL;
3966
3967 int needrights = required_set_rights(jcard);
3968
3969 /* Check permissions. */
3970 if (!jmap_hasrights(req, mboxname, needrights)) {
3971 json_array_append_new(invalid, json_string("addressbookId"));
3972 goto done;
3973 }
3974
3975 card = vparse_new_card("VCARD");
3976 vparse_add_entry(card, NULL, "PRODID", _prodid);
3977 vparse_add_entry(card, NULL, "VERSION", "3.0");
3978 vparse_add_entry(card, NULL, "UID", uid);
3979
3980 /* we need to create and append a record */
3981 if (!*mailbox || strcmp((*mailbox)->name, mboxname)) {
3982 jmap_closembox(req, mailbox);
3983 r = jmap_openmbox(req, mboxname, mailbox, 1);
3984 if (r == IMAP_MAILBOX_NONEXISTENT) {
3985 json_array_append_new(invalid, json_string("addressbookId"));
3986 r = 0;
3987 goto done;
3988 }
3989 else if (r) goto done;
3990 }
3991
3992 const char *name = NULL;
3993 const char *logfmt = NULL;
3994
3995 if (kind == CARDDAV_KIND_GROUP) {
3996 jmap_readprop(jcard, "name", 1, invalid, "s", &name);
3997
3998 vparse_add_entry(card, NULL, "N", name);
3999 vparse_add_entry(card, NULL, "FN", name);
4000 vparse_add_entry(card, NULL, "X-ADDRESSBOOKSERVER-KIND", "group");
4001
4002 /* it's legal to create an empty group */
4003 json_t *members = json_object_get(jcard, "contactIds");
4004 if (members) {
4005 _add_group_entries(req, card, members, invalid);
4006 }
4007
4008 /* it's legal to create an empty group */
4009 json_t *others = json_object_get(jcard, "otherAccountContactIds");
4010 if (others) {
4011 _add_othergroup_entries(req, card, others, invalid);
4012 }
4013
4014 logfmt = "jmap: create group %s/%s/%s (%s)";
4015 }
4016 else {
4017 flags = strarray_new();
4018 r = _json_to_card(req, cdata, card, jcard, flags, &annots, invalid);
4019
4020 logfmt = "jmap: create contact %s/%s (%s)";
4021 }
4022
4023 if (r || json_array_size(invalid)) {
4024 r = 0;
4025 goto done;
4026 }
4027
4028 syslog(LOG_NOTICE, logfmt, req->accountid, mboxname, uid, name);
4029 r = carddav_store(*mailbox, card, resourcename, 0, flags, &annots,
4030 req->accountid, req->authstate, ignorequota);
4031 if (r && r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
4032 syslog(LOG_ERR, "carddav_store failed for user %s: %s",
4033 req->accountid, error_message(r));
4034 goto done;
4035 }
4036 r = 0;
4037
4038 json_object_set_new(item, "id", json_string(uid));
4039
4040 done:
4041 vparse_free_card(card);
4042 free(mboxname);
4043 free(resourcename);
4044 strarray_free(flags);
4045 freeentryatts(annots);
4046 free(uid);
4047
4048 return r;
4049 }
4050
jmap_contact_set(struct jmap_req * req)4051 static int jmap_contact_set(struct jmap_req *req)
4052 {
4053 _contacts_set(req, CARDDAV_KIND_CONTACT);
4054 return 0;
4055 }
4056
jmap_contact_findblob(struct message_guid * content_guid,const char * part_id,struct mailbox * mbox,msgrecord_t * mr,struct buf * blob)4057 const struct body *jmap_contact_findblob(struct message_guid *content_guid,
4058 const char *part_id,
4059 struct mailbox *mbox,
4060 msgrecord_t *mr,
4061 struct buf *blob)
4062 {
4063 const struct body *ret = NULL;
4064 struct index_record record;
4065 struct vparse_card *vcard;
4066 const char *proppath = strstr(part_id, "/VCARD#");
4067
4068 if (!proppath) return NULL;
4069
4070 msgrecord_get_index_record(mr, &record);
4071 vcard = record_to_vcard(mbox, &record);
4072
4073 if (vcard) {
4074 static struct body subpart;
4075 char *type = NULL;
4076 struct vparse_entry *entry =
4077 vparse_get_entry(vcard->objects, NULL, proppath+7);
4078
4079 memset(&subpart, 0, sizeof(struct body));
4080
4081 if (entry &&
4082 vcard_prop_decode_value(entry, blob, &type, &subpart.content_guid) &&
4083 !message_guid_cmp(content_guid, &subpart.content_guid)) {
4084 /* Build a body part for the property */
4085 subpart.charset_enc = ENCODING_NONE;
4086 subpart.encoding = "BINARY";
4087 subpart.header_offset = 0;
4088 subpart.header_size = 0;
4089 subpart.content_offset = 0;
4090 subpart.content_size = buf_len(blob);
4091 ret = &subpart;
4092 }
4093
4094 free(type);
4095 vparse_free_card(vcard);
4096 }
4097
4098 return ret;
4099 }
4100
_contact_copy(jmap_req_t * req,json_t * jcard,struct carddav_db * src_db,json_t ** new_card,json_t ** set_err)4101 static void _contact_copy(jmap_req_t *req,
4102 json_t *jcard,
4103 struct carddav_db *src_db,
4104 json_t **new_card,
4105 json_t **set_err)
4106 {
4107 struct jmap_parser myparser = JMAP_PARSER_INITIALIZER;
4108 struct vparse_card *vcard = NULL;
4109 json_t *dst_card = NULL;
4110 struct mailbox *src_mbox = NULL;
4111 struct mailbox *dst_mbox = NULL;
4112 int r = 0;
4113
4114 /* Read mandatory properties */
4115 const char *src_id = json_string_value(json_object_get(jcard, "id"));
4116 if (!src_id) {
4117 jmap_parser_invalid(&myparser, "id");
4118 }
4119 if (json_array_size(myparser.invalid)) {
4120 *set_err = json_pack("{s:s s:O}", "type", "invalidProperties",
4121 "properties", myparser.invalid);
4122 goto done;
4123 }
4124
4125 /* Lookup event */
4126 struct carddav_data *cdata = NULL;
4127 r = carddav_lookup_uid(src_db, src_id, &cdata);
4128 if (r && r != CYRUSDB_NOTFOUND) {
4129 syslog(LOG_ERR, "carddav_lookup_uid(%s) failed: %s",
4130 src_id, error_message(r));
4131 goto done;
4132 }
4133 if (r == CYRUSDB_NOTFOUND || !cdata->dav.alive ||
4134 !cdata->dav.rowid || !cdata->dav.imap_uid) {
4135 *set_err = json_pack("{s:s}", "type", "notFound");
4136 goto done;
4137 }
4138 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS)) {
4139 *set_err = json_pack("{s:s}", "type", "notFound");
4140 goto done;
4141 }
4142
4143 /* Read source event */
4144 r = jmap_openmbox(req, cdata->dav.mailbox, &src_mbox, /*rw*/0);
4145 if (r) goto done;
4146 struct index_record record;
4147 r = mailbox_find_index_record(src_mbox, cdata->dav.imap_uid, &record);
4148 if (!r) vcard = record_to_vcard(src_mbox, &record);
4149 if (!vcard || !vcard->objects) {
4150 syslog(LOG_ERR, "contact_copy: can't convert %s to JMAP", src_id);
4151 r = IMAP_INTERNAL;
4152 goto done;
4153 }
4154
4155 /* Patch JMAP event */
4156 json_t *src_card = jmap_contact_from_vcard(vcard->objects, src_mbox, &record);
4157 if (src_card) {
4158 json_t *avatar = json_object_get(src_card, "avatar");
4159 if (avatar) {
4160 /* _blob_to_card() needs to know in which account to find blob */
4161 json_object_set(avatar, "accountId",
4162 json_object_get(req->args, "fromAccountId"));
4163 }
4164 json_object_del(src_card, "x-href"); // immutable and WILL change
4165 json_object_del(src_card, "x-hasPhoto"); // immutable and WILL change
4166 dst_card = jmap_patchobject_apply(src_card, jcard, NULL);
4167 }
4168 json_decref(src_card);
4169
4170 /* Create vcard */
4171 json_t *invalid = json_array();
4172 json_t *item = json_object();
4173 r = _contact_set_create(req, CARDDAV_KIND_CONTACT, dst_card,
4174 cdata, &dst_mbox, item, invalid);
4175 if (r || json_array_size(invalid)) {
4176 if (!r) {
4177 *set_err = json_pack("{s:s s:o}", "type", "invalidProperties",
4178 "properties", invalid);
4179 }
4180 else json_decref(invalid);
4181 json_decref(item);
4182 goto done;
4183 }
4184 json_decref(invalid);
4185
4186 *new_card = item;
4187
4188 done:
4189 if (r && *set_err == NULL) {
4190 if (r == CYRUSDB_NOTFOUND)
4191 *set_err = json_pack("{s:s}", "type", "notFound");
4192 else
4193 *set_err = jmap_server_error(r);
4194 }
4195 jmap_closembox(req, &dst_mbox);
4196 jmap_closembox(req, &src_mbox);
4197 if (vcard) vparse_free_card(vcard);
4198 json_decref(dst_card);
4199 jmap_parser_fini(&myparser);
4200 }
4201
jmap_contact_copy(struct jmap_req * req)4202 static int jmap_contact_copy(struct jmap_req *req)
4203 {
4204 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
4205 struct jmap_copy copy;
4206 json_t *err = NULL;
4207 struct carddav_db *src_db = NULL;
4208 json_t *destroy_cards = json_array();
4209
4210 /* Parse request */
4211 jmap_copy_parse(req, &parser, NULL, NULL, ©, &err);
4212 if (err) {
4213 jmap_error(req, err);
4214 goto done;
4215 }
4216
4217 src_db = carddav_open_userid(copy.from_account_id);
4218 if (!src_db) {
4219 jmap_error(req, json_pack("{s:s}", "type", "fromAccountNotFound"));
4220 goto done;
4221 }
4222
4223 /* Process request */
4224 const char *creation_id;
4225 json_t *jcard;
4226 json_object_foreach(copy.create, creation_id, jcard) {
4227 /* Copy event */
4228 json_t *set_err = NULL;
4229 json_t *new_card = NULL;
4230
4231 _contact_copy(req, jcard, src_db, /*dst_db,*/ &new_card, &set_err);
4232 if (set_err) {
4233 json_object_set_new(copy.not_created, creation_id, set_err);
4234 continue;
4235 }
4236
4237 // extract the id for later deletion
4238 json_array_append(destroy_cards, json_object_get(jcard, "id"));
4239
4240 /* Report event as created */
4241 json_object_set_new(copy.created, creation_id, new_card);
4242 const char *card_id = json_string_value(json_object_get(new_card, "id"));
4243 jmap_add_id(req, creation_id, card_id);
4244 }
4245
4246 /* Build response */
4247 jmap_ok(req, jmap_copy_reply(©));
4248
4249 /* Destroy originals, if requested */
4250 if (copy.on_success_destroy_original && json_array_size(destroy_cards)) {
4251 json_t *subargs = json_object();
4252 json_object_set(subargs, "destroy", destroy_cards);
4253 json_object_set_new(subargs, "accountId", json_string(copy.from_account_id));
4254 jmap_add_subreq(req, "Contact/set", subargs, NULL);
4255 }
4256
4257 done:
4258 json_decref(destroy_cards);
4259 if (src_db) carddav_close(src_db);
4260 jmap_parser_fini(&parser);
4261 jmap_copy_fini(©);
4262 return 0;
4263 }
4264