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