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;
57 int ignore_read_only_directories;
62 struct timeval tv_start;
66 chomp_newline (char *str)
68 if (str && str[strlen(str)-1] == '\n')
69 str[strlen(str)-1] = '\0';
72 /* Compute the number of seconds elapsed from start to end. */
74 tv_elapsed (struct timeval start, struct timeval end)
76 return ((end.tv_sec - start.tv_sec) +
77 (end.tv_usec - start.tv_usec) / 1e6);
81 print_formatted_seconds (double seconds)
87 printf ("almost no time");
92 hours = (int) seconds / 3600;
93 printf ("%dh ", hours);
94 seconds -= hours * 3600;
98 minutes = (int) seconds / 60;
99 printf ("%dm ", minutes);
100 seconds -= minutes * 60;
103 printf ("%ds", (int) seconds);
107 add_files_print_progress (add_files_state_t *state)
109 struct timeval tv_now;
110 double elapsed_overall, rate_overall;
112 gettimeofday (&tv_now, NULL);
114 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
115 rate_overall = (state->processed_files) / elapsed_overall;
117 printf ("Processed %d", state->processed_files);
119 if (state->total_files) {
120 printf (" of %d files (", state->total_files);
121 print_formatted_seconds ((state->total_files - state->processed_files) /
123 printf (" remaining). \r");
125 printf (" files (%d files/sec.) \r", (int) rate_overall);
131 /* Examine 'path' recursively as follows:
133 * o Ask the filesystem for the mtime of 'path' (path_mtime)
135 * o Ask the database for its timestamp of 'path' (path_dbtime)
137 * o If 'path_mtime' > 'path_dbtime'
139 * o For each regular file in 'path' with mtime newer than the
140 * 'path_dbtime' call add_message to add the file to the
143 * o For each sub-directory of path, recursively call into this
146 * o Tell the database to update its time of 'path' to 'path_mtime'
148 * The 'struct stat *st' must point to a structure that has already
149 * been initialized for 'path' by calling stat().
152 add_files_recursive (notmuch_database_t *notmuch,
155 add_files_state_t *state)
158 struct dirent *e, *entry = NULL;
162 time_t path_mtime, path_dbtime;
163 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
165 /* If we're told to, we bail out on encountering a read-only
166 * directory, (with this being a clear clue from the user to
167 * Notmuch that new mail won't be arriving there and we need not
169 if (state->ignore_read_only_directories &&
170 (st->st_mode & S_IWUSR) == 0)
175 path_mtime = st->st_mtime;
177 path_dbtime = notmuch_database_get_timestamp (notmuch, path);
179 dir = opendir (path);
181 fprintf (stderr, "Error opening directory %s: %s\n",
182 path, strerror (errno));
183 ret = NOTMUCH_STATUS_FILE_ERROR;
187 entry_length = offsetof (struct dirent, d_name) +
188 pathconf (path, _PC_NAME_MAX) + 1;
189 entry = malloc (entry_length);
192 err = readdir_r (dir, entry, &e);
194 fprintf (stderr, "Error reading directory: %s\n",
196 ret = NOTMUCH_STATUS_FILE_ERROR;
203 /* If this directory hasn't been modified since the last
204 * add_files, then we only need to look further for
205 * sub-directories. */
206 if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
209 /* Ignore special directories to avoid infinite recursion.
210 * Also ignore the .notmuch directory.
212 /* XXX: Eventually we'll want more sophistication to let the
213 * user specify files to be ignored. */
214 if (strcmp (entry->d_name, ".") == 0 ||
215 strcmp (entry->d_name, "..") == 0 ||
216 strcmp (entry->d_name, ".notmuch") ==0)
221 next = g_strdup_printf ("%s/%s", path, entry->d_name);
223 if (stat (next, st)) {
224 fprintf (stderr, "Error reading %s: %s\n",
225 next, strerror (errno));
226 ret = NOTMUCH_STATUS_FILE_ERROR;
230 if (S_ISREG (st->st_mode)) {
231 /* If the file hasn't been modified since the last
232 * add_files, then we need not look at it. */
233 if (st->st_mtime > path_dbtime) {
234 state->processed_files++;
236 status = notmuch_database_add_message (notmuch, next);
239 case NOTMUCH_STATUS_SUCCESS:
240 state->added_messages++;
242 /* Non-fatal issues (go on to next file) */
243 case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
244 /* Stay silent on this one. */
246 case NOTMUCH_STATUS_FILE_NOT_EMAIL:
247 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
250 /* Fatal issues. Don't process anymore. */
251 case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
252 fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
256 fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n", status);
260 if (state->processed_files % 1000 == 0)
261 add_files_print_progress (state);
263 } else if (S_ISDIR (st->st_mode)) {
264 status = add_files_recursive (notmuch, next, st, state);
265 if (status && ret == NOTMUCH_STATUS_SUCCESS)
273 notmuch_database_set_timestamp (notmuch, path, path_mtime);
286 /* This is the top-level entry point for add_files. It does a couple
287 * of error checks, and then calls into the recursive function,
288 * (avoiding the repeating of these error checks at every
289 * level---which would be useless becaues we already do a stat() at
290 * the level above). */
291 static notmuch_status_t
292 add_files (notmuch_database_t *notmuch,
294 add_files_state_t *state)
298 if (stat (path, &st)) {
299 fprintf (stderr, "Error reading directory %s: %s\n",
300 path, strerror (errno));
301 return NOTMUCH_STATUS_FILE_ERROR;
304 if (! S_ISDIR (st.st_mode)) {
305 fprintf (stderr, "Error: %s is not a directory.\n", path);
306 return NOTMUCH_STATUS_FILE_ERROR;
309 return add_files_recursive (notmuch, path, &st, state);
312 /* Recursively count all regular files in path and all sub-direcotries
313 * of path. The result is added to *count (which should be
314 * initialized to zero by the top-level caller before calling
317 count_files (const char *path, int *count)
320 struct dirent *entry, *e;
326 dir = opendir (path);
329 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
330 path, strerror (errno));
334 entry_length = offsetof (struct dirent, d_name) +
335 pathconf (path, _PC_NAME_MAX) + 1;
336 entry = malloc (entry_length);
339 err = readdir_r (dir, entry, &e);
341 fprintf (stderr, "Error reading directory: %s\n",
350 /* Ignore special directories to avoid infinite recursion.
351 * Also ignore the .notmuch directory.
353 /* XXX: Eventually we'll want more sophistication to let the
354 * user specify files to be ignored. */
355 if (strcmp (entry->d_name, ".") == 0 ||
356 strcmp (entry->d_name, "..") == 0 ||
357 strcmp (entry->d_name, ".notmuch") == 0)
362 next = g_strdup_printf ("%s/%s", path, entry->d_name);
366 if (S_ISREG (st.st_mode)) {
368 if (*count % 1000 == 0) {
369 printf ("Found %d files so far.\r", *count);
372 } else if (S_ISDIR (st.st_mode)) {
373 count_files (next, count);
385 setup_command (int argc, char *argv[])
387 notmuch_database_t *notmuch = NULL;
388 char *default_path, *mail_directory = NULL;
391 add_files_state_t add_files_state;
393 struct timeval tv_now;
394 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
396 printf ("Welcome to notmuch!\n\n");
398 printf ("The goal of notmuch is to help you manage and search your collection of\n"
399 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
401 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
402 "(where you already have mail stored and where messages will be delivered\n"
403 "in the future). This directory can contain any number of sub-directories\n"
404 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
405 "archives are perfect). If there are other, non-email files (such as\n"
406 "indexes maintained by other email programs) then notmuch will do its\n"
407 "best to detect those and ignore them.\n\n");
409 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
410 "messages), will not work with notmuch. If that's how your mail is currently\n"
411 "stored, we recommend you first convert it to maildir format with a utility\n"
412 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
413 "once the conversion is complete.\n\n");
416 default_path = notmuch_database_default_path ();
417 printf ("Top-level mail directory [%s]: ", default_path);
420 getline (&mail_directory, &line_size, stdin);
421 chomp_newline (mail_directory);
425 if (mail_directory == NULL || strlen (mail_directory) == 0) {
427 free (mail_directory);
428 mail_directory = default_path;
430 /* XXX: Instead of telling the user to use an environment
431 * variable here, we should really be writing out a configuration
432 * file and loading that on the next run. */
433 if (strcmp (mail_directory, default_path)) {
434 printf ("Note: Since you are not using the default path, you will want to set\n"
435 "the NOTMUCH_BASE environment variable to %s so that\n"
436 "future calls to notmuch commands will know where to find your mail.\n",
438 printf ("For example, if you are using bash for your shell, add:\n\n");
439 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
440 printf ("to your ~/.bashrc file.\n\n");
445 notmuch = notmuch_database_create (mail_directory);
446 if (notmuch == NULL) {
447 fprintf (stderr, "Failed to create new notmuch database at %s\n",
449 ret = NOTMUCH_STATUS_FILE_ERROR;
453 printf ("OK. Let's take a look at the mail we can find in the directory\n");
454 printf ("%s ...\n", mail_directory);
457 count_files (mail_directory, &count);
459 printf ("Found %d total files. That's not much mail.\n\n", count);
461 printf ("Next, we'll inspect the messages and create a database of threads:\n");
463 add_files_state.ignore_read_only_directories = FALSE;
464 add_files_state.total_files = count;
465 add_files_state.processed_files = 0;
466 add_files_state.added_messages = 0;
467 gettimeofday (&add_files_state.tv_start, NULL);
469 ret = add_files (notmuch, mail_directory, &add_files_state);
471 gettimeofday (&tv_now, NULL);
472 elapsed = tv_elapsed (add_files_state.tv_start,
474 printf ("Processed %d %s in ", add_files_state.processed_files,
475 add_files_state.processed_files == 1 ?
476 "file" : "total files");
477 print_formatted_seconds (elapsed);
479 printf (" (%d files/sec.). \n",
480 (int) (add_files_state.processed_files / elapsed));
484 if (add_files_state.added_messages) {
485 printf ("Added %d %s to the database.\n\n",
486 add_files_state.added_messages,
487 add_files_state.added_messages == 1 ?
488 "message" : "unique messages");
491 printf ("When new mail is delivered to %s in the future,\n"
492 "run \"notmuch new\" to add it to the database.\n\n",
494 printf ("And if you have any sub-directories that are archives (that is,\n"
495 "they will never receive new mail), marking these directores as\n"
496 "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
497 "much more efficient (it won't even look in those directories).\n\n");
500 printf ("Note: At least one error was encountered: %s\n",
501 notmuch_status_to_string (ret));
506 free (mail_directory);
508 notmuch_database_close (notmuch);
514 new_command (int argc, char *argv[])
516 notmuch_database_t *notmuch;
517 const char *mail_directory;
518 add_files_state_t add_files_state;
520 struct timeval tv_now;
523 notmuch = notmuch_database_open (NULL);
524 if (notmuch == NULL) {
529 mail_directory = notmuch_database_get_path (notmuch);
531 add_files_state.ignore_read_only_directories = TRUE;
532 add_files_state.total_files = 0;
533 add_files_state.processed_files = 0;
534 add_files_state.added_messages = 0;
535 gettimeofday (&add_files_state.tv_start, NULL);
537 ret = add_files (notmuch, mail_directory, &add_files_state);
539 gettimeofday (&tv_now, NULL);
540 elapsed = tv_elapsed (add_files_state.tv_start,
542 if (add_files_state.processed_files) {
543 printf ("Processed %d %s in ", add_files_state.processed_files,
544 add_files_state.processed_files == 1 ?
545 "file" : "total files");
546 print_formatted_seconds (elapsed);
548 printf (" (%d files/sec.). \n",
549 (int) (add_files_state.processed_files / elapsed));
554 if (add_files_state.added_messages) {
555 printf ("Added %d new %s to the database (not much, really).\n",
556 add_files_state.added_messages,
557 add_files_state.added_messages == 1 ?
558 "message" : "messages");
560 printf ("No new mail---and that's not much!\n");
564 printf ("Note: At least one error was encountered: %s\n",
565 notmuch_status_to_string (ret));
570 notmuch_database_close (notmuch);
576 search_command (int argc, char *argv[])
578 fprintf (stderr, "Error: search is not implemented yet.\n");
583 show_command (int argc, char *argv[])
585 fprintf (stderr, "Error: show is not implemented yet.\n");
590 dump_command (int argc, char *argv[])
593 notmuch_database_t *notmuch;
594 notmuch_query_t *query;
595 notmuch_results_t *results;
596 notmuch_message_t *message;
597 notmuch_tags_t *tags;
601 output = fopen (argv[0], "w");
602 if (output == NULL) {
603 fprintf (stderr, "Error opening %s for writing: %s\n",
604 argv[0], strerror (errno));
612 notmuch = notmuch_database_open (NULL);
613 if (notmuch == NULL) {
618 query = notmuch_query_create (notmuch, "");
620 fprintf (stderr, "Out of memory\n");
625 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
627 for (results = notmuch_query_search (query);
628 notmuch_results_has_more (results);
629 notmuch_results_advance (results))
632 message = notmuch_results_get (results);
635 "%s (", notmuch_message_get_message_id (message));
637 for (tags = notmuch_message_get_tags (message);
638 notmuch_tags_has_more (tags);
639 notmuch_tags_advance (tags))
642 fprintf (output, " ");
644 fprintf (output, "%s", notmuch_tags_get (tags));
649 fprintf (output, ")\n");
651 notmuch_message_destroy (message);
654 notmuch_query_destroy (query);
658 notmuch_database_close (notmuch);
659 if (output != stdout)
666 restore_command (int argc, char *argv[])
669 notmuch_database_t *notmuch;
671 size_t line_size, line_len;
677 input = fopen (argv[0], "r");
679 fprintf (stderr, "Error opening %s for reading: %s\n",
680 argv[0], strerror (errno));
685 printf ("No filename given. Reading dump from stdin.\n");
689 notmuch = notmuch_database_open (NULL);
690 if (notmuch == NULL) {
695 /* Dump output is one line per message. We match a sequence of
696 * non-space characters for the message-id, then one or more
697 * spaces, then a list of space-separated tags as a sequence of
698 * characters within literal '(' and ')'. */
700 "^([^ ]+) \\(([^)]*)\\)$",
703 while ((line_len = getline (&line, &line_size, input)) != -1) {
705 char *message_id, *tags, *tag, *next;
706 notmuch_message_t *message;
707 notmuch_status_t status;
709 chomp_newline (line);
711 rerr = xregexec (®ex, line, 3, match, 0);
712 if (rerr == REG_NOMATCH)
714 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
719 message_id = xstrndup (line + match[1].rm_so,
720 match[1].rm_eo - match[1].rm_so);
721 tags = xstrndup (line + match[2].rm_so,
722 match[2].rm_eo - match[2].rm_so);
726 message = notmuch_database_find_message (notmuch, message_id);
727 if (message == NULL) {
728 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
734 tag = strsep (&next, " ");
738 status = notmuch_message_add_tag (message, tag);
741 "Error applying tag %s to message %s:\n",
743 fprintf (stderr, "%s\n",
744 notmuch_status_to_string (status));
747 fprintf (stderr, "%s ", tag);
752 notmuch_message_destroy (message);
754 fprintf (stderr, ")\n");
766 notmuch_database_close (notmuch);
771 command_t commands[] = {
772 { "setup", setup_command,
773 "Interactively setup notmuch for first use.\n\n"
774 "\t\tInvoking notmuch with no command argument will run setup if\n"
775 "\t\tthe setup command has not previously been completed." },
776 { "new", new_command,
777 "Find and import any new messages.\n\n"
778 "\t\tScans all sub-directories of the database, adding new files\n"
779 "\t\tthat are found. Note: \"notmuch new\" will skip any\n"
780 "\t\tread-only directories, so you can use that to mark\n"
781 "\t\tdirectories that will not receive any new mail."},
782 { "search", search_command,
783 "<search-term> [...]\n\n"
784 "\t\tSearch for threads matching the given search terms.\n"
785 "\t\tOnce we actually implement search we'll document the\n"
786 "\t\tsyntax here." },
787 { "show", show_command,
789 "\t\tShow the thread with the given thread ID (see 'search')." },
790 { "dump", dump_command,
792 "\t\tCreate a plain-text dump of the tags for each message\n"
793 "\t\twriting to the given filename, if any, or to stdout.\n"
794 "\t\tThese tags are the only data in the notmuch database\n"
795 "\t\tthat can't be recreated from the messages themselves.\n"
796 "\t\tThe output of notmuch dump is therefore the only\n"
797 "\t\tcritical thing to backup (and much more friendly to\n"
798 "\t\tincremental backup than the native database files." },
799 { "restore", restore_command,
801 "\t\tRestore the tags from the given dump file (see 'dump')." }
810 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
811 fprintf (stderr, "\n");
812 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
813 fprintf (stderr, "\n");
815 for (i = 0; i < ARRAY_SIZE (commands); i++) {
816 command = &commands[i];
818 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
823 main (int argc, char *argv[])
829 return setup_command (0, NULL);
831 for (i = 0; i < ARRAY_SIZE (commands); i++) {
832 command = &commands[i];
834 if (strcmp (argv[1], command->name) == 0)
835 return (command->function) (argc - 2, &argv[2]);
838 /* Don't complain about "help" being an unknown command when we're
839 about to provide exactly what's wanted anyway. */
840 if (strcmp (argv[1], "help") == 0 ||
841 strcmp (argv[1], "--help") == 0)
843 fprintf (stderr, "The notmuch mail system.\n\n");
845 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);