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>
24 #define _GNU_SOURCE /* for getline */
31 #include <sys/types.h>
38 #include <glib.h> /* g_strdup_printf */
40 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
42 typedef int (*command_function_t) (int argc, char *argv[]);
44 typedef struct command {
46 command_function_t function;
53 struct timeval tv_start;
56 /* Compute the number of seconds elapsed from start to end. */
58 tv_elapsed (struct timeval start, struct timeval end)
60 return ((end.tv_sec - start.tv_sec) +
61 (end.tv_usec - start.tv_usec) / 1e6);
65 print_formatted_seconds (double seconds)
71 hours = (int) seconds / 3600;
72 printf ("%dh ", hours);
73 seconds -= hours * 3600;
77 minutes = (int) seconds / 60;
78 printf ("%dm ", minutes);
79 seconds -= minutes * 60;
82 printf ("%02ds", (int) seconds);
86 add_files_print_progress (add_files_state_t *state)
88 struct timeval tv_now;
89 double elapsed_overall, rate_overall;
91 gettimeofday (&tv_now, NULL);
93 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
94 rate_overall = (state->count) / elapsed_overall;
96 printf ("Added %d of %d messages (",
97 state->count, state->total_messages);
98 print_formatted_seconds ((state->total_messages - state->count) /
100 printf (" remaining).\r");
105 /* Recursively find all regular files in 'path' and add them to the
108 add_files (notmuch_database_t *notmuch, const char *path,
109 add_files_state_t *state)
112 struct dirent *entry, *e;
117 notmuch_status_t status;
119 dir = opendir (path);
122 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
123 path, strerror (errno));
127 entry_length = offsetof (struct dirent, d_name) +
128 pathconf (path, _PC_NAME_MAX) + 1;
129 entry = malloc (entry_length);
132 err = readdir_r (dir, entry, &e);
134 fprintf (stderr, "Error reading directory: %s\n",
143 /* Ignore special directories to avoid infinite recursion.
144 * Also ignore the .notmuch directory.
146 /* XXX: Eventually we'll want more sophistication to let the
147 * user specify files to be ignored. */
148 if (strcmp (entry->d_name, ".") == 0 ||
149 strcmp (entry->d_name, "..") == 0 ||
150 strcmp (entry->d_name, ".notmuch") ==0)
155 next = g_strdup_printf ("%s/%s", path, entry->d_name);
159 if (S_ISREG (st.st_mode)) {
160 status = notmuch_database_add_message (notmuch, next);
161 if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
162 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
167 if (state->count % 1000 == 0)
168 add_files_print_progress (state);
169 } else if (S_ISDIR (st.st_mode)) {
170 add_files (notmuch, next, state);
181 /* Recursively count all regular files in path and all sub-direcotries
182 * of path. The result is added to *count (which should be
183 * initialized to zero by the top-level caller before calling
186 count_files (const char *path, int *count)
189 struct dirent *entry, *e;
195 dir = opendir (path);
198 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
199 path, strerror (errno));
203 entry_length = offsetof (struct dirent, d_name) +
204 pathconf (path, _PC_NAME_MAX) + 1;
205 entry = malloc (entry_length);
208 err = readdir_r (dir, entry, &e);
210 fprintf (stderr, "Error reading directory: %s\n",
219 /* Ignore special directories to avoid infinite recursion.
220 * Also ignore the .notmuch directory.
222 /* XXX: Eventually we'll want more sophistication to let the
223 * user specify files to be ignored. */
224 if (strcmp (entry->d_name, ".") == 0 ||
225 strcmp (entry->d_name, "..") == 0 ||
226 strcmp (entry->d_name, ".notmuch") == 0)
231 next = g_strdup_printf ("%s/%s", path, entry->d_name);
235 if (S_ISREG (st.st_mode)) {
237 if (*count % 1000 == 0) {
238 printf ("Found %d files so far.\r", *count);
241 } else if (S_ISDIR (st.st_mode)) {
242 count_files (next, count);
254 setup_command (int argc, char *argv[])
256 notmuch_database_t *notmuch;
257 char *mail_directory, *default_path;
260 add_files_state_t add_files_state;
262 struct timeval tv_now;
264 printf ("Welcome to notmuch!\n\n");
266 printf ("The goal of notmuch is to help you manage and search your collection of\n"
267 "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
269 printf ("Notmuch needs to know the top-level directory of your email archive,\n"
270 "(where you already have mail stored and where messages will be delivered\n"
271 "in the future). This directory can contain any number of sub-directories\n"
272 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
273 "archives are perfect). If there are other, non-email files (such as\n"
274 "indexes maintained by other email programs) then notmuch will do its\n"
275 "best to detect those and ignore them.\n\n");
277 printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
278 "messages), will not work with notmuch. If that's how your mail is currently\n"
279 "stored, we recommend you first convert it to maildir format with a utility\n"
280 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
281 "once the conversion is complete.\n\n");
284 default_path = notmuch_database_default_path ();
285 printf ("Top-level mail directory [%s]: ", default_path);
288 mail_directory = NULL;
289 getline (&mail_directory, &line_size, stdin);
292 if (mail_directory &&
293 mail_directory[strlen(mail_directory)-1] == '\n')
295 mail_directory[strlen(mail_directory)-1] = '\0';
298 if (mail_directory == NULL || strlen (mail_directory) == 0) {
300 free (mail_directory);
301 mail_directory = default_path;
303 /* XXX: Instead of telling the user to use an environment
304 * variable here, we should really be writing out a configuration
305 * file and loading that on the next run. */
306 if (strcmp (mail_directory, default_path)) {
307 printf ("Note: Since you are not using the default path, you will want to set\n"
308 "the NOTMUCH_BASE environment variable to %s so that\n"
309 "future calls to notmuch commands will know where to find your mail.\n",
311 printf ("For example, if you are using bash for your shell, add:\n\n");
312 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
313 printf ("to your ~/.bashrc file.\n\n");
318 notmuch = notmuch_database_create (mail_directory);
319 if (notmuch == NULL) {
320 fprintf (stderr, "Failed to create new notmuch database at %s\n",
322 free (mail_directory);
326 printf ("OK. Let's take a look at the mail we can find in the directory\n");
327 printf ("%s ...\n", mail_directory);
330 count_files (mail_directory, &count);
332 printf ("Found %d total files. That's not much mail.\n\n", count);
334 printf ("Next, we'll inspect the messages and create a database of threads:\n");
336 add_files_state.total_messages = count;
337 add_files_state.count = 0;
338 gettimeofday (&add_files_state.tv_start, NULL);
340 add_files (notmuch, mail_directory, &add_files_state);
342 gettimeofday (&tv_now, NULL);
343 elapsed = tv_elapsed (add_files_state.tv_start,
345 printf ("Added %d total messages in ", add_files_state.count);
346 print_formatted_seconds (elapsed);
347 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
349 notmuch_database_close (notmuch);
351 free (mail_directory);
357 search_command (int argc, char *argv[])
359 fprintf (stderr, "Error: search is not implemented yet.\n");
364 show_command (int argc, char *argv[])
366 fprintf (stderr, "Error: show is not implemented yet.\n");
371 dump_command (int argc, char *argv[])
373 fprintf (stderr, "Error: dump is not implemented yet.\n");
378 restore_command (int argc, char *argv[])
380 fprintf (stderr, "Error: restore is not implemented yet.\n");
384 command_t commands[] = {
385 { "setup", setup_command,
386 "Interactively setup notmuch for first use.\n"
387 "\t\tInvoking notmuch with no command argument will run setup if\n"
388 "\t\tthe setup command has not previously been completed." },
389 { "search", search_command,
390 "<search-term> [...]\n\n"
391 "\t\tSearch for threads matching the given search terms.\n"
392 "\t\tOnce we actually implement search we'll document the\n"
393 "\t\tsyntax here." },
394 { "show", show_command,
396 "\t\tShow the thread with the given thread ID (see 'search')." },
397 { "dump", dump_command,
399 "\t\tCreate a plain-text dump of the tags for each message\n"
400 "\t\twriting to the given filename, if any, or to stdout.\n"
401 "\t\tThese tags are the only data in the notmuch database\n"
402 "\t\tthat can't be recreated from the messages themselves.\n"
403 "\t\tThe output of notmuch dump is therefore the only\n"
404 "\t\tcritical thing to backup (and much more friendly to\n"
405 "\t\tincremental backup than the native database files." },
406 { "restore", restore_command,
408 "\t\tRestore the tags from the given dump file (see 'dump')." }
417 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
418 fprintf (stderr, "\n");
419 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
420 fprintf (stderr, "\n");
422 for (i = 0; i < ARRAY_SIZE (commands); i++) {
423 command = &commands[i];
425 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
430 main (int argc, char *argv[])
436 return setup_command (0, NULL);
438 for (i = 0; i < ARRAY_SIZE (commands); i++) {
439 command = &commands[i];
441 if (strcmp (argv[1], command->name) == 0)
442 return (command->function) (argc - 2, &argv[2]);
445 /* Don't complain about "help" being an unknown command when we're
446 about to provide exactly what's wanted anyway. */
447 if (strcmp (argv[1], "help") == 0 ||
448 strcmp (argv[1], "--help") == 0)
450 fprintf (stderr, "The notmuch mail system.\n\n");
452 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);