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