Merge branch 'release'
[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
23 typedef enum {
24     OUTPUT_SUMMARY,
25     OUTPUT_THREADS,
26     OUTPUT_MESSAGES,
27     OUTPUT_FILES,
28     OUTPUT_TAGS
29 } output_t;
30
31 typedef struct search_format {
32     const char *results_start;
33     const char *item_start;
34     void (*item_id) (const void *ctx,
35                      const char *item_type,
36                      const char *item_id);
37     void (*thread_summary) (const void *ctx,
38                             const char *thread_id,
39                             const time_t date,
40                             const int matched,
41                             const int total,
42                             const char *authors,
43                             const char *subject);
44     const char *tag_start;
45     const char *tag;
46     const char *tag_sep;
47     const char *tag_end;
48     const char *item_sep;
49     const char *item_end;
50     const char *results_end;
51     const char *results_null;
52 } search_format_t;
53
54 static void
55 format_item_id_text (const void *ctx,
56                      const char *item_type,
57                      const char *item_id);
58
59 static void
60 format_thread_text (const void *ctx,
61                     const char *thread_id,
62                     const time_t date,
63                     const int matched,
64                     const int total,
65                     const char *authors,
66                     const char *subject);
67 static const search_format_t format_text = {
68     "",
69         "",
70             format_item_id_text,
71             format_thread_text,
72             " (",
73                 "%s", " ",
74             ")", "\n",
75         "",
76     "\n",
77     "",
78 };
79
80 static void
81 format_item_id_json (const void *ctx,
82                      const char *item_type,
83                      const char *item_id);
84
85 static void
86 format_thread_json (const void *ctx,
87                     const char *thread_id,
88                     const time_t date,
89                     const int matched,
90                     const int total,
91                     const char *authors,
92                     const char *subject);
93
94 /* Any changes to the JSON format should be reflected in the file
95  * devel/schemata. */
96 static const search_format_t format_json = {
97     "[",
98         "{",
99             format_item_id_json,
100             format_thread_json,
101             "\"tags\": [",
102                 "\"%s\"", ", ",
103             "]", ",\n",
104         "}",
105     "]\n",
106     "]\n",
107 };
108
109 static void
110 format_item_id_text (unused (const void *ctx),
111                      const char *item_type,
112                      const char *item_id)
113 {
114     printf ("%s%s", item_type, item_id);
115 }
116
117 static char *
118 sanitize_string (const void *ctx, const char *str)
119 {
120     char *out, *loop;
121
122     if (NULL == str)
123         return NULL;
124
125     loop = out = talloc_strdup (ctx, str);
126
127     for (; *loop; loop++) {
128         if ((unsigned char)(*loop) < 32)
129             *loop = '?';
130     }
131     return out;
132 }
133
134 static void
135 format_thread_text (const void *ctx,
136                     const char *thread_id,
137                     const time_t date,
138                     const int matched,
139                     const int total,
140                     const char *authors,
141                     const char *subject)
142 {
143     void *ctx_quote = talloc_new (ctx);
144
145     printf ("thread:%s %12s [%d/%d] %s; %s",
146             thread_id,
147             notmuch_time_relative_date (ctx, date),
148             matched,
149             total,
150             sanitize_string (ctx_quote, authors),
151             sanitize_string (ctx_quote, subject));
152
153     talloc_free (ctx_quote);
154 }
155
156 static void
157 format_item_id_json (const void *ctx,
158                      unused (const char *item_type),
159                      const char *item_id)
160 {
161     void *ctx_quote = talloc_new (ctx);
162
163     printf ("%s", json_quote_str (ctx_quote, item_id));
164
165     talloc_free (ctx_quote);
166     
167 }
168
169 static void
170 format_thread_json (const void *ctx,
171                     const char *thread_id,
172                     const time_t date,
173                     const int matched,
174                     const int total,
175                     const char *authors,
176                     const char *subject)
177 {
178     void *ctx_quote = talloc_new (ctx);
179
180     printf ("\"thread\": %s,\n"
181             "\"timestamp\": %ld,\n"
182             "\"date_relative\": \"%s\",\n"
183             "\"matched\": %d,\n"
184             "\"total\": %d,\n"
185             "\"authors\": %s,\n"
186             "\"subject\": %s,\n",
187             json_quote_str (ctx_quote, thread_id),
188             date,
189             notmuch_time_relative_date (ctx, date),
190             matched,
191             total,
192             json_quote_str (ctx_quote, authors),
193             json_quote_str (ctx_quote, subject));
194
195     talloc_free (ctx_quote);
196 }
197
198 static int
199 do_search_threads (const search_format_t *format,
200                    notmuch_query_t *query,
201                    notmuch_sort_t sort,
202                    output_t output,
203                    int offset,
204                    int limit)
205 {
206     notmuch_thread_t *thread;
207     notmuch_threads_t *threads;
208     notmuch_tags_t *tags;
209     time_t date;
210     int first_thread = 1;
211     int i;
212
213     if (output == OUTPUT_THREADS)
214         notmuch_query_set_omit_excluded_messages (query, TRUE);
215
216     if (offset < 0) {
217         offset += notmuch_query_count_threads (query);
218         if (offset < 0)
219             offset = 0;
220     }
221
222     threads = notmuch_query_search_threads (query);
223     if (threads == NULL)
224         return 1;
225
226     fputs (format->results_start, stdout);
227
228     for (i = 0;
229          notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
230          notmuch_threads_move_to_next (threads), i++)
231     {
232         int first_tag = 1;
233
234         thread = notmuch_threads_get (threads);
235
236         if (i < offset) {
237             notmuch_thread_destroy (thread);
238             continue;
239         }
240
241         if (! first_thread)
242             fputs (format->item_sep, stdout);
243
244         if (output == OUTPUT_THREADS) {
245             format->item_id (thread, "thread:",
246                              notmuch_thread_get_thread_id (thread));
247         } else { /* output == OUTPUT_SUMMARY */
248             fputs (format->item_start, stdout);
249
250             if (sort == NOTMUCH_SORT_OLDEST_FIRST)
251                 date = notmuch_thread_get_oldest_date (thread);
252             else
253                 date = notmuch_thread_get_newest_date (thread);
254
255             format->thread_summary (thread,
256                                     notmuch_thread_get_thread_id (thread),
257                                     date,
258                                     notmuch_thread_get_matched_messages (thread),
259                                     notmuch_thread_get_total_messages (thread),
260                                     notmuch_thread_get_authors (thread),
261                                     notmuch_thread_get_subject (thread));
262
263             fputs (format->tag_start, stdout);
264
265             for (tags = notmuch_thread_get_tags (thread);
266                  notmuch_tags_valid (tags);
267                  notmuch_tags_move_to_next (tags))
268             {
269                 if (! first_tag)
270                     fputs (format->tag_sep, stdout);
271                 printf (format->tag, notmuch_tags_get (tags));
272                 first_tag = 0;
273             }
274
275             fputs (format->tag_end, stdout);
276
277             fputs (format->item_end, stdout);
278         }
279
280         first_thread = 0;
281
282         notmuch_thread_destroy (thread);
283     }
284
285     if (first_thread)
286         fputs (format->results_null, stdout);
287     else
288         fputs (format->results_end, stdout);
289
290     return 0;
291 }
292
293 static int
294 do_search_messages (const search_format_t *format,
295                     notmuch_query_t *query,
296                     output_t output,
297                     int offset,
298                     int limit)
299 {
300     notmuch_message_t *message;
301     notmuch_messages_t *messages;
302     notmuch_filenames_t *filenames;
303     int first_message = 1;
304     int i;
305
306     notmuch_query_set_omit_excluded_messages (query, TRUE);
307
308     if (offset < 0) {
309         offset += notmuch_query_count_messages (query);
310         if (offset < 0)
311             offset = 0;
312     }
313
314     messages = notmuch_query_search_messages (query);
315     if (messages == NULL)
316         return 1;
317
318     fputs (format->results_start, stdout);
319
320     for (i = 0;
321          notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
322          notmuch_messages_move_to_next (messages), i++)
323     {
324         if (i < offset)
325             continue;
326
327         message = notmuch_messages_get (messages);
328
329         if (output == OUTPUT_FILES) {
330             filenames = notmuch_message_get_filenames (message);
331
332             for (;
333                  notmuch_filenames_valid (filenames);
334                  notmuch_filenames_move_to_next (filenames))
335             {
336                 if (! first_message)
337                     fputs (format->item_sep, stdout);
338
339                 format->item_id (message, "",
340                                  notmuch_filenames_get (filenames));
341
342                 first_message = 0;
343             }
344             
345             notmuch_filenames_destroy( filenames );
346
347         } else { /* output == OUTPUT_MESSAGES */
348             if (! first_message)
349                 fputs (format->item_sep, stdout);
350
351             format->item_id (message, "id:",
352                              notmuch_message_get_message_id (message));
353             first_message = 0;
354         }
355
356         notmuch_message_destroy (message);
357     }
358
359     notmuch_messages_destroy (messages);
360
361     if (first_message)
362         fputs (format->results_null, stdout);
363     else
364         fputs (format->results_end, stdout);
365
366     return 0;
367 }
368
369 static int
370 do_search_tags (notmuch_database_t *notmuch,
371                 const search_format_t *format,
372                 notmuch_query_t *query)
373 {
374     notmuch_messages_t *messages = NULL;
375     notmuch_tags_t *tags;
376     const char *tag;
377     int first_tag = 1;
378
379     notmuch_query_set_omit_excluded_messages (query, TRUE);
380     /* should the following only special case if no excluded terms
381      * specified? */
382
383     /* Special-case query of "*" for better performance. */
384     if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
385         tags = notmuch_database_get_all_tags (notmuch);
386     } else {
387         messages = notmuch_query_search_messages (query);
388         if (messages == NULL)
389             return 1;
390
391         tags = notmuch_messages_collect_tags (messages);
392     }
393     if (tags == NULL)
394         return 1;
395
396     fputs (format->results_start, stdout);
397
398     for (;
399          notmuch_tags_valid (tags);
400          notmuch_tags_move_to_next (tags))
401     {
402         tag = notmuch_tags_get (tags);
403
404         if (! first_tag)
405             fputs (format->item_sep, stdout);
406
407         format->item_id (tags, "", tag);
408
409         first_tag = 0;
410     }
411
412     notmuch_tags_destroy (tags);
413
414     if (messages)
415         notmuch_messages_destroy (messages);
416
417     if (first_tag)
418         fputs (format->results_null, stdout);
419     else
420         fputs (format->results_end, stdout);
421
422     return 0;
423 }
424
425 int
426 notmuch_search_command (void *ctx, int argc, char *argv[])
427 {
428     notmuch_config_t *config;
429     notmuch_database_t *notmuch;
430     notmuch_query_t *query;
431     char *query_str;
432     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
433     const search_format_t *format = &format_text;
434     int opt_index, ret;
435     output_t output = OUTPUT_SUMMARY;
436     int offset = 0;
437     int limit = -1; /* unlimited */
438     notmuch_bool_t no_exclude = FALSE;
439     unsigned int i;
440
441     enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
442         format_sel = NOTMUCH_FORMAT_TEXT;
443
444     notmuch_opt_desc_t options[] = {
445         { NOTMUCH_OPT_KEYWORD, &sort, "sort", 's',
446           (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
447                                   { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
448                                   { 0, 0 } } },
449         { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
450           (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
451                                   { "text", NOTMUCH_FORMAT_TEXT },
452                                   { 0, 0 } } },
453         { NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
454           (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
455                                   { "threads", OUTPUT_THREADS },
456                                   { "messages", OUTPUT_MESSAGES },
457                                   { "files", OUTPUT_FILES },
458                                   { "tags", OUTPUT_TAGS },
459                                   { 0, 0 } } },
460         { NOTMUCH_OPT_BOOLEAN, &no_exclude, "no-exclude", 'd', 0 },
461         { NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
462         { NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
463         { 0, 0, 0, 0, 0 }
464     };
465
466     opt_index = parse_arguments (argc, argv, options, 1);
467
468     if (opt_index < 0) {
469         return 1;
470     }
471
472     switch (format_sel) {
473     case NOTMUCH_FORMAT_TEXT:
474         format = &format_text;
475         break;
476     case NOTMUCH_FORMAT_JSON:
477         format = &format_json;
478         break;
479     }
480
481     config = notmuch_config_open (ctx, NULL, NULL);
482     if (config == NULL)
483         return 1;
484
485     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
486                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
487     if (notmuch == NULL)
488         return 1;
489
490     query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
491     if (query_str == NULL) {
492         fprintf (stderr, "Out of memory.\n");
493         return 1;
494     }
495     if (*query_str == '\0') {
496         fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
497         return 1;
498     }
499
500     query = notmuch_query_create (notmuch, query_str);
501     if (query == NULL) {
502         fprintf (stderr, "Out of memory\n");
503         return 1;
504     }
505
506     notmuch_query_set_sort (query, sort);
507
508     if (!no_exclude) {
509         const char **search_exclude_tags;
510         size_t search_exclude_tags_length;
511
512         search_exclude_tags = notmuch_config_get_search_exclude_tags
513             (config, &search_exclude_tags_length);
514         for (i = 0; i < search_exclude_tags_length; i++)
515             notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
516     }
517
518     switch (output) {
519     default:
520     case OUTPUT_SUMMARY:
521     case OUTPUT_THREADS:
522         ret = do_search_threads (format, query, sort, output, offset, limit);
523         break;
524     case OUTPUT_MESSAGES:
525     case OUTPUT_FILES:
526         ret = do_search_messages (format, query, output, offset, limit);
527         break;
528     case OUTPUT_TAGS:
529         ret = do_search_tags (notmuch, format, query);
530         break;
531     }
532
533     notmuch_query_destroy (query);
534     notmuch_database_close (notmuch);
535
536     return ret;
537 }