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;
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");
313 printf ("Top-level mail directory [~/mail]: ");
316 mail_directory = read_line ();
318 if (mail_directory == NULL || strlen (mail_directory) == 0) {
322 free (mail_directory);
324 home = getenv ("HOME");
326 fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
327 fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
331 mail_directory = g_strdup_printf ("%s/mail", home);
334 notmuch = notmuch_database_create (mail_directory);
335 if (notmuch == NULL) {
336 fprintf (stderr, "Failed to create new notmuch database at %s\n",
338 free (mail_directory);
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_messages = count;
353 add_files_state.count = 0;
354 gettimeofday (&add_files_state.tv_start, NULL);
356 add_files (notmuch, mail_directory, &add_files_state);
358 gettimeofday (&tv_now, NULL);
359 elapsed = tv_elapsed (add_files_state.tv_start,
361 printf ("Added %d total messages in ", add_files_state.count);
362 print_formatted_seconds (elapsed);
363 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
365 notmuch_database_close (notmuch);
367 free (mail_directory);
373 search_command (int argc, char *argv[])
375 fprintf (stderr, "Error: search is not implemented yet.\n");
380 show_command (int argc, char *argv[])
382 fprintf (stderr, "Error: show-thread is not implemented yet.\n");
386 command_t commands[] = {
387 { "setup", setup_command,
388 "Interactively setup notmuch for first use (no arguments).\n"
389 "\t\tInvoking notmuch with no command argument will run setup if\n"
390 "\t\the setup command has not previously been completed." },
391 { "search", search_command,
392 "Search for threads matching the given search terms." },
393 { "show", show_command,
394 "Show the thread with the given thread ID (see 'search')." }
403 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
404 fprintf (stderr, "\n");
405 fprintf (stderr, "Where <command> is one of the following:\n");
406 fprintf (stderr, "\n");
408 for (i = 0; i < ARRAY_SIZE (commands); i++) {
409 command = &commands[i];
411 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
416 main (int argc, char *argv[])
422 return setup_command (0, NULL);
424 for (i = 0; i < ARRAY_SIZE (commands); i++) {
425 command = &commands[i];
427 if (strcmp (argv[1], command->name) == 0)
428 return (command->function) (argc - 2, &argv[2]);
431 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);