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,
37 FILTER_FLAG_ADDR = 1 << 0,
38 FILTER_FLAG_NAME = 1 << 1,
39 FILTER_FLAG_AFOLD = 1 << 2,
44 notmuch_query_t *query;
50 filter_flag_t filter_flags;
53 /* Return two stable query strings that identify exactly the matched
54 * and unmatched messages currently in thread. If there are no
55 * matched or unmatched messages, the returned buffers will be
58 get_thread_query (notmuch_thread_t *thread,
59 char **matched_out, char **unmatched_out)
61 notmuch_messages_t *messages;
63 size_t escaped_len = 0;
65 *matched_out = *unmatched_out = NULL;
67 for (messages = notmuch_thread_get_messages (thread);
68 notmuch_messages_valid (messages);
69 notmuch_messages_move_to_next (messages))
71 notmuch_message_t *message = notmuch_messages_get (messages);
72 const char *mid = notmuch_message_get_message_id (message);
73 /* Determine which query buffer to extend */
74 char **buf = notmuch_message_get_flag (
75 message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
76 /* Add this message's id: query. Since "id" is an exclusive
77 * prefix, it is implicitly 'or'd together, so we only need to
78 * join queries with a space. */
79 if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
82 *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
84 *buf = talloc_strdup (thread, escaped);
88 talloc_free (escaped);
93 do_search_threads (search_options_t *o)
95 notmuch_thread_t *thread;
96 notmuch_threads_t *threads;
98 sprinter_t *format = o->format;
103 o->offset += notmuch_query_count_threads (o->query);
108 threads = notmuch_query_search_threads (o->query);
112 format->begin_list (format);
115 notmuch_threads_valid (threads) && (o->limit < 0 || i < o->offset + o->limit);
116 notmuch_threads_move_to_next (threads), i++)
118 thread = notmuch_threads_get (threads);
121 notmuch_thread_destroy (thread);
125 if (o->output == OUTPUT_THREADS) {
126 format->set_prefix (format, "thread");
127 format->string (format,
128 notmuch_thread_get_thread_id (thread));
129 format->separator (format);
130 } else { /* output == OUTPUT_SUMMARY */
131 void *ctx_quote = talloc_new (thread);
132 const char *authors = notmuch_thread_get_authors (thread);
133 const char *subject = notmuch_thread_get_subject (thread);
134 const char *thread_id = notmuch_thread_get_thread_id (thread);
135 int matched = notmuch_thread_get_matched_messages (thread);
136 int total = notmuch_thread_get_total_messages (thread);
137 const char *relative_date = NULL;
138 notmuch_bool_t first_tag = TRUE;
140 format->begin_map (format);
142 if (o->sort == NOTMUCH_SORT_OLDEST_FIRST)
143 date = notmuch_thread_get_oldest_date (thread);
145 date = notmuch_thread_get_newest_date (thread);
147 relative_date = notmuch_time_relative_date (ctx_quote, date);
149 if (format->is_text_printer) {
150 /* Special case for the text formatter */
151 printf ("thread:%s %12s [%d/%d] %s; %s (",
156 sanitize_string (ctx_quote, authors),
157 sanitize_string (ctx_quote, subject));
158 } else { /* Structured Output */
159 format->map_key (format, "thread");
160 format->string (format, thread_id);
161 format->map_key (format, "timestamp");
162 format->integer (format, date);
163 format->map_key (format, "date_relative");
164 format->string (format, relative_date);
165 format->map_key (format, "matched");
166 format->integer (format, matched);
167 format->map_key (format, "total");
168 format->integer (format, total);
169 format->map_key (format, "authors");
170 format->string (format, authors);
171 format->map_key (format, "subject");
172 format->string (format, subject);
173 if (notmuch_format_version >= 2) {
174 char *matched_query, *unmatched_query;
175 if (get_thread_query (thread, &matched_query,
176 &unmatched_query) < 0) {
177 fprintf (stderr, "Out of memory\n");
180 format->map_key (format, "query");
181 format->begin_list (format);
183 format->string (format, matched_query);
185 format->null (format);
187 format->string (format, unmatched_query);
189 format->null (format);
190 format->end (format);
194 talloc_free (ctx_quote);
196 format->map_key (format, "tags");
197 format->begin_list (format);
199 for (tags = notmuch_thread_get_tags (thread);
200 notmuch_tags_valid (tags);
201 notmuch_tags_move_to_next (tags))
203 const char *tag = notmuch_tags_get (tags);
205 if (format->is_text_printer) {
206 /* Special case for the text formatter */
212 } else { /* Structured Output */
213 format->string (format, tag);
217 if (format->is_text_printer)
220 format->end (format);
221 format->end (format);
222 format->separator (format);
225 notmuch_thread_destroy (thread);
228 format->end (format);
233 /* Returns TRUE iff name and/or addr is considered duplicite. */
234 static notmuch_bool_t
235 check_duplicite (const search_options_t *o, GHashTable *addrs, const char *name, const char *addr)
237 notmuch_bool_t duplicite;
240 if (o->filter_flags == 0)
243 if (o->filter_flags & FILTER_FLAG_AFOLD) {
244 gchar *folded = g_utf8_casefold (addr, -1);
245 addr = talloc_strdup (o->format, folded);
248 switch (o->filter_flags & (FILTER_FLAG_ADDR | FILTER_FLAG_NAME)) {
249 case FILTER_FLAG_NAME:
250 key = talloc_strdup (o->format, name); /* !name results in !key */
252 case FILTER_FLAG_ADDR:
253 key = talloc_strdup (o->format, addr);
255 case FILTER_FLAG_NAME | FILTER_FLAG_ADDR:
256 key = talloc_asprintf (o->format, "%s <%s>", name, addr);
259 INTERNAL_ERROR("invalid --filter_by flags");
262 if (o->filter_flags & FILTER_FLAG_AFOLD)
263 talloc_free ((char*)addr);
268 duplicite = g_hash_table_lookup_extended (addrs, key, NULL, NULL);
271 g_hash_table_insert (addrs, key, NULL);
279 print_address_list (const search_options_t *o, GHashTable *addrs,
280 InternetAddressList *list)
282 InternetAddress *address;
285 for (i = 0; i < internet_address_list_length (list); i++) {
286 address = internet_address_list_get_address (list, i);
287 if (INTERNET_ADDRESS_IS_GROUP (address)) {
288 InternetAddressGroup *group;
289 InternetAddressList *group_list;
291 group = INTERNET_ADDRESS_GROUP (address);
292 group_list = internet_address_group_get_members (group);
293 if (group_list == NULL)
296 print_address_list (o, addrs, group_list);
298 InternetAddressMailbox *mailbox;
303 mailbox = INTERNET_ADDRESS_MAILBOX (address);
305 name = internet_address_get_name (address);
306 addr = internet_address_mailbox_get_addr (mailbox);
308 if (check_duplicite (o, addrs, name, addr))
312 full_address = talloc_asprintf (o->format, "%s <%s>", name, addr);
314 full_address = talloc_strdup (o->format, addr);
317 fprintf (stderr, "Error: out of memory\n");
320 o->format->string (o->format, full_address);
321 o->format->separator (o->format);
323 talloc_free (full_address);
329 print_address_string (const search_options_t *o, GHashTable *addrs, const char *recipients)
331 InternetAddressList *list;
333 if (recipients == NULL)
336 list = internet_address_list_parse_string (recipients);
340 print_address_list (o, addrs, list);
344 _my_talloc_free_for_g_hash (void *ptr)
350 do_search_messages (search_options_t *o)
352 notmuch_message_t *message;
353 notmuch_messages_t *messages;
354 notmuch_filenames_t *filenames;
355 sprinter_t *format = o->format;
356 GHashTable *addresses = NULL;
359 if (o->output & OUTPUT_ADDRESSES)
360 addresses = g_hash_table_new_full (g_str_hash, g_str_equal,
361 _my_talloc_free_for_g_hash, NULL);
364 o->offset += notmuch_query_count_messages (o->query);
369 messages = notmuch_query_search_messages (o->query);
370 if (messages == NULL)
373 format->begin_list (format);
376 notmuch_messages_valid (messages) && (o->limit < 0 || i < o->offset + o->limit);
377 notmuch_messages_move_to_next (messages), i++)
382 message = notmuch_messages_get (messages);
384 if (o->output == OUTPUT_FILES) {
386 filenames = notmuch_message_get_filenames (message);
389 notmuch_filenames_valid (filenames);
390 notmuch_filenames_move_to_next (filenames), j++)
392 if (o->dupe < 0 || o->dupe == j) {
393 format->string (format, notmuch_filenames_get (filenames));
394 format->separator (format);
398 notmuch_filenames_destroy( filenames );
400 } else if (o->output == OUTPUT_MESSAGES) {
401 format->set_prefix (format, "id");
402 format->string (format,
403 notmuch_message_get_message_id (message));
404 format->separator (format);
406 if (o->output & OUTPUT_SENDER) {
409 addrs = notmuch_message_get_header (message, "from");
410 print_address_string (o, addresses, addrs);
413 if (o->output & OUTPUT_RECIPIENTS) {
414 const char *hdrs[] = { "to", "cc", "bcc" };
418 for (j = 0; j < ARRAY_SIZE (hdrs); j++) {
419 addrs = notmuch_message_get_header (message, hdrs[j]);
420 print_address_string (o, addresses, addrs);
425 notmuch_message_destroy (message);
429 g_hash_table_unref (addresses);
431 notmuch_messages_destroy (messages);
433 format->end (format);
439 do_search_tags (notmuch_database_t *notmuch,
441 notmuch_query_t *query)
443 notmuch_messages_t *messages = NULL;
444 notmuch_tags_t *tags;
447 /* should the following only special case if no excluded terms
450 /* Special-case query of "*" for better performance. */
451 if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
452 tags = notmuch_database_get_all_tags (notmuch);
454 messages = notmuch_query_search_messages (query);
455 if (messages == NULL)
458 tags = notmuch_messages_collect_tags (messages);
463 format->begin_list (format);
466 notmuch_tags_valid (tags);
467 notmuch_tags_move_to_next (tags))
469 tag = notmuch_tags_get (tags);
471 format->string (format, tag);
472 format->separator (format);
476 notmuch_tags_destroy (tags);
479 notmuch_messages_destroy (messages);
481 format->end (format);
487 notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
489 notmuch_database_t *notmuch;
490 search_options_t o = {
491 .sort = NOTMUCH_SORT_NEWEST_FIRST,
494 .limit = -1, /* unlimited */
500 notmuch_exclude_t exclude = NOTMUCH_EXCLUDE_TRUE;
506 NOTMUCH_FORMAT_TEXT0,
508 } format_sel = NOTMUCH_FORMAT_TEXT;
510 notmuch_opt_desc_t options[] = {
511 { NOTMUCH_OPT_KEYWORD, &o.sort, "sort", 's',
512 (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
513 { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
515 { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
516 (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
517 { "sexp", NOTMUCH_FORMAT_SEXP },
518 { "text", NOTMUCH_FORMAT_TEXT },
519 { "text0", NOTMUCH_FORMAT_TEXT0 },
521 { NOTMUCH_OPT_INT, ¬much_format_version, "format-version", 0, 0 },
522 { NOTMUCH_OPT_KEYWORD_FLAGS, &o.output, "output", 'o',
523 (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
524 { "threads", OUTPUT_THREADS },
525 { "messages", OUTPUT_MESSAGES },
526 { "sender", OUTPUT_SENDER },
527 { "recipients", OUTPUT_RECIPIENTS },
528 { "files", OUTPUT_FILES },
529 { "tags", OUTPUT_TAGS },
531 { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
532 (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
533 { "false", NOTMUCH_EXCLUDE_FALSE },
534 { "flag", NOTMUCH_EXCLUDE_FLAG },
535 { "all", NOTMUCH_EXCLUDE_ALL },
537 { NOTMUCH_OPT_INT, &o.offset, "offset", 'O', 0 },
538 { NOTMUCH_OPT_INT, &o.limit, "limit", 'L', 0 },
539 { NOTMUCH_OPT_INT, &o.dupe, "duplicate", 'D', 0 },
540 { NOTMUCH_OPT_KEYWORD_FLAGS, &o.filter_flags, "filter-by", 'b',
541 (notmuch_keyword_t []){ { "name", FILTER_FLAG_NAME },
542 { "addr", FILTER_FLAG_ADDR },
543 { "addrfold", FILTER_FLAG_ADDR | FILTER_FLAG_AFOLD },
548 opt_index = parse_arguments (argc, argv, options, 1);
553 o.output = OUTPUT_SUMMARY;
555 if (o.filter_flags && (o.output & ~OUTPUT_ADDRESSES)) {
556 fprintf (stderr, "Error: --filter_flag can only be used with address output.\n");
560 switch (format_sel) {
561 case NOTMUCH_FORMAT_TEXT:
562 o.format = sprinter_text_create (config, stdout);
564 case NOTMUCH_FORMAT_TEXT0:
565 if (o.output == OUTPUT_SUMMARY) {
566 fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
569 o.format = sprinter_text0_create (config, stdout);
571 case NOTMUCH_FORMAT_JSON:
572 o.format = sprinter_json_create (config, stdout);
574 case NOTMUCH_FORMAT_SEXP:
575 o.format = sprinter_sexp_create (config, stdout);
578 /* this should never happen */
579 INTERNAL_ERROR("no output format selected");
582 notmuch_exit_if_unsupported_format ();
584 if (notmuch_database_open (notmuch_config_get_database_path (config),
585 NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much))
588 query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
589 if (query_str == NULL) {
590 fprintf (stderr, "Out of memory.\n");
593 if (*query_str == '\0') {
594 fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
598 o.query = notmuch_query_create (notmuch, query_str);
599 if (o.query == NULL) {
600 fprintf (stderr, "Out of memory\n");
604 notmuch_query_set_sort (o.query, o.sort);
606 if (exclude == NOTMUCH_EXCLUDE_FLAG && o.output != OUTPUT_SUMMARY) {
607 /* If we are not doing summary output there is nowhere to
608 * print the excluded flag so fall back on including the
609 * excluded messages. */
610 fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
611 exclude = NOTMUCH_EXCLUDE_FALSE;
614 if (exclude != NOTMUCH_EXCLUDE_FALSE) {
615 const char **search_exclude_tags;
616 size_t search_exclude_tags_length;
618 search_exclude_tags = notmuch_config_get_search_exclude_tags
619 (config, &search_exclude_tags_length);
620 for (i = 0; i < search_exclude_tags_length; i++)
621 notmuch_query_add_tag_exclude (o.query, search_exclude_tags[i]);
622 notmuch_query_set_omit_excluded (o.query, exclude);
628 ret = do_search_threads (&o);
630 case OUTPUT_MESSAGES:
632 case OUTPUT_RECIPIENTS:
633 case OUTPUT_ADDRESSES:
635 ret = do_search_messages (&o);
638 ret = do_search_tags (notmuch, o.format, o.query);
641 fprintf (stderr, "Error: the combination of outputs is not supported.\n");
645 notmuch_query_destroy (o.query);
646 notmuch_database_destroy (notmuch);
648 talloc_free (o.format);
650 return ret ? EXIT_FAILURE : EXIT_SUCCESS;