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 hours = (int) seconds / 3600;
86 printf ("%dh ", hours);
87 seconds -= hours * 3600;
91 minutes = (int) seconds / 60;
92 printf ("%dm ", minutes);
93 seconds -= minutes * 60;
96 printf ("%ds", (int) seconds);
100 add_files_print_progress (add_files_state_t *state)
102 struct timeval tv_now;
103 double elapsed_overall, rate_overall;
105 gettimeofday (&tv_now, NULL);
107 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
108 rate_overall = (state->processed_files) / elapsed_overall;
110 printf ("Processed %d", state->processed_files);
112 if (state->total_files) {
113 printf (" of %d files (", state->total_files);
114 print_formatted_seconds ((state->total_files - state->processed_files) /
116 printf (" remaining). \r");
118 printf (" files (%d files/sec.) \r", (int) rate_overall);
124 /* Recursively find all regular files in 'path' and add them to the
127 add_files (notmuch_database_t *notmuch, const char *path,
128 add_files_state_t *state)
131 struct dirent *e, *entry = NULL;
136 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
138 dir = opendir (path);
140 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
141 path, strerror (errno));
142 ret = NOTMUCH_STATUS_FILE_ERROR;
146 entry_length = offsetof (struct dirent, d_name) +
147 pathconf (path, _PC_NAME_MAX) + 1;
148 entry = malloc (entry_length);
151 err = readdir_r (dir, entry, &e);
153 fprintf (stderr, "Error reading directory: %s\n",
155 ret = NOTMUCH_STATUS_FILE_ERROR;
162 /* Ignore special directories to avoid infinite recursion.
163 * Also ignore the .notmuch directory.
165 /* XXX: Eventually we'll want more sophistication to let the
166 * user specify files to be ignored. */
167 if (strcmp (entry->d_name, ".") == 0 ||
168 strcmp (entry->d_name, "..") == 0 ||
169 strcmp (entry->d_name, ".notmuch") ==0)
174 next = g_strdup_printf ("%s/%s", path, entry->d_name);
176 if (stat (next, &st)) {
177 fprintf (stderr, "Error reading %s: %s\n",
178 next, strerror (errno));
179 ret = NOTMUCH_STATUS_FILE_ERROR;
183 if (S_ISREG (st.st_mode)) {
184 state->processed_files++;
185 status = notmuch_database_add_message (notmuch, next);
186 if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
187 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
189 } else if (status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
190 state->added_messages++;
192 if (state->processed_files % 1000 == 0)
193 add_files_print_progress (state);
194 } else if (S_ISDIR (st.st_mode)) {
195 status = add_files (notmuch, next, state);
196 if (status && ret == NOTMUCH_STATUS_SUCCESS)
215 /* Recursively count all regular files in path and all sub-direcotries
216 * of path. The result is added to *count (which should be
217 * initialized to zero by the top-level caller before calling
220 count_files (const char *path, int *count)
223 struct dirent *entry, *e;
229 dir = opendir (path);
232 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
233 path, strerror (errno));
237 entry_length = offsetof (struct dirent, d_name) +
238 pathconf (path, _PC_NAME_MAX) + 1;
239 entry = malloc (entry_length);
242 err = readdir_r (dir, entry, &e);
244 fprintf (stderr, "Error reading directory: %s\n",
253 /* Ignore special directories to avoid infinite recursion.
254 * Also ignore the .notmuch directory.
256 /* XXX: Eventually we'll want more sophistication to let the
257 * user specify files to be ignored. */
258 if (strcmp (entry->d_name, ".") == 0 ||
259 strcmp (entry->d_name, "..") == 0 ||
260 strcmp (entry->d_name, ".notmuch") == 0)
265 next = g_strdup_printf ("%s/%s", path, entry->d_name);
269 if (S_ISREG (st.st_mode)) {
271 if (*count % 1000 == 0) {
272 printf ("Found %d files so far.\r", *count);
275 } else if (S_ISDIR (st.st_mode)) {
276 count_files (next, count);
288 setup_command (int argc, char *argv[])
290 notmuch_database_t *notmuch = NULL;
291 char *default_path, *mail_directory = NULL;
294 add_files_state_t add_files_state;
296 struct timeval tv_now;
297 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
299 printf ("Welcome to notmuch!\n\n");
301 printf ("The goal of notmuch is to help you manage and search your collection of\n"
302 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
304 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
305 "(where you already have mail stored and where messages will be delivered\n"
306 "in the future). This directory can contain any number of sub-directories\n"
307 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
308 "archives are perfect). If there are other, non-email files (such as\n"
309 "indexes maintained by other email programs) then notmuch will do its\n"
310 "best to detect those and ignore them.\n\n");
312 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
313 "messages), will not work with notmuch. If that's how your mail is currently\n"
314 "stored, we recommend you first convert it to maildir format with a utility\n"
315 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
316 "once the conversion is complete.\n\n");
319 default_path = notmuch_database_default_path ();
320 printf ("Top-level mail directory [%s]: ", default_path);
323 getline (&mail_directory, &line_size, stdin);
324 chomp_newline (mail_directory);
328 if (mail_directory == NULL || strlen (mail_directory) == 0) {
330 free (mail_directory);
331 mail_directory = default_path;
333 /* XXX: Instead of telling the user to use an environment
334 * variable here, we should really be writing out a configuration
335 * file and loading that on the next run. */
336 if (strcmp (mail_directory, default_path)) {
337 printf ("Note: Since you are not using the default path, you will want to set\n"
338 "the NOTMUCH_BASE environment variable to %s so that\n"
339 "future calls to notmuch commands will know where to find your mail.\n",
341 printf ("For example, if you are using bash for your shell, add:\n\n");
342 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
343 printf ("to your ~/.bashrc file.\n\n");
348 notmuch = notmuch_database_create (mail_directory);
349 if (notmuch == NULL) {
350 fprintf (stderr, "Failed to create new notmuch database at %s\n",
352 ret = NOTMUCH_STATUS_FILE_ERROR;
356 printf ("OK. Let's take a look at the mail we can find in the directory\n");
357 printf ("%s ...\n", mail_directory);
360 count_files (mail_directory, &count);
362 printf ("Found %d total files. That's not much mail.\n\n", count);
364 printf ("Next, we'll inspect the messages and create a database of threads:\n");
366 add_files_state.total_files = count;
367 add_files_state.processed_files = 0;
368 add_files_state.added_messages = 0;
369 gettimeofday (&add_files_state.tv_start, NULL);
371 ret = add_files (notmuch, mail_directory, &add_files_state);
373 gettimeofday (&tv_now, NULL);
374 elapsed = tv_elapsed (add_files_state.tv_start,
376 printf ("Processed %d total files in ", add_files_state.processed_files);
377 print_formatted_seconds (elapsed);
378 printf (" (%d files/sec.). \n",
379 (int) (add_files_state.processed_files / elapsed));
380 printf ("Added %d unique messages to the database.\n\n",
381 add_files_state.added_messages);
383 printf ("When new mail is delivered to %s in the future,\n"
384 "run \"notmuch new\" to add it to the database.\n",
388 printf ("Note: At least one error was encountered: %s\n",
389 notmuch_status_to_string (ret));
394 free (mail_directory);
396 notmuch_database_close (notmuch);
402 search_command (int argc, char *argv[])
404 fprintf (stderr, "Error: search is not implemented yet.\n");
409 show_command (int argc, char *argv[])
411 fprintf (stderr, "Error: show is not implemented yet.\n");
416 dump_command (int argc, char *argv[])
419 notmuch_database_t *notmuch;
420 notmuch_query_t *query;
421 notmuch_results_t *results;
422 notmuch_message_t *message;
423 notmuch_tags_t *tags;
427 output = fopen (argv[0], "w");
428 if (output == NULL) {
429 fprintf (stderr, "Error opening %s for writing: %s\n",
430 argv[0], strerror (errno));
438 notmuch = notmuch_database_open (NULL);
439 if (notmuch == NULL) {
444 query = notmuch_query_create (notmuch, "");
446 fprintf (stderr, "Out of memory\n");
451 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
453 for (results = notmuch_query_search (query);
454 notmuch_results_has_more (results);
455 notmuch_results_advance (results))
458 message = notmuch_results_get (results);
461 "%s (", notmuch_message_get_message_id (message));
463 for (tags = notmuch_message_get_tags (message);
464 notmuch_tags_has_more (tags);
465 notmuch_tags_advance (tags))
468 fprintf (output, " ");
470 fprintf (output, "%s", notmuch_tags_get (tags));
475 fprintf (output, ")\n");
477 notmuch_message_destroy (message);
480 notmuch_query_destroy (query);
484 notmuch_database_close (notmuch);
485 if (output != stdout)
492 restore_command (int argc, char *argv[])
495 notmuch_database_t *notmuch;
497 size_t line_size, line_len;
503 input = fopen (argv[0], "r");
505 fprintf (stderr, "Error opening %s for reading: %s\n",
506 argv[0], strerror (errno));
511 printf ("No filename given. Reading dump from stdin.\n");
515 notmuch = notmuch_database_open (NULL);
516 if (notmuch == NULL) {
521 /* Dump output is one line per message. We match a sequence of
522 * non-space characters for the message-id, then one or more
523 * spaces, then a list of space-separated tags as a sequence of
524 * characters within literal '(' and ')'. */
526 "^([^ ]+) \\(([^)]*)\\)$",
529 while ((line_len = getline (&line, &line_size, input)) != -1) {
531 char *message_id, *tags, *tag, *next;
532 notmuch_message_t *message;
533 notmuch_status_t status;
535 chomp_newline (line);
537 rerr = xregexec (®ex, line, 3, match, 0);
538 if (rerr == REG_NOMATCH)
540 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
545 message_id = xstrndup (line + match[1].rm_so,
546 match[1].rm_eo - match[1].rm_so);
547 tags = xstrndup (line + match[2].rm_so,
548 match[2].rm_eo - match[2].rm_so);
552 message = notmuch_database_find_message (notmuch, message_id);
553 if (message == NULL) {
554 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
560 tag = strsep (&next, " ");
564 status = notmuch_message_add_tag (message, tag);
567 "Error applying tag %s to message %s:\n",
569 fprintf (stderr, "%s\n",
570 notmuch_status_to_string (status));
573 fprintf (stderr, "%s ", tag);
578 notmuch_message_destroy (message);
580 fprintf (stderr, ")\n");
592 notmuch_database_close (notmuch);
597 command_t commands[] = {
598 { "setup", setup_command,
599 "Interactively setup notmuch for first use.\n"
600 "\t\tInvoking notmuch with no command argument will run setup if\n"
601 "\t\tthe setup command has not previously been completed." },
602 { "search", search_command,
603 "<search-term> [...]\n\n"
604 "\t\tSearch for threads matching the given search terms.\n"
605 "\t\tOnce we actually implement search we'll document the\n"
606 "\t\tsyntax here." },
607 { "show", show_command,
609 "\t\tShow the thread with the given thread ID (see 'search')." },
610 { "dump", dump_command,
612 "\t\tCreate a plain-text dump of the tags for each message\n"
613 "\t\twriting to the given filename, if any, or to stdout.\n"
614 "\t\tThese tags are the only data in the notmuch database\n"
615 "\t\tthat can't be recreated from the messages themselves.\n"
616 "\t\tThe output of notmuch dump is therefore the only\n"
617 "\t\tcritical thing to backup (and much more friendly to\n"
618 "\t\tincremental backup than the native database files." },
619 { "restore", restore_command,
621 "\t\tRestore the tags from the given dump file (see 'dump')." }
630 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
631 fprintf (stderr, "\n");
632 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
633 fprintf (stderr, "\n");
635 for (i = 0; i < ARRAY_SIZE (commands); i++) {
636 command = &commands[i];
638 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
643 main (int argc, char *argv[])
649 return setup_command (0, NULL);
651 for (i = 0; i < ARRAY_SIZE (commands); i++) {
652 command = &commands[i];
654 if (strcmp (argv[1], command->name) == 0)
655 return (command->function) (argc - 2, &argv[2]);
658 /* Don't complain about "help" being an unknown command when we're
659 about to provide exactly what's wanted anyway. */
660 if (strcmp (argv[1], "help") == 0 ||
661 strcmp (argv[1], "--help") == 0)
663 fprintf (stderr, "The notmuch mail system.\n\n");
665 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);