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 == NULL || strlen (mail_directory) == 0) {
294 free (mail_directory);
295 mail_directory = default_path;
297 if (mail_directory[strlen(mail_directory)-1] == '\n')
298 mail_directory[strlen(mail_directory)-1] = '\0';
299 /* XXX: Instead of telling the user to use an environment
300 * variable here, we should really be writing out a configuration
301 * file and loading that on the next run. */
302 if (strcmp (mail_directory, default_path)) {
303 printf ("Note: Since you are not using the default path, you will want to set\n"
304 "the NOTMUCH_BASE environment variable to %s so that\n"
305 "future calls to notmuch commands will know where to find your mail.\n",
307 printf ("For example, if you are using bash for your shell, add:\n\n");
308 printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
309 printf ("to your ~/.bashrc file.\n\n");
314 notmuch = notmuch_database_create (mail_directory);
315 if (notmuch == NULL) {
316 fprintf (stderr, "Failed to create new notmuch database at %s\n",
318 free (mail_directory);
322 printf ("OK. Let's take a look at the mail we can find in the directory\n");
323 printf ("%s ...\n", mail_directory);
326 count_files (mail_directory, &count);
328 printf ("Found %d total files. That's not much mail.\n\n", count);
330 printf ("Next, we'll inspect the messages and create a database of threads:\n");
332 add_files_state.total_messages = count;
333 add_files_state.count = 0;
334 gettimeofday (&add_files_state.tv_start, NULL);
336 add_files (notmuch, mail_directory, &add_files_state);
338 gettimeofday (&tv_now, NULL);
339 elapsed = tv_elapsed (add_files_state.tv_start,
341 printf ("Added %d total messages in ", add_files_state.count);
342 print_formatted_seconds (elapsed);
343 printf (" (%d messages/sec.). \n", (int) (add_files_state.count / elapsed));
345 notmuch_database_close (notmuch);
347 free (mail_directory);
353 search_command (int argc, char *argv[])
355 fprintf (stderr, "Error: search is not implemented yet.\n");
360 show_command (int argc, char *argv[])
362 fprintf (stderr, "Error: show is not implemented yet.\n");
367 dump_command (int argc, char *argv[])
369 fprintf (stderr, "Error: dump is not implemented yet.\n");
374 restore_command (int argc, char *argv[])
376 fprintf (stderr, "Error: restore is not implemented yet.\n");
380 command_t commands[] = {
381 { "setup", setup_command,
382 "Interactively setup notmuch for first use.\n"
383 "\t\tInvoking notmuch with no command argument will run setup if\n"
384 "\t\tthe setup command has not previously been completed." },
385 { "search", search_command,
386 "<search-term> [...]\n\n"
387 "\t\tSearch for threads matching the given search terms.\n"
388 "\t\tOnce we actually implement search we'll document the\n"
389 "\t\tsyntax here." },
390 { "show", show_command,
392 "\t\tShow the thread with the given thread ID (see 'search')." },
393 { "dump", dump_command,
395 "\t\tCreate a plain-text dump of the tags for each message\n"
396 "\t\twriting to the given filename, if any, or to stdout.\n"
397 "\t\tThese tags are the only data in the notmuch database\n"
398 "\t\tthat can't be recreated from the messages themselves.\n"
399 "\t\tThe output of notmuch dump is therefore the only\n"
400 "\t\tcritical thing to backup (and much more friendly to\n"
401 "\t\tincremental backup than the native database files." },
402 { "restore", restore_command,
404 "\t\tRestore the tags from the given dump file (see 'dump')." }
413 fprintf (stderr, "Usage: notmuch <command> [args...]\n");
414 fprintf (stderr, "\n");
415 fprintf (stderr, "Where <command> and [args...] are as follows:\n");
416 fprintf (stderr, "\n");
418 for (i = 0; i < ARRAY_SIZE (commands); i++) {
419 command = &commands[i];
421 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
426 main (int argc, char *argv[])
432 return setup_command (0, NULL);
434 for (i = 0; i < ARRAY_SIZE (commands); i++) {
435 command = &commands[i];
437 if (strcmp (argv[1], command->name) == 0)
438 return (command->function) (argc - 2, &argv[2]);
441 /* Don't complain about "help" being an unknown command when we're
442 about to provide exactly what's wanted anyway. */
443 if (strcmp (argv[1], "help") == 0 ||
444 strcmp (argv[1], "--help") == 0)
446 fprintf (stderr, "The notmuch mail system.\n\n");
448 fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);