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