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");
314 char *default_path = notmuch_database_default_path ();
315 printf ("Top-level mail directory [%s]: ", default_path);
320 mail_directory = read_line ();
322 if (mail_directory == NULL || strlen (mail_directory) == 0) {
324 free (mail_directory);
325 mail_directory = notmuch_database_default_path ();
328 notmuch = notmuch_database_create (mail_directory);
329 if (notmuch == NULL) {
330 fprintf (stderr, "Failed to create new notmuch database at %s\n",
332 free (mail_directory);
336 printf ("OK. Let's take a look at the mail we can find in the directory\n");
337 printf ("%s ...\n", mail_directory);
340 count_files (mail_directory, &count);
342 printf ("Found %d total files. That's not much mail.\n\n", count);
344 printf ("Next, we'll inspect the messages and create a database of threads:\n");
346 add_files_state.total_messages = count;
347 add_files_state.count = 0;
348 gettimeofday (&add_files_state.tv_start, NULL);
350 add_files (notmuch, mail_directory, &add_files_state);
352 gettimeofday (&tv_now, NULL);
353 elapsed = tv_elapsed (add_files_state.tv_start,
355 printf ("Added %d total messages in ", add_files_state.count);
356 print_formatted_seconds (elapsed);
357 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
359 notmuch_database_close (notmuch);
361 free (mail_directory);
367 search_command (int argc, char *argv[])
369 fprintf (stderr, "Error: search is not implemented yet.\n");
374 show_command (int argc, char *argv[])
376 fprintf (stderr, "Error: show is not implemented yet.\n");
381 dump_command (int argc, char *argv[])
383 fprintf (stderr, "Error: dump is not implemented yet.\n");
388 restore_command (int argc, char *argv[])
390 fprintf (stderr, "Error: restore is not implemented yet.\n");
394 command_t commands[] = {
395 { "setup", setup_command,
396 "Interactively setup notmuch for first use.\n"
397 "\t\tInvoking notmuch with no command argument will run setup if\n"
398 "\t\tthe setup command has not previously been completed." },
399 { "search", search_command,
400 "<search-term> [...]\n\n"
401 "\t\tSearch for threads matching the given search terms.\n"
402 "\t\tOnce we actually implement search we'll document the\n"
403 "\t\tsyntax here." },
404 { "show", show_command,
406 "\t\tShow the thread with the given thread ID (see 'search')." },
407 { "dump", dump_command,
409 "\t\tCreate a plain-text dump of the tags for each message\n"
410 "\t\twriting to the given filename, if any, or to stdout.\n"
411 "\t\tThese tags are the only data in the notmuch database\n"
412 "\t\tthat can't be recreated from the messages themselves.\n"
413 "\t\tThe output of notmuch dump is therefore the only\n"
414 "\t\tcritical thing to backup (and much more friendly to\n"
415 "\t\tincremental backup than the native database files." },
416 { "restore", restore_command,
418 "\t\tRestore the tags from the given dump file (see 'dump')." }
427 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
428 fprintf (stderr, "\n");
429 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
430 fprintf (stderr, "\n");
432 for (i = 0; i < ARRAY_SIZE (commands); i++) {
433 command = &commands[i];
435 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
440 main (int argc, char *argv[])
446 return setup_command (0, NULL);
448 for (i = 0; i < ARRAY_SIZE (commands); i++) {
449 command = &commands[i];
451 if (strcmp (argv[1], command->name) == 0)
452 return (command->function) (argc - 2, &argv[2]);
455 /* Don't complain about "help" being an unknown command when we're
456 about to provide exactly what's wanted anyway. */
457 if (strcmp (argv[1], "help") == 0 ||
458 strcmp (argv[1], "--help") == 0)
460 fprintf (stderr, "The notmuch mail system.\n\n");
462 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);