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;
149 dir = opendir (path);
152 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
153 path, strerror (errno));
157 entry_length = offsetof (struct dirent, d_name) +
158 pathconf (path, _PC_NAME_MAX) + 1;
159 entry = malloc (entry_length);
162 err = readdir_r (dir, entry, &e);
164 fprintf (stderr, "Error reading directory: %s\n",
173 /* Ignore special directories to avoid infinite recursion.
174 * Also ignore the .notmuch directory.
176 /* XXX: Eventually we'll want more sophistication to let the
177 * user specify files to be ignored. */
178 if (strcmp (entry->d_name, ".") == 0 ||
179 strcmp (entry->d_name, "..") == 0 ||
180 strcmp (entry->d_name, ".notmuch") ==0)
185 next = g_strdup_printf ("%s/%s", path, entry->d_name);
189 if (S_ISREG (st.st_mode)) {
190 notmuch_database_add_message (notmuch, next);
192 if (state->count % 1000 == 0)
193 add_files_print_progress (state);
194 } else if (S_ISDIR (st.st_mode)) {
195 add_files (notmuch, next, state);
206 /* Recursively count all regular files in path and all sub-direcotries
207 * of path. The result is added to *count (which should be
208 * initialized to zero by the top-level caller before calling
211 count_files (const char *path, int *count)
214 struct dirent *entry, *e;
220 dir = opendir (path);
223 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
224 path, strerror (errno));
228 entry_length = offsetof (struct dirent, d_name) +
229 pathconf (path, _PC_NAME_MAX) + 1;
230 entry = malloc (entry_length);
233 err = readdir_r (dir, entry, &e);
235 fprintf (stderr, "Error reading directory: %s\n",
244 /* Ignore special directories to avoid infinite recursion.
245 * Also ignore the .notmuch directory.
247 /* XXX: Eventually we'll want more sophistication to let the
248 * user specify files to be ignored. */
249 if (strcmp (entry->d_name, ".") == 0 ||
250 strcmp (entry->d_name, "..") == 0 ||
251 strcmp (entry->d_name, ".notmuch") == 0)
256 next = g_strdup_printf ("%s/%s", path, entry->d_name);
260 if (S_ISREG (st.st_mode)) {
262 if (*count % 1000 == 0) {
263 printf ("Found %d files so far.\r", *count);
266 } else if (S_ISDIR (st.st_mode)) {
267 count_files (next, count);
279 setup_command (int argc, char *argv[])
281 notmuch_database_t *notmuch;
282 char *mail_directory;
284 add_files_state_t add_files_state;
286 struct timeval tv_now;
288 printf ("Welcome to notmuch!\n\n");
290 printf ("The goal of notmuch is to help you manage and search your collection of\n"
291 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
293 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
294 "(where you already have mail stored and where messages will be delivered\n"
295 "in the future). This directory can contain any number of sub-directories\n"
296 "but the only files it contains should be individual email messages.\n"
297 "Either maildir or mh format directories are fine, but you will want to\n"
298 "move away any auxiliary files maintained by other email programs.\n\n");
300 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
301 "messages), will not work with notmuch. If that's how your mail is currently\n"
302 "stored, we recommend you first convert it to maildir format with a utility\n"
303 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
304 "once the conversion is complete.\n\n");
306 printf ("Top-level mail directory [~/mail]: ");
309 mail_directory = read_line ();
311 if (mail_directory == NULL || strlen (mail_directory) == 0) {
315 free (mail_directory);
317 home = getenv ("HOME");
319 fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
320 fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
324 mail_directory = g_strdup_printf ("%s/mail", home);
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-thread is not implemented yet.\n");
379 command_t commands[] = {
380 { "setup", setup_command,
381 "Interactively setup notmuch for first use (no arguments).\n"
382 "\t\tInvoking notmuch with no command argument will run setup if\n"
383 "\t\the setup command has not previously been completed." },
384 { "search", search_command,
385 "Search for threads matching the given search terms." },
386 { "show", show_command,
387 "Show the thread with the given thread ID (see 'search')." }
396 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
397 fprintf (stderr, "\n");
398 fprintf (stderr, "Where <command> is one of the following:\n");
399 fprintf (stderr, "\n");
401 for (i = 0; i < ARRAY_SIZE (commands); i++) {
402 command = &commands[i];
404 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
409 main (int argc, char *argv[])
415 return setup_command (0, NULL);
417 for (i = 0; i < ARRAY_SIZE (commands); i++) {
418 command = &commands[i];
420 if (strcmp (argv[1], command->name) == 0)
421 return (command->function) (argc - 2, &argv[2]);
424 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);