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