test: Test upgrade to ghost messages feature
[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,
27     OUTPUT_THREADS,
28     OUTPUT_MESSAGES,
29     OUTPUT_FILES,
30     OUTPUT_TAGS
31 } output_t;
32
33 /* Return two stable query strings that identify exactly the matched
34  * and unmatched messages currently in thread.  If there are no
35  * matched or unmatched messages, the returned buffers will be
36  * NULL. */
37 static int
38 get_thread_query (notmuch_thread_t *thread,
39                   char **matched_out, char **unmatched_out)
40 {
41     notmuch_messages_t *messages;
42     char *escaped = NULL;
43     size_t escaped_len = 0;
44
45     *matched_out = *unmatched_out = NULL;
46
47     for (messages = notmuch_thread_get_messages (thread);
48          notmuch_messages_valid (messages);
49          notmuch_messages_move_to_next (messages))
50     {
51         notmuch_message_t *message = notmuch_messages_get (messages);
52         const char *mid = notmuch_message_get_message_id (message);
53         /* Determine which query buffer to extend */
54         char **buf = notmuch_message_get_flag (
55             message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
56         /* Add this message's id: query.  Since "id" is an exclusive
57          * prefix, it is implicitly 'or'd together, so we only need to
58          * join queries with a space. */
59         if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
60             return -1;
61         if (*buf)
62             *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
63         else
64             *buf = talloc_strdup (thread, escaped);
65         if (!*buf)
66             return -1;
67     }
68     talloc_free (escaped);
69     return 0;
70 }
71
72 static int
73 do_search_threads (sprinter_t *format,
74                    notmuch_query_t *query,
75                    notmuch_sort_t sort,
76                    output_t output,
77                    int offset,
78                    int limit)
79 {
80     notmuch_thread_t *thread;
81     notmuch_threads_t *threads;
82     notmuch_tags_t *tags;
83     time_t date;
84     int i;
85
86     if (offset < 0) {
87         offset += notmuch_query_count_threads (query);
88         if (offset < 0)
89             offset = 0;
90     }
91
92     threads = notmuch_query_search_threads (query);
93     if (threads == NULL)
94         return 1;
95
96     format->begin_list (format);
97
98     for (i = 0;
99          notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
100          notmuch_threads_move_to_next (threads), i++)
101     {
102         thread = notmuch_threads_get (threads);
103
104         if (i < offset) {
105             notmuch_thread_destroy (thread);
106             continue;
107         }
108
109         if (output == OUTPUT_THREADS) {
110             format->set_prefix (format, "thread");
111             format->string (format,
112                             notmuch_thread_get_thread_id (thread));
113             format->separator (format);
114         } else { /* output == OUTPUT_SUMMARY */
115             void *ctx_quote = talloc_new (thread);
116             const char *authors = notmuch_thread_get_authors (thread);
117             const char *subject = notmuch_thread_get_subject (thread);
118             const char *thread_id = notmuch_thread_get_thread_id (thread);
119             int matched = notmuch_thread_get_matched_messages (thread);
120             int total = notmuch_thread_get_total_messages (thread);
121             const char *relative_date = NULL;
122             notmuch_bool_t first_tag = TRUE;
123
124             format->begin_map (format);
125
126             if (sort == NOTMUCH_SORT_OLDEST_FIRST)
127                 date = notmuch_thread_get_oldest_date (thread);
128             else
129                 date = notmuch_thread_get_newest_date (thread);
130
131             relative_date = notmuch_time_relative_date (ctx_quote, date);
132
133             if (format->is_text_printer) {
134                 /* Special case for the text formatter */
135                 printf ("thread:%s %12s [%d/%d] %s; %s (",
136                         thread_id,
137                         relative_date,
138                         matched,
139                         total,
140                         sanitize_string (ctx_quote, authors),
141                         sanitize_string (ctx_quote, subject));
142             } else { /* Structured Output */
143                 format->map_key (format, "thread");
144                 format->string (format, thread_id);
145                 format->map_key (format, "timestamp");
146                 format->integer (format, date);
147                 format->map_key (format, "date_relative");
148                 format->string (format, relative_date);
149                 format->map_key (format, "matched");
150                 format->integer (format, matched);
151                 format->map_key (format, "total");
152                 format->integer (format, total);
153                 format->map_key (format, "authors");
154                 format->string (format, authors);
155                 format->map_key (format, "subject");
156                 format->string (format, subject);
157                 if (notmuch_format_version >= 2) {
158                     char *matched_query, *unmatched_query;
159                     if (get_thread_query (thread, &matched_query,
160                                           &unmatched_query) < 0) {
161                         fprintf (stderr, "Out of memory\n");
162                         return 1;
163                     }
164                     format->map_key (format, "query");
165                     format->begin_list (format);
166                     if (matched_query)
167                         format->string (format, matched_query);
168                     else
169                         format->null (format);
170                     if (unmatched_query)
171                         format->string (format, unmatched_query);
172                     else
173                         format->null (format);
174                     format->end (format);
175                 }
176             }
177
178             talloc_free (ctx_quote);
179
180             format->map_key (format, "tags");
181             format->begin_list (format);
182
183             for (tags = notmuch_thread_get_tags (thread);
184                  notmuch_tags_valid (tags);
185                  notmuch_tags_move_to_next (tags))
186             {
187                 const char *tag = notmuch_tags_get (tags);
188
189                 if (format->is_text_printer) {
190                   /* Special case for the text formatter */
191                     if (first_tag)
192                         first_tag = FALSE;
193                     else
194                         fputc (' ', stdout);
195                     fputs (tag, stdout);
196                 } else { /* Structured Output */
197                     format->string (format, tag);
198                 }
199             }
200
201             if (format->is_text_printer)
202                 printf (")");
203
204             format->end (format);
205             format->end (format);
206             format->separator (format);
207         }
208
209         notmuch_thread_destroy (thread);
210     }
211
212     format->end (format);
213
214     return 0;
215 }
216
217 static int
218 do_search_messages (sprinter_t *format,
219                     notmuch_query_t *query,
220                     output_t output,
221                     int offset,
222                     int limit,
223                     int dupe)
224 {
225     notmuch_message_t *message;
226     notmuch_messages_t *messages;
227     notmuch_filenames_t *filenames;
228     int i;
229
230     if (offset < 0) {
231         offset += notmuch_query_count_messages (query);
232         if (offset < 0)
233             offset = 0;
234     }
235
236     messages = notmuch_query_search_messages (query);
237     if (messages == NULL)
238         return 1;
239
240     format->begin_list (format);
241
242     for (i = 0;
243          notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
244          notmuch_messages_move_to_next (messages), i++)
245     {
246         if (i < offset)
247             continue;
248
249         message = notmuch_messages_get (messages);
250
251         if (output == OUTPUT_FILES) {
252             int j;
253             filenames = notmuch_message_get_filenames (message);
254
255             for (j = 1;
256                  notmuch_filenames_valid (filenames);
257                  notmuch_filenames_move_to_next (filenames), j++)
258             {
259                 if (dupe < 0 || dupe == j) {
260                     format->string (format, notmuch_filenames_get (filenames));
261                     format->separator (format);
262                 }
263             }
264             
265             notmuch_filenames_destroy( filenames );
266
267         } else { /* output == OUTPUT_MESSAGES */
268             format->set_prefix (format, "id");
269             format->string (format,
270                             notmuch_message_get_message_id (message));
271             format->separator (format);
272         }
273
274         notmuch_message_destroy (message);
275     }
276
277     notmuch_messages_destroy (messages);
278
279     format->end (format);
280
281     return 0;
282 }
283
284 static int
285 do_search_tags (notmuch_database_t *notmuch,
286                 sprinter_t *format,
287                 notmuch_query_t *query)
288 {
289     notmuch_messages_t *messages = NULL;
290     notmuch_tags_t *tags;
291     const char *tag;
292
293     /* should the following only special case if no excluded terms
294      * specified? */
295
296     /* Special-case query of "*" for better performance. */
297     if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
298         tags = notmuch_database_get_all_tags (notmuch);
299     } else {
300         messages = notmuch_query_search_messages (query);
301         if (messages == NULL)
302             return 1;
303
304         tags = notmuch_messages_collect_tags (messages);
305     }
306     if (tags == NULL)
307         return 1;
308
309     format->begin_list (format);
310
311     for (;
312          notmuch_tags_valid (tags);
313          notmuch_tags_move_to_next (tags))
314     {
315         tag = notmuch_tags_get (tags);
316
317         format->string (format, tag);
318         format->separator (format);
319
320     }
321
322     notmuch_tags_destroy (tags);
323
324     if (messages)
325         notmuch_messages_destroy (messages);
326
327     format->end (format);
328
329     return 0;
330 }
331
332 int
333 notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
334 {
335     notmuch_database_t *notmuch;
336     notmuch_query_t *query;
337     char *query_str;
338     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
339     sprinter_t *format = NULL;
340     int opt_index, ret;
341     output_t output = OUTPUT_SUMMARY;
342     int offset = 0;
343     int limit = -1; /* unlimited */
344     notmuch_exclude_t exclude = NOTMUCH_EXCLUDE_TRUE;
345     int dupe = -1;
346     unsigned int i;
347
348     enum {
349         NOTMUCH_FORMAT_JSON,
350         NOTMUCH_FORMAT_TEXT,
351         NOTMUCH_FORMAT_TEXT0,
352         NOTMUCH_FORMAT_SEXP
353     } format_sel = NOTMUCH_FORMAT_TEXT;
354
355     notmuch_opt_desc_t options[] = {
356         { NOTMUCH_OPT_KEYWORD, &sort, "sort", 's',
357           (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
358                                   { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
359                                   { 0, 0 } } },
360         { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
361           (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
362                                   { "sexp", NOTMUCH_FORMAT_SEXP },
363                                   { "text", NOTMUCH_FORMAT_TEXT },
364                                   { "text0", NOTMUCH_FORMAT_TEXT0 },
365                                   { 0, 0 } } },
366         { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
367         { NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
368           (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
369                                   { "threads", OUTPUT_THREADS },
370                                   { "messages", OUTPUT_MESSAGES },
371                                   { "files", OUTPUT_FILES },
372                                   { "tags", OUTPUT_TAGS },
373                                   { 0, 0 } } },
374         { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
375           (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
376                                   { "false", NOTMUCH_EXCLUDE_FALSE },
377                                   { "flag", NOTMUCH_EXCLUDE_FLAG },
378                                   { "all", NOTMUCH_EXCLUDE_ALL },
379                                   { 0, 0 } } },
380         { NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
381         { NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
382         { NOTMUCH_OPT_INT, &dupe, "duplicate", 'D', 0  },
383         { 0, 0, 0, 0, 0 }
384     };
385
386     opt_index = parse_arguments (argc, argv, options, 1);
387     if (opt_index < 0)
388         return EXIT_FAILURE;
389
390     switch (format_sel) {
391     case NOTMUCH_FORMAT_TEXT:
392         format = sprinter_text_create (config, stdout);
393         break;
394     case NOTMUCH_FORMAT_TEXT0:
395         if (output == OUTPUT_SUMMARY) {
396             fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
397             return EXIT_FAILURE;
398         }
399         format = sprinter_text0_create (config, stdout);
400         break;
401     case NOTMUCH_FORMAT_JSON:
402         format = sprinter_json_create (config, stdout);
403         break;
404     case NOTMUCH_FORMAT_SEXP:
405         format = sprinter_sexp_create (config, stdout);
406         break;
407     default:
408         /* this should never happen */
409         INTERNAL_ERROR("no output format selected");
410     }
411
412     notmuch_exit_if_unsupported_format ();
413
414     if (notmuch_database_open (notmuch_config_get_database_path (config),
415                                NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
416         return EXIT_FAILURE;
417
418     query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
419     if (query_str == NULL) {
420         fprintf (stderr, "Out of memory.\n");
421         return EXIT_FAILURE;
422     }
423     if (*query_str == '\0') {
424         fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
425         return EXIT_FAILURE;
426     }
427
428     query = notmuch_query_create (notmuch, query_str);
429     if (query == NULL) {
430         fprintf (stderr, "Out of memory\n");
431         return EXIT_FAILURE;
432     }
433
434     notmuch_query_set_sort (query, sort);
435
436     if (exclude == NOTMUCH_EXCLUDE_FLAG && output != OUTPUT_SUMMARY) {
437         /* If we are not doing summary output there is nowhere to
438          * print the excluded flag so fall back on including the
439          * excluded messages. */
440         fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
441         exclude = NOTMUCH_EXCLUDE_FALSE;
442     }
443
444     if (exclude != NOTMUCH_EXCLUDE_FALSE) {
445         const char **search_exclude_tags;
446         size_t search_exclude_tags_length;
447
448         search_exclude_tags = notmuch_config_get_search_exclude_tags
449             (config, &search_exclude_tags_length);
450         for (i = 0; i < search_exclude_tags_length; i++)
451             notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
452         notmuch_query_set_omit_excluded (query, exclude);
453     }
454
455     switch (output) {
456     default:
457     case OUTPUT_SUMMARY:
458     case OUTPUT_THREADS:
459         ret = do_search_threads (format, query, sort, output, offset, limit);
460         break;
461     case OUTPUT_MESSAGES:
462     case OUTPUT_FILES:
463         ret = do_search_messages (format, query, output, offset, limit, dupe);
464         break;
465     case OUTPUT_TAGS:
466         ret = do_search_tags (notmuch, format, query);
467         break;
468     }
469
470     notmuch_query_destroy (query);
471     notmuch_database_destroy (notmuch);
472
473     talloc_free (format);
474
475     return ret ? EXIT_FAILURE : EXIT_SUCCESS;
476 }