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 printf ("almost no time");
90 hours = (int) seconds / 3600;
91 printf ("%dh ", hours);
92 seconds -= hours * 3600;
96 minutes = (int) seconds / 60;
97 printf ("%dm ", minutes);
98 seconds -= minutes * 60;
101 printf ("%ds", (int) seconds);
105 add_files_print_progress (add_files_state_t *state)
107 struct timeval tv_now;
108 double elapsed_overall, rate_overall;
110 gettimeofday (&tv_now, NULL);
112 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
113 rate_overall = (state->processed_files) / elapsed_overall;
115 printf ("Processed %d", state->processed_files);
117 if (state->total_files) {
118 printf (" of %d files (", state->total_files);
119 print_formatted_seconds ((state->total_files - state->processed_files) /
121 printf (" remaining). \r");
123 printf (" files (%d files/sec.) \r", (int) rate_overall);
129 /* Examine 'path' recursively as follows:
131 * o Ask the filesystem for the mtime of 'path' (path_mtime)
133 * o Ask the database for its timestamp of 'path' (path_dbtime)
135 * o If 'path_mtime' > 'path_dbtime'
137 * o For each regular file in 'path' with mtime newer than the
138 * 'path_dbtime' call add_message to add the file to the
141 * o For each sub-directory of path, recursively call into this
144 * o Tell the database to update its time of 'path' to 'path_mtime'
147 add_files (notmuch_database_t *notmuch, const char *path,
148 add_files_state_t *state)
151 struct dirent *e, *entry = NULL;
156 time_t path_mtime, path_dbtime;
157 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
159 if (stat (path, &st)) {
160 fprintf (stderr, "Error reading directory %s: %s\n",
161 path, strerror (errno));
162 return NOTMUCH_STATUS_FILE_ERROR;
165 if (! S_ISDIR (st.st_mode)) {
166 fprintf (stderr, "Error: %s is not a directory.\n", path);
167 return NOTMUCH_STATUS_FILE_ERROR;
170 path_mtime = st.st_mtime;
172 path_dbtime = notmuch_database_get_timestamp (notmuch, path);
174 dir = opendir (path);
176 fprintf (stderr, "Error opening directory %s: %s\n",
177 path, strerror (errno));
178 ret = NOTMUCH_STATUS_FILE_ERROR;
182 entry_length = offsetof (struct dirent, d_name) +
183 pathconf (path, _PC_NAME_MAX) + 1;
184 entry = malloc (entry_length);
187 err = readdir_r (dir, entry, &e);
189 fprintf (stderr, "Error reading directory: %s\n",
191 ret = NOTMUCH_STATUS_FILE_ERROR;
198 /* If this directory hasn't been modified since the last
199 * add_files, then we only need to look further for
200 * sub-directories. */
201 if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
204 /* Ignore special directories to avoid infinite recursion.
205 * Also ignore the .notmuch directory.
207 /* XXX: Eventually we'll want more sophistication to let the
208 * user specify files to be ignored. */
209 if (strcmp (entry->d_name, ".") == 0 ||
210 strcmp (entry->d_name, "..") == 0 ||
211 strcmp (entry->d_name, ".notmuch") ==0)
216 next = g_strdup_printf ("%s/%s", path, entry->d_name);
218 if (stat (next, &st)) {
219 fprintf (stderr, "Error reading %s: %s\n",
220 next, strerror (errno));
221 ret = NOTMUCH_STATUS_FILE_ERROR;
225 if (S_ISREG (st.st_mode)) {
226 /* If the file hasn't been modified since the last
227 * add_files, then we need not look at it. */
228 if (st.st_mtime > path_dbtime) {
229 state->processed_files++;
231 status = notmuch_database_add_message (notmuch, next);
234 case NOTMUCH_STATUS_SUCCESS:
235 state->added_messages++;
237 /* Non-fatal issues (go on to next file) */
238 case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
239 /* Stay silent on this one. */
241 case NOTMUCH_STATUS_FILE_NOT_EMAIL:
242 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
245 /* Fatal issues. Don't process anymore. */
246 case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
247 fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
251 fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n", status);
255 if (state->processed_files % 1000 == 0)
256 add_files_print_progress (state);
258 } else if (S_ISDIR (st.st_mode)) {
259 status = add_files (notmuch, next, state);
260 if (status && ret == NOTMUCH_STATUS_SUCCESS)
268 notmuch_database_set_timestamp (notmuch, path, path_mtime);
281 /* Recursively count all regular files in path and all sub-direcotries
282 * of path. The result is added to *count (which should be
283 * initialized to zero by the top-level caller before calling
286 count_files (const char *path, int *count)
289 struct dirent *entry, *e;
295 dir = opendir (path);
298 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
299 path, strerror (errno));
303 entry_length = offsetof (struct dirent, d_name) +
304 pathconf (path, _PC_NAME_MAX) + 1;
305 entry = malloc (entry_length);
308 err = readdir_r (dir, entry, &e);
310 fprintf (stderr, "Error reading directory: %s\n",
319 /* Ignore special directories to avoid infinite recursion.
320 * Also ignore the .notmuch directory.
322 /* XXX: Eventually we'll want more sophistication to let the
323 * user specify files to be ignored. */
324 if (strcmp (entry->d_name, ".") == 0 ||
325 strcmp (entry->d_name, "..") == 0 ||
326 strcmp (entry->d_name, ".notmuch") == 0)
331 next = g_strdup_printf ("%s/%s", path, entry->d_name);
335 if (S_ISREG (st.st_mode)) {
337 if (*count % 1000 == 0) {
338 printf ("Found %d files so far.\r", *count);
341 } else if (S_ISDIR (st.st_mode)) {
342 count_files (next, count);
354 setup_command (int argc, char *argv[])
356 notmuch_database_t *notmuch = NULL;
357 char *default_path, *mail_directory = NULL;
360 add_files_state_t add_files_state;
362 struct timeval tv_now;
363 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
365 printf ("Welcome to notmuch!\n\n");
367 printf ("The goal of notmuch is to help you manage and search your collection of\n"
368 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
370 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
371 "(where you already have mail stored and where messages will be delivered\n"
372 "in the future). This directory can contain any number of sub-directories\n"
373 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
374 "archives are perfect). If there are other, non-email files (such as\n"
375 "indexes maintained by other email programs) then notmuch will do its\n"
376 "best to detect those and ignore them.\n\n");
378 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
379 "messages), will not work with notmuch. If that's how your mail is currently\n"
380 "stored, we recommend you first convert it to maildir format with a utility\n"
381 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
382 "once the conversion is complete.\n\n");
385 default_path = notmuch_database_default_path ();
386 printf ("Top-level mail directory [%s]: ", default_path);
389 getline (&mail_directory, &line_size, stdin);
390 chomp_newline (mail_directory);
394 if (mail_directory == NULL || strlen (mail_directory) == 0) {
396 free (mail_directory);
397 mail_directory = default_path;
399 /* XXX: Instead of telling the user to use an environment
400 * variable here, we should really be writing out a configuration
401 * file and loading that on the next run. */
402 if (strcmp (mail_directory, default_path)) {
403 printf ("Note: Since you are not using the default path, you will want to set\n"
404 "the NOTMUCH_BASE environment variable to %s so that\n"
405 "future calls to notmuch commands will know where to find your mail.\n",
407 printf ("For example, if you are using bash for your shell, add:\n\n");
408 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
409 printf ("to your ~/.bashrc file.\n\n");
414 notmuch = notmuch_database_create (mail_directory);
415 if (notmuch == NULL) {
416 fprintf (stderr, "Failed to create new notmuch database at %s\n",
418 ret = NOTMUCH_STATUS_FILE_ERROR;
422 printf ("OK. Let's take a look at the mail we can find in the directory\n");
423 printf ("%s ...\n", mail_directory);
426 count_files (mail_directory, &count);
428 printf ("Found %d total files. That's not much mail.\n\n", count);
430 printf ("Next, we'll inspect the messages and create a database of threads:\n");
432 add_files_state.total_files = count;
433 add_files_state.processed_files = 0;
434 add_files_state.added_messages = 0;
435 gettimeofday (&add_files_state.tv_start, NULL);
437 ret = add_files (notmuch, mail_directory, &add_files_state);
439 gettimeofday (&tv_now, NULL);
440 elapsed = tv_elapsed (add_files_state.tv_start,
442 printf ("Processed %d total files in ", add_files_state.processed_files);
443 print_formatted_seconds (elapsed);
445 printf (" (%d files/sec.). \n",
446 (int) (add_files_state.processed_files / elapsed));
450 if (add_files_state.added_messages) {
451 printf ("Added %d %s to the database.\n\n",
452 add_files_state.added_messages,
453 add_files_state.added_messages == 1 ?
454 "message" : "unique messages");
457 printf ("When new mail is delivered to %s in the future,\n"
458 "run \"notmuch new\" to add it to the database.\n",
462 printf ("Note: At least one error was encountered: %s\n",
463 notmuch_status_to_string (ret));
468 free (mail_directory);
470 notmuch_database_close (notmuch);
476 new_command (int argc, char *argv[])
478 notmuch_database_t *notmuch;
479 const char *mail_directory;
480 add_files_state_t add_files_state;
482 struct timeval tv_now;
485 notmuch = notmuch_database_open (NULL);
486 if (notmuch == NULL) {
491 mail_directory = notmuch_database_get_path (notmuch);
493 add_files_state.total_files = 0;
494 add_files_state.processed_files = 0;
495 add_files_state.added_messages = 0;
496 gettimeofday (&add_files_state.tv_start, NULL);
498 ret = add_files (notmuch, mail_directory, &add_files_state);
500 gettimeofday (&tv_now, NULL);
501 elapsed = tv_elapsed (add_files_state.tv_start,
503 if (add_files_state.processed_files) {
504 printf ("Processed %d total files in ", add_files_state.processed_files);
505 print_formatted_seconds (elapsed);
507 printf (" (%d files/sec.). \n",
508 (int) (add_files_state.processed_files / elapsed));
513 if (add_files_state.added_messages) {
514 printf ("Added %d new %s to the database (not much, really).\n",
515 add_files_state.added_messages,
516 add_files_state.added_messages == 1 ?
517 "message" : "messages");
519 printf ("No new mail---and that's not much!\n");
523 printf ("Note: At least one error was encountered: %s\n",
524 notmuch_status_to_string (ret));
529 notmuch_database_close (notmuch);
535 search_command (int argc, char *argv[])
537 fprintf (stderr, "Error: search is not implemented yet.\n");
542 show_command (int argc, char *argv[])
544 fprintf (stderr, "Error: show is not implemented yet.\n");
549 dump_command (int argc, char *argv[])
552 notmuch_database_t *notmuch;
553 notmuch_query_t *query;
554 notmuch_results_t *results;
555 notmuch_message_t *message;
556 notmuch_tags_t *tags;
560 output = fopen (argv[0], "w");
561 if (output == NULL) {
562 fprintf (stderr, "Error opening %s for writing: %s\n",
563 argv[0], strerror (errno));
571 notmuch = notmuch_database_open (NULL);
572 if (notmuch == NULL) {
577 query = notmuch_query_create (notmuch, "");
579 fprintf (stderr, "Out of memory\n");
584 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
586 for (results = notmuch_query_search (query);
587 notmuch_results_has_more (results);
588 notmuch_results_advance (results))
591 message = notmuch_results_get (results);
594 "%s (", notmuch_message_get_message_id (message));
596 for (tags = notmuch_message_get_tags (message);
597 notmuch_tags_has_more (tags);
598 notmuch_tags_advance (tags))
601 fprintf (output, " ");
603 fprintf (output, "%s", notmuch_tags_get (tags));
608 fprintf (output, ")\n");
610 notmuch_message_destroy (message);
613 notmuch_query_destroy (query);
617 notmuch_database_close (notmuch);
618 if (output != stdout)
625 restore_command (int argc, char *argv[])
628 notmuch_database_t *notmuch;
630 size_t line_size, line_len;
636 input = fopen (argv[0], "r");
638 fprintf (stderr, "Error opening %s for reading: %s\n",
639 argv[0], strerror (errno));
644 printf ("No filename given. Reading dump from stdin.\n");
648 notmuch = notmuch_database_open (NULL);
649 if (notmuch == NULL) {
654 /* Dump output is one line per message. We match a sequence of
655 * non-space characters for the message-id, then one or more
656 * spaces, then a list of space-separated tags as a sequence of
657 * characters within literal '(' and ')'. */
659 "^([^ ]+) \\(([^)]*)\\)$",
662 while ((line_len = getline (&line, &line_size, input)) != -1) {
664 char *message_id, *tags, *tag, *next;
665 notmuch_message_t *message;
666 notmuch_status_t status;
668 chomp_newline (line);
670 rerr = xregexec (®ex, line, 3, match, 0);
671 if (rerr == REG_NOMATCH)
673 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
678 message_id = xstrndup (line + match[1].rm_so,
679 match[1].rm_eo - match[1].rm_so);
680 tags = xstrndup (line + match[2].rm_so,
681 match[2].rm_eo - match[2].rm_so);
685 message = notmuch_database_find_message (notmuch, message_id);
686 if (message == NULL) {
687 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
693 tag = strsep (&next, " ");
697 status = notmuch_message_add_tag (message, tag);
700 "Error applying tag %s to message %s:\n",
702 fprintf (stderr, "%s\n",
703 notmuch_status_to_string (status));
706 fprintf (stderr, "%s ", tag);
711 notmuch_message_destroy (message);
713 fprintf (stderr, ")\n");
725 notmuch_database_close (notmuch);
730 command_t commands[] = {
731 { "setup", setup_command,
732 "Interactively setup notmuch for first use.\n"
733 "\t\tInvoking notmuch with no command argument will run setup if\n"
734 "\t\tthe setup command has not previously been completed." },
735 { "new", new_command,
736 "Find and import any new messages."},
737 { "search", search_command,
738 "<search-term> [...]\n\n"
739 "\t\tSearch for threads matching the given search terms.\n"
740 "\t\tOnce we actually implement search we'll document the\n"
741 "\t\tsyntax here." },
742 { "show", show_command,
744 "\t\tShow the thread with the given thread ID (see 'search')." },
745 { "dump", dump_command,
747 "\t\tCreate a plain-text dump of the tags for each message\n"
748 "\t\twriting to the given filename, if any, or to stdout.\n"
749 "\t\tThese tags are the only data in the notmuch database\n"
750 "\t\tthat can't be recreated from the messages themselves.\n"
751 "\t\tThe output of notmuch dump is therefore the only\n"
752 "\t\tcritical thing to backup (and much more friendly to\n"
753 "\t\tincremental backup than the native database files." },
754 { "restore", restore_command,
756 "\t\tRestore the tags from the given dump file (see 'dump')." }
765 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
766 fprintf (stderr, "\n");
767 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
768 fprintf (stderr, "\n");
770 for (i = 0; i < ARRAY_SIZE (commands); i++) {
771 command = &commands[i];
773 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
778 main (int argc, char *argv[])
784 return setup_command (0, NULL);
786 for (i = 0; i < ARRAY_SIZE (commands); i++) {
787 command = &commands[i];
789 if (strcmp (argv[1], command->name) == 0)
790 return (command->function) (argc - 2, &argv[2]);
793 /* Don't complain about "help" being an unknown command when we're
794 about to provide exactly what's wanted anyway. */
795 if (strcmp (argv[1], "help") == 0 ||
796 strcmp (argv[1], "--help") == 0)
798 fprintf (stderr, "The notmuch mail system.\n\n");
800 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);