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