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