]> git.notmuchmail.org Git - notmuch/blob - notmuch.c
Make "notmuch new" ignore directories that are read-only.
[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     notmuch_database_set_timestamp (notmuch, path, path_mtime);
274
275   DONE:
276     if (next)
277         free (next);
278     if (entry)
279         free (entry);
280     if (dir)
281         closedir (dir);
282
283     return ret;
284 }
285
286 /* This is the top-level entry point for add_files. It does a couple
287  * of error checks, and then calls into the recursive function,
288  * (avoiding the repeating of these error checks at every
289  * level---which would be useless becaues we already do a stat() at
290  * the level above). */
291 static notmuch_status_t
292 add_files (notmuch_database_t *notmuch,
293            const char *path,
294            add_files_state_t *state)
295 {
296     struct stat st;
297
298     if (stat (path, &st)) {
299         fprintf (stderr, "Error reading directory %s: %s\n",
300                  path, strerror (errno));
301         return NOTMUCH_STATUS_FILE_ERROR;
302     }
303
304     if (! S_ISDIR (st.st_mode)) {
305         fprintf (stderr, "Error: %s is not a directory.\n", path);
306         return NOTMUCH_STATUS_FILE_ERROR;
307     }
308
309     return add_files_recursive (notmuch, path, &st, state);
310 }
311
312 /* Recursively count all regular files in path and all sub-direcotries
313  * of path.  The result is added to *count (which should be
314  * initialized to zero by the top-level caller before calling
315  * count_files). */
316 void
317 count_files (const char *path, int *count)
318 {
319     DIR *dir;
320     struct dirent *entry, *e;
321     int entry_length;
322     int err;
323     char *next;
324     struct stat st;
325
326     dir = opendir (path);
327
328     if (dir == NULL) {
329         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
330                  path, strerror (errno));
331         return;
332     }
333
334     entry_length = offsetof (struct dirent, d_name) +
335         pathconf (path, _PC_NAME_MAX) + 1;
336     entry = malloc (entry_length);
337
338     while (1) {
339         err = readdir_r (dir, entry, &e);
340         if (err) {
341             fprintf (stderr, "Error reading directory: %s\n",
342                      strerror (errno));
343             free (entry);
344             return;
345         }
346
347         if (e == NULL)
348             break;
349
350         /* Ignore special directories to avoid infinite recursion.
351          * Also ignore the .notmuch directory.
352          */
353         /* XXX: Eventually we'll want more sophistication to let the
354          * user specify files to be ignored. */
355         if (strcmp (entry->d_name, ".") == 0 ||
356             strcmp (entry->d_name, "..") == 0 ||
357             strcmp (entry->d_name, ".notmuch") == 0)
358         {
359             continue;
360         }
361
362         next = g_strdup_printf ("%s/%s", path, entry->d_name);
363
364         stat (next, &st);
365
366         if (S_ISREG (st.st_mode)) {
367             *count = *count + 1;
368             if (*count % 1000 == 0) {
369                 printf ("Found %d files so far.\r", *count);
370                 fflush (stdout);
371             }
372         } else if (S_ISDIR (st.st_mode)) {
373             count_files (next, count);
374         }
375
376         free (next);
377     }
378
379     free (entry);
380
381     closedir (dir);
382 }
383
384 int
385 setup_command (int argc, char *argv[])
386 {
387     notmuch_database_t *notmuch = NULL;
388     char *default_path, *mail_directory = NULL;
389     size_t line_size;
390     int count;
391     add_files_state_t add_files_state;
392     double elapsed;
393     struct timeval tv_now;
394     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
395
396     printf ("Welcome to notmuch!\n\n");
397
398     printf ("The goal of notmuch is to help you manage and search your collection of\n"
399             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
400
401     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
402             "(where you already have mail stored and where messages will be delivered\n"
403             "in the future). This directory can contain any number of sub-directories\n"
404             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
405             "archives are perfect). If there are other, non-email files (such as\n"
406             "indexes maintained by other email programs) then notmuch will do its\n"
407             "best to detect those and ignore them.\n\n");
408
409     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
410             "messages), will not work with notmuch. If that's how your mail is currently\n"
411             "stored, we recommend you first convert it to maildir format with a utility\n"
412             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
413             "once the conversion is complete.\n\n");
414
415
416     default_path = notmuch_database_default_path ();
417     printf ("Top-level mail directory [%s]: ", default_path);
418     fflush (stdout);
419
420     getline (&mail_directory, &line_size, stdin);
421     chomp_newline (mail_directory);
422
423     printf ("\n");
424
425     if (mail_directory == NULL || strlen (mail_directory) == 0) {
426         if (mail_directory)
427             free (mail_directory);
428         mail_directory = default_path;
429     } else {
430         /* XXX: Instead of telling the user to use an environment
431          * variable here, we should really be writing out a configuration
432          * file and loading that on the next run. */
433         if (strcmp (mail_directory, default_path)) {
434             printf ("Note: Since you are not using the default path, you will want to set\n"
435                     "the NOTMUCH_BASE environment variable to %s so that\n"
436                     "future calls to notmuch commands will know where to find your mail.\n",
437                     mail_directory);
438             printf ("For example, if you are using bash for your shell, add:\n\n");
439             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
440             printf ("to your ~/.bashrc file.\n\n");
441         }
442         free (default_path);
443     }
444
445     notmuch = notmuch_database_create (mail_directory);
446     if (notmuch == NULL) {
447         fprintf (stderr, "Failed to create new notmuch database at %s\n",
448                  mail_directory);
449         ret = NOTMUCH_STATUS_FILE_ERROR;
450         goto DONE;
451     }
452
453     printf ("OK. Let's take a look at the mail we can find in the directory\n");
454     printf ("%s ...\n", mail_directory);
455
456     count = 0;
457     count_files (mail_directory, &count);
458
459     printf ("Found %d total files. That's not much mail.\n\n", count);
460
461     printf ("Next, we'll inspect the messages and create a database of threads:\n");
462
463     add_files_state.ignore_read_only_directories = FALSE;
464     add_files_state.total_files = count;
465     add_files_state.processed_files = 0;
466     add_files_state.added_messages = 0;
467     gettimeofday (&add_files_state.tv_start, NULL);
468
469     ret = add_files (notmuch, mail_directory, &add_files_state);
470
471     gettimeofday (&tv_now, NULL);
472     elapsed = tv_elapsed (add_files_state.tv_start,
473                           tv_now);
474     printf ("Processed %d %s in ", add_files_state.processed_files,
475             add_files_state.processed_files == 1 ?
476             "file" : "total files");
477     print_formatted_seconds (elapsed);
478     if (elapsed > 1) {
479         printf (" (%d files/sec.).                 \n",
480                 (int) (add_files_state.processed_files / elapsed));
481     } else {
482         printf (".                    \n");
483     }
484     if (add_files_state.added_messages) {
485         printf ("Added %d %s to the database.\n\n",
486                 add_files_state.added_messages,
487                 add_files_state.added_messages == 1 ?
488                 "message" : "unique messages");
489     }
490
491     printf ("When new mail is delivered to %s in the future,\n"
492             "run \"notmuch new\" to add it to the database.\n\n",
493             mail_directory);
494     printf ("And if you have any sub-directories that are archives (that is,\n"
495             "they will never receive new mail), marking these directores as\n"
496             "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
497             "much more efficient (it won't even look in those directories).\n\n");
498
499     if (ret) {
500         printf ("Note: At least one error was encountered: %s\n",
501                 notmuch_status_to_string (ret));
502     }
503
504   DONE:
505     if (mail_directory)
506         free (mail_directory);
507     if (notmuch)
508         notmuch_database_close (notmuch);
509
510     return ret;
511 }
512
513 int
514 new_command (int argc, char *argv[])
515 {
516     notmuch_database_t *notmuch;
517     const char *mail_directory;
518     add_files_state_t add_files_state;
519     double elapsed;
520     struct timeval tv_now;
521     int ret = 0;
522
523     notmuch = notmuch_database_open (NULL);
524     if (notmuch == NULL) {
525         ret = 1;
526         goto DONE;
527     }
528
529     mail_directory = notmuch_database_get_path (notmuch);
530
531     add_files_state.ignore_read_only_directories = TRUE;
532     add_files_state.total_files = 0;
533     add_files_state.processed_files = 0;
534     add_files_state.added_messages = 0;
535     gettimeofday (&add_files_state.tv_start, NULL);
536
537     ret = add_files (notmuch, mail_directory, &add_files_state);
538
539     gettimeofday (&tv_now, NULL);
540     elapsed = tv_elapsed (add_files_state.tv_start,
541                           tv_now);
542     if (add_files_state.processed_files) {
543         printf ("Processed %d %s in ", add_files_state.processed_files,
544                 add_files_state.processed_files == 1 ?
545                 "file" : "total files");
546         print_formatted_seconds (elapsed);
547         if (elapsed > 1) {
548             printf (" (%d files/sec.).                 \n",
549                     (int) (add_files_state.processed_files / elapsed));
550         } else {
551             printf (".                    \n");
552         }
553     }
554     if (add_files_state.added_messages) {
555         printf ("Added %d new %s to the database (not much, really).\n",
556                 add_files_state.added_messages,
557                 add_files_state.added_messages == 1 ?
558                 "message" : "messages");
559     } else {
560         printf ("No new mail---and that's not much!\n");
561     }
562
563     if (ret) {
564         printf ("Note: At least one error was encountered: %s\n",
565                 notmuch_status_to_string (ret));
566     }
567
568   DONE:
569     if (notmuch)
570         notmuch_database_close (notmuch);
571
572     return ret;
573 }
574
575 int
576 search_command (int argc, char *argv[])
577 {
578     fprintf (stderr, "Error: search is not implemented yet.\n");
579     return 1;
580 }
581
582 int
583 show_command (int argc, char *argv[])
584 {
585     fprintf (stderr, "Error: show is not implemented yet.\n");
586     return 1;
587 }
588
589 int
590 dump_command (int argc, char *argv[])
591 {
592     FILE *output;
593     notmuch_database_t *notmuch;
594     notmuch_query_t *query;
595     notmuch_results_t *results;
596     notmuch_message_t *message;
597     notmuch_tags_t *tags;
598     int ret = 0;
599
600     if (argc) {
601         output = fopen (argv[0], "w");
602         if (output == NULL) {
603             fprintf (stderr, "Error opening %s for writing: %s\n",
604                      argv[0], strerror (errno));
605             ret = 1;
606             goto DONE;
607         }
608     } else {
609         output = stdout;
610     }
611
612     notmuch = notmuch_database_open (NULL);
613     if (notmuch == NULL) {
614         ret = 1;
615         goto DONE;
616     }
617
618     query = notmuch_query_create (notmuch, "");
619     if (query == NULL) {
620         fprintf (stderr, "Out of memory\n");
621         ret = 1;
622         goto DONE;
623     }
624
625     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
626
627     for (results = notmuch_query_search (query);
628          notmuch_results_has_more (results);
629          notmuch_results_advance (results))
630     {
631         int first = 1;
632         message = notmuch_results_get (results);
633
634         fprintf (output,
635                  "%s (", notmuch_message_get_message_id (message));
636
637         for (tags = notmuch_message_get_tags (message);
638              notmuch_tags_has_more (tags);
639              notmuch_tags_advance (tags))
640         {
641             if (! first)
642                 fprintf (output, " ");
643
644             fprintf (output, "%s", notmuch_tags_get (tags));
645
646             first = 0;
647         }
648
649         fprintf (output, ")\n");
650
651         notmuch_message_destroy (message);
652     }
653
654     notmuch_query_destroy (query);
655
656   DONE:
657     if (notmuch)
658         notmuch_database_close (notmuch);
659     if (output != stdout)
660         fclose (output);
661
662     return ret;
663 }
664
665 int
666 restore_command (int argc, char *argv[])
667 {
668     FILE *input;
669     notmuch_database_t *notmuch;
670     char *line = NULL;
671     size_t line_size, line_len;
672     regex_t regex;
673     int rerr;
674     int ret = 0;
675
676     if (argc) {
677         input = fopen (argv[0], "r");
678         if (input == NULL) {
679             fprintf (stderr, "Error opening %s for reading: %s\n",
680                      argv[0], strerror (errno));
681             ret = 1;
682             goto DONE;
683         }
684     } else {
685         printf ("No filename given. Reading dump from stdin.\n");
686         input = stdin;
687     }
688
689     notmuch = notmuch_database_open (NULL);
690     if (notmuch == NULL) {
691         ret = 1;
692         goto DONE;
693     }
694
695     /* Dump output is one line per message. We match a sequence of
696      * non-space characters for the message-id, then one or more
697      * spaces, then a list of space-separated tags as a sequence of
698      * characters within literal '(' and ')'. */
699     xregcomp (&regex,
700               "^([^ ]+) \\(([^)]*)\\)$",
701               REG_EXTENDED);
702
703     while ((line_len = getline (&line, &line_size, input)) != -1) {
704         regmatch_t match[3];
705         char *message_id, *tags, *tag, *next;
706         notmuch_message_t *message;
707         notmuch_status_t status;
708
709         chomp_newline (line);
710
711         rerr = xregexec (&regex, line, 3, match, 0);
712         if (rerr == REG_NOMATCH)
713         {
714             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
715                      line);
716             continue;
717         }
718
719         message_id = xstrndup (line + match[1].rm_so,
720                                match[1].rm_eo - match[1].rm_so);
721         tags = xstrndup (line + match[2].rm_so,
722                          match[2].rm_eo - match[2].rm_so);
723
724         if (strlen (tags)) {
725
726             message = notmuch_database_find_message (notmuch, message_id);
727             if (message == NULL) {
728                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
729                          message_id);
730             }
731
732             next = tags;
733             while (next) {
734                 tag = strsep (&next, " ");
735                 if (*tag == '\0')
736                     continue;
737                 if (message) {
738                     status = notmuch_message_add_tag (message, tag);
739                     if (status) {
740                         fprintf (stderr,
741                                  "Error applying tag %s to message %s:\n",
742                                  tag, message_id);
743                         fprintf (stderr, "%s\n",
744                                  notmuch_status_to_string (status));
745                     }
746                 } else {
747                     fprintf (stderr, "%s ", tag);
748                 }
749             }
750
751             if (message)
752                 notmuch_message_destroy (message);
753             else
754                 fprintf (stderr, ")\n");
755         }
756         free (message_id);
757         free (tags);
758     }
759
760     regfree (&regex);
761
762   DONE:
763     if (line)
764         free (line);
765     if (notmuch)
766         notmuch_database_close (notmuch);
767
768     return ret;
769 }
770
771 command_t commands[] = {
772     { "setup", setup_command,
773       "Interactively setup notmuch for first use.\n\n"
774       "\t\tInvoking notmuch with no command argument will run setup if\n"
775       "\t\tthe setup command has not previously been completed." },
776     { "new", new_command,
777       "Find and import any new messages.\n\n"
778       "\t\tScans all sub-directories of the database, adding new files\n"
779       "\t\tthat are found. Note: \"notmuch new\" will skip any\n"
780       "\t\tread-only directories, so you can use that to mark\n"
781       "\t\tdirectories that will not receive any new mail."},
782     { "search", search_command,
783       "<search-term> [...]\n\n"
784       "\t\tSearch for threads matching the given search terms.\n"
785       "\t\tOnce we actually implement search we'll document the\n"
786       "\t\tsyntax here." },
787     { "show", show_command,
788       "<thread-id>\n\n"
789       "\t\tShow the thread with the given thread ID (see 'search')." },
790     { "dump", dump_command,
791       "[<filename>]\n\n"
792       "\t\tCreate a plain-text dump of the tags for each message\n"
793       "\t\twriting to the given filename, if any, or to stdout.\n"
794       "\t\tThese tags are the only data in the notmuch database\n"
795       "\t\tthat can't be recreated from the messages themselves.\n"
796       "\t\tThe output of notmuch dump is therefore the only\n"
797       "\t\tcritical thing to backup (and much more friendly to\n"
798       "\t\tincremental backup than the native database files." },
799     { "restore", restore_command,
800       "<filename>\n\n"
801       "\t\tRestore the tags from the given dump file (see 'dump')." }
802 };
803
804 void
805 usage (void)
806 {
807     command_t *command;
808     int i;
809
810     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
811     fprintf (stderr, "\n");
812     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
813     fprintf (stderr, "\n");
814
815     for (i = 0; i < ARRAY_SIZE (commands); i++) {
816         command = &commands[i];
817
818         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
819     }
820 }
821     
822 int
823 main (int argc, char *argv[])
824 {
825     command_t *command;
826     int i;
827
828     if (argc == 1)
829         return setup_command (0, NULL);
830
831     for (i = 0; i < ARRAY_SIZE (commands); i++) {
832         command = &commands[i];
833
834         if (strcmp (argv[1], command->name) == 0)
835             return (command->function) (argc - 2, &argv[2]);
836     }
837
838     /* Don't complain about "help" being an unknown command when we're
839        about to provide exactly what's wanted anyway. */
840     if (strcmp (argv[1], "help") == 0 ||
841         strcmp (argv[1], "--help") == 0)
842     {
843         fprintf (stderr, "The notmuch mail system.\n\n");
844     } else {
845         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
846     }
847     usage ();
848     exit (1);
849
850     return 0;
851 }