]> git.notmuchmail.org Git - notmuch/blob - notmuch-search.c
86d54ba044357001c6e46f10d2fb2b76a3f00eba
[notmuch] / notmuch-search.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-client.h"
22 #include "sprinter.h"
23 #include "string-util.h"
24
25 typedef enum {
26     /* Search command */
27     OUTPUT_SUMMARY      = 1 << 0,
28     OUTPUT_THREADS      = 1 << 1,
29     OUTPUT_MESSAGES     = 1 << 2,
30     OUTPUT_FILES        = 1 << 3,
31     OUTPUT_TAGS         = 1 << 4,
32
33     /* Address command */
34     OUTPUT_SENDER       = 1 << 5,
35     OUTPUT_RECIPIENTS   = 1 << 6,
36 } output_t;
37
38 typedef enum {
39     NOTMUCH_FORMAT_JSON,
40     NOTMUCH_FORMAT_TEXT,
41     NOTMUCH_FORMAT_TEXT0,
42     NOTMUCH_FORMAT_SEXP
43 } format_sel_t;
44
45 typedef struct {
46     notmuch_database_t *notmuch;
47     format_sel_t format_sel;
48     sprinter_t *format;
49     notmuch_exclude_t exclude;
50     notmuch_query_t *query;
51     notmuch_sort_t sort;
52     output_t output;
53     int offset;
54     int limit;
55     int dupe;
56     GHashTable *addresses;
57 } search_context_t;
58
59 typedef struct {
60     const char *name;
61     const char *addr;
62 } mailbox_t;
63
64 /* Return two stable query strings that identify exactly the matched
65  * and unmatched messages currently in thread.  If there are no
66  * matched or unmatched messages, the returned buffers will be
67  * NULL. */
68 static int
69 get_thread_query (notmuch_thread_t *thread,
70                   char **matched_out, char **unmatched_out)
71 {
72     notmuch_messages_t *messages;
73     char *escaped = NULL;
74     size_t escaped_len = 0;
75
76     *matched_out = *unmatched_out = NULL;
77
78     for (messages = notmuch_thread_get_messages (thread);
79          notmuch_messages_valid (messages);
80          notmuch_messages_move_to_next (messages))
81     {
82         notmuch_message_t *message = notmuch_messages_get (messages);
83         const char *mid = notmuch_message_get_message_id (message);
84         /* Determine which query buffer to extend */
85         char **buf = notmuch_message_get_flag (
86             message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
87         /* Add this message's id: query.  Since "id" is an exclusive
88          * prefix, it is implicitly 'or'd together, so we only need to
89          * join queries with a space. */
90         if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
91             return -1;
92         if (*buf)
93             *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
94         else
95             *buf = talloc_strdup (thread, escaped);
96         if (!*buf)
97             return -1;
98     }
99     talloc_free (escaped);
100     return 0;
101 }
102
103 static int
104 do_search_threads (search_context_t *ctx)
105 {
106     notmuch_thread_t *thread;
107     notmuch_threads_t *threads;
108     notmuch_tags_t *tags;
109     sprinter_t *format = ctx->format;
110     time_t date;
111     int i;
112
113     if (ctx->offset < 0) {
114         ctx->offset += notmuch_query_count_threads (ctx->query);
115         if (ctx->offset < 0)
116             ctx->offset = 0;
117     }
118
119     threads = notmuch_query_search_threads (ctx->query);
120     if (threads == NULL)
121         return 1;
122
123     format->begin_list (format);
124
125     for (i = 0;
126          notmuch_threads_valid (threads) && (ctx->limit < 0 || i < ctx->offset + ctx->limit);
127          notmuch_threads_move_to_next (threads), i++)
128     {
129         thread = notmuch_threads_get (threads);
130
131         if (i < ctx->offset) {
132             notmuch_thread_destroy (thread);
133             continue;
134         }
135
136         if (ctx->output == OUTPUT_THREADS) {
137             format->set_prefix (format, "thread");
138             format->string (format,
139                             notmuch_thread_get_thread_id (thread));
140             format->separator (format);
141         } else { /* output == OUTPUT_SUMMARY */
142             void *ctx_quote = talloc_new (thread);
143             const char *authors = notmuch_thread_get_authors (thread);
144             const char *subject = notmuch_thread_get_subject (thread);
145             const char *thread_id = notmuch_thread_get_thread_id (thread);
146             int matched = notmuch_thread_get_matched_messages (thread);
147             int total = notmuch_thread_get_total_messages (thread);
148             const char *relative_date = NULL;
149             notmuch_bool_t first_tag = TRUE;
150
151             format->begin_map (format);
152
153             if (ctx->sort == NOTMUCH_SORT_OLDEST_FIRST)
154                 date = notmuch_thread_get_oldest_date (thread);
155             else
156                 date = notmuch_thread_get_newest_date (thread);
157
158             relative_date = notmuch_time_relative_date (ctx_quote, date);
159
160             if (format->is_text_printer) {
161                 /* Special case for the text formatter */
162                 printf ("thread:%s %12s [%d/%d] %s; %s (",
163                         thread_id,
164                         relative_date,
165                         matched,
166                         total,
167                         sanitize_string (ctx_quote, authors),
168                         sanitize_string (ctx_quote, subject));
169             } else { /* Structured Output */
170                 format->map_key (format, "thread");
171                 format->string (format, thread_id);
172                 format->map_key (format, "timestamp");
173                 format->integer (format, date);
174                 format->map_key (format, "date_relative");
175                 format->string (format, relative_date);
176                 format->map_key (format, "matched");
177                 format->integer (format, matched);
178                 format->map_key (format, "total");
179                 format->integer (format, total);
180                 format->map_key (format, "authors");
181                 format->string (format, authors);
182                 format->map_key (format, "subject");
183                 format->string (format, subject);
184                 if (notmuch_format_version >= 2) {
185                     char *matched_query, *unmatched_query;
186                     if (get_thread_query (thread, &matched_query,
187                                           &unmatched_query) < 0) {
188                         fprintf (stderr, "Out of memory\n");
189                         return 1;
190                     }
191                     format->map_key (format, "query");
192                     format->begin_list (format);
193                     if (matched_query)
194                         format->string (format, matched_query);
195                     else
196                         format->null (format);
197                     if (unmatched_query)
198                         format->string (format, unmatched_query);
199                     else
200                         format->null (format);
201                     format->end (format);
202                 }
203             }
204
205             talloc_free (ctx_quote);
206
207             format->map_key (format, "tags");
208             format->begin_list (format);
209
210             for (tags = notmuch_thread_get_tags (thread);
211                  notmuch_tags_valid (tags);
212                  notmuch_tags_move_to_next (tags))
213             {
214                 const char *tag = notmuch_tags_get (tags);
215
216                 if (format->is_text_printer) {
217                   /* Special case for the text formatter */
218                     if (first_tag)
219                         first_tag = FALSE;
220                     else
221                         fputc (' ', stdout);
222                     fputs (tag, stdout);
223                 } else { /* Structured Output */
224                     format->string (format, tag);
225                 }
226             }
227
228             if (format->is_text_printer)
229                 printf (")");
230
231             format->end (format);
232             format->end (format);
233             format->separator (format);
234         }
235
236         notmuch_thread_destroy (thread);
237     }
238
239     format->end (format);
240
241     return 0;
242 }
243
244 /* Returns TRUE iff name and addr is duplicate. If not, stores the
245  * name/addr pair in order to detect subsequent duplicates. */
246 static notmuch_bool_t
247 is_duplicate (const search_context_t *ctx, const char *name, const char *addr)
248 {
249     notmuch_bool_t duplicate;
250     char *key;
251
252     key = talloc_asprintf (ctx->format, "%s <%s>", name, addr);
253     if (! key)
254         return FALSE;
255
256     duplicate = g_hash_table_lookup_extended (ctx->addresses, key, NULL, NULL);
257
258     if (! duplicate)
259         g_hash_table_insert (ctx->addresses, key, NULL);
260     else
261         talloc_free (key);
262
263     return duplicate;
264 }
265
266 static void
267 print_mailbox (const search_context_t *ctx, const mailbox_t *mailbox)
268 {
269     const char *name = mailbox->name;
270     const char *addr = mailbox->addr;
271     sprinter_t *format = ctx->format;
272     InternetAddress *ia = internet_address_mailbox_new (name, addr);
273     char *name_addr;
274
275     /* name_addr has the name part quoted if necessary. Compare
276      * 'John Doe <john@doe.com>' vs. '"Doe, John" <john@doe.com>' */
277     name_addr = internet_address_to_string (ia, FALSE);
278
279     if (format->is_text_printer) {
280         format->string (format, name_addr);
281         format->separator (format);
282     } else {
283         format->begin_map (format);
284         format->map_key (format, "name");
285         format->string (format, name);
286         format->map_key (format, "address");
287         format->string (format, addr);
288         format->map_key (format, "name-addr");
289         format->string (format, name_addr);
290         format->end (format);
291         format->separator (format);
292     }
293
294     g_object_unref (ia);
295     g_free (name_addr);
296 }
297
298 /* Print addresses from InternetAddressList.  */
299 static void
300 process_address_list (const search_context_t *ctx,
301                       InternetAddressList *list)
302 {
303     InternetAddress *address;
304     int i;
305
306     for (i = 0; i < internet_address_list_length (list); i++) {
307         address = internet_address_list_get_address (list, i);
308         if (INTERNET_ADDRESS_IS_GROUP (address)) {
309             InternetAddressGroup *group;
310             InternetAddressList *group_list;
311
312             group = INTERNET_ADDRESS_GROUP (address);
313             group_list = internet_address_group_get_members (group);
314             if (group_list == NULL)
315                 continue;
316
317             process_address_list (ctx, group_list);
318         } else {
319             InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address);
320             mailbox_t mbx = {
321                 .name = internet_address_get_name (address),
322                 .addr = internet_address_mailbox_get_addr (mailbox),
323             };
324
325             if (is_duplicate (ctx, mbx.name, mbx.addr))
326                 continue;
327
328             print_mailbox (ctx, &mbx);
329         }
330     }
331 }
332
333 /* Print addresses from a message header.  */
334 static void
335 process_address_header (const search_context_t *ctx, const char *value)
336 {
337     InternetAddressList *list;
338
339     if (value == NULL)
340         return;
341
342     list = internet_address_list_parse_string (value);
343     if (list == NULL)
344         return;
345
346     process_address_list (ctx, list);
347
348     g_object_unref (list);
349 }
350
351 /* Destructor for talloc-allocated GHashTable keys and values. */
352 static void
353 _talloc_free_for_g_hash (void *ptr)
354 {
355     talloc_free (ptr);
356 }
357
358 static int
359 _count_filenames (notmuch_message_t *message)
360 {
361     notmuch_filenames_t *filenames;
362     int i = 0;
363
364     filenames = notmuch_message_get_filenames (message);
365
366     while (notmuch_filenames_valid (filenames)) {
367         notmuch_filenames_move_to_next (filenames);
368         i++;
369     }
370
371     notmuch_filenames_destroy (filenames);
372
373     return i;
374 }
375
376 static int
377 do_search_messages (search_context_t *ctx)
378 {
379     notmuch_message_t *message;
380     notmuch_messages_t *messages;
381     notmuch_filenames_t *filenames;
382     sprinter_t *format = ctx->format;
383     int i;
384
385     if (ctx->offset < 0) {
386         ctx->offset += notmuch_query_count_messages (ctx->query);
387         if (ctx->offset < 0)
388             ctx->offset = 0;
389     }
390
391     messages = notmuch_query_search_messages (ctx->query);
392     if (messages == NULL)
393         return 1;
394
395     format->begin_list (format);
396
397     for (i = 0;
398          notmuch_messages_valid (messages) && (ctx->limit < 0 || i < ctx->offset + ctx->limit);
399          notmuch_messages_move_to_next (messages), i++)
400     {
401         if (i < ctx->offset)
402             continue;
403
404         message = notmuch_messages_get (messages);
405
406         if (ctx->output == OUTPUT_FILES) {
407             int j;
408             filenames = notmuch_message_get_filenames (message);
409
410             for (j = 1;
411                  notmuch_filenames_valid (filenames);
412                  notmuch_filenames_move_to_next (filenames), j++)
413             {
414                 if (ctx->dupe < 0 || ctx->dupe == j) {
415                     format->string (format, notmuch_filenames_get (filenames));
416                     format->separator (format);
417                 }
418             }
419             
420             notmuch_filenames_destroy( filenames );
421
422         } else if (ctx->output == OUTPUT_MESSAGES) {
423             /* special case 1 for speed */
424             if (ctx->dupe <= 1 || ctx->dupe <= _count_filenames (message)) {
425                 format->set_prefix (format, "id");
426                 format->string (format,
427                                 notmuch_message_get_message_id (message));
428                 format->separator (format);
429             }
430         } else {
431             if (ctx->output & OUTPUT_SENDER) {
432                 const char *addrs;
433
434                 addrs = notmuch_message_get_header (message, "from");
435                 process_address_header (ctx, addrs);
436             }
437
438             if (ctx->output & OUTPUT_RECIPIENTS) {
439                 const char *hdrs[] = { "to", "cc", "bcc" };
440                 const char *addrs;
441                 size_t j;
442
443                 for (j = 0; j < ARRAY_SIZE (hdrs); j++) {
444                     addrs = notmuch_message_get_header (message, hdrs[j]);
445                     process_address_header (ctx, addrs);
446                 }
447             }
448         }
449
450         notmuch_message_destroy (message);
451     }
452
453     notmuch_messages_destroy (messages);
454
455     format->end (format);
456
457     return 0;
458 }
459
460 static int
461 do_search_tags (const search_context_t *ctx)
462 {
463     notmuch_messages_t *messages = NULL;
464     notmuch_tags_t *tags;
465     const char *tag;
466     sprinter_t *format = ctx->format;
467     notmuch_query_t *query = ctx->query;
468     notmuch_database_t *notmuch = ctx->notmuch;
469
470     /* should the following only special case if no excluded terms
471      * specified? */
472
473     /* Special-case query of "*" for better performance. */
474     if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
475         tags = notmuch_database_get_all_tags (notmuch);
476     } else {
477         messages = notmuch_query_search_messages (query);
478         if (messages == NULL)
479             return 1;
480
481         tags = notmuch_messages_collect_tags (messages);
482     }
483     if (tags == NULL)
484         return 1;
485
486     format->begin_list (format);
487
488     for (;
489          notmuch_tags_valid (tags);
490          notmuch_tags_move_to_next (tags))
491     {
492         tag = notmuch_tags_get (tags);
493
494         format->string (format, tag);
495         format->separator (format);
496
497     }
498
499     notmuch_tags_destroy (tags);
500
501     if (messages)
502         notmuch_messages_destroy (messages);
503
504     format->end (format);
505
506     return 0;
507 }
508
509 static int
510 _notmuch_search_prepare (search_context_t *ctx, notmuch_config_t *config, int argc, char *argv[])
511 {
512     char *query_str;
513     unsigned int i;
514
515     switch (ctx->format_sel) {
516     case NOTMUCH_FORMAT_TEXT:
517         ctx->format = sprinter_text_create (config, stdout);
518         break;
519     case NOTMUCH_FORMAT_TEXT0:
520         if (ctx->output == OUTPUT_SUMMARY) {
521             fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
522             return EXIT_FAILURE;
523         }
524         ctx->format = sprinter_text0_create (config, stdout);
525         break;
526     case NOTMUCH_FORMAT_JSON:
527         ctx->format = sprinter_json_create (config, stdout);
528         break;
529     case NOTMUCH_FORMAT_SEXP:
530         ctx->format = sprinter_sexp_create (config, stdout);
531         break;
532     default:
533         /* this should never happen */
534         INTERNAL_ERROR("no output format selected");
535     }
536
537     notmuch_exit_if_unsupported_format ();
538
539     if (notmuch_database_open (notmuch_config_get_database_path (config),
540                                NOTMUCH_DATABASE_MODE_READ_ONLY, &ctx->notmuch))
541         return EXIT_FAILURE;
542
543     query_str = query_string_from_args (ctx->notmuch, argc, argv);
544     if (query_str == NULL) {
545         fprintf (stderr, "Out of memory.\n");
546         return EXIT_FAILURE;
547     }
548     if (*query_str == '\0') {
549         fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
550         return EXIT_FAILURE;
551     }
552
553     ctx->query = notmuch_query_create (ctx->notmuch, query_str);
554     if (ctx->query == NULL) {
555         fprintf (stderr, "Out of memory\n");
556         return EXIT_FAILURE;
557     }
558
559     notmuch_query_set_sort (ctx->query, ctx->sort);
560
561     if (ctx->exclude == NOTMUCH_EXCLUDE_FLAG && ctx->output != OUTPUT_SUMMARY) {
562         /* If we are not doing summary output there is nowhere to
563          * print the excluded flag so fall back on including the
564          * excluded messages. */
565         fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
566         ctx->exclude = NOTMUCH_EXCLUDE_FALSE;
567     }
568
569     if (ctx->exclude != NOTMUCH_EXCLUDE_FALSE) {
570         const char **search_exclude_tags;
571         size_t search_exclude_tags_length;
572
573         search_exclude_tags = notmuch_config_get_search_exclude_tags
574             (config, &search_exclude_tags_length);
575         for (i = 0; i < search_exclude_tags_length; i++)
576             notmuch_query_add_tag_exclude (ctx->query, search_exclude_tags[i]);
577         notmuch_query_set_omit_excluded (ctx->query, ctx->exclude);
578     }
579
580     return 0;
581 }
582
583 static void
584 _notmuch_search_cleanup (search_context_t *ctx)
585 {
586     notmuch_query_destroy (ctx->query);
587     notmuch_database_destroy (ctx->notmuch);
588
589     talloc_free (ctx->format);
590 }
591
592 static search_context_t search_context = {
593     .format_sel = NOTMUCH_FORMAT_TEXT,
594     .exclude = NOTMUCH_EXCLUDE_TRUE,
595     .sort = NOTMUCH_SORT_NEWEST_FIRST,
596     .output = 0,
597     .offset = 0,
598     .limit = -1, /* unlimited */
599     .dupe = -1,
600 };
601
602 static const notmuch_opt_desc_t common_options[] = {
603     { NOTMUCH_OPT_KEYWORD, &search_context.sort, "sort", 's',
604       (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
605                               { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
606                               { 0, 0 } } },
607     { NOTMUCH_OPT_KEYWORD, &search_context.format_sel, "format", 'f',
608       (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
609                               { "sexp", NOTMUCH_FORMAT_SEXP },
610                               { "text", NOTMUCH_FORMAT_TEXT },
611                               { "text0", NOTMUCH_FORMAT_TEXT0 },
612                               { 0, 0 } } },
613     { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
614     { 0, 0, 0, 0, 0 }
615 };
616
617 int
618 notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
619 {
620     search_context_t *ctx = &search_context;
621     int opt_index, ret;
622
623     notmuch_opt_desc_t options[] = {
624         { NOTMUCH_OPT_KEYWORD, &ctx->output, "output", 'o',
625           (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
626                                   { "threads", OUTPUT_THREADS },
627                                   { "messages", OUTPUT_MESSAGES },
628                                   { "files", OUTPUT_FILES },
629                                   { "tags", OUTPUT_TAGS },
630                                   { 0, 0 } } },
631         { NOTMUCH_OPT_KEYWORD, &ctx->exclude, "exclude", 'x',
632           (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
633                                   { "false", NOTMUCH_EXCLUDE_FALSE },
634                                   { "flag", NOTMUCH_EXCLUDE_FLAG },
635                                   { "all", NOTMUCH_EXCLUDE_ALL },
636                                   { 0, 0 } } },
637         { NOTMUCH_OPT_INT, &ctx->offset, "offset", 'O', 0 },
638         { NOTMUCH_OPT_INT, &ctx->limit, "limit", 'L', 0  },
639         { NOTMUCH_OPT_INT, &ctx->dupe, "duplicate", 'D', 0  },
640         { NOTMUCH_OPT_INHERIT, &common_options, NULL, 0, 0 },
641         { 0, 0, 0, 0, 0 }
642     };
643
644     ctx->output = OUTPUT_SUMMARY;
645     opt_index = parse_arguments (argc, argv, options, 1);
646     if (opt_index < 0)
647         return EXIT_FAILURE;
648
649     if (ctx->output != OUTPUT_FILES && ctx->output != OUTPUT_MESSAGES &&
650         ctx->dupe != -1) {
651         fprintf (stderr, "Error: --duplicate=N is only supported with --output=files and --output=messages.\n");
652         return EXIT_FAILURE;
653     }
654
655     if (_notmuch_search_prepare (ctx, config,
656                                  argc - opt_index, argv + opt_index))
657         return EXIT_FAILURE;
658
659     switch (ctx->output) {
660     case OUTPUT_SUMMARY:
661     case OUTPUT_THREADS:
662         ret = do_search_threads (ctx);
663         break;
664     case OUTPUT_MESSAGES:
665     case OUTPUT_FILES:
666         ret = do_search_messages (ctx);
667         break;
668     case OUTPUT_TAGS:
669         ret = do_search_tags (ctx);
670         break;
671     default:
672         INTERNAL_ERROR ("Unexpected output");
673     }
674
675     _notmuch_search_cleanup (ctx);
676
677     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
678 }
679
680 int
681 notmuch_address_command (notmuch_config_t *config, int argc, char *argv[])
682 {
683     search_context_t *ctx = &search_context;
684     int opt_index, ret;
685
686     notmuch_opt_desc_t options[] = {
687         { NOTMUCH_OPT_KEYWORD_FLAGS, &ctx->output, "output", 'o',
688           (notmuch_keyword_t []){ { "sender", OUTPUT_SENDER },
689                                   { "recipients", OUTPUT_RECIPIENTS },
690                                   { 0, 0 } } },
691         { NOTMUCH_OPT_KEYWORD, &ctx->exclude, "exclude", 'x',
692           (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
693                                   { "false", NOTMUCH_EXCLUDE_FALSE },
694                                   { 0, 0 } } },
695         { NOTMUCH_OPT_INHERIT, &common_options, NULL, 0, 0 },
696         { 0, 0, 0, 0, 0 }
697     };
698
699     opt_index = parse_arguments (argc, argv, options, 1);
700     if (opt_index < 0)
701         return EXIT_FAILURE;
702
703     if (! ctx->output)
704         ctx->output = OUTPUT_SENDER | OUTPUT_RECIPIENTS;
705
706     if (_notmuch_search_prepare (ctx, config,
707                                  argc - opt_index, argv + opt_index))
708         return EXIT_FAILURE;
709
710     ctx->addresses = g_hash_table_new_full (g_str_hash, g_str_equal,
711                                             _talloc_free_for_g_hash, NULL);
712
713     ret = do_search_messages (ctx);
714
715     g_hash_table_unref (ctx->addresses);
716
717
718     _notmuch_search_cleanup (ctx);
719
720     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
721 }