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