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 %s in ", add_files_state.processed_files,
443 add_files_state.processed_files == 1 ?
444 "file" : "total files");
445 print_formatted_seconds (elapsed);
447 printf (" (%d files/sec.). \n",
448 (int) (add_files_state.processed_files / elapsed));
452 if (add_files_state.added_messages) {
453 printf ("Added %d %s to the database.\n\n",
454 add_files_state.added_messages,
455 add_files_state.added_messages == 1 ?
456 "message" : "unique messages");
459 printf ("When new mail is delivered to %s in the future,\n"
460 "run \"notmuch new\" to add it to the database.\n",
464 printf ("Note: At least one error was encountered: %s\n",
465 notmuch_status_to_string (ret));
470 free (mail_directory);
472 notmuch_database_close (notmuch);
478 new_command (int argc, char *argv[])
480 notmuch_database_t *notmuch;
481 const char *mail_directory;
482 add_files_state_t add_files_state;
484 struct timeval tv_now;
487 notmuch = notmuch_database_open (NULL);
488 if (notmuch == NULL) {
493 mail_directory = notmuch_database_get_path (notmuch);
495 add_files_state.total_files = 0;
496 add_files_state.processed_files = 0;
497 add_files_state.added_messages = 0;
498 gettimeofday (&add_files_state.tv_start, NULL);
500 ret = add_files (notmuch, mail_directory, &add_files_state);
502 gettimeofday (&tv_now, NULL);
503 elapsed = tv_elapsed (add_files_state.tv_start,
505 if (add_files_state.processed_files) {
506 printf ("Processed %d %s in ", add_files_state.processed_files,
507 add_files_state.processed_files == 1 ?
508 "file" : "total files");
509 print_formatted_seconds (elapsed);
511 printf (" (%d files/sec.). \n",
512 (int) (add_files_state.processed_files / elapsed));
517 if (add_files_state.added_messages) {
518 printf ("Added %d new %s to the database (not much, really).\n",
519 add_files_state.added_messages,
520 add_files_state.added_messages == 1 ?
521 "message" : "messages");
523 printf ("No new mail---and that's not much!\n");
527 printf ("Note: At least one error was encountered: %s\n",
528 notmuch_status_to_string (ret));
533 notmuch_database_close (notmuch);
539 search_command (int argc, char *argv[])
541 fprintf (stderr, "Error: search is not implemented yet.\n");
546 show_command (int argc, char *argv[])
548 fprintf (stderr, "Error: show is not implemented yet.\n");
553 dump_command (int argc, char *argv[])
556 notmuch_database_t *notmuch;
557 notmuch_query_t *query;
558 notmuch_results_t *results;
559 notmuch_message_t *message;
560 notmuch_tags_t *tags;
564 output = fopen (argv[0], "w");
565 if (output == NULL) {
566 fprintf (stderr, "Error opening %s for writing: %s\n",
567 argv[0], strerror (errno));
575 notmuch = notmuch_database_open (NULL);
576 if (notmuch == NULL) {
581 query = notmuch_query_create (notmuch, "");
583 fprintf (stderr, "Out of memory\n");
588 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
590 for (results = notmuch_query_search (query);
591 notmuch_results_has_more (results);
592 notmuch_results_advance (results))
595 message = notmuch_results_get (results);
598 "%s (", notmuch_message_get_message_id (message));
600 for (tags = notmuch_message_get_tags (message);
601 notmuch_tags_has_more (tags);
602 notmuch_tags_advance (tags))
605 fprintf (output, " ");
607 fprintf (output, "%s", notmuch_tags_get (tags));
612 fprintf (output, ")\n");
614 notmuch_message_destroy (message);
617 notmuch_query_destroy (query);
621 notmuch_database_close (notmuch);
622 if (output != stdout)
629 restore_command (int argc, char *argv[])
632 notmuch_database_t *notmuch;
634 size_t line_size, line_len;
640 input = fopen (argv[0], "r");
642 fprintf (stderr, "Error opening %s for reading: %s\n",
643 argv[0], strerror (errno));
648 printf ("No filename given. Reading dump from stdin.\n");
652 notmuch = notmuch_database_open (NULL);
653 if (notmuch == NULL) {
658 /* Dump output is one line per message. We match a sequence of
659 * non-space characters for the message-id, then one or more
660 * spaces, then a list of space-separated tags as a sequence of
661 * characters within literal '(' and ')'. */
663 "^([^ ]+) \\(([^)]*)\\)$",
666 while ((line_len = getline (&line, &line_size, input)) != -1) {
668 char *message_id, *tags, *tag, *next;
669 notmuch_message_t *message;
670 notmuch_status_t status;
672 chomp_newline (line);
674 rerr = xregexec (®ex, line, 3, match, 0);
675 if (rerr == REG_NOMATCH)
677 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
682 message_id = xstrndup (line + match[1].rm_so,
683 match[1].rm_eo - match[1].rm_so);
684 tags = xstrndup (line + match[2].rm_so,
685 match[2].rm_eo - match[2].rm_so);
689 message = notmuch_database_find_message (notmuch, message_id);
690 if (message == NULL) {
691 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
697 tag = strsep (&next, " ");
701 status = notmuch_message_add_tag (message, tag);
704 "Error applying tag %s to message %s:\n",
706 fprintf (stderr, "%s\n",
707 notmuch_status_to_string (status));
710 fprintf (stderr, "%s ", tag);
715 notmuch_message_destroy (message);
717 fprintf (stderr, ")\n");
729 notmuch_database_close (notmuch);
734 command_t commands[] = {
735 { "setup", setup_command,
736 "Interactively setup notmuch for first use.\n"
737 "\t\tInvoking notmuch with no command argument will run setup if\n"
738 "\t\tthe setup command has not previously been completed." },
739 { "new", new_command,
740 "Find and import any new messages."},
741 { "search", search_command,
742 "<search-term> [...]\n\n"
743 "\t\tSearch for threads matching the given search terms.\n"
744 "\t\tOnce we actually implement search we'll document the\n"
745 "\t\tsyntax here." },
746 { "show", show_command,
748 "\t\tShow the thread with the given thread ID (see 'search')." },
749 { "dump", dump_command,
751 "\t\tCreate a plain-text dump of the tags for each message\n"
752 "\t\twriting to the given filename, if any, or to stdout.\n"
753 "\t\tThese tags are the only data in the notmuch database\n"
754 "\t\tthat can't be recreated from the messages themselves.\n"
755 "\t\tThe output of notmuch dump is therefore the only\n"
756 "\t\tcritical thing to backup (and much more friendly to\n"
757 "\t\tincremental backup than the native database files." },
758 { "restore", restore_command,
760 "\t\tRestore the tags from the given dump file (see 'dump')." }
769 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
770 fprintf (stderr, "\n");
771 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
772 fprintf (stderr, "\n");
774 for (i = 0; i < ARRAY_SIZE (commands); i++) {
775 command = &commands[i];
777 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
782 main (int argc, char *argv[])
788 return setup_command (0, NULL);
790 for (i = 0; i < ARRAY_SIZE (commands); i++) {
791 command = &commands[i];
793 if (strcmp (argv[1], command->name) == 0)
794 return (command->function) (argc - 2, &argv[2]);
797 /* Don't complain about "help" being an unknown command when we're
798 about to provide exactly what's wanted anyway. */
799 if (strcmp (argv[1], "help") == 0 ||
800 strcmp (argv[1], "--help") == 0)
802 fprintf (stderr, "The notmuch mail system.\n\n");
804 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);