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>
24 #define _GNU_SOURCE /* for getline */
27 /* This is separate from notmuch-private.h because we're trying to
28 * keep notmuch.c from looking into any internals, (which helps us
29 * 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;
270 char *mail_directory, *default_path;
273 add_files_state_t add_files_state;
275 struct timeval tv_now;
277 printf ("Welcome to notmuch!\n\n");
279 printf ("The goal of notmuch is to help you manage and search your collection of\n"
280 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
282 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
283 "(where you already have mail stored and where messages will be delivered\n"
284 "in the future). This directory can contain any number of sub-directories\n"
285 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
286 "archives are perfect). If there are other, non-email files (such as\n"
287 "indexes maintained by other email programs) then notmuch will do its\n"
288 "best to detect those and ignore them.\n\n");
290 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
291 "messages), will not work with notmuch. If that's how your mail is currently\n"
292 "stored, we recommend you first convert it to maildir format with a utility\n"
293 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
294 "once the conversion is complete.\n\n");
297 default_path = notmuch_database_default_path ();
298 printf ("Top-level mail directory [%s]: ", default_path);
301 mail_directory = NULL;
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 free (mail_directory);
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));
358 notmuch_database_close (notmuch);
360 free (mail_directory);
366 search_command (int argc, char *argv[])
368 fprintf (stderr, "Error: search is not implemented yet.\n");
373 show_command (int argc, char *argv[])
375 fprintf (stderr, "Error: show is not implemented yet.\n");
380 dump_command (int argc, char *argv[])
383 notmuch_database_t *notmuch = NULL;
384 notmuch_query_t *query;
385 notmuch_results_t *results;
386 notmuch_message_t *message;
387 notmuch_tags_t *tags;
391 output = fopen (argv[0], "w");
392 if (output == NULL) {
393 fprintf (stderr, "Error opening %s for writing: %s\n",
394 argv[0], strerror (errno));
402 notmuch = notmuch_database_open (NULL);
403 if (notmuch == NULL) {
408 query = notmuch_query_create (notmuch, "");
410 fprintf (stderr, "Out of memory\n");
415 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
417 for (results = notmuch_query_search (query);
418 notmuch_results_has_more (results);
419 notmuch_results_advance (results))
422 message = notmuch_results_get (results);
425 "%s (", notmuch_message_get_message_id (message));
427 for (tags = notmuch_message_get_tags (message);
428 notmuch_tags_has_more (tags);
429 notmuch_tags_advance (tags))
432 fprintf (output, " ");
434 fprintf (output, "%s", notmuch_tags_get (tags));
439 fprintf (output, ")\n");
441 notmuch_message_destroy (message);
444 notmuch_query_destroy (query);
448 notmuch_database_close (notmuch);
449 if (output != stdout)
456 restore_command (int argc, char *argv[])
459 notmuch_database_t *notmuch = NULL;
461 size_t line_size, line_len;
467 input = fopen (argv[0], "r");
469 fprintf (stderr, "Error opening %s for reading: %s\n",
470 argv[0], strerror (errno));
475 printf ("No filename given. Reading dump from stdin.\n");
479 notmuch = notmuch_database_open (NULL);
480 if (notmuch == NULL) {
485 /* Dump output is one line per message. We match a sequence of
486 * non-space characters for the message-id, then one or more
487 * spaces, then a list of space-separated tags as a sequence of
488 * characters within literal '(' and ')'. */
490 "^([^ ]+) \\(([^)]*)\\)$",
493 while ((line_len = getline (&line, &line_size, input)) != -1) {
495 char *message_id, *tags, *tag, *next;
496 notmuch_message_t *message;
497 notmuch_status_t status;
499 chomp_newline (line);
501 rerr = xregexec (®ex, line, 3, match, 0);
502 if (rerr == REG_NOMATCH)
504 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
509 message_id = xstrndup (line + match[1].rm_so,
510 match[1].rm_eo - match[1].rm_so);
511 tags = xstrndup (line + match[2].rm_so,
512 match[2].rm_eo - match[2].rm_so);
516 message = notmuch_database_find_message (notmuch, message_id);
517 if (message == NULL) {
518 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
524 tag = strsep (&next, " ");
528 status = notmuch_message_add_tag (message, tag);
531 "Error applying tag %s to message %s:\n",
533 fprintf (stderr, "%s\n",
534 notmuch_status_to_string (status));
537 fprintf (stderr, "%s ", tag);
542 notmuch_message_destroy (message);
544 fprintf (stderr, ")\n");
556 notmuch_database_close (notmuch);
561 command_t commands[] = {
562 { "setup", setup_command,
563 "Interactively setup notmuch for first use.\n"
564 "\t\tInvoking notmuch with no command argument will run setup if\n"
565 "\t\tthe setup command has not previously been completed." },
566 { "search", search_command,
567 "<search-term> [...]\n\n"
568 "\t\tSearch for threads matching the given search terms.\n"
569 "\t\tOnce we actually implement search we'll document the\n"
570 "\t\tsyntax here." },
571 { "show", show_command,
573 "\t\tShow the thread with the given thread ID (see 'search')." },
574 { "dump", dump_command,
576 "\t\tCreate a plain-text dump of the tags for each message\n"
577 "\t\twriting to the given filename, if any, or to stdout.\n"
578 "\t\tThese tags are the only data in the notmuch database\n"
579 "\t\tthat can't be recreated from the messages themselves.\n"
580 "\t\tThe output of notmuch dump is therefore the only\n"
581 "\t\tcritical thing to backup (and much more friendly to\n"
582 "\t\tincremental backup than the native database files." },
583 { "restore", restore_command,
585 "\t\tRestore the tags from the given dump file (see 'dump')." }
594 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
595 fprintf (stderr, "\n");
596 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
597 fprintf (stderr, "\n");
599 for (i = 0; i < ARRAY_SIZE (commands); i++) {
600 command = &commands[i];
602 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
607 main (int argc, char *argv[])
613 return setup_command (0, NULL);
615 for (i = 0; i < ARRAY_SIZE (commands); i++) {
616 command = &commands[i];
618 if (strcmp (argv[1], command->name) == 0)
619 return (command->function) (argc - 2, &argv[2]);
622 /* Don't complain about "help" being an unknown command when we're
623 about to provide exactly what's wanted anyway. */
624 if (strcmp (argv[1], "help") == 0 ||
625 strcmp (argv[1], "--help") == 0)
627 fprintf (stderr, "The notmuch mail system.\n\n");
629 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);