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