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