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