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 *entry, *e;
136 notmuch_status_t status;
138 dir = opendir (path);
141 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
142 path, strerror (errno));
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",
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);
178 if (S_ISREG (st.st_mode)) {
179 state->processed_files++;
180 status = notmuch_database_add_message (notmuch, next);
181 if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
182 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
184 } else if (status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
185 state->added_messages++;
187 if (state->processed_files % 1000 == 0)
188 add_files_print_progress (state);
189 } else if (S_ISDIR (st.st_mode)) {
190 add_files (notmuch, next, state);
201 /* Recursively count all regular files in path and all sub-direcotries
202 * of path. The result is added to *count (which should be
203 * initialized to zero by the top-level caller before calling
206 count_files (const char *path, int *count)
209 struct dirent *entry, *e;
215 dir = opendir (path);
218 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
219 path, strerror (errno));
223 entry_length = offsetof (struct dirent, d_name) +
224 pathconf (path, _PC_NAME_MAX) + 1;
225 entry = malloc (entry_length);
228 err = readdir_r (dir, entry, &e);
230 fprintf (stderr, "Error reading directory: %s\n",
239 /* Ignore special directories to avoid infinite recursion.
240 * Also ignore the .notmuch directory.
242 /* XXX: Eventually we'll want more sophistication to let the
243 * user specify files to be ignored. */
244 if (strcmp (entry->d_name, ".") == 0 ||
245 strcmp (entry->d_name, "..") == 0 ||
246 strcmp (entry->d_name, ".notmuch") == 0)
251 next = g_strdup_printf ("%s/%s", path, entry->d_name);
255 if (S_ISREG (st.st_mode)) {
257 if (*count % 1000 == 0) {
258 printf ("Found %d files so far.\r", *count);
261 } else if (S_ISDIR (st.st_mode)) {
262 count_files (next, count);
274 setup_command (int argc, char *argv[])
276 notmuch_database_t *notmuch = NULL;
277 char *default_path, *mail_directory = NULL;
280 add_files_state_t add_files_state;
282 struct timeval tv_now;
283 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
285 printf ("Welcome to notmuch!\n\n");
287 printf ("The goal of notmuch is to help you manage and search your collection of\n"
288 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
290 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
291 "(where you already have mail stored and where messages will be delivered\n"
292 "in the future). This directory can contain any number of sub-directories\n"
293 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
294 "archives are perfect). If there are other, non-email files (such as\n"
295 "indexes maintained by other email programs) then notmuch will do its\n"
296 "best to detect those and ignore them.\n\n");
298 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
299 "messages), will not work with notmuch. If that's how your mail is currently\n"
300 "stored, we recommend you first convert it to maildir format with a utility\n"
301 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
302 "once the conversion is complete.\n\n");
305 default_path = notmuch_database_default_path ();
306 printf ("Top-level mail directory [%s]: ", default_path);
309 getline (&mail_directory, &line_size, stdin);
310 chomp_newline (mail_directory);
314 if (mail_directory == NULL || strlen (mail_directory) == 0) {
316 free (mail_directory);
317 mail_directory = default_path;
319 /* XXX: Instead of telling the user to use an environment
320 * variable here, we should really be writing out a configuration
321 * file and loading that on the next run. */
322 if (strcmp (mail_directory, default_path)) {
323 printf ("Note: Since you are not using the default path, you will want to set\n"
324 "the NOTMUCH_BASE environment variable to %s so that\n"
325 "future calls to notmuch commands will know where to find your mail.\n",
327 printf ("For example, if you are using bash for your shell, add:\n\n");
328 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
329 printf ("to your ~/.bashrc file.\n\n");
334 notmuch = notmuch_database_create (mail_directory);
335 if (notmuch == NULL) {
336 fprintf (stderr, "Failed to create new notmuch database at %s\n",
338 ret = NOTMUCH_STATUS_FILE_ERROR;
342 printf ("OK. Let's take a look at the mail we can find in the directory\n");
343 printf ("%s ...\n", mail_directory);
346 count_files (mail_directory, &count);
348 printf ("Found %d total files. That's not much mail.\n\n", count);
350 printf ("Next, we'll inspect the messages and create a database of threads:\n");
352 add_files_state.total_files = count;
353 add_files_state.processed_files = 0;
354 add_files_state.added_messages = 0;
355 gettimeofday (&add_files_state.tv_start, NULL);
357 add_files (notmuch, mail_directory, &add_files_state);
359 gettimeofday (&tv_now, NULL);
360 elapsed = tv_elapsed (add_files_state.tv_start,
362 printf ("Processed %d total files in ", add_files_state.processed_files);
363 print_formatted_seconds (elapsed);
364 printf (" (%d files/sec.). \n",
365 (int) (add_files_state.processed_files / elapsed));
366 printf ("Added %d unique messages to the database.\n\n",
367 add_files_state.added_messages);
369 printf ("When new mail is delivered to %s in the future,\n"
370 "run \"notmuch new\" to add it to the database.\n",
375 free (mail_directory);
377 notmuch_database_close (notmuch);
383 search_command (int argc, char *argv[])
385 fprintf (stderr, "Error: search is not implemented yet.\n");
390 show_command (int argc, char *argv[])
392 fprintf (stderr, "Error: show is not implemented yet.\n");
397 dump_command (int argc, char *argv[])
400 notmuch_database_t *notmuch;
401 notmuch_query_t *query;
402 notmuch_results_t *results;
403 notmuch_message_t *message;
404 notmuch_tags_t *tags;
408 output = fopen (argv[0], "w");
409 if (output == NULL) {
410 fprintf (stderr, "Error opening %s for writing: %s\n",
411 argv[0], strerror (errno));
419 notmuch = notmuch_database_open (NULL);
420 if (notmuch == NULL) {
425 query = notmuch_query_create (notmuch, "");
427 fprintf (stderr, "Out of memory\n");
432 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
434 for (results = notmuch_query_search (query);
435 notmuch_results_has_more (results);
436 notmuch_results_advance (results))
439 message = notmuch_results_get (results);
442 "%s (", notmuch_message_get_message_id (message));
444 for (tags = notmuch_message_get_tags (message);
445 notmuch_tags_has_more (tags);
446 notmuch_tags_advance (tags))
449 fprintf (output, " ");
451 fprintf (output, "%s", notmuch_tags_get (tags));
456 fprintf (output, ")\n");
458 notmuch_message_destroy (message);
461 notmuch_query_destroy (query);
465 notmuch_database_close (notmuch);
466 if (output != stdout)
473 restore_command (int argc, char *argv[])
476 notmuch_database_t *notmuch;
478 size_t line_size, line_len;
484 input = fopen (argv[0], "r");
486 fprintf (stderr, "Error opening %s for reading: %s\n",
487 argv[0], strerror (errno));
492 printf ("No filename given. Reading dump from stdin.\n");
496 notmuch = notmuch_database_open (NULL);
497 if (notmuch == NULL) {
502 /* Dump output is one line per message. We match a sequence of
503 * non-space characters for the message-id, then one or more
504 * spaces, then a list of space-separated tags as a sequence of
505 * characters within literal '(' and ')'. */
507 "^([^ ]+) \\(([^)]*)\\)$",
510 while ((line_len = getline (&line, &line_size, input)) != -1) {
512 char *message_id, *tags, *tag, *next;
513 notmuch_message_t *message;
514 notmuch_status_t status;
516 chomp_newline (line);
518 rerr = xregexec (®ex, line, 3, match, 0);
519 if (rerr == REG_NOMATCH)
521 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
526 message_id = xstrndup (line + match[1].rm_so,
527 match[1].rm_eo - match[1].rm_so);
528 tags = xstrndup (line + match[2].rm_so,
529 match[2].rm_eo - match[2].rm_so);
533 message = notmuch_database_find_message (notmuch, message_id);
534 if (message == NULL) {
535 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
541 tag = strsep (&next, " ");
545 status = notmuch_message_add_tag (message, tag);
548 "Error applying tag %s to message %s:\n",
550 fprintf (stderr, "%s\n",
551 notmuch_status_to_string (status));
554 fprintf (stderr, "%s ", tag);
559 notmuch_message_destroy (message);
561 fprintf (stderr, ")\n");
573 notmuch_database_close (notmuch);
578 command_t commands[] = {
579 { "setup", setup_command,
580 "Interactively setup notmuch for first use.\n"
581 "\t\tInvoking notmuch with no command argument will run setup if\n"
582 "\t\tthe setup command has not previously been completed." },
583 { "search", search_command,
584 "<search-term> [...]\n\n"
585 "\t\tSearch for threads matching the given search terms.\n"
586 "\t\tOnce we actually implement search we'll document the\n"
587 "\t\tsyntax here." },
588 { "show", show_command,
590 "\t\tShow the thread with the given thread ID (see 'search')." },
591 { "dump", dump_command,
593 "\t\tCreate a plain-text dump of the tags for each message\n"
594 "\t\twriting to the given filename, if any, or to stdout.\n"
595 "\t\tThese tags are the only data in the notmuch database\n"
596 "\t\tthat can't be recreated from the messages themselves.\n"
597 "\t\tThe output of notmuch dump is therefore the only\n"
598 "\t\tcritical thing to backup (and much more friendly to\n"
599 "\t\tincremental backup than the native database files." },
600 { "restore", restore_command,
602 "\t\tRestore the tags from the given dump file (see 'dump')." }
611 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
612 fprintf (stderr, "\n");
613 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
614 fprintf (stderr, "\n");
616 for (i = 0; i < ARRAY_SIZE (commands); i++) {
617 command = &commands[i];
619 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
624 main (int argc, char *argv[])
630 return setup_command (0, NULL);
632 for (i = 0; i < ARRAY_SIZE (commands); i++) {
633 command = &commands[i];
635 if (strcmp (argv[1], command->name) == 0)
636 return (command->function) (argc - 2, &argv[2]);
639 /* Don't complain about "help" being an unknown command when we're
640 about to provide exactly what's wanted anyway. */
641 if (strcmp (argv[1], "help") == 0 ||
642 strcmp (argv[1], "--help") == 0)
644 fprintf (stderr, "The notmuch mail system.\n\n");
646 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);