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);
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 /* Ignore special directories to avoid infinite recursion.
257 * Also ignore the .notmuch directory.
259 /* XXX: Eventually we'll want more sophistication to let the
260 * user specify files to be ignored. */
261 if (strcmp (entry->d_name, ".") == 0 ||
262 strcmp (entry->d_name, "..") == 0 ||
263 strcmp (entry->d_name, ".notmuch") == 0)
268 next = g_strdup_printf ("%s/%s", path, entry->d_name);
272 if (S_ISREG (st.st_mode)) {
274 if (*count % 1000 == 0) {
275 printf ("Found %d files so far.\r", *count);
278 } else if (S_ISDIR (st.st_mode)) {
279 count_files (next, count);
291 setup_command (int argc, char *argv[])
293 notmuch_database_t *notmuch;
294 char *mail_directory;
296 add_files_state_t add_files_state;
299 printf ("Welcome to notmuch!\n\n");
301 printf ("The goal of notmuch is to help you manage and search your collection of\n"
302 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
304 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
305 "(where you already have mail stored and where messages will be delivered\n"
306 "in the future). This directory can contain any number of sub-directories\n"
307 "but the only files it contains should be individual email messages.\n"
308 "Either maildir or mh format directories are fine, but you will want to\n"
309 "move away any auxiliary files maintained by other email programs.\n\n");
311 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
312 "messages), will not work with notmuch. If that's how your mail is currently\n"
313 "stored, we recommend you first convert it to maildir format with a utility\n"
314 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
315 "once the conversion is complete.\n\n");
317 printf ("Top-level mail directory [~/mail]: ");
320 mail_directory = read_line ();
322 if (mail_directory == NULL || strlen (mail_directory) == 0) {
326 free (mail_directory);
328 home = getenv ("HOME");
330 fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
331 fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
335 mail_directory = g_strdup_printf ("%s/mail", home);
338 notmuch = notmuch_database_create (mail_directory);
339 if (notmuch == NULL) {
340 fprintf (stderr, "Failed to create new notmuch database at %s\n",
342 free (mail_directory);
346 printf ("OK. Let's take a look at the mail we can find in the directory\n");
347 printf ("%s ...\n", mail_directory);
350 count_files (mail_directory, &count);
352 printf ("Found %d total files. That's not much mail.\n\n", count);
354 printf ("Next, we'll inspect the messages and create a database of threads:\n");
356 add_files_state.messages_total = count;
357 add_files_state.count = 0;
358 add_files_state.count_last = 0;
359 gettimeofday (&add_files_state.tv_start, NULL);
360 add_files_state.tv_last = add_files_state.tv_start;
362 add_files (notmuch, mail_directory, &add_files_state);
364 gettimeofday (&add_files_state.tv_last, NULL);
365 elapsed = tv_elapsed (add_files_state.tv_start,
366 add_files_state.tv_last);
367 printf ("Added %d total messages in ", add_files_state.count);
368 print_formatted_seconds (elapsed);
369 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
371 notmuch_database_close (notmuch);
373 free (mail_directory);
379 search_command (int argc, char *argv[])
381 fprintf (stderr, "Error: search is not implemented yet.\n");
386 show_command (int argc, char *argv[])
388 fprintf (stderr, "Error: show-thread is not implemented yet.\n");
392 command_t commands[] = {
393 { "setup", setup_command,
394 "Interactively setup notmuch for first use (no arguments).\n"
395 "\t\tInvoking notmuch with no command argument will run setup if\n"
396 "\t\the setup command has not previously been completed." },
397 { "search", search_command,
398 "Search for threads matching the given search terms." },
399 { "show", show_command,
400 "Show the thread with the given thread ID (see 'search')." }
409 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
410 fprintf (stderr, "\n");
411 fprintf (stderr, "Where <command> is one of the following:\n");
412 fprintf (stderr, "\n");
414 for (i = 0; i < ARRAY_SIZE (commands); i++) {
415 command = &commands[i];
417 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
422 main (int argc, char *argv[])
428 return setup_command (0, NULL);
430 for (i = 0; i < ARRAY_SIZE (commands); i++) {
431 command = &commands[i];
433 if (strcmp (argv[1], command->name) == 0)
434 return (command->function) (argc - 2, &argv[2]);
437 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);