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,
40 UNIQUE_ADDR_CASEFOLD = 1 << 3,
42 UNIQUE_BOTH = UNIQUE_NAME | UNIQUE_ADDR,
47 notmuch_query_t *query;
56 /* Return two stable query strings that identify exactly the matched
57 * and unmatched messages currently in thread. If there are no
58 * matched or unmatched messages, the returned buffers will be
61 get_thread_query (notmuch_thread_t *thread,
62 char **matched_out, char **unmatched_out)
64 notmuch_messages_t *messages;
66 size_t escaped_len = 0;
68 *matched_out = *unmatched_out = NULL;
70 for (messages = notmuch_thread_get_messages (thread);
71 notmuch_messages_valid (messages);
72 notmuch_messages_move_to_next (messages))
74 notmuch_message_t *message = notmuch_messages_get (messages);
75 const char *mid = notmuch_message_get_message_id (message);
76 /* Determine which query buffer to extend */
77 char **buf = notmuch_message_get_flag (
78 message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
79 /* Add this message's id: query. Since "id" is an exclusive
80 * prefix, it is implicitly 'or'd together, so we only need to
81 * join queries with a space. */
82 if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
85 *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
87 *buf = talloc_strdup (thread, escaped);
91 talloc_free (escaped);
96 do_search_threads (search_options_t *o)
98 notmuch_thread_t *thread;
99 notmuch_threads_t *threads;
100 notmuch_tags_t *tags;
101 sprinter_t *format = o->format;
106 o->offset += notmuch_query_count_threads (o->query);
111 threads = notmuch_query_search_threads (o->query);
115 format->begin_list (format);
118 notmuch_threads_valid (threads) && (o->limit < 0 || i < o->offset + o->limit);
119 notmuch_threads_move_to_next (threads), i++)
121 thread = notmuch_threads_get (threads);
124 notmuch_thread_destroy (thread);
128 if (o->output == OUTPUT_THREADS) {
129 format->set_prefix (format, "thread");
130 format->string (format,
131 notmuch_thread_get_thread_id (thread));
132 format->separator (format);
133 } else { /* output == OUTPUT_SUMMARY */
134 void *ctx_quote = talloc_new (thread);
135 const char *authors = notmuch_thread_get_authors (thread);
136 const char *subject = notmuch_thread_get_subject (thread);
137 const char *thread_id = notmuch_thread_get_thread_id (thread);
138 int matched = notmuch_thread_get_matched_messages (thread);
139 int total = notmuch_thread_get_total_messages (thread);
140 const char *relative_date = NULL;
141 notmuch_bool_t first_tag = TRUE;
143 format->begin_map (format);
145 if (o->sort == NOTMUCH_SORT_OLDEST_FIRST)
146 date = notmuch_thread_get_oldest_date (thread);
148 date = notmuch_thread_get_newest_date (thread);
150 relative_date = notmuch_time_relative_date (ctx_quote, date);
152 if (format->is_text_printer) {
153 /* Special case for the text formatter */
154 printf ("thread:%s %12s [%d/%d] %s; %s (",
159 sanitize_string (ctx_quote, authors),
160 sanitize_string (ctx_quote, subject));
161 } else { /* Structured Output */
162 format->map_key (format, "thread");
163 format->string (format, thread_id);
164 format->map_key (format, "timestamp");
165 format->integer (format, date);
166 format->map_key (format, "date_relative");
167 format->string (format, relative_date);
168 format->map_key (format, "matched");
169 format->integer (format, matched);
170 format->map_key (format, "total");
171 format->integer (format, total);
172 format->map_key (format, "authors");
173 format->string (format, authors);
174 format->map_key (format, "subject");
175 format->string (format, subject);
176 if (notmuch_format_version >= 2) {
177 char *matched_query, *unmatched_query;
178 if (get_thread_query (thread, &matched_query,
179 &unmatched_query) < 0) {
180 fprintf (stderr, "Out of memory\n");
183 format->map_key (format, "query");
184 format->begin_list (format);
186 format->string (format, matched_query);
188 format->null (format);
190 format->string (format, unmatched_query);
192 format->null (format);
193 format->end (format);
197 talloc_free (ctx_quote);
199 format->map_key (format, "tags");
200 format->begin_list (format);
202 for (tags = notmuch_thread_get_tags (thread);
203 notmuch_tags_valid (tags);
204 notmuch_tags_move_to_next (tags))
206 const char *tag = notmuch_tags_get (tags);
208 if (format->is_text_printer) {
209 /* Special case for the text formatter */
215 } else { /* Structured Output */
216 format->string (format, tag);
220 if (format->is_text_printer)
223 format->end (format);
224 format->end (format);
225 format->separator (format);
228 notmuch_thread_destroy (thread);
231 format->end (format);
236 /* Returns TRUE iff name and/or addr is considered unique. */
237 static notmuch_bool_t
238 check_unique (const search_options_t *o, GHashTable *addrs, const char *name, const char *addr)
240 notmuch_bool_t unique;
243 if (o->unique == UNIQUE_NONE)
246 if (o->unique & UNIQUE_ADDR_CASEFOLD) {
247 gchar *folded = g_utf8_casefold (addr, -1);
248 addr = talloc_strdup (o->format, folded);
251 switch (o->unique & UNIQUE_BOTH) {
253 key = talloc_strdup (o->format, name); /* !name results in !key */
256 key = talloc_strdup (o->format, addr);
259 key = talloc_asprintf (o->format, "%s <%s>", name, addr);
262 INTERNAL_ERROR("invalid --unique flags");
268 unique = !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_unique (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,
492 .output = OUTPUT_SUMMARY,
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, &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 { "addresses", OUTPUT_ADDRESSES },
529 { "files", OUTPUT_FILES },
530 { "tags", OUTPUT_TAGS },
532 { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
533 (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
534 { "false", NOTMUCH_EXCLUDE_FALSE },
535 { "flag", NOTMUCH_EXCLUDE_FLAG },
536 { "all", NOTMUCH_EXCLUDE_ALL },
538 { NOTMUCH_OPT_INT, &o.offset, "offset", 'O', 0 },
539 { NOTMUCH_OPT_INT, &o.limit, "limit", 'L', 0 },
540 { NOTMUCH_OPT_INT, &o.dupe, "duplicate", 'D', 0 },
541 { NOTMUCH_OPT_FLAGS, &o.unique, "unique", 'u',
542 (notmuch_keyword_t []){ { "none", UNIQUE_NONE },
543 { "name", UNIQUE_NAME },
544 { "addr", UNIQUE_ADDR },
545 { "addrfold", UNIQUE_ADDR | UNIQUE_ADDR_CASEFOLD },
550 opt_index = parse_arguments (argc, argv, options, 1);
554 if (o.unique && (o.output & ~OUTPUT_ADDRESSES)) {
555 fprintf (stderr, "Error: --unique can only be used with address output.\n");
558 if ((o.unique & UNIQUE_NONE) &&
559 (o.unique & ~UNIQUE_NONE)) {
560 fprintf (stderr, "Error: --unique=none cannot be combined with other flags.\n");
564 o.unique = UNIQUE_ADDR | UNIQUE_ADDR_CASEFOLD;
566 switch (format_sel) {
567 case NOTMUCH_FORMAT_TEXT:
568 o.format = sprinter_text_create (config, stdout);
570 case NOTMUCH_FORMAT_TEXT0:
571 if (o.output == OUTPUT_SUMMARY) {
572 fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
575 o.format = sprinter_text0_create (config, stdout);
577 case NOTMUCH_FORMAT_JSON:
578 o.format = sprinter_json_create (config, stdout);
580 case NOTMUCH_FORMAT_SEXP:
581 o.format = sprinter_sexp_create (config, stdout);
584 /* this should never happen */
585 INTERNAL_ERROR("no output format selected");
588 notmuch_exit_if_unsupported_format ();
590 if (notmuch_database_open (notmuch_config_get_database_path (config),
591 NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much))
594 query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
595 if (query_str == NULL) {
596 fprintf (stderr, "Out of memory.\n");
599 if (*query_str == '\0') {
600 fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
604 o.query = notmuch_query_create (notmuch, query_str);
605 if (o.query == NULL) {
606 fprintf (stderr, "Out of memory\n");
610 notmuch_query_set_sort (o.query, o.sort);
612 if (exclude == NOTMUCH_EXCLUDE_FLAG && o.output != OUTPUT_SUMMARY) {
613 /* If we are not doing summary output there is nowhere to
614 * print the excluded flag so fall back on including the
615 * excluded messages. */
616 fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
617 exclude = NOTMUCH_EXCLUDE_FALSE;
620 if (exclude != NOTMUCH_EXCLUDE_FALSE) {
621 const char **search_exclude_tags;
622 size_t search_exclude_tags_length;
624 search_exclude_tags = notmuch_config_get_search_exclude_tags
625 (config, &search_exclude_tags_length);
626 for (i = 0; i < search_exclude_tags_length; i++)
627 notmuch_query_add_tag_exclude (o.query, search_exclude_tags[i]);
628 notmuch_query_set_omit_excluded (o.query, exclude);
635 ret = do_search_threads (&o);
637 case OUTPUT_MESSAGES:
639 case OUTPUT_RECIPIENTS:
640 case OUTPUT_ADDRESSES:
642 ret = do_search_messages (&o);
645 ret = do_search_tags (notmuch, o.format, o.query);
649 notmuch_query_destroy (o.query);
650 notmuch_database_destroy (notmuch);
652 talloc_free (o.format);
654 return ret ? EXIT_FAILURE : EXIT_SUCCESS;