515ec213f2f0d49c618ed3a9a92724a3dbab6b8b
[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 #ifndef _GNU_SOURCE
22 #define _GNU_SOURCE /* for getline */
23 #endif
24 #include <stdio.h>
25
26 #include "notmuch.h"
27
28 /* This is separate from notmuch-private.h because we're trying to
29  * keep notmuch.c from looking into any internals, (which helps us
30  * develop notmuch.h into a plausible library interface).
31  */
32 #include "xutil.h"
33
34 #include <stddef.h>
35 #include <string.h>
36 #include <sys/stat.h>
37 #include <sys/time.h>
38 #include <unistd.h>
39 #include <dirent.h>
40 #include <errno.h>
41
42 #include <talloc.h>
43
44 #include <glib.h> /* g_strdup_printf */
45
46 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
47
48 typedef int (*command_function_t) (int argc, char *argv[]);
49
50 typedef struct command {
51     const char *name;
52     command_function_t function;
53     const char *usage;
54 } command_t;
55
56 typedef struct {
57     int ignore_read_only_directories;
58
59     int total_files;
60     int processed_files;
61     int added_messages;
62     struct timeval tv_start;
63 } add_files_state_t;
64
65 static void
66 chomp_newline (char *str)
67 {
68     if (str && str[strlen(str)-1] == '\n')
69         str[strlen(str)-1] = '\0';
70 }
71
72 /* Compute the number of seconds elapsed from start to end. */
73 double
74 tv_elapsed (struct timeval start, struct timeval end)
75 {
76     return ((end.tv_sec - start.tv_sec) +
77             (end.tv_usec - start.tv_usec) / 1e6);
78 }
79
80 void
81 print_formatted_seconds (double seconds)
82 {
83     int hours;
84     int minutes;
85
86     if (seconds < 1) {
87         printf ("almost no time");
88         return;
89     }
90
91     if (seconds > 3600) {
92         hours = (int) seconds / 3600;
93         printf ("%dh ", hours);
94         seconds -= hours * 3600;
95     }
96
97     if (seconds > 60) {
98         minutes = (int) seconds / 60;
99         printf ("%dm ", minutes);
100         seconds -= minutes * 60;
101     }
102
103     printf ("%ds", (int) seconds);
104 }
105
106 void
107 add_files_print_progress (add_files_state_t *state)
108 {
109     struct timeval tv_now;
110     double elapsed_overall, rate_overall;
111
112     gettimeofday (&tv_now, NULL);
113
114     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
115     rate_overall = (state->processed_files) / elapsed_overall;
116
117     printf ("Processed %d", state->processed_files);
118
119     if (state->total_files) {
120         printf (" of %d files (", state->total_files);
121         print_formatted_seconds ((state->total_files - state->processed_files) /
122                                  rate_overall);
123         printf (" remaining).      \r");
124     } else {
125         printf (" files (%d files/sec.)    \r", (int) rate_overall);
126     }
127
128     fflush (stdout);
129 }
130
131 /* Examine 'path' recursively as follows:
132  *
133  *   o Ask the filesystem for the mtime of 'path' (path_mtime)
134  *
135  *   o Ask the database for its timestamp of 'path' (path_dbtime)
136  *
137  *   o If 'path_mtime' > 'path_dbtime'
138  *
139  *       o For each regular file in 'path' with mtime newer than the
140  *         'path_dbtime' call add_message to add the file to the
141  *         database.
142  *
143  *       o For each sub-directory of path, recursively call into this
144  *         same function.
145  *
146  *   o Tell the database to update its time of 'path' to 'path_mtime'
147  *
148  * The 'struct stat *st' must point to a structure that has already
149  * been initialized for 'path' by calling stat().
150  */
151 notmuch_status_t
152 add_files_recursive (notmuch_database_t *notmuch,
153                      const char *path,
154                      struct stat *st,
155                      add_files_state_t *state)
156 {
157     DIR *dir = NULL;
158     struct dirent *e, *entry = NULL;
159     int entry_length;
160     int err;
161     char *next = NULL;
162     time_t path_mtime, path_dbtime;
163     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
164
165     /* If we're told to, we bail out on encountering a read-only
166      * directory, (with this being a clear clue from the user to
167      * Notmuch that new mail won't be arriving there and we need not
168      * look. */
169     if (state->ignore_read_only_directories &&
170         (st->st_mode & S_IWUSR) == 0)
171     {
172         goto DONE;
173     }
174
175     path_mtime = st->st_mtime;
176
177     path_dbtime = notmuch_database_get_timestamp (notmuch, path);
178
179     dir = opendir (path);
180     if (dir == NULL) {
181         fprintf (stderr, "Error opening directory %s: %s\n",
182                  path, strerror (errno));
183         ret = NOTMUCH_STATUS_FILE_ERROR;
184         goto DONE;
185     }
186
187     entry_length = offsetof (struct dirent, d_name) +
188         pathconf (path, _PC_NAME_MAX) + 1;
189     entry = malloc (entry_length);
190
191     while (1) {
192         err = readdir_r (dir, entry, &e);
193         if (err) {
194             fprintf (stderr, "Error reading directory: %s\n",
195                      strerror (errno));
196             ret = NOTMUCH_STATUS_FILE_ERROR;
197             goto DONE;
198         }
199
200         if (e == NULL)
201             break;
202
203         /* If this directory hasn't been modified since the last
204          * add_files, then we only need to look further for
205          * sub-directories. */
206         if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
207             continue;
208
209         /* Ignore special directories to avoid infinite recursion.
210          * Also ignore the .notmuch directory.
211          */
212         /* XXX: Eventually we'll want more sophistication to let the
213          * user specify files to be ignored. */
214         if (strcmp (entry->d_name, ".") == 0 ||
215             strcmp (entry->d_name, "..") == 0 ||
216             strcmp (entry->d_name, ".notmuch") ==0)
217         {
218             continue;
219         }
220
221         next = g_strdup_printf ("%s/%s", path, entry->d_name);
222
223         if (stat (next, st)) {
224             fprintf (stderr, "Error reading %s: %s\n",
225                      next, strerror (errno));
226             ret = NOTMUCH_STATUS_FILE_ERROR;
227             continue;
228         }
229
230         if (S_ISREG (st->st_mode)) {
231             /* If the file hasn't been modified since the last
232              * add_files, then we need not look at it. */
233             if (st->st_mtime > path_dbtime) {
234                 state->processed_files++;
235
236                 status = notmuch_database_add_message (notmuch, next);
237                 switch (status) {
238                     /* success */
239                     case NOTMUCH_STATUS_SUCCESS:
240                         state->added_messages++;
241                         break;
242                     /* Non-fatal issues (go on to next file) */
243                     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
244                         /* Stay silent on this one. */
245                         break;
246                     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
247                         fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
248                                  next);
249                         break;
250                     /* Fatal issues. Don't process anymore. */
251                     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
252                         fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
253                         ret = status;
254                         goto DONE;
255                     default:
256                         fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n",  status);
257                         ret = status;
258                         goto DONE;
259                 }
260                 if (state->processed_files % 1000 == 0)
261                     add_files_print_progress (state);
262             }
263         } else if (S_ISDIR (st->st_mode)) {
264             status = add_files_recursive (notmuch, next, st, state);
265             if (status && ret == NOTMUCH_STATUS_SUCCESS)
266                 ret = status;
267         }
268
269         free (next);
270         next = NULL;
271     }
272
273     status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
274     if (status && ret == NOTMUCH_STATUS_SUCCESS)
275         ret = status;
276
277   DONE:
278     if (next)
279         free (next);
280     if (entry)
281         free (entry);
282     if (dir)
283         closedir (dir);
284
285     return ret;
286 }
287
288 /* This is the top-level entry point for add_files. It does a couple
289  * of error checks, and then calls into the recursive function,
290  * (avoiding the repeating of these error checks at every
291  * level---which would be useless becaues we already do a stat() at
292  * the level above). */
293 static notmuch_status_t
294 add_files (notmuch_database_t *notmuch,
295            const char *path,
296            add_files_state_t *state)
297 {
298     struct stat st;
299
300     if (stat (path, &st)) {
301         fprintf (stderr, "Error reading directory %s: %s\n",
302                  path, strerror (errno));
303         return NOTMUCH_STATUS_FILE_ERROR;
304     }
305
306     if (! S_ISDIR (st.st_mode)) {
307         fprintf (stderr, "Error: %s is not a directory.\n", path);
308         return NOTMUCH_STATUS_FILE_ERROR;
309     }
310
311     return add_files_recursive (notmuch, path, &st, state);
312 }
313
314 /* Recursively count all regular files in path and all sub-direcotries
315  * of path.  The result is added to *count (which should be
316  * initialized to zero by the top-level caller before calling
317  * count_files). */
318 void
319 count_files (const char *path, int *count)
320 {
321     DIR *dir;
322     struct dirent *entry, *e;
323     int entry_length;
324     int err;
325     char *next;
326     struct stat st;
327
328     dir = opendir (path);
329
330     if (dir == NULL) {
331         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
332                  path, strerror (errno));
333         return;
334     }
335
336     entry_length = offsetof (struct dirent, d_name) +
337         pathconf (path, _PC_NAME_MAX) + 1;
338     entry = malloc (entry_length);
339
340     while (1) {
341         err = readdir_r (dir, entry, &e);
342         if (err) {
343             fprintf (stderr, "Error reading directory: %s\n",
344                      strerror (errno));
345             free (entry);
346             return;
347         }
348
349         if (e == NULL)
350             break;
351
352         /* Ignore special directories to avoid infinite recursion.
353          * Also ignore the .notmuch directory.
354          */
355         /* XXX: Eventually we'll want more sophistication to let the
356          * user specify files to be ignored. */
357         if (strcmp (entry->d_name, ".") == 0 ||
358             strcmp (entry->d_name, "..") == 0 ||
359             strcmp (entry->d_name, ".notmuch") == 0)
360         {
361             continue;
362         }
363
364         next = g_strdup_printf ("%s/%s", path, entry->d_name);
365
366         stat (next, &st);
367
368         if (S_ISREG (st.st_mode)) {
369             *count = *count + 1;
370             if (*count % 1000 == 0) {
371                 printf ("Found %d files so far.\r", *count);
372                 fflush (stdout);
373             }
374         } else if (S_ISDIR (st.st_mode)) {
375             count_files (next, count);
376         }
377
378         free (next);
379     }
380
381     free (entry);
382
383     closedir (dir);
384 }
385
386 int
387 setup_command (int argc, char *argv[])
388 {
389     notmuch_database_t *notmuch = NULL;
390     char *default_path, *mail_directory = NULL;
391     size_t line_size;
392     int count;
393     add_files_state_t add_files_state;
394     double elapsed;
395     struct timeval tv_now;
396     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
397
398     printf ("Welcome to notmuch!\n\n");
399
400     printf ("The goal of notmuch is to help you manage and search your collection of\n"
401             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
402
403     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
404             "(where you already have mail stored and where messages will be delivered\n"
405             "in the future). This directory can contain any number of sub-directories\n"
406             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
407             "archives are perfect). If there are other, non-email files (such as\n"
408             "indexes maintained by other email programs) then notmuch will do its\n"
409             "best to detect those and ignore them.\n\n");
410
411     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
412             "messages), will not work with notmuch. If that's how your mail is currently\n"
413             "stored, we recommend you first convert it to maildir format with a utility\n"
414             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
415             "once the conversion is complete.\n\n");
416
417
418     default_path = notmuch_database_default_path ();
419     printf ("Top-level mail directory [%s]: ", default_path);
420     fflush (stdout);
421
422     getline (&mail_directory, &line_size, stdin);
423     chomp_newline (mail_directory);
424
425     printf ("\n");
426
427     if (mail_directory == NULL || strlen (mail_directory) == 0) {
428         if (mail_directory)
429             free (mail_directory);
430         mail_directory = default_path;
431     } else {
432         /* XXX: Instead of telling the user to use an environment
433          * variable here, we should really be writing out a configuration
434          * file and loading that on the next run. */
435         if (strcmp (mail_directory, default_path)) {
436             printf ("Note: Since you are not using the default path, you will want to set\n"
437                     "the NOTMUCH_BASE environment variable to %s so that\n"
438                     "future calls to notmuch commands will know where to find your mail.\n",
439                     mail_directory);
440             printf ("For example, if you are using bash for your shell, add:\n\n");
441             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
442             printf ("to your ~/.bashrc file.\n\n");
443         }
444         free (default_path);
445     }
446
447     notmuch = notmuch_database_create (mail_directory);
448     if (notmuch == NULL) {
449         fprintf (stderr, "Failed to create new notmuch database at %s\n",
450                  mail_directory);
451         ret = NOTMUCH_STATUS_FILE_ERROR;
452         goto DONE;
453     }
454
455     printf ("OK. Let's take a look at the mail we can find in the directory\n");
456     printf ("%s ...\n", mail_directory);
457
458     count = 0;
459     count_files (mail_directory, &count);
460
461     printf ("Found %d total files. That's not much mail.\n\n", count);
462
463     printf ("Next, we'll inspect the messages and create a database of threads:\n");
464
465     add_files_state.ignore_read_only_directories = FALSE;
466     add_files_state.total_files = count;
467     add_files_state.processed_files = 0;
468     add_files_state.added_messages = 0;
469     gettimeofday (&add_files_state.tv_start, NULL);
470
471     ret = add_files (notmuch, mail_directory, &add_files_state);
472
473     gettimeofday (&tv_now, NULL);
474     elapsed = tv_elapsed (add_files_state.tv_start,
475                           tv_now);
476     printf ("Processed %d %s in ", add_files_state.processed_files,
477             add_files_state.processed_files == 1 ?
478             "file" : "total files");
479     print_formatted_seconds (elapsed);
480     if (elapsed > 1) {
481         printf (" (%d files/sec.).                 \n",
482                 (int) (add_files_state.processed_files / elapsed));
483     } else {
484         printf (".                    \n");
485     }
486     if (add_files_state.added_messages) {
487         printf ("Added %d %s to the database.\n\n",
488                 add_files_state.added_messages,
489                 add_files_state.added_messages == 1 ?
490                 "message" : "unique messages");
491     }
492
493     printf ("When new mail is delivered to %s in the future,\n"
494             "run \"notmuch new\" to add it to the database.\n\n",
495             mail_directory);
496     printf ("And if you have any sub-directories that are archives (that is,\n"
497             "they will never receive new mail), marking these directores as\n"
498             "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
499             "much more efficient (it won't even look in those directories).\n\n");
500
501     if (ret) {
502         printf ("Note: At least one error was encountered: %s\n",
503                 notmuch_status_to_string (ret));
504     }
505
506   DONE:
507     if (mail_directory)
508         free (mail_directory);
509     if (notmuch)
510         notmuch_database_close (notmuch);
511
512     return ret;
513 }
514
515 int
516 new_command (int argc, char *argv[])
517 {
518     notmuch_database_t *notmuch;
519     const char *mail_directory;
520     add_files_state_t add_files_state;
521     double elapsed;
522     struct timeval tv_now;
523     int ret = 0;
524
525     notmuch = notmuch_database_open (NULL);
526     if (notmuch == NULL) {
527         ret = 1;
528         goto DONE;
529     }
530
531     mail_directory = notmuch_database_get_path (notmuch);
532
533     add_files_state.ignore_read_only_directories = TRUE;
534     add_files_state.total_files = 0;
535     add_files_state.processed_files = 0;
536     add_files_state.added_messages = 0;
537     gettimeofday (&add_files_state.tv_start, NULL);
538
539     ret = add_files (notmuch, mail_directory, &add_files_state);
540
541     gettimeofday (&tv_now, NULL);
542     elapsed = tv_elapsed (add_files_state.tv_start,
543                           tv_now);
544     if (add_files_state.processed_files) {
545         printf ("Processed %d %s in ", add_files_state.processed_files,
546                 add_files_state.processed_files == 1 ?
547                 "file" : "total files");
548         print_formatted_seconds (elapsed);
549         if (elapsed > 1) {
550             printf (" (%d files/sec.).                 \n",
551                     (int) (add_files_state.processed_files / elapsed));
552         } else {
553             printf (".                    \n");
554         }
555     }
556     if (add_files_state.added_messages) {
557         printf ("Added %d new %s to the database (not much, really).\n",
558                 add_files_state.added_messages,
559                 add_files_state.added_messages == 1 ?
560                 "message" : "messages");
561     } else {
562         printf ("No new mail---and that's not much.\n");
563     }
564
565     if (ret) {
566         printf ("Note: At least one error was encountered: %s\n",
567                 notmuch_status_to_string (ret));
568     }
569
570   DONE:
571     if (notmuch)
572         notmuch_database_close (notmuch);
573
574     return ret;
575 }
576
577 int
578 search_command (int argc, char *argv[])
579 {
580     fprintf (stderr, "Error: search is not implemented yet.\n");
581     return 1;
582 }
583
584 int
585 show_command (int argc, char *argv[])
586 {
587     fprintf (stderr, "Error: show is not implemented yet.\n");
588     return 1;
589 }
590
591 int
592 dump_command (int argc, char *argv[])
593 {
594     FILE *output;
595     notmuch_database_t *notmuch = NULL;
596     notmuch_query_t *query;
597     notmuch_results_t *results;
598     notmuch_message_t *message;
599     notmuch_tags_t *tags;
600     int ret = 0;
601
602     if (argc) {
603         output = fopen (argv[0], "w");
604         if (output == NULL) {
605             fprintf (stderr, "Error opening %s for writing: %s\n",
606                      argv[0], strerror (errno));
607             ret = 1;
608             goto DONE;
609         }
610     } else {
611         output = stdout;
612     }
613
614     notmuch = notmuch_database_open (NULL);
615     if (notmuch == NULL) {
616         ret = 1;
617         goto DONE;
618     }
619
620     query = notmuch_query_create (notmuch, "");
621     if (query == NULL) {
622         fprintf (stderr, "Out of memory\n");
623         ret = 1;
624         goto DONE;
625     }
626
627     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
628
629     for (results = notmuch_query_search (query);
630          notmuch_results_has_more (results);
631          notmuch_results_advance (results))
632     {
633         int first = 1;
634         message = notmuch_results_get (results);
635
636         fprintf (output,
637                  "%s (", notmuch_message_get_message_id (message));
638
639         for (tags = notmuch_message_get_tags (message);
640              notmuch_tags_has_more (tags);
641              notmuch_tags_advance (tags))
642         {
643             if (! first)
644                 fprintf (output, " ");
645
646             fprintf (output, "%s", notmuch_tags_get (tags));
647
648             first = 0;
649         }
650
651         fprintf (output, ")\n");
652
653         notmuch_message_destroy (message);
654     }
655
656     notmuch_query_destroy (query);
657
658   DONE:
659     if (notmuch)
660         notmuch_database_close (notmuch);
661     if (output != stdout)
662         fclose (output);
663
664     return ret;
665 }
666
667 int
668 restore_command (int argc, char *argv[])
669 {
670     FILE *input;
671     notmuch_database_t *notmuch = NULL;
672     char *line = NULL;
673     size_t line_size, line_len;
674     regex_t regex;
675     int rerr;
676     int ret = 0;
677
678     if (argc) {
679         input = fopen (argv[0], "r");
680         if (input == NULL) {
681             fprintf (stderr, "Error opening %s for reading: %s\n",
682                      argv[0], strerror (errno));
683             ret = 1;
684             goto DONE;
685         }
686     } else {
687         printf ("No filename given. Reading dump from stdin.\n");
688         input = stdin;
689     }
690
691     notmuch = notmuch_database_open (NULL);
692     if (notmuch == NULL) {
693         ret = 1;
694         goto DONE;
695     }
696
697     /* Dump output is one line per message. We match a sequence of
698      * non-space characters for the message-id, then one or more
699      * spaces, then a list of space-separated tags as a sequence of
700      * characters within literal '(' and ')'. */
701     xregcomp (&regex,
702               "^([^ ]+) \\(([^)]*)\\)$",
703               REG_EXTENDED);
704
705     while ((line_len = getline (&line, &line_size, input)) != -1) {
706         regmatch_t match[3];
707         char *message_id, *tags, *tag, *next;
708         notmuch_message_t *message;
709         notmuch_status_t status;
710
711         chomp_newline (line);
712
713         rerr = xregexec (&regex, line, 3, match, 0);
714         if (rerr == REG_NOMATCH)
715         {
716             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
717                      line);
718             continue;
719         }
720
721         message_id = xstrndup (line + match[1].rm_so,
722                                match[1].rm_eo - match[1].rm_so);
723         tags = xstrndup (line + match[2].rm_so,
724                          match[2].rm_eo - match[2].rm_so);
725
726         if (strlen (tags)) {
727
728             message = notmuch_database_find_message (notmuch, message_id);
729             if (message == NULL) {
730                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
731                          message_id);
732             }
733
734             next = tags;
735             while (next) {
736                 tag = strsep (&next, " ");
737                 if (*tag == '\0')
738                     continue;
739                 if (message) {
740                     status = notmuch_message_add_tag (message, tag);
741                     if (status) {
742                         fprintf (stderr,
743                                  "Error applying tag %s to message %s:\n",
744                                  tag, message_id);
745                         fprintf (stderr, "%s\n",
746                                  notmuch_status_to_string (status));
747                     }
748                 } else {
749                     fprintf (stderr, "%s ", tag);
750                 }
751             }
752
753             if (message)
754                 notmuch_message_destroy (message);
755             else
756                 fprintf (stderr, ")\n");
757         }
758         free (message_id);
759         free (tags);
760     }
761
762     regfree (&regex);
763
764   DONE:
765     if (line)
766         free (line);
767     if (notmuch)
768         notmuch_database_close (notmuch);
769
770     return ret;
771 }
772
773 command_t commands[] = {
774     { "setup", setup_command,
775       "Interactively setup notmuch for first use.\n\n"
776       "\t\tInvoking notmuch with no command argument will run setup if\n"
777       "\t\tthe setup command has not previously been completed." },
778     { "new", new_command,
779       "Find and import any new messages.\n\n"
780       "\t\tScans all sub-directories of the database, adding new files\n"
781       "\t\tthat are found. Note: \"notmuch new\" will skip any\n"
782       "\t\tread-only directories, so you can use that to mark\n"
783       "\t\tdirectories that will not receive any new mail."},
784     { "search", search_command,
785       "<search-term> [...]\n\n"
786       "\t\tSearch for threads matching the given search terms.\n"
787       "\t\tOnce we actually implement search we'll document the\n"
788       "\t\tsyntax here." },
789     { "show", show_command,
790       "<thread-id>\n\n"
791       "\t\tShow the thread with the given thread ID (see 'search')." },
792     { "dump", dump_command,
793       "[<filename>]\n\n"
794       "\t\tCreate a plain-text dump of the tags for each message\n"
795       "\t\twriting to the given filename, if any, or to stdout.\n"
796       "\t\tThese tags are the only data in the notmuch database\n"
797       "\t\tthat can't be recreated from the messages themselves.\n"
798       "\t\tThe output of notmuch dump is therefore the only\n"
799       "\t\tcritical thing to backup (and much more friendly to\n"
800       "\t\tincremental backup than the native database files." },
801     { "restore", restore_command,
802       "<filename>\n\n"
803       "\t\tRestore the tags from the given dump file (see 'dump')." }
804 };
805
806 void
807 usage (void)
808 {
809     command_t *command;
810     int i;
811
812     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
813     fprintf (stderr, "\n");
814     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
815     fprintf (stderr, "\n");
816
817     for (i = 0; i < ARRAY_SIZE (commands); i++) {
818         command = &commands[i];
819
820         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
821     }
822 }
823     
824 int
825 main (int argc, char *argv[])
826 {
827     command_t *command;
828     int i;
829
830     if (argc == 1)
831         return setup_command (0, NULL);
832
833     for (i = 0; i < ARRAY_SIZE (commands); i++) {
834         command = &commands[i];
835
836         if (strcmp (argv[1], command->name) == 0)
837             return (command->function) (argc - 2, &argv[2]);
838     }
839
840     /* Don't complain about "help" being an unknown command when we're
841        about to provide exactly what's wanted anyway. */
842     if (strcmp (argv[1], "help") == 0 ||
843         strcmp (argv[1], "--help") == 0)
844     {
845         fprintf (stderr, "The notmuch mail system.\n\n");
846     } else {
847         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
848     }
849     usage ();
850     exit (1);
851
852     return 0;
853 }