]> git.notmuchmail.org Git - notmuch/blob - notmuch-search.c
emacs: Replace other search text properties with result property
[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     /* should the following only special case if no excluded terms
375      * specified? */
376
377     /* Special-case query of "*" for better performance. */
378     if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
379         tags = notmuch_database_get_all_tags (notmuch);
380     } else {
381         messages = notmuch_query_search_messages (query);
382         if (messages == NULL)
383             return 1;
384
385         tags = notmuch_messages_collect_tags (messages);
386     }
387     if (tags == NULL)
388         return 1;
389
390     fputs (format->results_start, stdout);
391
392     for (;
393          notmuch_tags_valid (tags);
394          notmuch_tags_move_to_next (tags))
395     {
396         tag = notmuch_tags_get (tags);
397
398         if (! first_tag)
399             fputs (format->item_sep, stdout);
400
401         format->item_id (tags, "", tag);
402
403         first_tag = 0;
404     }
405
406     notmuch_tags_destroy (tags);
407
408     if (messages)
409         notmuch_messages_destroy (messages);
410
411     if (first_tag)
412         fputs (format->results_null, stdout);
413     else
414         fputs (format->results_end, stdout);
415
416     return 0;
417 }
418
419 enum {
420     EXCLUDE_TRUE,
421     EXCLUDE_FALSE,
422     EXCLUDE_FLAG,
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     int exclude = EXCLUDE_TRUE;
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_KEYWORD, &exclude, "exclude", 'x',
461           (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
462                                   { "false", EXCLUDE_FALSE },
463                                   { "flag", EXCLUDE_FLAG },
464                                   { 0, 0 } } },
465         { NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
466         { NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
467         { 0, 0, 0, 0, 0 }
468     };
469
470     opt_index = parse_arguments (argc, argv, options, 1);
471
472     if (opt_index < 0) {
473         return 1;
474     }
475
476     switch (format_sel) {
477     case NOTMUCH_FORMAT_TEXT:
478         format = &format_text;
479         break;
480     case NOTMUCH_FORMAT_JSON:
481         format = &format_json;
482         break;
483     }
484
485     config = notmuch_config_open (ctx, NULL, NULL);
486     if (config == NULL)
487         return 1;
488
489     if (notmuch_database_open (notmuch_config_get_database_path (config),
490                                NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
491         return 1;
492
493     query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
494     if (query_str == NULL) {
495         fprintf (stderr, "Out of memory.\n");
496         return 1;
497     }
498     if (*query_str == '\0') {
499         fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
500         return 1;
501     }
502
503     query = notmuch_query_create (notmuch, query_str);
504     if (query == NULL) {
505         fprintf (stderr, "Out of memory\n");
506         return 1;
507     }
508
509     notmuch_query_set_sort (query, sort);
510
511     if (exclude == EXCLUDE_FLAG && output != OUTPUT_SUMMARY) {
512         /* If we are not doing summary output there is nowhere to
513          * print the excluded flag so fall back on including the
514          * excluded messages. */
515         fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
516         exclude = EXCLUDE_FALSE;
517     }
518
519     if (exclude == EXCLUDE_TRUE || exclude == EXCLUDE_FLAG) {
520         const char **search_exclude_tags;
521         size_t search_exclude_tags_length;
522
523         search_exclude_tags = notmuch_config_get_search_exclude_tags
524             (config, &search_exclude_tags_length);
525         for (i = 0; i < search_exclude_tags_length; i++)
526             notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
527         if (exclude == EXCLUDE_FLAG)
528             notmuch_query_set_omit_excluded (query, FALSE);
529     }
530
531     switch (output) {
532     default:
533     case OUTPUT_SUMMARY:
534     case OUTPUT_THREADS:
535         ret = do_search_threads (format, query, sort, output, offset, limit);
536         break;
537     case OUTPUT_MESSAGES:
538     case OUTPUT_FILES:
539         ret = do_search_messages (format, query, output, offset, limit);
540         break;
541     case OUTPUT_TAGS:
542         ret = do_search_tags (notmuch, format, query);
543         break;
544     }
545
546     notmuch_query_destroy (query);
547     notmuch_database_destroy (notmuch);
548
549     return ret;
550 }