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>
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);
85 struct timeval tv_start;
86 struct timeval tv_last;
89 /* Compute the number of seconds elapsed from start to end. */
91 tv_elapsed (struct timeval start, struct timeval end)
93 return ((end.tv_sec - start.tv_sec) +
94 (end.tv_usec - start.tv_usec) / 1e6);
98 print_formatted_seconds (double seconds)
103 if (seconds > 3600) {
104 hours = (int) seconds / 3600;
105 printf ("%d:", hours);
106 seconds -= hours * 3600;
110 minutes = (int) seconds / 60;
114 printf ("%02d:", minutes);
115 seconds -= minutes * 60;
117 printf ("%02d", (int) seconds);
121 add_files_print_progress (add_files_state_t *state)
123 struct timeval tv_now;
124 double ratio_complete;
125 double elapsed_current, rate_current;
126 double elapsed_overall;
128 gettimeofday (&tv_now, NULL);
130 ratio_complete = (double) state->count / state->messages_total;
131 elapsed_current = tv_elapsed (state->tv_last, tv_now);
132 rate_current = (state->count - state->count_last) / elapsed_current;
133 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
135 printf ("Added %d messages at %d messages/sec. ",
136 state->count, (int) rate_current);
137 print_formatted_seconds (elapsed_overall);
139 print_formatted_seconds (elapsed_overall / ratio_complete);
140 printf (" elapsed (%.2f%%). \r", 100 * ratio_complete);
144 state->tv_last = tv_now;
145 state->count_last = state->count;
148 /* Recursively find all regular files in 'path' and add them to the
151 add_files (notmuch_database_t *notmuch, const char *path,
152 add_files_state_t *state)
155 struct dirent *entry, *e;
161 dir = opendir (path);
164 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
165 path, strerror (errno));
169 entry_length = offsetof (struct dirent, d_name) +
170 pathconf (path, _PC_NAME_MAX) + 1;
171 entry = malloc (entry_length);
174 err = readdir_r (dir, entry, &e);
176 fprintf (stderr, "Error reading directory: %s\n",
185 /* Ignore special directories to avoid infinite recursion.
186 * Also ignore the .notmuch directory.
188 /* XXX: Eventually we'll want more sophistication to let the
189 * user specify files to be ignored. */
190 if (strcmp (entry->d_name, ".") == 0 ||
191 strcmp (entry->d_name, "..") == 0 ||
192 strcmp (entry->d_name, ".notmuch") ==0)
197 next = g_strdup_printf ("%s/%s", path, entry->d_name);
201 if (S_ISREG (st.st_mode)) {
202 notmuch_database_add_message (notmuch, next);
204 if (state->count % 1000 == 0)
205 add_files_print_progress (state);
206 } else if (S_ISDIR (st.st_mode)) {
207 add_files (notmuch, next, state);
218 /* Recursively count all regular files in path and all sub-direcotries
219 * of path. The result is added to *count (which should be
220 * initialized to zero by the top-level caller before calling
223 count_files (const char *path, int *count)
226 struct dirent *entry, *e;
232 dir = opendir (path);
235 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
236 path, strerror (errno));
240 entry_length = offsetof (struct dirent, d_name) +
241 pathconf (path, _PC_NAME_MAX) + 1;
242 entry = malloc (entry_length);
245 err = readdir_r (dir, entry, &e);
247 fprintf (stderr, "Error reading directory: %s\n",
256 /* Skip these special directories to avoid infinite recursion. */
257 if (strcmp (entry->d_name, ".") == 0 ||
258 strcmp (entry->d_name, "..") == 0)
263 next = g_strdup_printf ("%s/%s", path, entry->d_name);
267 if (S_ISREG (st.st_mode)) {
269 if (*count % 1000 == 0) {
270 printf ("Found %d files so far.\r", *count);
273 } else if (S_ISDIR (st.st_mode)) {
274 count_files (next, count);
286 setup_command (int argc, char *argv[])
288 notmuch_database_t *notmuch;
289 char *mail_directory;
291 add_files_state_t add_files_state;
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 "but the only files it contains should be individual email messages.\n"
303 "Either maildir or mh format directories are fine, but you will want to\n"
304 "move away any auxiliary files maintained by other email programs.\n\n");
306 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
307 "messages), will not work with notmuch. If that's how your mail is currently\n"
308 "stored, we recommend you first convert it to maildir format with a utility\n"
309 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
310 "once the conversion is complete.\n\n");
312 printf ("Top-level mail directory [~/mail]: ");
315 mail_directory = read_line ();
317 if (mail_directory == NULL || strlen (mail_directory) == 0) {
321 free (mail_directory);
323 home = getenv ("HOME");
325 fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
326 fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
330 mail_directory = g_strdup_printf ("%s/mail", home);
333 notmuch = notmuch_database_create (mail_directory);
334 if (notmuch == NULL) {
335 fprintf (stderr, "Failed to create new notmuch database at %s\n",
337 free (mail_directory);
341 printf ("OK. Let's take a look at the mail we can find in the directory\n");
342 printf ("%s ...\n", mail_directory);
345 count_files (mail_directory, &count);
347 printf ("Found %d total files. That's not much mail.\n\n", count);
349 printf ("Next, we'll inspect the messages and create a database of threads:\n");
351 add_files_state.messages_total = count;
352 add_files_state.count = 0;
353 add_files_state.count_last = 0;
354 gettimeofday (&add_files_state.tv_start, NULL);
355 add_files_state.tv_last = add_files_state.tv_start;
357 add_files (notmuch, mail_directory, &add_files_state);
359 gettimeofday (&add_files_state.tv_last, NULL);
360 elapsed = tv_elapsed (add_files_state.tv_start,
361 add_files_state.tv_last);
362 printf ("Added %d total messages in ", add_files_state.count);
363 print_formatted_seconds (elapsed);
364 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
366 notmuch_database_close (notmuch);
368 free (mail_directory);
374 search_command (int argc, char *argv[])
376 fprintf (stderr, "Error: search is not implemented yet.\n");
381 show_command (int argc, char *argv[])
383 fprintf (stderr, "Error: show-thread is not implemented yet.\n");
387 command_t commands[] = {
388 { "setup", setup_command,
389 "Interactively setup notmuch for first use (no arguments).\n"
390 "\t\tInvoking notmuch with no command argument will run setup if\n"
391 "\t\the setup command has not previously been completed." },
392 { "search", search_command,
393 "Search for threads matching the given search terms." },
394 { "show", show_command,
395 "Show the thread with the given thread ID (see 'search')." }
404 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
405 fprintf (stderr, "\n");
406 fprintf (stderr, "Where <command> is one of the following:\n");
407 fprintf (stderr, "\n");
409 for (i = 0; i < ARRAY_SIZE (commands); i++) {
410 command = &commands[i];
412 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
417 main (int argc, char *argv[])
423 return setup_command (0, NULL);
425 for (i = 0; i < ARRAY_SIZE (commands); i++) {
426 command = &commands[i];
428 if (strcmp (argv[1], command->name) == 0)
429 return (command->function) (argc - 2, &argv[2]);
432 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);