]> git.notmuchmail.org Git - notmuch/blob - notmuch.c
Add new "notmuch tag" command for adding/removing tags.
[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 /* Construct a single query string from the passed arguments, using
639  * 'ctx' as the talloc owner for all allocations.
640  *
641  * Currently, the arguments are just connected with space characters,
642  * but we might do more processing in the future, (such as inserting
643  * any AND operators needed to work around Xapian QueryParser bugs).
644  *
645  * This function returns NULL in case of insufficient memory.
646  */
647 static char *
648 query_string_from_args (void *ctx, int argc, char *argv[])
649 {
650     char *query_string;
651     int i;
652
653     query_string = talloc_strdup (ctx, "");
654     if (query_string == NULL)
655         return NULL;
656
657     for (i = 0; i < argc; i++) {
658         if (i != 0) {
659             query_string = talloc_strdup_append (query_string, " ");
660             if (query_string == NULL)
661                 return NULL;
662         }
663
664         query_string = talloc_strdup_append (query_string, argv[i]);
665         if (query_string == NULL)
666             return NULL;
667     }
668
669     return query_string;
670 }
671
672 static int
673 search_command (int argc, char *argv[])
674 {
675     void *local = talloc_new (NULL);
676     notmuch_database_t *notmuch = NULL;
677     notmuch_query_t *query;
678     notmuch_thread_results_t *results;
679     notmuch_thread_t *thread;
680     notmuch_tags_t *tags;
681     char *query_str;
682     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
683
684     notmuch = notmuch_database_open (NULL);
685     if (notmuch == NULL) {
686         ret = 1;
687         goto DONE;
688     }
689
690     query_str = query_string_from_args (local, argc, argv);
691
692     query = notmuch_query_create (notmuch, query_str);
693     if (query == NULL) {
694         fprintf (stderr, "Out of memory\n");
695         ret = 1;
696         goto DONE;
697     }
698
699     for (results = notmuch_query_search_threads (query);
700          notmuch_thread_results_has_more (results);
701          notmuch_thread_results_advance (results))
702     {
703         int first = 1;
704
705         thread = notmuch_thread_results_get (results);
706
707         printf ("%s %s",
708                 notmuch_thread_get_thread_id (thread),
709                 notmuch_thread_get_subject (thread));
710
711         printf (" (");
712         for (tags = notmuch_thread_get_tags (thread);
713              notmuch_tags_has_more (tags);
714              notmuch_tags_advance (tags))
715         {
716             if (! first)
717                 printf (" ");
718             printf ("%s", notmuch_tags_get (tags));
719             first = 0;
720         }
721         printf (")\n");
722
723         notmuch_thread_destroy (thread);
724     }
725
726     notmuch_query_destroy (query);
727
728   DONE:
729     if (notmuch)
730         notmuch_database_close (notmuch);
731     talloc_free (local);
732
733     return ret;
734 }
735
736 static int
737 show_command (unused (int argc), unused (char *argv[]))
738 {
739     fprintf (stderr, "Error: show is not implemented yet.\n");
740     return 1;
741 }
742
743 static int
744 tag_command (unused (int argc), unused (char *argv[]))
745 {
746     void *local;
747     int *add_tags, *remove_tags;
748     int add_tags_count = 0;
749     int remove_tags_count = 0;
750     char *query_string;
751     notmuch_database_t *notmuch = NULL;
752     notmuch_query_t *query;
753     notmuch_message_results_t *results;
754     notmuch_message_t *message;
755     int ret = 0;
756     int i;
757
758     local = talloc_new (NULL);
759     if (local == NULL) {
760         ret = 1;
761         goto DONE;
762     }
763
764     add_tags = talloc_size (local, argc * sizeof (int));
765     if (add_tags == NULL) {
766         ret = 1;
767         goto DONE;
768     }
769
770     remove_tags = talloc_size (local, argc * sizeof (int));
771     if (remove_tags == NULL) {
772         ret = 1;
773         goto DONE;
774     }
775
776     for (i = 0; i < argc; i++) {
777         if (strcmp (argv[i], "--") == 0) {
778             i++;
779             break;
780         }
781         if (argv[i][0] == '+') {
782             add_tags[add_tags_count++] = i;
783         } else if (argv[i][0] == '-') {
784             remove_tags[remove_tags_count++] = i;
785         } else {
786             break;
787         }
788     }
789
790     if (add_tags_count == 0 && remove_tags_count == 0) {
791         fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
792         ret = 1;
793         goto DONE;
794     }
795
796     if (i == argc) {
797         fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
798         ret = 1;
799         goto DONE;
800     }
801
802     notmuch = notmuch_database_open (NULL);
803     if (notmuch == NULL) {
804         ret = 1;
805         goto DONE;
806     }
807
808     query_string = query_string_from_args (local, argc - i, &argv[i]);
809
810     query = notmuch_query_create (notmuch, query_string);
811     if (query == NULL) {
812         fprintf (stderr, "Out of memory.\n");
813         ret = 1;
814         goto DONE;
815     }
816
817     for (results = notmuch_query_search_messages (query);
818          notmuch_message_results_has_more (results);
819          notmuch_message_results_advance (results))
820     {
821         message = notmuch_message_results_get (results);
822
823         notmuch_message_freeze (message);
824
825         for (i = 0; i < remove_tags_count; i++)
826             notmuch_message_remove_tag (message,
827                                         argv[remove_tags[i]] + 1);
828
829         for (i = 0; i < add_tags_count; i++)
830             notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
831
832         notmuch_message_thaw (message);
833
834         notmuch_message_destroy (message);
835     }
836
837     notmuch_query_destroy (query);
838
839   DONE:
840     if (notmuch)
841         notmuch_database_close (notmuch);
842
843     talloc_free (local);
844
845     return ret;
846 }
847
848 static int
849 dump_command (int argc, char *argv[])
850 {
851     FILE *output = NULL;
852     notmuch_database_t *notmuch = NULL;
853     notmuch_query_t *query;
854     notmuch_message_results_t *results;
855     notmuch_message_t *message;
856     notmuch_tags_t *tags;
857     int ret = 0;
858
859     if (argc) {
860         output = fopen (argv[0], "w");
861         if (output == NULL) {
862             fprintf (stderr, "Error opening %s for writing: %s\n",
863                      argv[0], strerror (errno));
864             ret = 1;
865             goto DONE;
866         }
867     } else {
868         output = stdout;
869     }
870
871     notmuch = notmuch_database_open (NULL);
872     if (notmuch == NULL) {
873         ret = 1;
874         goto DONE;
875     }
876
877     query = notmuch_query_create (notmuch, "");
878     if (query == NULL) {
879         fprintf (stderr, "Out of memory\n");
880         ret = 1;
881         goto DONE;
882     }
883
884     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
885
886     for (results = notmuch_query_search_messages (query);
887          notmuch_message_results_has_more (results);
888          notmuch_message_results_advance (results))
889     {
890         int first = 1;
891         message = notmuch_message_results_get (results);
892
893         fprintf (output,
894                  "%s (", notmuch_message_get_message_id (message));
895
896         for (tags = notmuch_message_get_tags (message);
897              notmuch_tags_has_more (tags);
898              notmuch_tags_advance (tags))
899         {
900             if (! first)
901                 fprintf (output, " ");
902
903             fprintf (output, "%s", notmuch_tags_get (tags));
904
905             first = 0;
906         }
907
908         fprintf (output, ")\n");
909
910         notmuch_message_destroy (message);
911     }
912
913     notmuch_query_destroy (query);
914
915   DONE:
916     if (notmuch)
917         notmuch_database_close (notmuch);
918     if (output && output != stdout)
919         fclose (output);
920
921     return ret;
922 }
923
924 static int
925 restore_command (int argc, char *argv[])
926 {
927     FILE *input = NULL;
928     notmuch_database_t *notmuch = NULL;
929     char *line = NULL;
930     size_t line_size;
931     ssize_t line_len;
932     regex_t regex;
933     int rerr;
934     int ret = 0;
935
936     if (argc) {
937         input = fopen (argv[0], "r");
938         if (input == NULL) {
939             fprintf (stderr, "Error opening %s for reading: %s\n",
940                      argv[0], strerror (errno));
941             ret = 1;
942             goto DONE;
943         }
944     } else {
945         printf ("No filename given. Reading dump from stdin.\n");
946         input = stdin;
947     }
948
949     notmuch = notmuch_database_open (NULL);
950     if (notmuch == NULL) {
951         ret = 1;
952         goto DONE;
953     }
954
955     /* Dump output is one line per message. We match a sequence of
956      * non-space characters for the message-id, then one or more
957      * spaces, then a list of space-separated tags as a sequence of
958      * characters within literal '(' and ')'. */
959     xregcomp (&regex,
960               "^([^ ]+) \\(([^)]*)\\)$",
961               REG_EXTENDED);
962
963     while ((line_len = getline (&line, &line_size, input)) != -1) {
964         regmatch_t match[3];
965         char *message_id, *tags, *tag, *next;
966         notmuch_message_t *message;
967         notmuch_status_t status;
968
969         chomp_newline (line);
970
971         rerr = xregexec (&regex, line, 3, match, 0);
972         if (rerr == REG_NOMATCH)
973         {
974             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
975                      line);
976             continue;
977         }
978
979         message_id = xstrndup (line + match[1].rm_so,
980                                match[1].rm_eo - match[1].rm_so);
981         tags = xstrndup (line + match[2].rm_so,
982                          match[2].rm_eo - match[2].rm_so);
983
984         if (strlen (tags)) {
985
986             message = notmuch_database_find_message (notmuch, message_id);
987             if (message == NULL) {
988                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
989                          message_id);
990                 goto NEXT_LINE;
991             }
992
993             notmuch_message_freeze (message);
994
995             notmuch_message_remove_all_tags (message);
996
997             next = tags;
998             while (next) {
999                 tag = strsep (&next, " ");
1000                 if (*tag == '\0')
1001                     continue;
1002                 status = notmuch_message_add_tag (message, tag);
1003                 if (status) {
1004                     fprintf (stderr,
1005                              "Error applying tag %s to message %s:\n",
1006                              tag, message_id);
1007                     fprintf (stderr, "%s\n",
1008                              notmuch_status_to_string (status));
1009                 }
1010             }
1011
1012             notmuch_message_thaw (message);
1013             notmuch_message_destroy (message);
1014         }
1015       NEXT_LINE:
1016         free (message_id);
1017         free (tags);
1018     }
1019
1020     regfree (&regex);
1021
1022   DONE:
1023     if (line)
1024         free (line);
1025     if (notmuch)
1026         notmuch_database_close (notmuch);
1027     if (input && input != stdin)
1028         fclose (input);
1029
1030     return ret;
1031 }
1032
1033 command_t commands[] = {
1034     { "setup", setup_command,
1035       "Interactively setup notmuch for first use.\n\n"
1036       "\t\tInvoking notmuch with no command argument will run setup if\n"
1037       "\t\tthe setup command has not previously been completed." },
1038     { "new", new_command,
1039       "Find and import any new messages.\n\n"
1040       "\t\tScans all sub-directories of the database, adding new messages\n"
1041       "\t\tthat are found. Each new message will be tagges as both\n"
1042       "\t\t\"inbox\" and \"unread\".\n"
1043       "\n"
1044       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
1045       "\t\tso you can use that to mark tdirectories that will not\n"
1046       "\t\treceive any new mail (and make \"notmuch new\" faster)." },
1047     { "search", search_command,
1048       "<search-term> [...]\n\n"
1049       "\t\tSearch for threads matching the given search terms.\n"
1050       "\t\tOnce we actually implement search we'll document the\n"
1051       "\t\tsyntax here." },
1052     { "show", show_command,
1053       "<thread-id>\n\n"
1054       "\t\tShow the thread with the given thread ID (see 'search')." },
1055     { "tag", tag_command,
1056       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
1057       "\t\tAdd or remove the specified tags to all messages matching\n"
1058       "\t\tthe specified search terms. The search terms are handled\n"
1059       "\t\texactly as in 'search' so one can use that command first\n"
1060       "\t\tto see what will be modified.\n\n"
1061       "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n"
1062       "\t\tremoved. For each message, tag removal is before tag addition.\n\n"
1063       "\t\tThe beginning of <search-terms> is recognized by the first\n"
1064       "\t\targument that begins with neither '+' nor '-'. Support for\n"
1065       "\t\tan initial search term beginning with '+' or '-' is provided\n"
1066       "\t\tby allowing the user to specify a \"--\" argument to separate\n"
1067       "\t\tthe tags from the search terms." },
1068     { "dump", dump_command,
1069       "[<filename>]\n\n"
1070       "\t\tCreate a plain-text dump of the tags for each message\n"
1071       "\t\twriting to the given filename, if any, or to stdout.\n"
1072       "\t\tThese tags are the only data in the notmuch database\n"
1073       "\t\tthat can't be recreated from the messages themselves.\n"
1074       "\t\tThe output of notmuch dump is therefore the only\n"
1075       "\t\tcritical thing to backup (and much more friendly to\n"
1076       "\t\tincremental backup than the native database files." },
1077     { "restore", restore_command,
1078       "<filename>\n\n"
1079       "\t\tRestore the tags from the given dump file (see 'dump')." }
1080 };
1081
1082 static void
1083 usage (void)
1084 {
1085     command_t *command;
1086     unsigned int i;
1087
1088     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
1089     fprintf (stderr, "\n");
1090     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
1091     fprintf (stderr, "\n");
1092
1093     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1094         command = &commands[i];
1095
1096         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
1097     }
1098 }
1099     
1100 int
1101 main (int argc, char *argv[])
1102 {
1103     command_t *command;
1104     unsigned int i;
1105
1106     if (argc == 1)
1107         return setup_command (0, NULL);
1108
1109     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1110         command = &commands[i];
1111
1112         if (strcmp (argv[1], command->name) == 0)
1113             return (command->function) (argc - 2, &argv[2]);
1114     }
1115
1116     /* Don't complain about "help" being an unknown command when we're
1117        about to provide exactly what's wanted anyway. */
1118     if (strcmp (argv[1], "help") == 0 ||
1119         strcmp (argv[1], "--help") == 0)
1120     {
1121         fprintf (stderr, "The notmuch mail system.\n\n");
1122         usage ();
1123         return 0;
1124     } else {
1125         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
1126         usage ();
1127         return 1;
1128     }
1129 }