NEWS: cli: manual page for notmuch configuration options
[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 (offset < 0) {
214         offset += notmuch_query_count_threads (query);
215         if (offset < 0)
216             offset = 0;
217     }
218
219     threads = notmuch_query_search_threads (query);
220     if (threads == NULL)
221         return 1;
222
223     fputs (format->results_start, stdout);
224
225     for (i = 0;
226          notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
227          notmuch_threads_move_to_next (threads), i++)
228     {
229         int first_tag = 1;
230
231         thread = notmuch_threads_get (threads);
232
233         if (i < offset) {
234             notmuch_thread_destroy (thread);
235             continue;
236         }
237
238         if (! first_thread)
239             fputs (format->item_sep, stdout);
240
241         if (output == OUTPUT_THREADS) {
242             format->item_id (thread, "thread:",
243                              notmuch_thread_get_thread_id (thread));
244         } else { /* output == OUTPUT_SUMMARY */
245             fputs (format->item_start, stdout);
246
247             if (sort == NOTMUCH_SORT_OLDEST_FIRST)
248                 date = notmuch_thread_get_oldest_date (thread);
249             else
250                 date = notmuch_thread_get_newest_date (thread);
251
252             format->thread_summary (thread,
253                                     notmuch_thread_get_thread_id (thread),
254                                     date,
255                                     notmuch_thread_get_matched_messages (thread),
256                                     notmuch_thread_get_total_messages (thread),
257                                     notmuch_thread_get_authors (thread),
258                                     notmuch_thread_get_subject (thread));
259
260             fputs (format->tag_start, stdout);
261
262             for (tags = notmuch_thread_get_tags (thread);
263                  notmuch_tags_valid (tags);
264                  notmuch_tags_move_to_next (tags))
265             {
266                 if (! first_tag)
267                     fputs (format->tag_sep, stdout);
268                 printf (format->tag, notmuch_tags_get (tags));
269                 first_tag = 0;
270             }
271
272             fputs (format->tag_end, stdout);
273
274             fputs (format->item_end, stdout);
275         }
276
277         first_thread = 0;
278
279         notmuch_thread_destroy (thread);
280     }
281
282     if (first_thread)
283         fputs (format->results_null, stdout);
284     else
285         fputs (format->results_end, stdout);
286
287     return 0;
288 }
289
290 static int
291 do_search_messages (const search_format_t *format,
292                     notmuch_query_t *query,
293                     output_t output,
294                     int offset,
295                     int limit)
296 {
297     notmuch_message_t *message;
298     notmuch_messages_t *messages;
299     notmuch_filenames_t *filenames;
300     int first_message = 1;
301     int i;
302
303     if (offset < 0) {
304         offset += notmuch_query_count_messages (query);
305         if (offset < 0)
306             offset = 0;
307     }
308
309     messages = notmuch_query_search_messages (query);
310     if (messages == NULL)
311         return 1;
312
313     fputs (format->results_start, stdout);
314
315     for (i = 0;
316          notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
317          notmuch_messages_move_to_next (messages), i++)
318     {
319         if (i < offset)
320             continue;
321
322         message = notmuch_messages_get (messages);
323
324         if (output == OUTPUT_FILES) {
325             filenames = notmuch_message_get_filenames (message);
326
327             for (;
328                  notmuch_filenames_valid (filenames);
329                  notmuch_filenames_move_to_next (filenames))
330             {
331                 if (! first_message)
332                     fputs (format->item_sep, stdout);
333
334                 format->item_id (message, "",
335                                  notmuch_filenames_get (filenames));
336
337                 first_message = 0;
338             }
339             
340             notmuch_filenames_destroy( filenames );
341
342         } else { /* output == OUTPUT_MESSAGES */
343             if (! first_message)
344                 fputs (format->item_sep, stdout);
345
346             format->item_id (message, "id:",
347                              notmuch_message_get_message_id (message));
348             first_message = 0;
349         }
350
351         notmuch_message_destroy (message);
352     }
353
354     notmuch_messages_destroy (messages);
355
356     if (first_message)
357         fputs (format->results_null, stdout);
358     else
359         fputs (format->results_end, stdout);
360
361     return 0;
362 }
363
364 static int
365 do_search_tags (notmuch_database_t *notmuch,
366                 const search_format_t *format,
367                 notmuch_query_t *query)
368 {
369     notmuch_messages_t *messages = NULL;
370     notmuch_tags_t *tags;
371     const char *tag;
372     int first_tag = 1;
373
374     /* Special-case query of "*" for better performance. */
375     if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
376         tags = notmuch_database_get_all_tags (notmuch);
377     } else {
378         messages = notmuch_query_search_messages (query);
379         if (messages == NULL)
380             return 1;
381
382         tags = notmuch_messages_collect_tags (messages);
383     }
384     if (tags == NULL)
385         return 1;
386
387     fputs (format->results_start, stdout);
388
389     for (;
390          notmuch_tags_valid (tags);
391          notmuch_tags_move_to_next (tags))
392     {
393         tag = notmuch_tags_get (tags);
394
395         if (! first_tag)
396             fputs (format->item_sep, stdout);
397
398         format->item_id (tags, "", tag);
399
400         first_tag = 0;
401     }
402
403     notmuch_tags_destroy (tags);
404
405     if (messages)
406         notmuch_messages_destroy (messages);
407
408     if (first_tag)
409         fputs (format->results_null, stdout);
410     else
411         fputs (format->results_end, stdout);
412
413     return 0;
414 }
415
416 int
417 notmuch_search_command (void *ctx, int argc, char *argv[])
418 {
419     notmuch_config_t *config;
420     notmuch_database_t *notmuch;
421     notmuch_query_t *query;
422     char *query_str;
423     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
424     const search_format_t *format = &format_text;
425     int opt_index, ret;
426     output_t output = OUTPUT_SUMMARY;
427     int offset = 0;
428     int limit = -1; /* unlimited */
429     const char **search_exclude_tags;
430     size_t search_exclude_tags_length;
431     unsigned int i;
432
433     enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
434         format_sel = NOTMUCH_FORMAT_TEXT;
435
436     notmuch_opt_desc_t options[] = {
437         { NOTMUCH_OPT_KEYWORD, &sort, "sort", 's',
438           (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
439                                   { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
440                                   { 0, 0 } } },
441         { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
442           (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
443                                   { "text", NOTMUCH_FORMAT_TEXT },
444                                   { 0, 0 } } },
445         { NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
446           (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
447                                   { "threads", OUTPUT_THREADS },
448                                   { "messages", OUTPUT_MESSAGES },
449                                   { "files", OUTPUT_FILES },
450                                   { "tags", OUTPUT_TAGS },
451                                   { 0, 0 } } },
452         { NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
453         { NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
454         { 0, 0, 0, 0, 0 }
455     };
456
457     opt_index = parse_arguments (argc, argv, options, 1);
458
459     if (opt_index < 0) {
460         return 1;
461     }
462
463     switch (format_sel) {
464     case NOTMUCH_FORMAT_TEXT:
465         format = &format_text;
466         break;
467     case NOTMUCH_FORMAT_JSON:
468         format = &format_json;
469         break;
470     }
471
472     config = notmuch_config_open (ctx, NULL, NULL);
473     if (config == NULL)
474         return 1;
475
476     notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
477                                      NOTMUCH_DATABASE_MODE_READ_ONLY);
478     if (notmuch == NULL)
479         return 1;
480
481     query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
482     if (query_str == NULL) {
483         fprintf (stderr, "Out of memory.\n");
484         return 1;
485     }
486     if (*query_str == '\0') {
487         fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
488         return 1;
489     }
490
491     query = notmuch_query_create (notmuch, query_str);
492     if (query == NULL) {
493         fprintf (stderr, "Out of memory\n");
494         return 1;
495     }
496
497     notmuch_query_set_sort (query, sort);
498
499     search_exclude_tags = notmuch_config_get_search_exclude_tags
500         (config, &search_exclude_tags_length);
501     for (i = 0; i < search_exclude_tags_length; i++)
502         notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
503
504     switch (output) {
505     default:
506     case OUTPUT_SUMMARY:
507     case OUTPUT_THREADS:
508         ret = do_search_threads (format, query, sort, output, offset, limit);
509         break;
510     case OUTPUT_MESSAGES:
511     case OUTPUT_FILES:
512         ret = do_search_messages (format, query, output, offset, limit);
513         break;
514     case OUTPUT_TAGS:
515         ret = do_search_tags (notmuch, format, query);
516         break;
517     }
518
519     notmuch_query_destroy (query);
520     notmuch_database_close (notmuch);
521
522     return ret;
523 }