1 /* notmuch - Not much of an email program, (just index and search)
3 * Copyright © 2009 Carl Worth
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.
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.
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/ .
18 * Author: Carl Worth <cworth@cworth.org>
21 #include "notmuch-client.h"
23 #include "string-util.h"
26 OUTPUT_SUMMARY = 1 << 0,
27 OUTPUT_THREADS = 1 << 1,
28 OUTPUT_MESSAGES = 1 << 2,
29 OUTPUT_FILES = 1 << 3,
31 OUTPUT_SENDER = 1 << 5,
32 OUTPUT_RECIPIENTS = 1 << 6,
33 OUTPUT_ADDRESSES = OUTPUT_SENDER | OUTPUT_RECIPIENTS,
38 notmuch_query_t *query;
46 /* Return two stable query strings that identify exactly the matched
47 * and unmatched messages currently in thread. If there are no
48 * matched or unmatched messages, the returned buffers will be
51 get_thread_query (notmuch_thread_t *thread,
52 char **matched_out, char **unmatched_out)
54 notmuch_messages_t *messages;
56 size_t escaped_len = 0;
58 *matched_out = *unmatched_out = NULL;
60 for (messages = notmuch_thread_get_messages (thread);
61 notmuch_messages_valid (messages);
62 notmuch_messages_move_to_next (messages))
64 notmuch_message_t *message = notmuch_messages_get (messages);
65 const char *mid = notmuch_message_get_message_id (message);
66 /* Determine which query buffer to extend */
67 char **buf = notmuch_message_get_flag (
68 message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
69 /* Add this message's id: query. Since "id" is an exclusive
70 * prefix, it is implicitly 'or'd together, so we only need to
71 * join queries with a space. */
72 if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
75 *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
77 *buf = talloc_strdup (thread, escaped);
81 talloc_free (escaped);
86 do_search_threads (search_options_t *o)
88 notmuch_thread_t *thread;
89 notmuch_threads_t *threads;
91 sprinter_t *format = o->format;
96 o->offset += notmuch_query_count_threads (o->query);
101 threads = notmuch_query_search_threads (o->query);
105 format->begin_list (format);
108 notmuch_threads_valid (threads) && (o->limit < 0 || i < o->offset + o->limit);
109 notmuch_threads_move_to_next (threads), i++)
111 thread = notmuch_threads_get (threads);
114 notmuch_thread_destroy (thread);
118 if (o->output == OUTPUT_THREADS) {
119 format->set_prefix (format, "thread");
120 format->string (format,
121 notmuch_thread_get_thread_id (thread));
122 format->separator (format);
123 } else { /* output == OUTPUT_SUMMARY */
124 void *ctx_quote = talloc_new (thread);
125 const char *authors = notmuch_thread_get_authors (thread);
126 const char *subject = notmuch_thread_get_subject (thread);
127 const char *thread_id = notmuch_thread_get_thread_id (thread);
128 int matched = notmuch_thread_get_matched_messages (thread);
129 int total = notmuch_thread_get_total_messages (thread);
130 const char *relative_date = NULL;
131 notmuch_bool_t first_tag = TRUE;
133 format->begin_map (format);
135 if (o->sort == NOTMUCH_SORT_OLDEST_FIRST)
136 date = notmuch_thread_get_oldest_date (thread);
138 date = notmuch_thread_get_newest_date (thread);
140 relative_date = notmuch_time_relative_date (ctx_quote, date);
142 if (format->is_text_printer) {
143 /* Special case for the text formatter */
144 printf ("thread:%s %12s [%d/%d] %s; %s (",
149 sanitize_string (ctx_quote, authors),
150 sanitize_string (ctx_quote, subject));
151 } else { /* Structured Output */
152 format->map_key (format, "thread");
153 format->string (format, thread_id);
154 format->map_key (format, "timestamp");
155 format->integer (format, date);
156 format->map_key (format, "date_relative");
157 format->string (format, relative_date);
158 format->map_key (format, "matched");
159 format->integer (format, matched);
160 format->map_key (format, "total");
161 format->integer (format, total);
162 format->map_key (format, "authors");
163 format->string (format, authors);
164 format->map_key (format, "subject");
165 format->string (format, subject);
166 if (notmuch_format_version >= 2) {
167 char *matched_query, *unmatched_query;
168 if (get_thread_query (thread, &matched_query,
169 &unmatched_query) < 0) {
170 fprintf (stderr, "Out of memory\n");
173 format->map_key (format, "query");
174 format->begin_list (format);
176 format->string (format, matched_query);
178 format->null (format);
180 format->string (format, unmatched_query);
182 format->null (format);
183 format->end (format);
187 talloc_free (ctx_quote);
189 format->map_key (format, "tags");
190 format->begin_list (format);
192 for (tags = notmuch_thread_get_tags (thread);
193 notmuch_tags_valid (tags);
194 notmuch_tags_move_to_next (tags))
196 const char *tag = notmuch_tags_get (tags);
198 if (format->is_text_printer) {
199 /* Special case for the text formatter */
205 } else { /* Structured Output */
206 format->string (format, tag);
210 if (format->is_text_printer)
213 format->end (format);
214 format->end (format);
215 format->separator (format);
218 notmuch_thread_destroy (thread);
221 format->end (format);
227 print_address_list (const search_options_t *o, InternetAddressList *list)
229 InternetAddress *address;
232 for (i = 0; i < internet_address_list_length (list); i++) {
233 address = internet_address_list_get_address (list, i);
234 if (INTERNET_ADDRESS_IS_GROUP (address)) {
235 InternetAddressGroup *group;
236 InternetAddressList *group_list;
238 group = INTERNET_ADDRESS_GROUP (address);
239 group_list = internet_address_group_get_members (group);
240 if (group_list == NULL)
243 print_address_list (o, group_list);
245 InternetAddressMailbox *mailbox;
250 mailbox = INTERNET_ADDRESS_MAILBOX (address);
252 name = internet_address_get_name (address);
253 addr = internet_address_mailbox_get_addr (mailbox);
256 full_address = talloc_asprintf (o->format, "%s <%s>", name, addr);
258 full_address = talloc_strdup (o->format, addr);
261 fprintf (stderr, "Error: out of memory\n");
264 o->format->string (o->format, full_address);
265 o->format->separator (o->format);
267 talloc_free (full_address);
273 print_address_string (const search_options_t *o, const char *recipients)
275 InternetAddressList *list;
277 if (recipients == NULL)
280 list = internet_address_list_parse_string (recipients);
284 print_address_list (o, list);
288 do_search_messages (search_options_t *o)
290 notmuch_message_t *message;
291 notmuch_messages_t *messages;
292 notmuch_filenames_t *filenames;
293 sprinter_t *format = o->format;
297 o->offset += notmuch_query_count_messages (o->query);
302 messages = notmuch_query_search_messages (o->query);
303 if (messages == NULL)
306 format->begin_list (format);
309 notmuch_messages_valid (messages) && (o->limit < 0 || i < o->offset + o->limit);
310 notmuch_messages_move_to_next (messages), i++)
315 message = notmuch_messages_get (messages);
317 if (o->output == OUTPUT_FILES) {
319 filenames = notmuch_message_get_filenames (message);
322 notmuch_filenames_valid (filenames);
323 notmuch_filenames_move_to_next (filenames), j++)
325 if (o->dupe < 0 || o->dupe == j) {
326 format->string (format, notmuch_filenames_get (filenames));
327 format->separator (format);
331 notmuch_filenames_destroy( filenames );
333 } else if (o->output == OUTPUT_MESSAGES) {
334 format->set_prefix (format, "id");
335 format->string (format,
336 notmuch_message_get_message_id (message));
337 format->separator (format);
339 if (o->output & OUTPUT_SENDER) {
342 addrs = notmuch_message_get_header (message, "from");
343 print_address_string (o, addrs);
346 if (o->output & OUTPUT_RECIPIENTS) {
347 const char *hdrs[] = { "to", "cc", "bcc" };
351 for (j = 0; j < ARRAY_SIZE (hdrs); j++) {
352 addrs = notmuch_message_get_header (message, hdrs[j]);
353 print_address_string (o, addrs);
358 notmuch_message_destroy (message);
361 notmuch_messages_destroy (messages);
363 format->end (format);
369 do_search_tags (notmuch_database_t *notmuch,
371 notmuch_query_t *query)
373 notmuch_messages_t *messages = NULL;
374 notmuch_tags_t *tags;
377 /* should the following only special case if no excluded terms
380 /* Special-case query of "*" for better performance. */
381 if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
382 tags = notmuch_database_get_all_tags (notmuch);
384 messages = notmuch_query_search_messages (query);
385 if (messages == NULL)
388 tags = notmuch_messages_collect_tags (messages);
393 format->begin_list (format);
396 notmuch_tags_valid (tags);
397 notmuch_tags_move_to_next (tags))
399 tag = notmuch_tags_get (tags);
401 format->string (format, tag);
402 format->separator (format);
406 notmuch_tags_destroy (tags);
409 notmuch_messages_destroy (messages);
411 format->end (format);
417 notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
419 notmuch_database_t *notmuch;
420 search_options_t o = {
421 .sort = NOTMUCH_SORT_NEWEST_FIRST,
422 .output = OUTPUT_SUMMARY,
424 .limit = -1, /* unlimited */
429 notmuch_exclude_t exclude = NOTMUCH_EXCLUDE_TRUE;
435 NOTMUCH_FORMAT_TEXT0,
437 } format_sel = NOTMUCH_FORMAT_TEXT;
439 notmuch_opt_desc_t options[] = {
440 { NOTMUCH_OPT_KEYWORD, &o.sort, "sort", 's',
441 (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
442 { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
444 { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
445 (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
446 { "sexp", NOTMUCH_FORMAT_SEXP },
447 { "text", NOTMUCH_FORMAT_TEXT },
448 { "text0", NOTMUCH_FORMAT_TEXT0 },
450 { NOTMUCH_OPT_INT, ¬much_format_version, "format-version", 0, 0 },
451 { NOTMUCH_OPT_KEYWORD, &o.output, "output", 'o',
452 (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
453 { "threads", OUTPUT_THREADS },
454 { "messages", OUTPUT_MESSAGES },
455 { "sender", OUTPUT_SENDER },
456 { "recipients", OUTPUT_RECIPIENTS },
457 { "addresses", OUTPUT_ADDRESSES },
458 { "files", OUTPUT_FILES },
459 { "tags", OUTPUT_TAGS },
461 { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
462 (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
463 { "false", NOTMUCH_EXCLUDE_FALSE },
464 { "flag", NOTMUCH_EXCLUDE_FLAG },
465 { "all", NOTMUCH_EXCLUDE_ALL },
467 { NOTMUCH_OPT_INT, &o.offset, "offset", 'O', 0 },
468 { NOTMUCH_OPT_INT, &o.limit, "limit", 'L', 0 },
469 { NOTMUCH_OPT_INT, &o.dupe, "duplicate", 'D', 0 },
473 opt_index = parse_arguments (argc, argv, options, 1);
477 switch (format_sel) {
478 case NOTMUCH_FORMAT_TEXT:
479 o.format = sprinter_text_create (config, stdout);
481 case NOTMUCH_FORMAT_TEXT0:
482 if (o.output == OUTPUT_SUMMARY) {
483 fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
486 o.format = sprinter_text0_create (config, stdout);
488 case NOTMUCH_FORMAT_JSON:
489 o.format = sprinter_json_create (config, stdout);
491 case NOTMUCH_FORMAT_SEXP:
492 o.format = sprinter_sexp_create (config, stdout);
495 /* this should never happen */
496 INTERNAL_ERROR("no output format selected");
499 notmuch_exit_if_unsupported_format ();
501 if (notmuch_database_open (notmuch_config_get_database_path (config),
502 NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much))
505 query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
506 if (query_str == NULL) {
507 fprintf (stderr, "Out of memory.\n");
510 if (*query_str == '\0') {
511 fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
515 o.query = notmuch_query_create (notmuch, query_str);
516 if (o.query == NULL) {
517 fprintf (stderr, "Out of memory\n");
521 notmuch_query_set_sort (o.query, o.sort);
523 if (exclude == NOTMUCH_EXCLUDE_FLAG && o.output != OUTPUT_SUMMARY) {
524 /* If we are not doing summary output there is nowhere to
525 * print the excluded flag so fall back on including the
526 * excluded messages. */
527 fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
528 exclude = NOTMUCH_EXCLUDE_FALSE;
531 if (exclude != NOTMUCH_EXCLUDE_FALSE) {
532 const char **search_exclude_tags;
533 size_t search_exclude_tags_length;
535 search_exclude_tags = notmuch_config_get_search_exclude_tags
536 (config, &search_exclude_tags_length);
537 for (i = 0; i < search_exclude_tags_length; i++)
538 notmuch_query_add_tag_exclude (o.query, search_exclude_tags[i]);
539 notmuch_query_set_omit_excluded (o.query, exclude);
546 ret = do_search_threads (&o);
548 case OUTPUT_MESSAGES:
550 case OUTPUT_RECIPIENTS:
551 case OUTPUT_ADDRESSES:
553 ret = do_search_messages (&o);
556 ret = do_search_tags (notmuch, o.format, o.query);
560 notmuch_query_destroy (o.query);
561 notmuch_database_destroy (notmuch);
563 talloc_free (o.format);
565 return ret ? EXIT_FAILURE : EXIT_SUCCESS;