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