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