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 #define unused(x) x __attribute__ ((unused))
46 /* There's no point in continuing when we've detected that we've done
47 * something wrong internally (as opposed to the user passing in a
50 * Note that __location__ comes from talloc.h.
52 #define INTERNAL_ERROR(format, ...) \
55 "Internal error: " format " (%s)\n", \
56 ##__VA_ARGS__, __location__); \
60 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
62 typedef int (*command_function_t) (int argc, char *argv[]);
64 typedef struct command {
66 command_function_t function;
70 typedef void (*add_files_callback_t) (notmuch_message_t *message);
73 int ignore_read_only_directories;
74 int saw_read_only_directory;
79 struct timeval tv_start;
81 add_files_callback_t callback;
85 chomp_newline (char *str)
87 if (str && str[strlen(str)-1] == '\n')
88 str[strlen(str)-1] = '\0';
91 /* Compute the number of seconds elapsed from start to end. */
93 tv_elapsed (struct timeval start, struct timeval end)
95 return ((end.tv_sec - start.tv_sec) +
96 (end.tv_usec - start.tv_usec) / 1e6);
100 print_formatted_seconds (double seconds)
106 printf ("almost no time");
110 if (seconds > 3600) {
111 hours = (int) seconds / 3600;
112 printf ("%dh ", hours);
113 seconds -= hours * 3600;
117 minutes = (int) seconds / 60;
118 printf ("%dm ", minutes);
119 seconds -= minutes * 60;
122 printf ("%ds", (int) seconds);
126 add_files_print_progress (add_files_state_t *state)
128 struct timeval tv_now;
129 double elapsed_overall, rate_overall;
131 gettimeofday (&tv_now, NULL);
133 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
134 rate_overall = (state->processed_files) / elapsed_overall;
136 printf ("Processed %d", state->processed_files);
138 if (state->total_files) {
139 printf (" of %d files (", state->total_files);
140 print_formatted_seconds ((state->total_files - state->processed_files) /
142 printf (" remaining). \r");
144 printf (" files (%d files/sec.) \r", (int) rate_overall);
150 /* Examine 'path' recursively as follows:
152 * o Ask the filesystem for the mtime of 'path' (path_mtime)
154 * o Ask the database for its timestamp of 'path' (path_dbtime)
156 * o If 'path_mtime' > 'path_dbtime'
158 * o For each regular file in 'path' with mtime newer than the
159 * 'path_dbtime' call add_message to add the file to the
162 * o For each sub-directory of path, recursively call into this
165 * o Tell the database to update its time of 'path' to 'path_mtime'
167 * The 'struct stat *st' must point to a structure that has already
168 * been initialized for 'path' by calling stat().
170 static notmuch_status_t
171 add_files_recursive (notmuch_database_t *notmuch,
174 add_files_state_t *state)
177 struct dirent *e, *entry = NULL;
181 time_t path_mtime, path_dbtime;
182 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
183 notmuch_message_t *message = NULL, **closure;
185 /* If we're told to, we bail out on encountering a read-only
186 * directory, (with this being a clear clue from the user to
187 * Notmuch that new mail won't be arriving there and we need not
189 if (state->ignore_read_only_directories &&
190 (st->st_mode & S_IWUSR) == 0)
192 state->saw_read_only_directory = TRUE;
196 path_mtime = st->st_mtime;
198 path_dbtime = notmuch_database_get_timestamp (notmuch, path);
200 dir = opendir (path);
202 fprintf (stderr, "Error opening directory %s: %s\n",
203 path, strerror (errno));
204 ret = NOTMUCH_STATUS_FILE_ERROR;
208 entry_length = offsetof (struct dirent, d_name) +
209 pathconf (path, _PC_NAME_MAX) + 1;
210 entry = malloc (entry_length);
213 err = readdir_r (dir, entry, &e);
215 fprintf (stderr, "Error reading directory: %s\n",
217 ret = NOTMUCH_STATUS_FILE_ERROR;
224 /* If this directory hasn't been modified since the last
225 * add_files, then we only need to look further for
226 * sub-directories. */
227 if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
230 /* Ignore special directories to avoid infinite recursion.
231 * Also ignore the .notmuch directory.
233 /* XXX: Eventually we'll want more sophistication to let the
234 * user specify files to be ignored. */
235 if (strcmp (entry->d_name, ".") == 0 ||
236 strcmp (entry->d_name, "..") == 0 ||
237 strcmp (entry->d_name, ".notmuch") ==0)
242 next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
244 if (stat (next, st)) {
245 fprintf (stderr, "Error reading %s: %s\n",
246 next, strerror (errno));
247 ret = NOTMUCH_STATUS_FILE_ERROR;
251 if (S_ISREG (st->st_mode)) {
252 /* If the file hasn't been modified since the last
253 * add_files, then we need not look at it. */
254 if (st->st_mtime > path_dbtime) {
255 state->processed_files++;
262 status = notmuch_database_add_message (notmuch, next, closure);
265 case NOTMUCH_STATUS_SUCCESS:
266 state->added_messages++;
268 (state->callback) (message);
270 /* Non-fatal issues (go on to next file) */
271 case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
272 /* Stay silent on this one. */
274 case NOTMUCH_STATUS_FILE_NOT_EMAIL:
275 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
278 /* Fatal issues. Don't process anymore. */
279 case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
280 case NOTMUCH_STATUS_OUT_OF_MEMORY:
281 fprintf (stderr, "Error: %s. Halting processing.\n",
282 notmuch_status_to_string (status));
286 case NOTMUCH_STATUS_FILE_ERROR:
287 case NOTMUCH_STATUS_NULL_POINTER:
288 case NOTMUCH_STATUS_TAG_TOO_LONG:
289 case NOTMUCH_STATUS_LAST_STATUS:
290 INTERNAL_ERROR ("add_message returned unexpected value: %d", status);
295 notmuch_message_destroy (message);
299 if (state->processed_files % 1000 == 0)
300 add_files_print_progress (state);
302 } else if (S_ISDIR (st->st_mode)) {
303 status = add_files_recursive (notmuch, next, st, state);
304 if (status && ret == NOTMUCH_STATUS_SUCCESS)
312 status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
313 if (status && ret == NOTMUCH_STATUS_SUCCESS)
327 /* This is the top-level entry point for add_files. It does a couple
328 * of error checks, and then calls into the recursive function,
329 * (avoiding the repeating of these error checks at every
330 * level---which would be useless becaues we already do a stat() at
331 * the level above). */
332 static notmuch_status_t
333 add_files (notmuch_database_t *notmuch,
335 add_files_state_t *state)
339 if (stat (path, &st)) {
340 fprintf (stderr, "Error reading directory %s: %s\n",
341 path, strerror (errno));
342 return NOTMUCH_STATUS_FILE_ERROR;
345 if (! S_ISDIR (st.st_mode)) {
346 fprintf (stderr, "Error: %s is not a directory.\n", path);
347 return NOTMUCH_STATUS_FILE_ERROR;
350 return add_files_recursive (notmuch, path, &st, state);
353 /* Recursively count all regular files in path and all sub-direcotries
354 * of path. The result is added to *count (which should be
355 * initialized to zero by the top-level caller before calling
358 count_files (const char *path, int *count)
361 struct dirent *e, *entry = NULL;
367 dir = opendir (path);
370 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
371 path, strerror (errno));
375 entry_length = offsetof (struct dirent, d_name) +
376 pathconf (path, _PC_NAME_MAX) + 1;
377 entry = malloc (entry_length);
380 err = readdir_r (dir, entry, &e);
382 fprintf (stderr, "Error reading directory: %s\n",
391 /* Ignore special directories to avoid infinite recursion.
392 * Also ignore the .notmuch directory.
394 /* XXX: Eventually we'll want more sophistication to let the
395 * user specify files to be ignored. */
396 if (strcmp (entry->d_name, ".") == 0 ||
397 strcmp (entry->d_name, "..") == 0 ||
398 strcmp (entry->d_name, ".notmuch") == 0)
403 if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
405 fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
406 path, entry->d_name);
412 if (S_ISREG (st.st_mode)) {
414 if (*count % 1000 == 0) {
415 printf ("Found %d files so far.\r", *count);
418 } else if (S_ISDIR (st.st_mode)) {
419 count_files (next, count);
433 setup_command (unused (int argc), unused (char *argv[]))
435 notmuch_database_t *notmuch = NULL;
436 char *default_path, *mail_directory = NULL;
439 add_files_state_t add_files_state;
441 struct timeval tv_now;
442 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
444 printf ("Welcome to notmuch!\n\n");
446 printf ("The goal of notmuch is to help you manage and search your collection of\n"
447 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
449 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
450 "(where you already have mail stored and where messages will be delivered\n"
451 "in the future). This directory can contain any number of sub-directories\n"
452 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
453 "archives are perfect). If there are other, non-email files (such as\n"
454 "indexes maintained by other email programs) then notmuch will do its\n"
455 "best to detect those and ignore them.\n\n");
457 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
458 "messages), will not work with notmuch. If that's how your mail is currently\n"
459 "stored, we recommend you first convert it to maildir format with a utility\n"
460 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
461 "once the conversion is complete.\n\n");
464 default_path = notmuch_database_default_path ();
465 printf ("Top-level mail directory [%s]: ", default_path);
468 getline (&mail_directory, &line_size, stdin);
469 chomp_newline (mail_directory);
473 if (mail_directory == NULL || strlen (mail_directory) == 0) {
475 free (mail_directory);
476 mail_directory = default_path;
478 /* XXX: Instead of telling the user to use an environment
479 * variable here, we should really be writing out a configuration
480 * file and loading that on the next run. */
481 if (strcmp (mail_directory, default_path)) {
482 printf ("Note: Since you are not using the default path, you will want to set\n"
483 "the NOTMUCH_BASE environment variable to %s so that\n"
484 "future calls to notmuch commands will know where to find your mail.\n",
486 printf ("For example, if you are using bash for your shell, add:\n\n");
487 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
488 printf ("to your ~/.bashrc file.\n\n");
493 notmuch = notmuch_database_create (mail_directory);
494 if (notmuch == NULL) {
495 fprintf (stderr, "Failed to create new notmuch database at %s\n",
497 ret = NOTMUCH_STATUS_FILE_ERROR;
501 printf ("OK. Let's take a look at the mail we can find in the directory\n");
502 printf ("%s ...\n", mail_directory);
505 count_files (mail_directory, &count);
507 printf ("Found %d total files. That's not much mail.\n\n", count);
509 printf ("Next, we'll inspect the messages and create a database of threads:\n");
511 add_files_state.ignore_read_only_directories = FALSE;
512 add_files_state.saw_read_only_directory = FALSE;
513 add_files_state.total_files = count;
514 add_files_state.processed_files = 0;
515 add_files_state.added_messages = 0;
516 add_files_state.callback = NULL;
517 gettimeofday (&add_files_state.tv_start, NULL);
519 ret = add_files (notmuch, mail_directory, &add_files_state);
521 gettimeofday (&tv_now, NULL);
522 elapsed = tv_elapsed (add_files_state.tv_start,
524 printf ("Processed %d %s in ", add_files_state.processed_files,
525 add_files_state.processed_files == 1 ?
526 "file" : "total files");
527 print_formatted_seconds (elapsed);
529 printf (" (%d files/sec.). \n",
530 (int) (add_files_state.processed_files / elapsed));
534 if (add_files_state.added_messages) {
535 printf ("Added %d %s to the database.\n\n",
536 add_files_state.added_messages,
537 add_files_state.added_messages == 1 ?
538 "message" : "unique messages");
541 printf ("When new mail is delivered to %s in the future,\n"
542 "run \"notmuch new\" to add it to the database.\n\n",
546 printf ("Note: At least one error was encountered: %s\n",
547 notmuch_status_to_string (ret));
552 free (mail_directory);
554 notmuch_database_close (notmuch);
560 tag_inbox_and_unread (notmuch_message_t *message)
562 notmuch_message_add_tag (message, "inbox");
563 notmuch_message_add_tag (message, "unread");
567 new_command (unused (int argc), unused (char *argv[]))
569 notmuch_database_t *notmuch;
570 const char *mail_directory;
571 add_files_state_t add_files_state;
573 struct timeval tv_now;
576 notmuch = notmuch_database_open (NULL);
577 if (notmuch == NULL) {
582 mail_directory = notmuch_database_get_path (notmuch);
584 add_files_state.ignore_read_only_directories = TRUE;
585 add_files_state.saw_read_only_directory = FALSE;
586 add_files_state.total_files = 0;
587 add_files_state.processed_files = 0;
588 add_files_state.added_messages = 0;
589 add_files_state.callback = tag_inbox_and_unread;
590 gettimeofday (&add_files_state.tv_start, NULL);
592 ret = add_files (notmuch, mail_directory, &add_files_state);
594 gettimeofday (&tv_now, NULL);
595 elapsed = tv_elapsed (add_files_state.tv_start,
597 if (add_files_state.processed_files) {
598 printf ("Processed %d %s in ", add_files_state.processed_files,
599 add_files_state.processed_files == 1 ?
600 "file" : "total files");
601 print_formatted_seconds (elapsed);
603 printf (" (%d files/sec.). \n",
604 (int) (add_files_state.processed_files / elapsed));
609 if (add_files_state.added_messages) {
610 printf ("Added %d new %s to the database (not much, really).\n",
611 add_files_state.added_messages,
612 add_files_state.added_messages == 1 ?
613 "message" : "messages");
615 printf ("No new mail---and that's not much.\n");
618 if (elapsed > 1 && ! add_files_state.saw_read_only_directory) {
619 printf ("\nTip: If you have any sub-directories that are archives (that is,\n"
620 "they will never receive new mail), marking these directores as\n"
621 "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
622 "much more efficient (it won't even look in those directories).\n");
626 printf ("\nNote: At least one error was encountered: %s\n",
627 notmuch_status_to_string (ret));
632 notmuch_database_close (notmuch);
638 search_command (int argc, char *argv[])
640 void *local = talloc_new (NULL);
641 notmuch_database_t *notmuch = NULL;
642 notmuch_query_t *query;
643 notmuch_thread_results_t *results;
644 notmuch_thread_t *thread;
645 notmuch_tags_t *tags;
648 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
650 notmuch = notmuch_database_open (NULL);
651 if (notmuch == NULL) {
656 /* XXX: Should add xtalloc wrappers here and use them. */
657 query_str = talloc_strdup (local, "");
659 for (i = 0; i < argc; i++) {
661 query_str = talloc_asprintf_append (query_str, " ");
663 query_str = talloc_asprintf_append (query_str, "%s", argv[i]);
666 query = notmuch_query_create (notmuch, query_str);
668 fprintf (stderr, "Out of memory\n");
673 for (results = notmuch_query_search_threads (query);
674 notmuch_thread_results_has_more (results);
675 notmuch_thread_results_advance (results))
679 thread = notmuch_thread_results_get (results);
682 notmuch_thread_get_thread_id (thread),
683 notmuch_thread_get_subject (thread));
686 for (tags = notmuch_thread_get_tags (thread);
687 notmuch_tags_has_more (tags);
688 notmuch_tags_advance (tags))
692 printf ("%s", notmuch_tags_get (tags));
697 notmuch_thread_destroy (thread);
700 notmuch_query_destroy (query);
704 notmuch_database_close (notmuch);
711 show_command (unused (int argc), unused (char *argv[]))
713 fprintf (stderr, "Error: show is not implemented yet.\n");
718 dump_command (int argc, char *argv[])
721 notmuch_database_t *notmuch = NULL;
722 notmuch_query_t *query;
723 notmuch_message_results_t *results;
724 notmuch_message_t *message;
725 notmuch_tags_t *tags;
729 output = fopen (argv[0], "w");
730 if (output == NULL) {
731 fprintf (stderr, "Error opening %s for writing: %s\n",
732 argv[0], strerror (errno));
740 notmuch = notmuch_database_open (NULL);
741 if (notmuch == NULL) {
746 query = notmuch_query_create (notmuch, "");
748 fprintf (stderr, "Out of memory\n");
753 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
755 for (results = notmuch_query_search_messages (query);
756 notmuch_message_results_has_more (results);
757 notmuch_message_results_advance (results))
760 message = notmuch_message_results_get (results);
763 "%s (", notmuch_message_get_message_id (message));
765 for (tags = notmuch_message_get_tags (message);
766 notmuch_tags_has_more (tags);
767 notmuch_tags_advance (tags))
770 fprintf (output, " ");
772 fprintf (output, "%s", notmuch_tags_get (tags));
777 fprintf (output, ")\n");
779 notmuch_message_destroy (message);
782 notmuch_query_destroy (query);
786 notmuch_database_close (notmuch);
787 if (output && output != stdout)
794 restore_command (int argc, char *argv[])
797 notmuch_database_t *notmuch = NULL;
806 input = fopen (argv[0], "r");
808 fprintf (stderr, "Error opening %s for reading: %s\n",
809 argv[0], strerror (errno));
814 printf ("No filename given. Reading dump from stdin.\n");
818 notmuch = notmuch_database_open (NULL);
819 if (notmuch == NULL) {
824 /* Dump output is one line per message. We match a sequence of
825 * non-space characters for the message-id, then one or more
826 * spaces, then a list of space-separated tags as a sequence of
827 * characters within literal '(' and ')'. */
829 "^([^ ]+) \\(([^)]*)\\)$",
832 while ((line_len = getline (&line, &line_size, input)) != -1) {
834 char *message_id, *tags, *tag, *next;
835 notmuch_message_t *message;
836 notmuch_status_t status;
838 chomp_newline (line);
840 rerr = xregexec (®ex, line, 3, match, 0);
841 if (rerr == REG_NOMATCH)
843 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
848 message_id = xstrndup (line + match[1].rm_so,
849 match[1].rm_eo - match[1].rm_so);
850 tags = xstrndup (line + match[2].rm_so,
851 match[2].rm_eo - match[2].rm_so);
855 message = notmuch_database_find_message (notmuch, message_id);
856 if (message == NULL) {
857 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
863 tag = strsep (&next, " ");
867 status = notmuch_message_add_tag (message, tag);
870 "Error applying tag %s to message %s:\n",
872 fprintf (stderr, "%s\n",
873 notmuch_status_to_string (status));
876 fprintf (stderr, "%s%s",
877 tag == tags ? "" : " ", tag);
882 notmuch_message_destroy (message);
884 fprintf (stderr, ")\n");
896 notmuch_database_close (notmuch);
897 if (input && input != stdin)
903 command_t commands[] = {
904 { "setup", setup_command,
905 "Interactively setup notmuch for first use.\n\n"
906 "\t\tInvoking notmuch with no command argument will run setup if\n"
907 "\t\tthe setup command has not previously been completed." },
908 { "new", new_command,
909 "Find and import any new messages.\n\n"
910 "\t\tScans all sub-directories of the database, adding new messages\n"
911 "\t\tthat are found. Each new message will be tagges as both\n"
912 "\t\t\"inbox\" and \"unread\".\n"
914 "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
915 "\t\tso you can use that to mark tdirectories that will not\n"
916 "\t\treceive any new mail (and make \"notmuch new\" faster)." },
917 { "search", search_command,
918 "<search-term> [...]\n\n"
919 "\t\tSearch for threads matching the given search terms.\n"
920 "\t\tOnce we actually implement search we'll document the\n"
921 "\t\tsyntax here." },
922 { "show", show_command,
924 "\t\tShow the thread with the given thread ID (see 'search')." },
925 { "dump", dump_command,
927 "\t\tCreate a plain-text dump of the tags for each message\n"
928 "\t\twriting to the given filename, if any, or to stdout.\n"
929 "\t\tThese tags are the only data in the notmuch database\n"
930 "\t\tthat can't be recreated from the messages themselves.\n"
931 "\t\tThe output of notmuch dump is therefore the only\n"
932 "\t\tcritical thing to backup (and much more friendly to\n"
933 "\t\tincremental backup than the native database files." },
934 { "restore", restore_command,
936 "\t\tRestore the tags from the given dump file (see 'dump')." }
945 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
946 fprintf (stderr, "\n");
947 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
948 fprintf (stderr, "\n");
950 for (i = 0; i < ARRAY_SIZE (commands); i++) {
951 command = &commands[i];
953 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
958 main (int argc, char *argv[])
964 return setup_command (0, NULL);
966 for (i = 0; i < ARRAY_SIZE (commands); i++) {
967 command = &commands[i];
969 if (strcmp (argv[1], command->name) == 0)
970 return (command->function) (argc - 2, &argv[2]);
973 /* Don't complain about "help" being an unknown command when we're
974 about to provide exactly what's wanted anyway. */
975 if (strcmp (argv[1], "help") == 0 ||
976 strcmp (argv[1], "--help") == 0)
978 fprintf (stderr, "The notmuch mail system.\n\n");
982 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);