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'
146 * The 'struct stat *st' must point to a structure that has already
147 * been initialized for 'path' by calling stat().
150 add_files_recursive (notmuch_database_t *notmuch,
153 add_files_state_t *state)
156 struct dirent *e, *entry = NULL;
160 time_t path_mtime, path_dbtime;
161 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
163 path_mtime = st->st_mtime;
165 path_dbtime = notmuch_database_get_timestamp (notmuch, path);
167 dir = opendir (path);
169 fprintf (stderr, "Error opening directory %s: %s\n",
170 path, strerror (errno));
171 ret = NOTMUCH_STATUS_FILE_ERROR;
175 entry_length = offsetof (struct dirent, d_name) +
176 pathconf (path, _PC_NAME_MAX) + 1;
177 entry = malloc (entry_length);
180 err = readdir_r (dir, entry, &e);
182 fprintf (stderr, "Error reading directory: %s\n",
184 ret = NOTMUCH_STATUS_FILE_ERROR;
191 /* If this directory hasn't been modified since the last
192 * add_files, then we only need to look further for
193 * sub-directories. */
194 if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
197 /* Ignore special directories to avoid infinite recursion.
198 * Also ignore the .notmuch directory.
200 /* XXX: Eventually we'll want more sophistication to let the
201 * user specify files to be ignored. */
202 if (strcmp (entry->d_name, ".") == 0 ||
203 strcmp (entry->d_name, "..") == 0 ||
204 strcmp (entry->d_name, ".notmuch") ==0)
209 next = g_strdup_printf ("%s/%s", path, entry->d_name);
211 if (stat (next, st)) {
212 fprintf (stderr, "Error reading %s: %s\n",
213 next, strerror (errno));
214 ret = NOTMUCH_STATUS_FILE_ERROR;
218 if (S_ISREG (st->st_mode)) {
219 /* If the file hasn't been modified since the last
220 * add_files, then we need not look at it. */
221 if (st->st_mtime > path_dbtime) {
222 state->processed_files++;
224 status = notmuch_database_add_message (notmuch, next);
227 case NOTMUCH_STATUS_SUCCESS:
228 state->added_messages++;
230 /* Non-fatal issues (go on to next file) */
231 case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
232 /* Stay silent on this one. */
234 case NOTMUCH_STATUS_FILE_NOT_EMAIL:
235 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
238 /* Fatal issues. Don't process anymore. */
239 case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
240 fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
244 fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n", status);
248 if (state->processed_files % 1000 == 0)
249 add_files_print_progress (state);
251 } else if (S_ISDIR (st->st_mode)) {
252 status = add_files_recursive (notmuch, next, st, state);
253 if (status && ret == NOTMUCH_STATUS_SUCCESS)
261 notmuch_database_set_timestamp (notmuch, path, path_mtime);
274 /* This is the top-level entry point for add_files. It does a couple
275 * of error checks, and then calls into the recursive function,
276 * (avoiding the repeating of these error checks at every
277 * level---which would be useless becaues we already do a stat() at
278 * the level above). */
279 static notmuch_status_t
280 add_files (notmuch_database_t *notmuch,
282 add_files_state_t *state)
286 if (stat (path, &st)) {
287 fprintf (stderr, "Error reading directory %s: %s\n",
288 path, strerror (errno));
289 return NOTMUCH_STATUS_FILE_ERROR;
292 if (! S_ISDIR (st.st_mode)) {
293 fprintf (stderr, "Error: %s is not a directory.\n", path);
294 return NOTMUCH_STATUS_FILE_ERROR;
297 return add_files_recursive (notmuch, path, &st, state);
300 /* Recursively count all regular files in path and all sub-direcotries
301 * of path. The result is added to *count (which should be
302 * initialized to zero by the top-level caller before calling
305 count_files (const char *path, int *count)
308 struct dirent *entry, *e;
314 dir = opendir (path);
317 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
318 path, strerror (errno));
322 entry_length = offsetof (struct dirent, d_name) +
323 pathconf (path, _PC_NAME_MAX) + 1;
324 entry = malloc (entry_length);
327 err = readdir_r (dir, entry, &e);
329 fprintf (stderr, "Error reading directory: %s\n",
338 /* Ignore special directories to avoid infinite recursion.
339 * Also ignore the .notmuch directory.
341 /* XXX: Eventually we'll want more sophistication to let the
342 * user specify files to be ignored. */
343 if (strcmp (entry->d_name, ".") == 0 ||
344 strcmp (entry->d_name, "..") == 0 ||
345 strcmp (entry->d_name, ".notmuch") == 0)
350 next = g_strdup_printf ("%s/%s", path, entry->d_name);
354 if (S_ISREG (st.st_mode)) {
356 if (*count % 1000 == 0) {
357 printf ("Found %d files so far.\r", *count);
360 } else if (S_ISDIR (st.st_mode)) {
361 count_files (next, count);
373 setup_command (int argc, char *argv[])
375 notmuch_database_t *notmuch = NULL;
376 char *default_path, *mail_directory = NULL;
379 add_files_state_t add_files_state;
381 struct timeval tv_now;
382 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
384 printf ("Welcome to notmuch!\n\n");
386 printf ("The goal of notmuch is to help you manage and search your collection of\n"
387 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
389 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
390 "(where you already have mail stored and where messages will be delivered\n"
391 "in the future). This directory can contain any number of sub-directories\n"
392 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
393 "archives are perfect). If there are other, non-email files (such as\n"
394 "indexes maintained by other email programs) then notmuch will do its\n"
395 "best to detect those and ignore them.\n\n");
397 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
398 "messages), will not work with notmuch. If that's how your mail is currently\n"
399 "stored, we recommend you first convert it to maildir format with a utility\n"
400 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
401 "once the conversion is complete.\n\n");
404 default_path = notmuch_database_default_path ();
405 printf ("Top-level mail directory [%s]: ", default_path);
408 getline (&mail_directory, &line_size, stdin);
409 chomp_newline (mail_directory);
413 if (mail_directory == NULL || strlen (mail_directory) == 0) {
415 free (mail_directory);
416 mail_directory = default_path;
418 /* XXX: Instead of telling the user to use an environment
419 * variable here, we should really be writing out a configuration
420 * file and loading that on the next run. */
421 if (strcmp (mail_directory, default_path)) {
422 printf ("Note: Since you are not using the default path, you will want to set\n"
423 "the NOTMUCH_BASE environment variable to %s so that\n"
424 "future calls to notmuch commands will know where to find your mail.\n",
426 printf ("For example, if you are using bash for your shell, add:\n\n");
427 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
428 printf ("to your ~/.bashrc file.\n\n");
433 notmuch = notmuch_database_create (mail_directory);
434 if (notmuch == NULL) {
435 fprintf (stderr, "Failed to create new notmuch database at %s\n",
437 ret = NOTMUCH_STATUS_FILE_ERROR;
441 printf ("OK. Let's take a look at the mail we can find in the directory\n");
442 printf ("%s ...\n", mail_directory);
445 count_files (mail_directory, &count);
447 printf ("Found %d total files. That's not much mail.\n\n", count);
449 printf ("Next, we'll inspect the messages and create a database of threads:\n");
451 add_files_state.total_files = count;
452 add_files_state.processed_files = 0;
453 add_files_state.added_messages = 0;
454 gettimeofday (&add_files_state.tv_start, NULL);
456 ret = add_files (notmuch, mail_directory, &add_files_state);
458 gettimeofday (&tv_now, NULL);
459 elapsed = tv_elapsed (add_files_state.tv_start,
461 printf ("Processed %d %s in ", add_files_state.processed_files,
462 add_files_state.processed_files == 1 ?
463 "file" : "total files");
464 print_formatted_seconds (elapsed);
466 printf (" (%d files/sec.). \n",
467 (int) (add_files_state.processed_files / elapsed));
471 if (add_files_state.added_messages) {
472 printf ("Added %d %s to the database.\n\n",
473 add_files_state.added_messages,
474 add_files_state.added_messages == 1 ?
475 "message" : "unique messages");
478 printf ("When new mail is delivered to %s in the future,\n"
479 "run \"notmuch new\" to add it to the database.\n",
483 printf ("Note: At least one error was encountered: %s\n",
484 notmuch_status_to_string (ret));
489 free (mail_directory);
491 notmuch_database_close (notmuch);
497 new_command (int argc, char *argv[])
499 notmuch_database_t *notmuch;
500 const char *mail_directory;
501 add_files_state_t add_files_state;
503 struct timeval tv_now;
506 notmuch = notmuch_database_open (NULL);
507 if (notmuch == NULL) {
512 mail_directory = notmuch_database_get_path (notmuch);
514 add_files_state.total_files = 0;
515 add_files_state.processed_files = 0;
516 add_files_state.added_messages = 0;
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 if (add_files_state.processed_files) {
525 printf ("Processed %d %s in ", add_files_state.processed_files,
526 add_files_state.processed_files == 1 ?
527 "file" : "total files");
528 print_formatted_seconds (elapsed);
530 printf (" (%d files/sec.). \n",
531 (int) (add_files_state.processed_files / elapsed));
536 if (add_files_state.added_messages) {
537 printf ("Added %d new %s to the database (not much, really).\n",
538 add_files_state.added_messages,
539 add_files_state.added_messages == 1 ?
540 "message" : "messages");
542 printf ("No new mail---and that's not much!\n");
546 printf ("Note: At least one error was encountered: %s\n",
547 notmuch_status_to_string (ret));
552 notmuch_database_close (notmuch);
558 search_command (int argc, char *argv[])
560 fprintf (stderr, "Error: search is not implemented yet.\n");
565 show_command (int argc, char *argv[])
567 fprintf (stderr, "Error: show is not implemented yet.\n");
572 dump_command (int argc, char *argv[])
575 notmuch_database_t *notmuch;
576 notmuch_query_t *query;
577 notmuch_results_t *results;
578 notmuch_message_t *message;
579 notmuch_tags_t *tags;
583 output = fopen (argv[0], "w");
584 if (output == NULL) {
585 fprintf (stderr, "Error opening %s for writing: %s\n",
586 argv[0], strerror (errno));
594 notmuch = notmuch_database_open (NULL);
595 if (notmuch == NULL) {
600 query = notmuch_query_create (notmuch, "");
602 fprintf (stderr, "Out of memory\n");
607 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
609 for (results = notmuch_query_search (query);
610 notmuch_results_has_more (results);
611 notmuch_results_advance (results))
614 message = notmuch_results_get (results);
617 "%s (", notmuch_message_get_message_id (message));
619 for (tags = notmuch_message_get_tags (message);
620 notmuch_tags_has_more (tags);
621 notmuch_tags_advance (tags))
624 fprintf (output, " ");
626 fprintf (output, "%s", notmuch_tags_get (tags));
631 fprintf (output, ")\n");
633 notmuch_message_destroy (message);
636 notmuch_query_destroy (query);
640 notmuch_database_close (notmuch);
641 if (output != stdout)
648 restore_command (int argc, char *argv[])
651 notmuch_database_t *notmuch;
653 size_t line_size, line_len;
659 input = fopen (argv[0], "r");
661 fprintf (stderr, "Error opening %s for reading: %s\n",
662 argv[0], strerror (errno));
667 printf ("No filename given. Reading dump from stdin.\n");
671 notmuch = notmuch_database_open (NULL);
672 if (notmuch == NULL) {
677 /* Dump output is one line per message. We match a sequence of
678 * non-space characters for the message-id, then one or more
679 * spaces, then a list of space-separated tags as a sequence of
680 * characters within literal '(' and ')'. */
682 "^([^ ]+) \\(([^)]*)\\)$",
685 while ((line_len = getline (&line, &line_size, input)) != -1) {
687 char *message_id, *tags, *tag, *next;
688 notmuch_message_t *message;
689 notmuch_status_t status;
691 chomp_newline (line);
693 rerr = xregexec (®ex, line, 3, match, 0);
694 if (rerr == REG_NOMATCH)
696 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
701 message_id = xstrndup (line + match[1].rm_so,
702 match[1].rm_eo - match[1].rm_so);
703 tags = xstrndup (line + match[2].rm_so,
704 match[2].rm_eo - match[2].rm_so);
708 message = notmuch_database_find_message (notmuch, message_id);
709 if (message == NULL) {
710 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
716 tag = strsep (&next, " ");
720 status = notmuch_message_add_tag (message, tag);
723 "Error applying tag %s to message %s:\n",
725 fprintf (stderr, "%s\n",
726 notmuch_status_to_string (status));
729 fprintf (stderr, "%s ", tag);
734 notmuch_message_destroy (message);
736 fprintf (stderr, ")\n");
748 notmuch_database_close (notmuch);
753 command_t commands[] = {
754 { "setup", setup_command,
755 "Interactively setup notmuch for first use.\n"
756 "\t\tInvoking notmuch with no command argument will run setup if\n"
757 "\t\tthe setup command has not previously been completed." },
758 { "new", new_command,
759 "Find and import any new messages."},
760 { "search", search_command,
761 "<search-term> [...]\n\n"
762 "\t\tSearch for threads matching the given search terms.\n"
763 "\t\tOnce we actually implement search we'll document the\n"
764 "\t\tsyntax here." },
765 { "show", show_command,
767 "\t\tShow the thread with the given thread ID (see 'search')." },
768 { "dump", dump_command,
770 "\t\tCreate a plain-text dump of the tags for each message\n"
771 "\t\twriting to the given filename, if any, or to stdout.\n"
772 "\t\tThese tags are the only data in the notmuch database\n"
773 "\t\tthat can't be recreated from the messages themselves.\n"
774 "\t\tThe output of notmuch dump is therefore the only\n"
775 "\t\tcritical thing to backup (and much more friendly to\n"
776 "\t\tincremental backup than the native database files." },
777 { "restore", restore_command,
779 "\t\tRestore the tags from the given dump file (see 'dump')." }
788 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
789 fprintf (stderr, "\n");
790 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
791 fprintf (stderr, "\n");
793 for (i = 0; i < ARRAY_SIZE (commands); i++) {
794 command = &commands[i];
796 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
801 main (int argc, char *argv[])
807 return setup_command (0, NULL);
809 for (i = 0; i < ARRAY_SIZE (commands); i++) {
810 command = &commands[i];
812 if (strcmp (argv[1], command->name) == 0)
813 return (command->function) (argc - 2, &argv[2]);
816 /* Don't complain about "help" being an unknown command when we're
817 about to provide exactly what's wanted anyway. */
818 if (strcmp (argv[1], "help") == 0 ||
819 strcmp (argv[1], "--help") == 0)
821 fprintf (stderr, "The notmuch mail system.\n\n");
823 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);