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