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;
59 struct timeval tv_start;
63 chomp_newline (char *str)
65 if (str && str[strlen(str)-1] == '\n')
66 str[strlen(str)-1] = '\0';
69 /* Compute the number of seconds elapsed from start to end. */
71 tv_elapsed (struct timeval start, struct timeval end)
73 return ((end.tv_sec - start.tv_sec) +
74 (end.tv_usec - start.tv_usec) / 1e6);
78 print_formatted_seconds (double seconds)
84 hours = (int) seconds / 3600;
85 printf ("%dh ", hours);
86 seconds -= hours * 3600;
90 minutes = (int) seconds / 60;
91 printf ("%dm ", minutes);
92 seconds -= minutes * 60;
95 printf ("%02ds", (int) seconds);
99 add_files_print_progress (add_files_state_t *state)
101 struct timeval tv_now;
102 double elapsed_overall, rate_overall;
104 gettimeofday (&tv_now, NULL);
106 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
107 rate_overall = (state->count) / elapsed_overall;
109 printf ("Added %d of %d messages (",
110 state->count, state->total_messages);
111 print_formatted_seconds ((state->total_messages - state->count) /
113 printf (" remaining). \r");
118 /* Recursively find all regular files in 'path' and add them to the
121 add_files (notmuch_database_t *notmuch, const char *path,
122 add_files_state_t *state)
125 struct dirent *entry, *e;
130 notmuch_status_t status;
132 dir = opendir (path);
135 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
136 path, strerror (errno));
140 entry_length = offsetof (struct dirent, d_name) +
141 pathconf (path, _PC_NAME_MAX) + 1;
142 entry = malloc (entry_length);
145 err = readdir_r (dir, entry, &e);
147 fprintf (stderr, "Error reading directory: %s\n",
156 /* Ignore special directories to avoid infinite recursion.
157 * Also ignore the .notmuch directory.
159 /* XXX: Eventually we'll want more sophistication to let the
160 * user specify files to be ignored. */
161 if (strcmp (entry->d_name, ".") == 0 ||
162 strcmp (entry->d_name, "..") == 0 ||
163 strcmp (entry->d_name, ".notmuch") ==0)
168 next = g_strdup_printf ("%s/%s", path, entry->d_name);
172 if (S_ISREG (st.st_mode)) {
173 status = notmuch_database_add_message (notmuch, next);
174 if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
175 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
180 if (state->count % 1000 == 0)
181 add_files_print_progress (state);
182 } else if (S_ISDIR (st.st_mode)) {
183 add_files (notmuch, next, state);
194 /* Recursively count all regular files in path and all sub-direcotries
195 * of path. The result is added to *count (which should be
196 * initialized to zero by the top-level caller before calling
199 count_files (const char *path, int *count)
202 struct dirent *entry, *e;
208 dir = opendir (path);
211 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
212 path, strerror (errno));
216 entry_length = offsetof (struct dirent, d_name) +
217 pathconf (path, _PC_NAME_MAX) + 1;
218 entry = malloc (entry_length);
221 err = readdir_r (dir, entry, &e);
223 fprintf (stderr, "Error reading directory: %s\n",
232 /* Ignore special directories to avoid infinite recursion.
233 * Also ignore the .notmuch directory.
235 /* XXX: Eventually we'll want more sophistication to let the
236 * user specify files to be ignored. */
237 if (strcmp (entry->d_name, ".") == 0 ||
238 strcmp (entry->d_name, "..") == 0 ||
239 strcmp (entry->d_name, ".notmuch") == 0)
244 next = g_strdup_printf ("%s/%s", path, entry->d_name);
248 if (S_ISREG (st.st_mode)) {
250 if (*count % 1000 == 0) {
251 printf ("Found %d files so far.\r", *count);
254 } else if (S_ISDIR (st.st_mode)) {
255 count_files (next, count);
267 setup_command (int argc, char *argv[])
269 notmuch_database_t *notmuch = NULL;
270 char *default_path, *mail_directory = NULL;
273 add_files_state_t add_files_state;
275 struct timeval tv_now;
276 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
278 printf ("Welcome to notmuch!\n\n");
280 printf ("The goal of notmuch is to help you manage and search your collection of\n"
281 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
283 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
284 "(where you already have mail stored and where messages will be delivered\n"
285 "in the future). This directory can contain any number of sub-directories\n"
286 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
287 "archives are perfect). If there are other, non-email files (such as\n"
288 "indexes maintained by other email programs) then notmuch will do its\n"
289 "best to detect those and ignore them.\n\n");
291 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
292 "messages), will not work with notmuch. If that's how your mail is currently\n"
293 "stored, we recommend you first convert it to maildir format with a utility\n"
294 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
295 "once the conversion is complete.\n\n");
298 default_path = notmuch_database_default_path ();
299 printf ("Top-level mail directory [%s]: ", default_path);
302 getline (&mail_directory, &line_size, stdin);
303 chomp_newline (mail_directory);
307 if (mail_directory == NULL || strlen (mail_directory) == 0) {
309 free (mail_directory);
310 mail_directory = default_path;
312 /* XXX: Instead of telling the user to use an environment
313 * variable here, we should really be writing out a configuration
314 * file and loading that on the next run. */
315 if (strcmp (mail_directory, default_path)) {
316 printf ("Note: Since you are not using the default path, you will want to set\n"
317 "the NOTMUCH_BASE environment variable to %s so that\n"
318 "future calls to notmuch commands will know where to find your mail.\n",
320 printf ("For example, if you are using bash for your shell, add:\n\n");
321 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
322 printf ("to your ~/.bashrc file.\n\n");
327 notmuch = notmuch_database_create (mail_directory);
328 if (notmuch == NULL) {
329 fprintf (stderr, "Failed to create new notmuch database at %s\n",
331 ret = NOTMUCH_STATUS_FILE_ERROR;
335 printf ("OK. Let's take a look at the mail we can find in the directory\n");
336 printf ("%s ...\n", mail_directory);
339 count_files (mail_directory, &count);
341 printf ("Found %d total files. That's not much mail.\n\n", count);
343 printf ("Next, we'll inspect the messages and create a database of threads:\n");
345 add_files_state.total_messages = count;
346 add_files_state.count = 0;
347 gettimeofday (&add_files_state.tv_start, NULL);
349 add_files (notmuch, mail_directory, &add_files_state);
351 gettimeofday (&tv_now, NULL);
352 elapsed = tv_elapsed (add_files_state.tv_start,
354 printf ("Added %d total messages in ", add_files_state.count);
355 print_formatted_seconds (elapsed);
356 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
360 free (mail_directory);
362 notmuch_database_close (notmuch);
368 search_command (int argc, char *argv[])
370 fprintf (stderr, "Error: search is not implemented yet.\n");
375 show_command (int argc, char *argv[])
377 fprintf (stderr, "Error: show is not implemented yet.\n");
382 dump_command (int argc, char *argv[])
385 notmuch_database_t *notmuch = NULL;
386 notmuch_query_t *query;
387 notmuch_results_t *results;
388 notmuch_message_t *message;
389 notmuch_tags_t *tags;
393 output = fopen (argv[0], "w");
394 if (output == NULL) {
395 fprintf (stderr, "Error opening %s for writing: %s\n",
396 argv[0], strerror (errno));
404 notmuch = notmuch_database_open (NULL);
405 if (notmuch == NULL) {
410 query = notmuch_query_create (notmuch, "");
412 fprintf (stderr, "Out of memory\n");
417 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
419 for (results = notmuch_query_search (query);
420 notmuch_results_has_more (results);
421 notmuch_results_advance (results))
424 message = notmuch_results_get (results);
427 "%s (", notmuch_message_get_message_id (message));
429 for (tags = notmuch_message_get_tags (message);
430 notmuch_tags_has_more (tags);
431 notmuch_tags_advance (tags))
434 fprintf (output, " ");
436 fprintf (output, "%s", notmuch_tags_get (tags));
441 fprintf (output, ")\n");
443 notmuch_message_destroy (message);
446 notmuch_query_destroy (query);
450 notmuch_database_close (notmuch);
451 if (output != stdout)
458 restore_command (int argc, char *argv[])
461 notmuch_database_t *notmuch = NULL;
463 size_t line_size, line_len;
469 input = fopen (argv[0], "r");
471 fprintf (stderr, "Error opening %s for reading: %s\n",
472 argv[0], strerror (errno));
477 printf ("No filename given. Reading dump from stdin.\n");
481 notmuch = notmuch_database_open (NULL);
482 if (notmuch == NULL) {
487 /* Dump output is one line per message. We match a sequence of
488 * non-space characters for the message-id, then one or more
489 * spaces, then a list of space-separated tags as a sequence of
490 * characters within literal '(' and ')'. */
492 "^([^ ]+) \\(([^)]*)\\)$",
495 while ((line_len = getline (&line, &line_size, input)) != -1) {
497 char *message_id, *tags, *tag, *next;
498 notmuch_message_t *message;
499 notmuch_status_t status;
501 chomp_newline (line);
503 rerr = xregexec (®ex, line, 3, match, 0);
504 if (rerr == REG_NOMATCH)
506 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
511 message_id = xstrndup (line + match[1].rm_so,
512 match[1].rm_eo - match[1].rm_so);
513 tags = xstrndup (line + match[2].rm_so,
514 match[2].rm_eo - match[2].rm_so);
518 message = notmuch_database_find_message (notmuch, message_id);
519 if (message == NULL) {
520 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
526 tag = strsep (&next, " ");
530 status = notmuch_message_add_tag (message, tag);
533 "Error applying tag %s to message %s:\n",
535 fprintf (stderr, "%s\n",
536 notmuch_status_to_string (status));
539 fprintf (stderr, "%s ", tag);
544 notmuch_message_destroy (message);
546 fprintf (stderr, ")\n");
558 notmuch_database_close (notmuch);
563 command_t commands[] = {
564 { "setup", setup_command,
565 "Interactively setup notmuch for first use.\n"
566 "\t\tInvoking notmuch with no command argument will run setup if\n"
567 "\t\tthe setup command has not previously been completed." },
568 { "search", search_command,
569 "<search-term> [...]\n\n"
570 "\t\tSearch for threads matching the given search terms.\n"
571 "\t\tOnce we actually implement search we'll document the\n"
572 "\t\tsyntax here." },
573 { "show", show_command,
575 "\t\tShow the thread with the given thread ID (see 'search')." },
576 { "dump", dump_command,
578 "\t\tCreate a plain-text dump of the tags for each message\n"
579 "\t\twriting to the given filename, if any, or to stdout.\n"
580 "\t\tThese tags are the only data in the notmuch database\n"
581 "\t\tthat can't be recreated from the messages themselves.\n"
582 "\t\tThe output of notmuch dump is therefore the only\n"
583 "\t\tcritical thing to backup (and much more friendly to\n"
584 "\t\tincremental backup than the native database files." },
585 { "restore", restore_command,
587 "\t\tRestore the tags from the given dump file (see 'dump')." }
596 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
597 fprintf (stderr, "\n");
598 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
599 fprintf (stderr, "\n");
601 for (i = 0; i < ARRAY_SIZE (commands); i++) {
602 command = &commands[i];
604 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
609 main (int argc, char *argv[])
615 return setup_command (0, NULL);
617 for (i = 0; i < ARRAY_SIZE (commands); i++) {
618 command = &commands[i];
620 if (strcmp (argv[1], command->name) == 0)
621 return (command->function) (argc - 2, &argv[2]);
624 /* Don't complain about "help" being an unknown command when we're
625 about to provide exactly what's wanted anyway. */
626 if (strcmp (argv[1], "help") == 0 ||
627 strcmp (argv[1], "--help") == 0)
629 fprintf (stderr, "The notmuch mail system.\n\n");
631 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);