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>
26 #include <sys/types.h>
33 #include <glib.h> /* GIOChannel */
35 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
37 typedef int (*command_function_t) (int argc, char *argv[]);
39 typedef struct command {
41 command_function_t function;
45 /* Read a line from stdin, without any line-terminator character. The
46 * return value is a newly allocated string. The caller should free()
47 * the string when finished with it.
49 * This function returns NULL if EOF is encountered before any
50 * characters are input (otherwise it returns those characters).
57 GIOStatus g_io_status;
60 GIOChannel *channel = g_io_channel_unix_new (fileno (stdin));
62 g_io_status = g_io_channel_read_line (channel, &result,
63 &length, NULL, &error);
65 if (g_io_status == EOF)
68 if (g_io_status != G_IO_STATUS_NORMAL) {
69 fprintf(stderr, "Read error: %s\n", error->message);
73 if (length && result[length - 1] == '\n')
74 result[length - 1] = '\0';
77 g_io_channel_unref (channel);
84 struct timeval tv_start;
87 /* Compute the number of seconds elapsed from start to end. */
89 tv_elapsed (struct timeval start, struct timeval end)
91 return ((end.tv_sec - start.tv_sec) +
92 (end.tv_usec - start.tv_usec) / 1e6);
96 print_formatted_seconds (double seconds)
101 if (seconds > 3600) {
102 hours = (int) seconds / 3600;
103 printf ("%dh ", hours);
104 seconds -= hours * 3600;
108 minutes = (int) seconds / 60;
109 printf ("%dm ", minutes);
110 seconds -= minutes * 60;
113 printf ("%02ds", (int) seconds);
117 add_files_print_progress (add_files_state_t *state)
119 struct timeval tv_now;
120 double elapsed_overall, rate_overall;
122 gettimeofday (&tv_now, NULL);
124 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
125 rate_overall = (state->count) / elapsed_overall;
127 printf ("Added %d of %d messages (",
128 state->count, state->total_messages);
129 print_formatted_seconds ((state->total_messages - state->count) /
131 printf (" remaining).\r");
136 /* Recursively find all regular files in 'path' and add them to the
139 add_files (notmuch_database_t *notmuch, const char *path,
140 add_files_state_t *state)
143 struct dirent *entry, *e;
148 notmuch_status_t status;
150 dir = opendir (path);
153 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
154 path, strerror (errno));
158 entry_length = offsetof (struct dirent, d_name) +
159 pathconf (path, _PC_NAME_MAX) + 1;
160 entry = malloc (entry_length);
163 err = readdir_r (dir, entry, &e);
165 fprintf (stderr, "Error reading directory: %s\n",
174 /* Ignore special directories to avoid infinite recursion.
175 * Also ignore the .notmuch directory.
177 /* XXX: Eventually we'll want more sophistication to let the
178 * user specify files to be ignored. */
179 if (strcmp (entry->d_name, ".") == 0 ||
180 strcmp (entry->d_name, "..") == 0 ||
181 strcmp (entry->d_name, ".notmuch") ==0)
186 next = g_strdup_printf ("%s/%s", path, entry->d_name);
190 if (S_ISREG (st.st_mode)) {
191 status = notmuch_database_add_message (notmuch, next);
192 if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
193 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
198 if (state->count % 1000 == 0)
199 add_files_print_progress (state);
200 } else if (S_ISDIR (st.st_mode)) {
201 add_files (notmuch, next, state);
212 /* Recursively count all regular files in path and all sub-direcotries
213 * of path. The result is added to *count (which should be
214 * initialized to zero by the top-level caller before calling
217 count_files (const char *path, int *count)
220 struct dirent *entry, *e;
226 dir = opendir (path);
229 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
230 path, strerror (errno));
234 entry_length = offsetof (struct dirent, d_name) +
235 pathconf (path, _PC_NAME_MAX) + 1;
236 entry = malloc (entry_length);
239 err = readdir_r (dir, entry, &e);
241 fprintf (stderr, "Error reading directory: %s\n",
250 /* Ignore special directories to avoid infinite recursion.
251 * Also ignore the .notmuch directory.
253 /* XXX: Eventually we'll want more sophistication to let the
254 * user specify files to be ignored. */
255 if (strcmp (entry->d_name, ".") == 0 ||
256 strcmp (entry->d_name, "..") == 0 ||
257 strcmp (entry->d_name, ".notmuch") == 0)
262 next = g_strdup_printf ("%s/%s", path, entry->d_name);
266 if (S_ISREG (st.st_mode)) {
268 if (*count % 1000 == 0) {
269 printf ("Found %d files so far.\r", *count);
272 } else if (S_ISDIR (st.st_mode)) {
273 count_files (next, count);
285 setup_command (int argc, char *argv[])
287 notmuch_database_t *notmuch;
288 char *mail_directory, *default_path;
290 add_files_state_t add_files_state;
292 struct timeval tv_now;
294 printf ("Welcome to notmuch!\n\n");
296 printf ("The goal of notmuch is to help you manage and search your collection of\n"
297 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
299 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
300 "(where you already have mail stored and where messages will be delivered\n"
301 "in the future). This directory can contain any number of sub-directories\n"
302 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
303 "archives are perfect). If there are other, non-email files (such as\n"
304 "indexes maintained by other email programs) then notmuch will do its\n"
305 "best to detect those and ignore them.\n\n");
307 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
308 "messages), will not work with notmuch. If that's how your mail is currently\n"
309 "stored, we recommend you first convert it to maildir format with a utility\n"
310 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
311 "once the conversion is complete.\n\n");
314 default_path = notmuch_database_default_path ();
315 printf ("Top-level mail directory [%s]: ", default_path);
318 mail_directory = read_line ();
321 if (mail_directory == NULL || strlen (mail_directory) == 0) {
323 free (mail_directory);
324 mail_directory = default_path;
326 /* XXX: Instead of telling the user to use an environment
327 * variable here, we should really be writing out a configuration
328 * file and loading that on the next run. */
329 if (strcmp (mail_directory, default_path)) {
330 printf ("Note: Since you are not using the default path, you will want to set\n"
331 "the NOTMUCH_BASE environment variable to %s so that\n"
332 "future calls to notmuch commands will know where to find your mail.\n",
334 printf ("For example, if you are using bash for your shell, add:\n\n");
335 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
336 printf ("to your ~/.bashrc file.\n\n");
341 notmuch = notmuch_database_create (mail_directory);
342 if (notmuch == NULL) {
343 fprintf (stderr, "Failed to create new notmuch database at %s\n",
345 free (mail_directory);
349 printf ("OK. Let's take a look at the mail we can find in the directory\n");
350 printf ("%s ...\n", mail_directory);
353 count_files (mail_directory, &count);
355 printf ("Found %d total files. That's not much mail.\n\n", count);
357 printf ("Next, we'll inspect the messages and create a database of threads:\n");
359 add_files_state.total_messages = count;
360 add_files_state.count = 0;
361 gettimeofday (&add_files_state.tv_start, NULL);
363 add_files (notmuch, mail_directory, &add_files_state);
365 gettimeofday (&tv_now, NULL);
366 elapsed = tv_elapsed (add_files_state.tv_start,
368 printf ("Added %d total messages in ", add_files_state.count);
369 print_formatted_seconds (elapsed);
370 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
372 notmuch_database_close (notmuch);
374 free (mail_directory);
380 search_command (int argc, char *argv[])
382 fprintf (stderr, "Error: search is not implemented yet.\n");
387 show_command (int argc, char *argv[])
389 fprintf (stderr, "Error: show is not implemented yet.\n");
394 dump_command (int argc, char *argv[])
396 fprintf (stderr, "Error: dump is not implemented yet.\n");
401 restore_command (int argc, char *argv[])
403 fprintf (stderr, "Error: restore is not implemented yet.\n");
407 command_t commands[] = {
408 { "setup", setup_command,
409 "Interactively setup notmuch for first use.\n"
410 "\t\tInvoking notmuch with no command argument will run setup if\n"
411 "\t\tthe setup command has not previously been completed." },
412 { "search", search_command,
413 "<search-term> [...]\n\n"
414 "\t\tSearch for threads matching the given search terms.\n"
415 "\t\tOnce we actually implement search we'll document the\n"
416 "\t\tsyntax here." },
417 { "show", show_command,
419 "\t\tShow the thread with the given thread ID (see 'search')." },
420 { "dump", dump_command,
422 "\t\tCreate a plain-text dump of the tags for each message\n"
423 "\t\twriting to the given filename, if any, or to stdout.\n"
424 "\t\tThese tags are the only data in the notmuch database\n"
425 "\t\tthat can't be recreated from the messages themselves.\n"
426 "\t\tThe output of notmuch dump is therefore the only\n"
427 "\t\tcritical thing to backup (and much more friendly to\n"
428 "\t\tincremental backup than the native database files." },
429 { "restore", restore_command,
431 "\t\tRestore the tags from the given dump file (see 'dump')." }
440 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
441 fprintf (stderr, "\n");
442 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
443 fprintf (stderr, "\n");
445 for (i = 0; i < ARRAY_SIZE (commands); i++) {
446 command = &commands[i];
448 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
453 main (int argc, char *argv[])
459 return setup_command (0, NULL);
461 for (i = 0; i < ARRAY_SIZE (commands); i++) {
462 command = &commands[i];
464 if (strcmp (argv[1], command->name) == 0)
465 return (command->function) (argc - 2, &argv[2]);
468 /* Don't complain about "help" being an unknown command when we're
469 about to provide exactly what's wanted anyway. */
470 if (strcmp (argv[1], "help") == 0 ||
471 strcmp (argv[1], "--help") == 0)
473 fprintf (stderr, "The notmuch mail system.\n\n");
475 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);