d35bda7263085f63ce644598e0c07a240ecbfb2e
[notmuch] / notmuch.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  *
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.
9  *
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.
14  *
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/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch.h"
22
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <sys/types.h>
27 #include <sys/stat.h>
28 #include <sys/time.h>
29 #include <unistd.h>
30 #include <dirent.h>
31 #include <errno.h>
32
33 #include <glib.h> /* GIOChannel */
34
35 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
36
37 typedef int (*command_function_t) (int argc, char *argv[]);
38
39 typedef struct command {
40     const char *name;
41     command_function_t function;
42     const char *usage;
43 } command_t;
44
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.
48  *
49  * This function returns NULL if EOF is encountered before any
50  * characters are input (otherwise it returns those characters).
51  */
52 char *
53 read_line (void)
54 {
55     char *result = NULL;
56     GError *error = NULL;
57     GIOStatus g_io_status;
58     gsize length;
59
60     GIOChannel *channel = g_io_channel_unix_new (fileno (stdin));
61
62     g_io_status = g_io_channel_read_line (channel, &result,
63                                           &length, NULL, &error);
64
65     if (g_io_status == EOF)
66         goto DONE;
67
68     if (g_io_status != G_IO_STATUS_NORMAL) {
69         fprintf(stderr, "Read error: %s\n", error->message);
70         exit (1);
71     }
72
73     if (length && result[length - 1] == '\n')
74         result[length - 1] = '\0';
75
76   DONE:
77     g_io_channel_unref (channel);
78     return result;
79 }
80
81 typedef struct {
82     int total_messages;
83     int count;
84     struct timeval tv_start;
85 } add_files_state_t;
86
87 /* Compute the number of seconds elapsed from start to end. */
88 double
89 tv_elapsed (struct timeval start, struct timeval end)
90 {
91     return ((end.tv_sec - start.tv_sec) +
92             (end.tv_usec - start.tv_usec) / 1e6);
93 }
94
95 void
96 print_formatted_seconds (double seconds)
97 {
98     int hours;
99     int minutes;
100
101     if (seconds > 3600) {
102         hours = (int) seconds / 3600;
103         printf ("%dh ", hours);
104         seconds -= hours * 3600;
105     }
106
107     if (seconds > 60) {
108         minutes = (int) seconds / 60;
109         printf ("%dm ", minutes);
110         seconds -= minutes * 60;
111     }
112
113     printf ("%02ds", (int) seconds);
114 }
115
116 void
117 add_files_print_progress (add_files_state_t *state)
118 {
119     struct timeval tv_now;
120     double elapsed_overall, rate_overall;
121
122     gettimeofday (&tv_now, NULL);
123
124     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
125     rate_overall = (state->count) / elapsed_overall;
126
127     printf ("Added %d of %d messages (",
128             state->count, state->total_messages);
129     print_formatted_seconds ((state->total_messages - state->count) /
130                              rate_overall);
131     printf (" remaining).\r");
132
133     fflush (stdout);
134 }
135
136 /* Recursively find all regular files in 'path' and add them to the
137  * database. */
138 void
139 add_files (notmuch_database_t *notmuch, const char *path,
140            add_files_state_t *state)
141 {
142     DIR *dir;
143     struct dirent *entry, *e;
144     int entry_length;
145     int err;
146     char *next;
147     struct stat st;
148     notmuch_status_t status;
149
150     dir = opendir (path);
151
152     if (dir == NULL) {
153         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
154                  path, strerror (errno));
155         return;
156     }
157
158     entry_length = offsetof (struct dirent, d_name) +
159         pathconf (path, _PC_NAME_MAX) + 1;
160     entry = malloc (entry_length);
161
162     while (1) {
163         err = readdir_r (dir, entry, &e);
164         if (err) {
165             fprintf (stderr, "Error reading directory: %s\n",
166                      strerror (errno));
167             free (entry);
168             return;
169         }
170
171         if (e == NULL)
172             break;
173
174         /* Ignore special directories to avoid infinite recursion.
175          * Also ignore the .notmuch directory.
176          */
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)
182         {
183             continue;
184         }
185
186         next = g_strdup_printf ("%s/%s", path, entry->d_name);
187
188         stat (next, &st);
189
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",
194                          next);
195             } else {
196                 state->count++;
197             }
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);
202         }
203
204         free (next);
205     }
206
207     free (entry);
208
209     closedir (dir);
210 }
211
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
215  * count_files). */
216 void
217 count_files (const char *path, int *count)
218 {
219     DIR *dir;
220     struct dirent *entry, *e;
221     int entry_length;
222     int err;
223     char *next;
224     struct stat st;
225
226     dir = opendir (path);
227
228     if (dir == NULL) {
229         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
230                  path, strerror (errno));
231         return;
232     }
233
234     entry_length = offsetof (struct dirent, d_name) +
235         pathconf (path, _PC_NAME_MAX) + 1;
236     entry = malloc (entry_length);
237
238     while (1) {
239         err = readdir_r (dir, entry, &e);
240         if (err) {
241             fprintf (stderr, "Error reading directory: %s\n",
242                      strerror (errno));
243             free (entry);
244             return;
245         }
246
247         if (e == NULL)
248             break;
249
250         /* Ignore special directories to avoid infinite recursion.
251          * Also ignore the .notmuch directory.
252          */
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)
258         {
259             continue;
260         }
261
262         next = g_strdup_printf ("%s/%s", path, entry->d_name);
263
264         stat (next, &st);
265
266         if (S_ISREG (st.st_mode)) {
267             *count = *count + 1;
268             if (*count % 1000 == 0) {
269                 printf ("Found %d files so far.\r", *count);
270                 fflush (stdout);
271             }
272         } else if (S_ISDIR (st.st_mode)) {
273             count_files (next, count);
274         }
275
276         free (next);
277     }
278
279     free (entry);
280
281     closedir (dir);
282 }
283
284 int
285 setup_command (int argc, char *argv[])
286 {
287     notmuch_database_t *notmuch;
288     char *mail_directory, *default_path;
289     int count;
290     add_files_state_t add_files_state;
291     double elapsed;
292     struct timeval tv_now;
293
294     printf ("Welcome to notmuch!\n\n");
295
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");
298
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");
306
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");
312
313
314     default_path = notmuch_database_default_path ();
315     printf ("Top-level mail directory [%s]: ", default_path);
316     fflush (stdout);
317
318     mail_directory = read_line ();
319     printf ("\n");
320
321     if (mail_directory == NULL || strlen (mail_directory) == 0) {
322         if (mail_directory)
323             free (mail_directory);
324         mail_directory = default_path;
325     } else {
326         /* XXX: Instead of telling the user to use an environment
327          * variable here, we should really be writing out a configuration
328          * file and loading that on the next run. */
329         if (strcmp (mail_directory, default_path)) {
330             printf ("Note: Since you are not using the default path, you will want to set\n"
331                     "the NOTMUCH_BASE environment variable to %s so that\n"
332                     "future calls to notmuch commands will know where to find your mail.\n",
333                     mail_directory);
334             printf ("For example, if you are using bash for your shell, add:\n\n");
335             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
336             printf ("to your ~/.bashrc file.\n\n");
337         }
338         free (default_path);
339     }
340
341     notmuch = notmuch_database_create (mail_directory);
342     if (notmuch == NULL) {
343         fprintf (stderr, "Failed to create new notmuch database at %s\n",
344                  mail_directory);
345         free (mail_directory);
346         return 1;
347     }
348
349     printf ("OK. Let's take a look at the mail we can find in the directory\n");
350     printf ("%s ...\n", mail_directory);
351
352     count = 0;
353     count_files (mail_directory, &count);
354
355     printf ("Found %d total files. That's not much mail.\n\n", count);
356
357     printf ("Next, we'll inspect the messages and create a database of threads:\n");
358
359     add_files_state.total_messages = count;
360     add_files_state.count = 0;
361     gettimeofday (&add_files_state.tv_start, NULL);
362
363     add_files (notmuch, mail_directory, &add_files_state);
364
365     gettimeofday (&tv_now, NULL);
366     elapsed = tv_elapsed (add_files_state.tv_start,
367                           tv_now);
368     printf ("Added %d total messages in ", add_files_state.count);
369     print_formatted_seconds (elapsed);
370     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
371
372     notmuch_database_close (notmuch);
373
374     free (mail_directory);
375     
376     return 0;
377 }
378
379 int
380 search_command (int argc, char *argv[])
381 {
382     fprintf (stderr, "Error: search is not implemented yet.\n");
383     return 1;
384 }
385
386 int
387 show_command (int argc, char *argv[])
388 {
389     fprintf (stderr, "Error: show is not implemented yet.\n");
390     return 1;
391 }
392
393 int
394 dump_command (int argc, char *argv[])
395 {
396     fprintf (stderr, "Error: dump is not implemented yet.\n");
397     return 1;
398 }
399
400 int
401 restore_command (int argc, char *argv[])
402 {
403     fprintf (stderr, "Error: restore is not implemented yet.\n");
404     return 1;
405 }
406
407 command_t commands[] = {
408     { "setup", setup_command,
409       "Interactively setup notmuch for first use.\n"
410       "\t\tInvoking notmuch with no command argument will run setup if\n"
411       "\t\tthe setup command has not previously been completed." },
412     { "search", search_command,
413       "<search-term> [...]\n\n"
414       "\t\tSearch for threads matching the given search terms.\n"
415       "\t\tOnce we actually implement search we'll document the\n"
416       "\t\tsyntax here." },
417     { "show", show_command,
418       "<thread-id>\n\n"
419       "\t\tShow the thread with the given thread ID (see 'search')." },
420     { "dump", dump_command,
421       "[<filename>]\n\n"
422       "\t\tCreate a plain-text dump of the tags for each message\n"
423       "\t\twriting to the given filename, if any, or to stdout.\n"
424       "\t\tThese tags are the only data in the notmuch database\n"
425       "\t\tthat can't be recreated from the messages themselves.\n"
426       "\t\tThe output of notmuch dump is therefore the only\n"
427       "\t\tcritical thing to backup (and much more friendly to\n"
428       "\t\tincremental backup than the native database files." },
429     { "restore", restore_command,
430       "<filename>\n\n"
431       "\t\tRestore the tags from the given dump file (see 'dump')." }
432 };
433
434 void
435 usage (void)
436 {
437     command_t *command;
438     int i;
439
440     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
441     fprintf (stderr, "\n");
442     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
443     fprintf (stderr, "\n");
444
445     for (i = 0; i < ARRAY_SIZE (commands); i++) {
446         command = &commands[i];
447
448         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
449     }
450 }
451     
452 int
453 main (int argc, char *argv[])
454 {
455     command_t *command;
456     int i;
457
458     if (argc == 1)
459         return setup_command (0, NULL);
460
461     for (i = 0; i < ARRAY_SIZE (commands); i++) {
462         command = &commands[i];
463
464         if (strcmp (argv[1], command->name) == 0)
465             return (command->function) (argc - 2, &argv[2]);
466     }
467
468     /* Don't complain about "help" being an unknown command when we're
469        about to provide exactly what's wanted anyway. */
470     if (strcmp (argv[1], "help") == 0 ||
471         strcmp (argv[1], "--help") == 0)
472     {
473         fprintf (stderr, "The notmuch mail system.\n\n");
474     } else {
475         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
476     }
477     usage ();
478     exit (1);
479
480     return 0;
481 }