Add a preliminary "notmuch search" command.
[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     void *local = talloc_new (NULL);
581     notmuch_database_t *notmuch = NULL;
582     notmuch_query_t *query;
583     notmuch_results_t *results;
584     notmuch_message_t *message;
585     notmuch_tags_t *tags;
586     char *query_str;
587     int i;
588     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
589
590     notmuch = notmuch_database_open (NULL);
591     if (notmuch == NULL) {
592         ret = 1;
593         goto DONE;
594     }
595
596     /* XXX: Should add xtalloc wrappers here and use them. */
597     query_str = talloc_strdup (local, "");
598
599     for (i = 0; i < argc; i++) {
600         if (i != 0)
601             query_str = talloc_asprintf_append (query_str, " ");
602
603         query_str = talloc_asprintf_append (query_str, "%s", argv[i]);
604     }
605
606     query = notmuch_query_create (notmuch, query_str);
607     if (query == NULL) {
608         fprintf (stderr, "Out of memory\n");
609         ret = 1;
610         goto DONE;
611     }
612
613     for (results = notmuch_query_search (query);
614          notmuch_results_has_more (results);
615          notmuch_results_advance (results))
616     {
617         int first = 1;
618         message = notmuch_results_get (results);
619
620         printf ("%s (", notmuch_message_get_message_id (message));
621
622         for (tags = notmuch_message_get_tags (message);
623              notmuch_tags_has_more (tags);
624              notmuch_tags_advance (tags))
625         {
626             if (! first)
627                 printf (" ");
628
629             printf ("%s", notmuch_tags_get (tags));
630
631             first = 0;
632         }
633
634         printf (")\n");
635
636         notmuch_message_destroy (message);
637     }
638
639     notmuch_query_destroy (query);
640
641   DONE:
642     if (notmuch)
643         notmuch_database_close (notmuch);
644     talloc_free (local);
645
646     return ret;
647 }
648
649 int
650 show_command (int argc, char *argv[])
651 {
652     fprintf (stderr, "Error: show is not implemented yet.\n");
653     return 1;
654 }
655
656 int
657 dump_command (int argc, char *argv[])
658 {
659     FILE *output;
660     notmuch_database_t *notmuch = NULL;
661     notmuch_query_t *query;
662     notmuch_results_t *results;
663     notmuch_message_t *message;
664     notmuch_tags_t *tags;
665     int ret = 0;
666
667     if (argc) {
668         output = fopen (argv[0], "w");
669         if (output == NULL) {
670             fprintf (stderr, "Error opening %s for writing: %s\n",
671                      argv[0], strerror (errno));
672             ret = 1;
673             goto DONE;
674         }
675     } else {
676         output = stdout;
677     }
678
679     notmuch = notmuch_database_open (NULL);
680     if (notmuch == NULL) {
681         ret = 1;
682         goto DONE;
683     }
684
685     query = notmuch_query_create (notmuch, "");
686     if (query == NULL) {
687         fprintf (stderr, "Out of memory\n");
688         ret = 1;
689         goto DONE;
690     }
691
692     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
693
694     for (results = notmuch_query_search (query);
695          notmuch_results_has_more (results);
696          notmuch_results_advance (results))
697     {
698         int first = 1;
699         message = notmuch_results_get (results);
700
701         fprintf (output,
702                  "%s (", notmuch_message_get_message_id (message));
703
704         for (tags = notmuch_message_get_tags (message);
705              notmuch_tags_has_more (tags);
706              notmuch_tags_advance (tags))
707         {
708             if (! first)
709                 fprintf (output, " ");
710
711             fprintf (output, "%s", notmuch_tags_get (tags));
712
713             first = 0;
714         }
715
716         fprintf (output, ")\n");
717
718         notmuch_message_destroy (message);
719     }
720
721     notmuch_query_destroy (query);
722
723   DONE:
724     if (notmuch)
725         notmuch_database_close (notmuch);
726     if (output != stdout)
727         fclose (output);
728
729     return ret;
730 }
731
732 int
733 restore_command (int argc, char *argv[])
734 {
735     FILE *input;
736     notmuch_database_t *notmuch = NULL;
737     char *line = NULL;
738     size_t line_size, line_len;
739     regex_t regex;
740     int rerr;
741     int ret = 0;
742
743     if (argc) {
744         input = fopen (argv[0], "r");
745         if (input == NULL) {
746             fprintf (stderr, "Error opening %s for reading: %s\n",
747                      argv[0], strerror (errno));
748             ret = 1;
749             goto DONE;
750         }
751     } else {
752         printf ("No filename given. Reading dump from stdin.\n");
753         input = stdin;
754     }
755
756     notmuch = notmuch_database_open (NULL);
757     if (notmuch == NULL) {
758         ret = 1;
759         goto DONE;
760     }
761
762     /* Dump output is one line per message. We match a sequence of
763      * non-space characters for the message-id, then one or more
764      * spaces, then a list of space-separated tags as a sequence of
765      * characters within literal '(' and ')'. */
766     xregcomp (&regex,
767               "^([^ ]+) \\(([^)]*)\\)$",
768               REG_EXTENDED);
769
770     while ((line_len = getline (&line, &line_size, input)) != -1) {
771         regmatch_t match[3];
772         char *message_id, *tags, *tag, *next;
773         notmuch_message_t *message;
774         notmuch_status_t status;
775
776         chomp_newline (line);
777
778         rerr = xregexec (&regex, line, 3, match, 0);
779         if (rerr == REG_NOMATCH)
780         {
781             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
782                      line);
783             continue;
784         }
785
786         message_id = xstrndup (line + match[1].rm_so,
787                                match[1].rm_eo - match[1].rm_so);
788         tags = xstrndup (line + match[2].rm_so,
789                          match[2].rm_eo - match[2].rm_so);
790
791         if (strlen (tags)) {
792
793             message = notmuch_database_find_message (notmuch, message_id);
794             if (message == NULL) {
795                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
796                          message_id);
797             }
798
799             next = tags;
800             while (next) {
801                 tag = strsep (&next, " ");
802                 if (*tag == '\0')
803                     continue;
804                 if (message) {
805                     status = notmuch_message_add_tag (message, tag);
806                     if (status) {
807                         fprintf (stderr,
808                                  "Error applying tag %s to message %s:\n",
809                                  tag, message_id);
810                         fprintf (stderr, "%s\n",
811                                  notmuch_status_to_string (status));
812                     }
813                 } else {
814                     fprintf (stderr, "%s ", tag);
815                 }
816             }
817
818             if (message)
819                 notmuch_message_destroy (message);
820             else
821                 fprintf (stderr, ")\n");
822         }
823         free (message_id);
824         free (tags);
825     }
826
827     regfree (&regex);
828
829   DONE:
830     if (line)
831         free (line);
832     if (notmuch)
833         notmuch_database_close (notmuch);
834
835     return ret;
836 }
837
838 command_t commands[] = {
839     { "setup", setup_command,
840       "Interactively setup notmuch for first use.\n\n"
841       "\t\tInvoking notmuch with no command argument will run setup if\n"
842       "\t\tthe setup command has not previously been completed." },
843     { "new", new_command,
844       "Find and import any new messages.\n\n"
845       "\t\tScans all sub-directories of the database, adding new files\n"
846       "\t\tthat are found. Note: \"notmuch new\" will skip any\n"
847       "\t\tread-only directories, so you can use that to mark\n"
848       "\t\tdirectories that will not receive any new mail."},
849     { "search", search_command,
850       "<search-term> [...]\n\n"
851       "\t\tSearch for threads matching the given search terms.\n"
852       "\t\tOnce we actually implement search we'll document the\n"
853       "\t\tsyntax here." },
854     { "show", show_command,
855       "<thread-id>\n\n"
856       "\t\tShow the thread with the given thread ID (see 'search')." },
857     { "dump", dump_command,
858       "[<filename>]\n\n"
859       "\t\tCreate a plain-text dump of the tags for each message\n"
860       "\t\twriting to the given filename, if any, or to stdout.\n"
861       "\t\tThese tags are the only data in the notmuch database\n"
862       "\t\tthat can't be recreated from the messages themselves.\n"
863       "\t\tThe output of notmuch dump is therefore the only\n"
864       "\t\tcritical thing to backup (and much more friendly to\n"
865       "\t\tincremental backup than the native database files." },
866     { "restore", restore_command,
867       "<filename>\n\n"
868       "\t\tRestore the tags from the given dump file (see 'dump')." }
869 };
870
871 void
872 usage (void)
873 {
874     command_t *command;
875     int i;
876
877     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
878     fprintf (stderr, "\n");
879     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
880     fprintf (stderr, "\n");
881
882     for (i = 0; i < ARRAY_SIZE (commands); i++) {
883         command = &commands[i];
884
885         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
886     }
887 }
888     
889 int
890 main (int argc, char *argv[])
891 {
892     command_t *command;
893     int i;
894
895     if (argc == 1)
896         return setup_command (0, NULL);
897
898     for (i = 0; i < ARRAY_SIZE (commands); i++) {
899         command = &commands[i];
900
901         if (strcmp (argv[1], command->name) == 0)
902             return (command->function) (argc - 2, &argv[2]);
903     }
904
905     /* Don't complain about "help" being an unknown command when we're
906        about to provide exactly what's wanted anyway. */
907     if (strcmp (argv[1], "help") == 0 ||
908         strcmp (argv[1], "--help") == 0)
909     {
910         fprintf (stderr, "The notmuch mail system.\n\n");
911     } else {
912         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
913     }
914     usage ();
915     exit (1);
916
917     return 0;
918 }