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", &region);
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, &copy, &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(&copy));
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(&copy);
4262     return 0;
4263 }
4264