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