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>
22 #define _GNU_SOURCE /* for getline */
28 /* This is separate from notmuch-private.h because we're trying to
29 * keep notmuch.c from looking into any internals, (which helps us
30 * develop notmuch.h into a plausible library interface).
44 #include <glib.h> /* g_strdup_printf */
46 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
48 typedef int (*command_function_t) (int argc, char *argv[]);
50 typedef struct command {
52 command_function_t function;
60 struct timeval tv_start;
64 chomp_newline (char *str)
66 if (str && str[strlen(str)-1] == '\n')
67 str[strlen(str)-1] = '\0';
70 /* Compute the number of seconds elapsed from start to end. */
72 tv_elapsed (struct timeval start, struct timeval end)
74 return ((end.tv_sec - start.tv_sec) +
75 (end.tv_usec - start.tv_usec) / 1e6);
79 print_formatted_seconds (double seconds)
85 hours = (int) seconds / 3600;
86 printf ("%dh ", hours);
87 seconds -= hours * 3600;
91 minutes = (int) seconds / 60;
92 printf ("%dm ", minutes);
93 seconds -= minutes * 60;
96 printf ("%ds", (int) seconds);
100 add_files_print_progress (add_files_state_t *state)
102 struct timeval tv_now;
103 double elapsed_overall, rate_overall;
105 gettimeofday (&tv_now, NULL);
107 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
108 rate_overall = (state->processed_files) / elapsed_overall;
110 printf ("Processed %d", state->processed_files);
112 if (state->total_files) {
113 printf (" of %d files (", state->total_files);
114 print_formatted_seconds ((state->total_files - state->processed_files) /
116 printf (" remaining). \r");
118 printf (" files (%d files/sec.) \r", (int) rate_overall);
124 /* Examine 'path' recursively as follows:
126 * o Ask the filesystem for the mtime of 'path' (path_mtime)
128 * o Ask the database for its timestamp of 'path' (path_dbtime)
130 * o If 'path_mtime' > 'path_dbtime'
132 * o For each regular file in 'path' with mtime newer than the
133 * 'path_dbtime' call add_message to add the file to the
136 * o For each sub-directory of path, recursively call into this
139 * o Tell the database to update its time of 'path' to 'path_mtime'
142 add_files (notmuch_database_t *notmuch, const char *path,
143 add_files_state_t *state)
146 struct dirent *e, *entry = NULL;
151 time_t path_mtime, path_dbtime;
152 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
154 if (stat (path, &st)) {
155 fprintf (stderr, "Error reading directory %s: %s\n",
156 path, strerror (errno));
157 return NOTMUCH_STATUS_FILE_ERROR;
160 if (! S_ISDIR (st.st_mode)) {
161 fprintf (stderr, "Error: %s is not a directory.\n", path);
162 return NOTMUCH_STATUS_FILE_ERROR;
165 path_mtime = st.st_mtime;
167 path_dbtime = notmuch_database_get_timestamp (notmuch, path);
169 dir = opendir (path);
171 fprintf (stderr, "Error opening directory %s: %s\n",
172 path, strerror (errno));
173 ret = NOTMUCH_STATUS_FILE_ERROR;
177 entry_length = offsetof (struct dirent, d_name) +
178 pathconf (path, _PC_NAME_MAX) + 1;
179 entry = malloc (entry_length);
182 err = readdir_r (dir, entry, &e);
184 fprintf (stderr, "Error reading directory: %s\n",
186 ret = NOTMUCH_STATUS_FILE_ERROR;
193 /* If this directory hasn't been modified since the last
194 * add_files, then we only need to look further for
195 * sub-directories. */
196 if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
199 /* Ignore special directories to avoid infinite recursion.
200 * Also ignore the .notmuch directory.
202 /* XXX: Eventually we'll want more sophistication to let the
203 * user specify files to be ignored. */
204 if (strcmp (entry->d_name, ".") == 0 ||
205 strcmp (entry->d_name, "..") == 0 ||
206 strcmp (entry->d_name, ".notmuch") ==0)
211 next = g_strdup_printf ("%s/%s", path, entry->d_name);
213 if (stat (next, &st)) {
214 fprintf (stderr, "Error reading %s: %s\n",
215 next, strerror (errno));
216 ret = NOTMUCH_STATUS_FILE_ERROR;
220 if (S_ISREG (st.st_mode)) {
221 /* If the file hasn't been modified since the last
222 * add_files, then we need not look at it. */
223 if (st.st_mtime > path_dbtime) {
224 state->processed_files++;
226 status = notmuch_database_add_message (notmuch, next);
229 case NOTMUCH_STATUS_SUCCESS:
230 state->added_messages++;
232 /* Non-fatal issues (go on to next file) */
233 case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
234 /* Stay silent on this one. */
236 case NOTMUCH_STATUS_FILE_NOT_EMAIL:
237 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
240 /* Fatal issues. Don't process anymore. */
241 case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
242 fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
246 fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n", status);
250 if (state->processed_files % 1000 == 0)
251 add_files_print_progress (state);
253 } else if (S_ISDIR (st.st_mode)) {
254 status = add_files (notmuch, next, state);
255 if (status && ret == NOTMUCH_STATUS_SUCCESS)
263 notmuch_database_set_timestamp (notmuch, path, path_mtime);
276 /* Recursively count all regular files in path and all sub-direcotries
277 * of path. The result is added to *count (which should be
278 * initialized to zero by the top-level caller before calling
281 count_files (const char *path, int *count)
284 struct dirent *entry, *e;
290 dir = opendir (path);
293 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
294 path, strerror (errno));
298 entry_length = offsetof (struct dirent, d_name) +
299 pathconf (path, _PC_NAME_MAX) + 1;
300 entry = malloc (entry_length);
303 err = readdir_r (dir, entry, &e);
305 fprintf (stderr, "Error reading directory: %s\n",
314 /* Ignore special directories to avoid infinite recursion.
315 * Also ignore the .notmuch directory.
317 /* XXX: Eventually we'll want more sophistication to let the
318 * user specify files to be ignored. */
319 if (strcmp (entry->d_name, ".") == 0 ||
320 strcmp (entry->d_name, "..") == 0 ||
321 strcmp (entry->d_name, ".notmuch") == 0)
326 next = g_strdup_printf ("%s/%s", path, entry->d_name);
330 if (S_ISREG (st.st_mode)) {
332 if (*count % 1000 == 0) {
333 printf ("Found %d files so far.\r", *count);
336 } else if (S_ISDIR (st.st_mode)) {
337 count_files (next, count);
349 setup_command (int argc, char *argv[])
351 notmuch_database_t *notmuch = NULL;
352 char *default_path, *mail_directory = NULL;
355 add_files_state_t add_files_state;
357 struct timeval tv_now;
358 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
360 printf ("Welcome to notmuch!\n\n");
362 printf ("The goal of notmuch is to help you manage and search your collection of\n"
363 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
365 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
366 "(where you already have mail stored and where messages will be delivered\n"
367 "in the future). This directory can contain any number of sub-directories\n"
368 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
369 "archives are perfect). If there are other, non-email files (such as\n"
370 "indexes maintained by other email programs) then notmuch will do its\n"
371 "best to detect those and ignore them.\n\n");
373 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
374 "messages), will not work with notmuch. If that's how your mail is currently\n"
375 "stored, we recommend you first convert it to maildir format with a utility\n"
376 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
377 "once the conversion is complete.\n\n");
380 default_path = notmuch_database_default_path ();
381 printf ("Top-level mail directory [%s]: ", default_path);
384 getline (&mail_directory, &line_size, stdin);
385 chomp_newline (mail_directory);
389 if (mail_directory == NULL || strlen (mail_directory) == 0) {
391 free (mail_directory);
392 mail_directory = default_path;
394 /* XXX: Instead of telling the user to use an environment
395 * variable here, we should really be writing out a configuration
396 * file and loading that on the next run. */
397 if (strcmp (mail_directory, default_path)) {
398 printf ("Note: Since you are not using the default path, you will want to set\n"
399 "the NOTMUCH_BASE environment variable to %s so that\n"
400 "future calls to notmuch commands will know where to find your mail.\n",
402 printf ("For example, if you are using bash for your shell, add:\n\n");
403 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
404 printf ("to your ~/.bashrc file.\n\n");
409 notmuch = notmuch_database_create (mail_directory);
410 if (notmuch == NULL) {
411 fprintf (stderr, "Failed to create new notmuch database at %s\n",
413 ret = NOTMUCH_STATUS_FILE_ERROR;
417 printf ("OK. Let's take a look at the mail we can find in the directory\n");
418 printf ("%s ...\n", mail_directory);
421 count_files (mail_directory, &count);
423 printf ("Found %d total files. That's not much mail.\n\n", count);
425 printf ("Next, we'll inspect the messages and create a database of threads:\n");
427 add_files_state.total_files = count;
428 add_files_state.processed_files = 0;
429 add_files_state.added_messages = 0;
430 gettimeofday (&add_files_state.tv_start, NULL);
432 ret = add_files (notmuch, mail_directory, &add_files_state);
434 gettimeofday (&tv_now, NULL);
435 elapsed = tv_elapsed (add_files_state.tv_start,
437 printf ("Processed %d total files in ", add_files_state.processed_files);
438 print_formatted_seconds (elapsed);
439 printf (" (%d files/sec.). \n",
440 (int) (add_files_state.processed_files / elapsed));
441 printf ("Added %d unique messages to the database.\n\n",
442 add_files_state.added_messages);
444 printf ("When new mail is delivered to %s in the future,\n"
445 "run \"notmuch new\" to add it to the database.\n",
449 printf ("Note: At least one error was encountered: %s\n",
450 notmuch_status_to_string (ret));
455 free (mail_directory);
457 notmuch_database_close (notmuch);
463 new_command (int argc, char *argv[])
465 notmuch_database_t *notmuch;
466 const char *mail_directory;
467 add_files_state_t add_files_state;
469 struct timeval tv_now;
472 notmuch = notmuch_database_open (NULL);
473 if (notmuch == NULL) {
478 mail_directory = notmuch_database_get_path (notmuch);
480 add_files_state.total_files = 0;
481 add_files_state.processed_files = 0;
482 add_files_state.added_messages = 0;
483 gettimeofday (&add_files_state.tv_start, NULL);
485 ret = add_files (notmuch, mail_directory, &add_files_state);
487 gettimeofday (&tv_now, NULL);
488 elapsed = tv_elapsed (add_files_state.tv_start,
490 if (add_files_state.processed_files) {
491 printf ("Processed %d total files in ", add_files_state.processed_files);
492 print_formatted_seconds (elapsed);
493 printf (" (%d files/sec.). \n",
494 (int) (add_files_state.processed_files / elapsed));
496 if (add_files_state.added_messages) {
497 printf ("Added %d new messages to the database (not much, really).\n",
498 add_files_state.added_messages);
500 printf ("No new mail---and that's not much!.\n");
504 printf ("Note: At least one error was encountered: %s\n",
505 notmuch_status_to_string (ret));
510 notmuch_database_close (notmuch);
516 search_command (int argc, char *argv[])
518 fprintf (stderr, "Error: search is not implemented yet.\n");
523 show_command (int argc, char *argv[])
525 fprintf (stderr, "Error: show is not implemented yet.\n");
530 dump_command (int argc, char *argv[])
533 notmuch_database_t *notmuch;
534 notmuch_query_t *query;
535 notmuch_results_t *results;
536 notmuch_message_t *message;
537 notmuch_tags_t *tags;
541 output = fopen (argv[0], "w");
542 if (output == NULL) {
543 fprintf (stderr, "Error opening %s for writing: %s\n",
544 argv[0], strerror (errno));
552 notmuch = notmuch_database_open (NULL);
553 if (notmuch == NULL) {
558 query = notmuch_query_create (notmuch, "");
560 fprintf (stderr, "Out of memory\n");
565 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
567 for (results = notmuch_query_search (query);
568 notmuch_results_has_more (results);
569 notmuch_results_advance (results))
572 message = notmuch_results_get (results);
575 "%s (", notmuch_message_get_message_id (message));
577 for (tags = notmuch_message_get_tags (message);
578 notmuch_tags_has_more (tags);
579 notmuch_tags_advance (tags))
582 fprintf (output, " ");
584 fprintf (output, "%s", notmuch_tags_get (tags));
589 fprintf (output, ")\n");
591 notmuch_message_destroy (message);
594 notmuch_query_destroy (query);
598 notmuch_database_close (notmuch);
599 if (output != stdout)
606 restore_command (int argc, char *argv[])
609 notmuch_database_t *notmuch;
611 size_t line_size, line_len;
617 input = fopen (argv[0], "r");
619 fprintf (stderr, "Error opening %s for reading: %s\n",
620 argv[0], strerror (errno));
625 printf ("No filename given. Reading dump from stdin.\n");
629 notmuch = notmuch_database_open (NULL);
630 if (notmuch == NULL) {
635 /* Dump output is one line per message. We match a sequence of
636 * non-space characters for the message-id, then one or more
637 * spaces, then a list of space-separated tags as a sequence of
638 * characters within literal '(' and ')'. */
640 "^([^ ]+) \\(([^)]*)\\)$",
643 while ((line_len = getline (&line, &line_size, input)) != -1) {
645 char *message_id, *tags, *tag, *next;
646 notmuch_message_t *message;
647 notmuch_status_t status;
649 chomp_newline (line);
651 rerr = xregexec (®ex, line, 3, match, 0);
652 if (rerr == REG_NOMATCH)
654 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
659 message_id = xstrndup (line + match[1].rm_so,
660 match[1].rm_eo - match[1].rm_so);
661 tags = xstrndup (line + match[2].rm_so,
662 match[2].rm_eo - match[2].rm_so);
666 message = notmuch_database_find_message (notmuch, message_id);
667 if (message == NULL) {
668 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
674 tag = strsep (&next, " ");
678 status = notmuch_message_add_tag (message, tag);
681 "Error applying tag %s to message %s:\n",
683 fprintf (stderr, "%s\n",
684 notmuch_status_to_string (status));
687 fprintf (stderr, "%s ", tag);
692 notmuch_message_destroy (message);
694 fprintf (stderr, ")\n");
706 notmuch_database_close (notmuch);
711 command_t commands[] = {
712 { "setup", setup_command,
713 "Interactively setup notmuch for first use.\n"
714 "\t\tInvoking notmuch with no command argument will run setup if\n"
715 "\t\tthe setup command has not previously been completed." },
716 { "new", new_command,
717 "Find and import any new messages."},
718 { "search", search_command,
719 "<search-term> [...]\n\n"
720 "\t\tSearch for threads matching the given search terms.\n"
721 "\t\tOnce we actually implement search we'll document the\n"
722 "\t\tsyntax here." },
723 { "show", show_command,
725 "\t\tShow the thread with the given thread ID (see 'search')." },
726 { "dump", dump_command,
728 "\t\tCreate a plain-text dump of the tags for each message\n"
729 "\t\twriting to the given filename, if any, or to stdout.\n"
730 "\t\tThese tags are the only data in the notmuch database\n"
731 "\t\tthat can't be recreated from the messages themselves.\n"
732 "\t\tThe output of notmuch dump is therefore the only\n"
733 "\t\tcritical thing to backup (and much more friendly to\n"
734 "\t\tincremental backup than the native database files." },
735 { "restore", restore_command,
737 "\t\tRestore the tags from the given dump file (see 'dump')." }
746 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
747 fprintf (stderr, "\n");
748 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
749 fprintf (stderr, "\n");
751 for (i = 0; i < ARRAY_SIZE (commands); i++) {
752 command = &commands[i];
754 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
759 main (int argc, char *argv[])
765 return setup_command (0, NULL);
767 for (i = 0; i < ARRAY_SIZE (commands); i++) {
768 command = &commands[i];
770 if (strcmp (argv[1], command->name) == 0)
771 return (command->function) (argc - 2, &argv[2]);
774 /* Don't complain about "help" being an unknown command when we're
775 about to provide exactly what's wanted anyway. */
776 if (strcmp (argv[1], "help") == 0 ||
777 strcmp (argv[1], "--help") == 0)
779 fprintf (stderr, "The notmuch mail system.\n\n");
781 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);