f47e4fcbb65d6ddf805ecbedb07c4fa61a097861
[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 #include <signal.h>
42
43 #include <talloc.h>
44
45 #define unused(x) x __attribute__ ((unused))
46
47 /* There's no point in continuing when we've detected that we've done
48  * something wrong internally (as opposed to the user passing in a
49  * bogus value).
50  *
51  * Note that __location__ comes from talloc.h.
52  */
53 #define INTERNAL_ERROR(format, ...)                     \
54     do {                                                \
55         fprintf(stderr,                                 \
56                 "Internal error: " format " (%s)\n",    \
57                 ##__VA_ARGS__, __location__);           \
58         exit (1);                                       \
59     } while (0)
60
61 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
62
63 typedef int (*command_function_t) (int argc, char *argv[]);
64
65 typedef struct command {
66     const char *name;
67     command_function_t function;
68     const char *summary;
69     const char *documentation;
70 } command_t;
71
72 typedef void (*add_files_callback_t) (notmuch_message_t *message);
73
74 typedef struct {
75     int ignore_read_only_directories;
76     int saw_read_only_directory;
77
78     int total_files;
79     int processed_files;
80     int added_messages;
81     struct timeval tv_start;
82
83     add_files_callback_t callback;
84 } add_files_state_t;
85
86 static void
87 chomp_newline (char *str)
88 {
89     if (str && str[strlen(str)-1] == '\n')
90         str[strlen(str)-1] = '\0';
91 }
92
93 /* Compute the number of seconds elapsed from start to end. */
94 static double
95 tv_elapsed (struct timeval start, struct timeval end)
96 {
97     return ((end.tv_sec - start.tv_sec) +
98             (end.tv_usec - start.tv_usec) / 1e6);
99 }
100
101 static void
102 print_formatted_seconds (double seconds)
103 {
104     int hours;
105     int minutes;
106
107     if (seconds < 1) {
108         printf ("almost no time");
109         return;
110     }
111
112     if (seconds > 3600) {
113         hours = (int) seconds / 3600;
114         printf ("%dh ", hours);
115         seconds -= hours * 3600;
116     }
117
118     if (seconds > 60) {
119         minutes = (int) seconds / 60;
120         printf ("%dm ", minutes);
121         seconds -= minutes * 60;
122     }
123
124     printf ("%ds", (int) seconds);
125 }
126
127 static volatile sig_atomic_t do_add_files_print_progress = 0;
128
129 static void
130 handle_sigalrm (unused (int signal))
131 {
132     do_add_files_print_progress = 1;
133 }
134
135 static void
136 add_files_print_progress (add_files_state_t *state)
137 {
138     struct timeval tv_now;
139     double elapsed_overall, rate_overall;
140
141     gettimeofday (&tv_now, NULL);
142
143     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
144     rate_overall = (state->processed_files) / elapsed_overall;
145
146     printf ("Processed %d", state->processed_files);
147
148     if (state->total_files) {
149         printf (" of %d files (", state->total_files);
150         print_formatted_seconds ((state->total_files - state->processed_files) /
151                                  rate_overall);
152         printf (" remaining).      \r");
153     } else {
154         printf (" files (%d files/sec.)    \r", (int) rate_overall);
155     }
156
157     fflush (stdout);
158 }
159
160 /* Examine 'path' recursively as follows:
161  *
162  *   o Ask the filesystem for the mtime of 'path' (path_mtime)
163  *
164  *   o Ask the database for its timestamp of 'path' (path_dbtime)
165  *
166  *   o If 'path_mtime' > 'path_dbtime'
167  *
168  *       o For each regular file in 'path' with mtime newer than the
169  *         'path_dbtime' call add_message to add the file to the
170  *         database.
171  *
172  *       o For each sub-directory of path, recursively call into this
173  *         same function.
174  *
175  *   o Tell the database to update its time of 'path' to 'path_mtime'
176  *
177  * The 'struct stat *st' must point to a structure that has already
178  * been initialized for 'path' by calling stat().
179  */
180 static notmuch_status_t
181 add_files_recursive (notmuch_database_t *notmuch,
182                      const char *path,
183                      struct stat *st,
184                      add_files_state_t *state)
185 {
186     DIR *dir = NULL;
187     struct dirent *e, *entry = NULL;
188     int entry_length;
189     int err;
190     char *next = NULL;
191     time_t path_mtime, path_dbtime;
192     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
193     notmuch_message_t *message = NULL, **closure;
194
195     /* If we're told to, we bail out on encountering a read-only
196      * directory, (with this being a clear clue from the user to
197      * Notmuch that new mail won't be arriving there and we need not
198      * look. */
199     if (state->ignore_read_only_directories &&
200         (st->st_mode & S_IWUSR) == 0)
201     {
202         state->saw_read_only_directory = TRUE;
203         goto DONE;
204     }
205
206     path_mtime = st->st_mtime;
207
208     path_dbtime = notmuch_database_get_timestamp (notmuch, path);
209
210     dir = opendir (path);
211     if (dir == NULL) {
212         fprintf (stderr, "Error opening directory %s: %s\n",
213                  path, strerror (errno));
214         ret = NOTMUCH_STATUS_FILE_ERROR;
215         goto DONE;
216     }
217
218     entry_length = offsetof (struct dirent, d_name) +
219         pathconf (path, _PC_NAME_MAX) + 1;
220     entry = malloc (entry_length);
221
222     while (1) {
223         err = readdir_r (dir, entry, &e);
224         if (err) {
225             fprintf (stderr, "Error reading directory: %s\n",
226                      strerror (errno));
227             ret = NOTMUCH_STATUS_FILE_ERROR;
228             goto DONE;
229         }
230
231         if (e == NULL)
232             break;
233
234         /* If this directory hasn't been modified since the last
235          * add_files, then we only need to look further for
236          * sub-directories. */
237         if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
238             continue;
239
240         /* Ignore special directories to avoid infinite recursion.
241          * Also ignore the .notmuch directory.
242          */
243         /* XXX: Eventually we'll want more sophistication to let the
244          * user specify files to be ignored. */
245         if (strcmp (entry->d_name, ".") == 0 ||
246             strcmp (entry->d_name, "..") == 0 ||
247             strcmp (entry->d_name, ".notmuch") ==0)
248         {
249             continue;
250         }
251
252         next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
253
254         if (stat (next, st)) {
255             fprintf (stderr, "Error reading %s: %s\n",
256                      next, strerror (errno));
257             ret = NOTMUCH_STATUS_FILE_ERROR;
258             continue;
259         }
260
261         if (S_ISREG (st->st_mode)) {
262             /* If the file hasn't been modified since the last
263              * add_files, then we need not look at it. */
264             if (st->st_mtime > path_dbtime) {
265                 state->processed_files++;
266
267                 if (state->callback)
268                     closure = &message;
269                 else
270                     closure = NULL;
271
272                 status = notmuch_database_add_message (notmuch, next, closure);
273                 switch (status) {
274                     /* success */
275                     case NOTMUCH_STATUS_SUCCESS:
276                         state->added_messages++;
277                         if (state->callback)
278                             (state->callback) (message);
279                         break;
280                     /* Non-fatal issues (go on to next file) */
281                     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
282                         /* Stay silent on this one. */
283                         break;
284                     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
285                         fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
286                                  next);
287                         break;
288                     /* Fatal issues. Don't process anymore. */
289                     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
290                     case NOTMUCH_STATUS_OUT_OF_MEMORY:
291                         fprintf (stderr, "Error: %s. Halting processing.\n",
292                                  notmuch_status_to_string (status));
293                         ret = status;
294                         goto DONE;
295                     default:
296                     case NOTMUCH_STATUS_FILE_ERROR:
297                     case NOTMUCH_STATUS_NULL_POINTER:
298                     case NOTMUCH_STATUS_TAG_TOO_LONG:
299                     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
300                     case NOTMUCH_STATUS_LAST_STATUS:
301                         INTERNAL_ERROR ("add_message returned unexpected value: %d",  status);
302                         goto DONE;
303                 }
304
305                 if (message) {
306                     notmuch_message_destroy (message);
307                     message = NULL;
308                 }
309
310                 if (do_add_files_print_progress) {
311                     do_add_files_print_progress = 0;
312                     add_files_print_progress (state);
313                 }
314             }
315         } else if (S_ISDIR (st->st_mode)) {
316             status = add_files_recursive (notmuch, next, st, state);
317             if (status && ret == NOTMUCH_STATUS_SUCCESS)
318                 ret = status;
319         }
320
321         talloc_free (next);
322         next = NULL;
323     }
324
325     status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
326     if (status && ret == NOTMUCH_STATUS_SUCCESS)
327         ret = status;
328
329   DONE:
330     if (next)
331         talloc_free (next);
332     if (entry)
333         free (entry);
334     if (dir)
335         closedir (dir);
336
337     return ret;
338 }
339
340 /* This is the top-level entry point for add_files. It does a couple
341  * of error checks, sets up the progress-printing timer and then calls
342  * into the recursive function. */
343 static notmuch_status_t
344 add_files (notmuch_database_t *notmuch,
345            const char *path,
346            add_files_state_t *state)
347 {
348     struct stat st;
349     notmuch_status_t status;
350     struct sigaction action;
351     struct itimerval timerval;
352
353     if (stat (path, &st)) {
354         fprintf (stderr, "Error reading directory %s: %s\n",
355                  path, strerror (errno));
356         return NOTMUCH_STATUS_FILE_ERROR;
357     }
358
359     if (! S_ISDIR (st.st_mode)) {
360         fprintf (stderr, "Error: %s is not a directory.\n", path);
361         return NOTMUCH_STATUS_FILE_ERROR;
362     }
363
364     /* Setup our handler for SIGALRM */
365     memset (&action, 0, sizeof (struct sigaction));
366     action.sa_handler = handle_sigalrm;
367     sigemptyset (&action.sa_mask);
368     action.sa_flags = SA_RESTART;
369     sigaction (SIGALRM, &action, NULL);
370
371     /* Then start a timer to send SIGALRM once per second. */
372     timerval.it_interval.tv_sec = 1;
373     timerval.it_interval.tv_usec = 0;
374     timerval.it_value.tv_sec = 1;
375     timerval.it_value.tv_usec = 0;
376     setitimer (ITIMER_REAL, &timerval, NULL);
377
378     status = add_files_recursive (notmuch, path, &st, state);
379
380     /* Now stop the timer. */
381     timerval.it_interval.tv_sec = 0;
382     timerval.it_interval.tv_usec = 0;
383     timerval.it_value.tv_sec = 0;
384     timerval.it_value.tv_usec = 0;
385     setitimer (ITIMER_REAL, &timerval, NULL);
386
387     /* And disable the signal handler. */
388     action.sa_handler = SIG_IGN;
389     sigaction (SIGALRM, &action, NULL);
390
391     return status;
392 }
393
394 /* Recursively count all regular files in path and all sub-direcotries
395  * of path.  The result is added to *count (which should be
396  * initialized to zero by the top-level caller before calling
397  * count_files). */
398 static void
399 count_files (const char *path, int *count)
400 {
401     DIR *dir;
402     struct dirent *e, *entry = NULL;
403     int entry_length;
404     int err;
405     char *next;
406     struct stat st;
407
408     dir = opendir (path);
409
410     if (dir == NULL) {
411         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
412                  path, strerror (errno));
413         goto DONE;
414     }
415
416     entry_length = offsetof (struct dirent, d_name) +
417         pathconf (path, _PC_NAME_MAX) + 1;
418     entry = malloc (entry_length);
419
420     while (1) {
421         err = readdir_r (dir, entry, &e);
422         if (err) {
423             fprintf (stderr, "Error reading directory: %s\n",
424                      strerror (errno));
425             free (entry);
426             goto DONE;
427         }
428
429         if (e == NULL)
430             break;
431
432         /* Ignore special directories to avoid infinite recursion.
433          * Also ignore the .notmuch directory.
434          */
435         /* XXX: Eventually we'll want more sophistication to let the
436          * user specify files to be ignored. */
437         if (strcmp (entry->d_name, ".") == 0 ||
438             strcmp (entry->d_name, "..") == 0 ||
439             strcmp (entry->d_name, ".notmuch") == 0)
440         {
441             continue;
442         }
443
444         if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
445             next = NULL;
446             fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
447                      path, entry->d_name);
448             continue;
449         }
450
451         stat (next, &st);
452
453         if (S_ISREG (st.st_mode)) {
454             *count = *count + 1;
455             if (*count % 1000 == 0) {
456                 printf ("Found %d files so far.\r", *count);
457                 fflush (stdout);
458             }
459         } else if (S_ISDIR (st.st_mode)) {
460             count_files (next, count);
461         }
462
463         free (next);
464     }
465
466   DONE:
467     if (entry)
468         free (entry);
469
470     closedir (dir);
471 }
472
473 static int
474 setup_command (unused (int argc), unused (char *argv[]))
475 {
476     notmuch_database_t *notmuch = NULL;
477     char *default_path, *mail_directory = NULL;
478     size_t line_size;
479     int count;
480     add_files_state_t add_files_state;
481     double elapsed;
482     struct timeval tv_now;
483     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
484
485     printf ("Welcome to notmuch!\n\n");
486
487     printf ("The goal of notmuch is to help you manage and search your collection of\n"
488             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
489
490     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
491             "(where you already have mail stored and where messages will be delivered\n"
492             "in the future). This directory can contain any number of sub-directories\n"
493             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
494             "archives are perfect). If there are other, non-email files (such as\n"
495             "indexes maintained by other email programs) then notmuch will do its\n"
496             "best to detect those and ignore them.\n\n");
497
498     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
499             "messages), will not work with notmuch. If that's how your mail is currently\n"
500             "stored, we recommend you first convert it to maildir format with a utility\n"
501             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
502             "once the conversion is complete.\n\n");
503
504
505     default_path = notmuch_database_default_path ();
506     printf ("Top-level mail directory [%s]: ", default_path);
507     fflush (stdout);
508
509     getline (&mail_directory, &line_size, stdin);
510     chomp_newline (mail_directory);
511
512     printf ("\n");
513
514     if (mail_directory == NULL || strlen (mail_directory) == 0) {
515         if (mail_directory)
516             free (mail_directory);
517         mail_directory = default_path;
518     } else {
519         /* XXX: Instead of telling the user to use an environment
520          * variable here, we should really be writing out a configuration
521          * file and loading that on the next run. */
522         if (strcmp (mail_directory, default_path)) {
523             printf ("Note: Since you are not using the default path, you will want to set\n"
524                     "the NOTMUCH_BASE environment variable to %s so that\n"
525                     "future calls to notmuch commands will know where to find your mail.\n",
526                     mail_directory);
527             printf ("For example, if you are using bash for your shell, add:\n\n");
528             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
529             printf ("to your ~/.bashrc file.\n\n");
530         }
531         free (default_path);
532     }
533
534     /* Coerce th directory into an absolute directory name. */
535     if (*mail_directory != '/') {
536         char *cwd, *absolute_mail_directory;
537
538         cwd = getcwd (NULL, 0);
539         if (cwd == NULL) {
540             fprintf (stderr, "Out of memory.\n");
541             exit (1);
542         }
543
544         if (asprintf (&absolute_mail_directory, "%s/%s",
545                       cwd, mail_directory) < 0)
546         {
547             fprintf (stderr, "Out of memory.\n");
548             exit (1);
549         }
550
551         free (cwd);
552         free (mail_directory);
553         mail_directory = absolute_mail_directory;
554
555         printf ("Abs: %s\n", mail_directory);
556     }
557
558     notmuch = notmuch_database_create (mail_directory);
559     if (notmuch == NULL) {
560         fprintf (stderr, "Failed to create new notmuch database at %s\n",
561                  mail_directory);
562         ret = NOTMUCH_STATUS_FILE_ERROR;
563         goto DONE;
564     }
565
566     printf ("OK. Let's take a look at the mail we can find in the directory\n");
567     printf ("%s ...\n", mail_directory);
568
569     count = 0;
570     count_files (mail_directory, &count);
571
572     printf ("Found %d total files. That's not much mail.\n\n", count);
573
574     printf ("Next, we'll inspect the messages and create a database of threads:\n");
575
576     add_files_state.ignore_read_only_directories = FALSE;
577     add_files_state.saw_read_only_directory = FALSE;
578     add_files_state.total_files = count;
579     add_files_state.processed_files = 0;
580     add_files_state.added_messages = 0;
581     add_files_state.callback = NULL;
582     gettimeofday (&add_files_state.tv_start, NULL);
583
584     ret = add_files (notmuch, mail_directory, &add_files_state);
585
586     gettimeofday (&tv_now, NULL);
587     elapsed = tv_elapsed (add_files_state.tv_start,
588                           tv_now);
589     printf ("Processed %d %s in ", add_files_state.processed_files,
590             add_files_state.processed_files == 1 ?
591             "file" : "total files");
592     print_formatted_seconds (elapsed);
593     if (elapsed > 1) {
594         printf (" (%d files/sec.).                 \n",
595                 (int) (add_files_state.processed_files / elapsed));
596     } else {
597         printf (".                    \n");
598     }
599     if (add_files_state.added_messages) {
600         printf ("Added %d %s to the database.\n\n",
601                 add_files_state.added_messages,
602                 add_files_state.added_messages == 1 ?
603                 "message" : "unique messages");
604     }
605
606     printf ("When new mail is delivered to %s in the future,\n"
607             "run \"notmuch new\" to add it to the database.\n\n",
608             mail_directory);
609
610     if (ret) {
611         printf ("Note: At least one error was encountered: %s\n",
612                 notmuch_status_to_string (ret));
613     }
614
615   DONE:
616     if (mail_directory)
617         free (mail_directory);
618     if (notmuch)
619         notmuch_database_close (notmuch);
620
621     return ret;
622 }
623
624 static void
625 tag_inbox_and_unread (notmuch_message_t *message)
626 {
627     notmuch_message_add_tag (message, "inbox");
628     notmuch_message_add_tag (message, "unread");
629 }
630
631 static int
632 new_command (unused (int argc), unused (char *argv[]))
633 {
634     notmuch_database_t *notmuch;
635     const char *mail_directory;
636     add_files_state_t add_files_state;
637     double elapsed;
638     struct timeval tv_now;
639     int ret = 0;
640
641     notmuch = notmuch_database_open (NULL);
642     if (notmuch == NULL) {
643         ret = 1;
644         goto DONE;
645     }
646
647     mail_directory = notmuch_database_get_path (notmuch);
648
649     add_files_state.ignore_read_only_directories = TRUE;
650     add_files_state.saw_read_only_directory = FALSE;
651     add_files_state.total_files = 0;
652     add_files_state.processed_files = 0;
653     add_files_state.added_messages = 0;
654     add_files_state.callback = tag_inbox_and_unread;
655     gettimeofday (&add_files_state.tv_start, NULL);
656
657     ret = add_files (notmuch, mail_directory, &add_files_state);
658
659     gettimeofday (&tv_now, NULL);
660     elapsed = tv_elapsed (add_files_state.tv_start,
661                           tv_now);
662     if (add_files_state.processed_files) {
663         printf ("Processed %d %s in ", add_files_state.processed_files,
664                 add_files_state.processed_files == 1 ?
665                 "file" : "total files");
666         print_formatted_seconds (elapsed);
667         if (elapsed > 1) {
668             printf (" (%d files/sec.).                 \n",
669                     (int) (add_files_state.processed_files / elapsed));
670         } else {
671             printf (".                    \n");
672         }
673     }
674     if (add_files_state.added_messages) {
675         printf ("Added %d new %s to the database (not much, really).\n",
676                 add_files_state.added_messages,
677                 add_files_state.added_messages == 1 ?
678                 "message" : "messages");
679     } else {
680         printf ("No new mail---and that's not much.\n");
681     }
682
683     if (elapsed > 1 && ! add_files_state.saw_read_only_directory) {
684         printf ("\nTip: If you have any sub-directories that are archives (that is,\n"
685                 "they will never receive new mail), marking these directores as\n"
686                 "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
687                 "much more efficient (it won't even look in those directories).\n");
688     }
689
690     if (ret) {
691         printf ("\nNote: At least one error was encountered: %s\n",
692                 notmuch_status_to_string (ret));
693     }
694
695   DONE:
696     if (notmuch)
697         notmuch_database_close (notmuch);
698
699     return ret;
700 }
701
702 /* Construct a single query string from the passed arguments, using
703  * 'ctx' as the talloc owner for all allocations.
704  *
705  * Currently, the arguments are just connected with space characters,
706  * but we might do more processing in the future, (such as inserting
707  * any AND operators needed to work around Xapian QueryParser bugs).
708  *
709  * This function returns NULL in case of insufficient memory.
710  */
711 static char *
712 query_string_from_args (void *ctx, int argc, char *argv[])
713 {
714     char *query_string;
715     int i;
716
717     query_string = talloc_strdup (ctx, "");
718     if (query_string == NULL)
719         return NULL;
720
721     for (i = 0; i < argc; i++) {
722         if (i != 0) {
723             query_string = talloc_strdup_append (query_string, " ");
724             if (query_string == NULL)
725                 return NULL;
726         }
727
728         query_string = talloc_strdup_append (query_string, argv[i]);
729         if (query_string == NULL)
730             return NULL;
731     }
732
733     return query_string;
734 }
735
736 static int
737 search_command (int argc, char *argv[])
738 {
739     void *local = talloc_new (NULL);
740     notmuch_database_t *notmuch = NULL;
741     notmuch_query_t *query;
742     notmuch_thread_results_t *results;
743     notmuch_thread_t *thread;
744     notmuch_tags_t *tags;
745     char *query_str;
746     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
747
748     notmuch = notmuch_database_open (NULL);
749     if (notmuch == NULL) {
750         ret = 1;
751         goto DONE;
752     }
753
754     query_str = query_string_from_args (local, argc, argv);
755
756     query = notmuch_query_create (notmuch, query_str);
757     if (query == NULL) {
758         fprintf (stderr, "Out of memory\n");
759         ret = 1;
760         goto DONE;
761     }
762
763     for (results = notmuch_query_search_threads (query);
764          notmuch_thread_results_has_more (results);
765          notmuch_thread_results_advance (results))
766     {
767         int first = 1;
768
769         thread = notmuch_thread_results_get (results);
770
771         printf ("%s %s",
772                 notmuch_thread_get_thread_id (thread),
773                 notmuch_thread_get_subject (thread));
774
775         printf (" (");
776         for (tags = notmuch_thread_get_tags (thread);
777              notmuch_tags_has_more (tags);
778              notmuch_tags_advance (tags))
779         {
780             if (! first)
781                 printf (" ");
782             printf ("%s", notmuch_tags_get (tags));
783             first = 0;
784         }
785         printf (")\n");
786
787         notmuch_thread_destroy (thread);
788     }
789
790     notmuch_query_destroy (query);
791
792   DONE:
793     if (notmuch)
794         notmuch_database_close (notmuch);
795     talloc_free (local);
796
797     return ret;
798 }
799
800 static int
801 show_command (unused (int argc), unused (char *argv[]))
802 {
803     void *local = talloc_new (NULL);
804     char *query_string;
805     notmuch_database_t *notmuch = NULL;
806     notmuch_query_t *query = NULL;
807     notmuch_message_results_t *messages;
808     notmuch_message_t *message;
809     const char *filename;
810     FILE *file;
811     int ret = 0;
812     int c;
813
814     if (argc != 1) {
815         fprintf (stderr, "Error: \"notmuch show\" requires exactly one thread-ID argument.\n");
816         ret = 1;
817         goto DONE;
818     }
819
820     notmuch = notmuch_database_open (NULL);
821     if (notmuch == NULL) {
822         ret = 1;
823         goto DONE;
824     }
825
826     query_string = talloc_asprintf (local, "thread:%s", argv[0]);
827     if (query_string == NULL) {
828         fprintf (stderr, "Out of memory\n");
829         ret = 1;
830         goto DONE;
831     }
832
833     query = notmuch_query_create (notmuch, query_string);
834     if (query == NULL) {
835         fprintf (stderr, "Out of memory\n");
836         ret = 1;
837         goto DONE;
838     }
839
840     for (messages = notmuch_query_search_messages (query);
841          notmuch_message_results_has_more (messages);
842          notmuch_message_results_advance (messages))
843     {
844         message = notmuch_message_results_get (messages);
845
846         printf ("%%message{\n");
847
848         printf ("%%header{\n");
849
850         printf ("%s", notmuch_message_get_all_headers (message));
851
852         printf ("%%header}\n");
853
854         filename = notmuch_message_get_filename (message);
855
856         file = fopen (filename, "r");
857         if (file) {
858             size_t header_size = notmuch_message_get_header_size (message);
859             fseek (file, header_size + 1, SEEK_SET);
860             while (1) {
861                 c = fgetc (file);
862                 if (c == EOF)
863                     break;
864                 putchar (c);
865             }
866         }
867         fclose (file);
868
869         printf ("%%message}\n");
870
871         notmuch_message_destroy (message);
872     }
873
874   DONE:
875     if (local)
876         talloc_free (local);
877
878     if (query)
879         notmuch_query_destroy (query);
880
881     if (notmuch)
882         notmuch_database_close (notmuch);
883
884     return ret;
885 }
886
887 static int
888 tag_command (unused (int argc), unused (char *argv[]))
889 {
890     void *local;
891     int *add_tags, *remove_tags;
892     int add_tags_count = 0;
893     int remove_tags_count = 0;
894     char *query_string;
895     notmuch_database_t *notmuch = NULL;
896     notmuch_query_t *query;
897     notmuch_message_results_t *results;
898     notmuch_message_t *message;
899     int ret = 0;
900     int i;
901
902     local = talloc_new (NULL);
903     if (local == NULL) {
904         ret = 1;
905         goto DONE;
906     }
907
908     add_tags = talloc_size (local, argc * sizeof (int));
909     if (add_tags == NULL) {
910         ret = 1;
911         goto DONE;
912     }
913
914     remove_tags = talloc_size (local, argc * sizeof (int));
915     if (remove_tags == NULL) {
916         ret = 1;
917         goto DONE;
918     }
919
920     for (i = 0; i < argc; i++) {
921         if (strcmp (argv[i], "--") == 0) {
922             i++;
923             break;
924         }
925         if (argv[i][0] == '+') {
926             add_tags[add_tags_count++] = i;
927         } else if (argv[i][0] == '-') {
928             remove_tags[remove_tags_count++] = i;
929         } else {
930             break;
931         }
932     }
933
934     if (add_tags_count == 0 && remove_tags_count == 0) {
935         fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
936         ret = 1;
937         goto DONE;
938     }
939
940     if (i == argc) {
941         fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
942         ret = 1;
943         goto DONE;
944     }
945
946     notmuch = notmuch_database_open (NULL);
947     if (notmuch == NULL) {
948         ret = 1;
949         goto DONE;
950     }
951
952     query_string = query_string_from_args (local, argc - i, &argv[i]);
953
954     query = notmuch_query_create (notmuch, query_string);
955     if (query == NULL) {
956         fprintf (stderr, "Out of memory.\n");
957         ret = 1;
958         goto DONE;
959     }
960
961     for (results = notmuch_query_search_messages (query);
962          notmuch_message_results_has_more (results);
963          notmuch_message_results_advance (results))
964     {
965         message = notmuch_message_results_get (results);
966
967         notmuch_message_freeze (message);
968
969         for (i = 0; i < remove_tags_count; i++)
970             notmuch_message_remove_tag (message,
971                                         argv[remove_tags[i]] + 1);
972
973         for (i = 0; i < add_tags_count; i++)
974             notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
975
976         notmuch_message_thaw (message);
977
978         notmuch_message_destroy (message);
979     }
980
981     notmuch_query_destroy (query);
982
983   DONE:
984     if (notmuch)
985         notmuch_database_close (notmuch);
986
987     talloc_free (local);
988
989     return ret;
990 }
991
992 static int
993 dump_command (int argc, char *argv[])
994 {
995     FILE *output = NULL;
996     notmuch_database_t *notmuch = NULL;
997     notmuch_query_t *query;
998     notmuch_message_results_t *results;
999     notmuch_message_t *message;
1000     notmuch_tags_t *tags;
1001     int ret = 0;
1002
1003     if (argc) {
1004         output = fopen (argv[0], "w");
1005         if (output == NULL) {
1006             fprintf (stderr, "Error opening %s for writing: %s\n",
1007                      argv[0], strerror (errno));
1008             ret = 1;
1009             goto DONE;
1010         }
1011     } else {
1012         output = stdout;
1013     }
1014
1015     notmuch = notmuch_database_open (NULL);
1016     if (notmuch == NULL) {
1017         ret = 1;
1018         goto DONE;
1019     }
1020
1021     query = notmuch_query_create (notmuch, "");
1022     if (query == NULL) {
1023         fprintf (stderr, "Out of memory\n");
1024         ret = 1;
1025         goto DONE;
1026     }
1027
1028     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
1029
1030     for (results = notmuch_query_search_messages (query);
1031          notmuch_message_results_has_more (results);
1032          notmuch_message_results_advance (results))
1033     {
1034         int first = 1;
1035         message = notmuch_message_results_get (results);
1036
1037         fprintf (output,
1038                  "%s (", notmuch_message_get_message_id (message));
1039
1040         for (tags = notmuch_message_get_tags (message);
1041              notmuch_tags_has_more (tags);
1042              notmuch_tags_advance (tags))
1043         {
1044             if (! first)
1045                 fprintf (output, " ");
1046
1047             fprintf (output, "%s", notmuch_tags_get (tags));
1048
1049             first = 0;
1050         }
1051
1052         fprintf (output, ")\n");
1053
1054         notmuch_message_destroy (message);
1055     }
1056
1057     notmuch_query_destroy (query);
1058
1059   DONE:
1060     if (notmuch)
1061         notmuch_database_close (notmuch);
1062     if (output && output != stdout)
1063         fclose (output);
1064
1065     return ret;
1066 }
1067
1068 static int
1069 restore_command (int argc, char *argv[])
1070 {
1071     FILE *input = NULL;
1072     notmuch_database_t *notmuch = NULL;
1073     char *line = NULL;
1074     size_t line_size;
1075     ssize_t line_len;
1076     regex_t regex;
1077     int rerr;
1078     int ret = 0;
1079
1080     if (argc) {
1081         input = fopen (argv[0], "r");
1082         if (input == NULL) {
1083             fprintf (stderr, "Error opening %s for reading: %s\n",
1084                      argv[0], strerror (errno));
1085             ret = 1;
1086             goto DONE;
1087         }
1088     } else {
1089         printf ("No filename given. Reading dump from stdin.\n");
1090         input = stdin;
1091     }
1092
1093     notmuch = notmuch_database_open (NULL);
1094     if (notmuch == NULL) {
1095         ret = 1;
1096         goto DONE;
1097     }
1098
1099     /* Dump output is one line per message. We match a sequence of
1100      * non-space characters for the message-id, then one or more
1101      * spaces, then a list of space-separated tags as a sequence of
1102      * characters within literal '(' and ')'. */
1103     xregcomp (&regex,
1104               "^([^ ]+) \\(([^)]*)\\)$",
1105               REG_EXTENDED);
1106
1107     while ((line_len = getline (&line, &line_size, input)) != -1) {
1108         regmatch_t match[3];
1109         char *message_id, *tags, *tag, *next;
1110         notmuch_message_t *message;
1111         notmuch_status_t status;
1112
1113         chomp_newline (line);
1114
1115         rerr = xregexec (&regex, line, 3, match, 0);
1116         if (rerr == REG_NOMATCH)
1117         {
1118             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
1119                      line);
1120             continue;
1121         }
1122
1123         message_id = xstrndup (line + match[1].rm_so,
1124                                match[1].rm_eo - match[1].rm_so);
1125         tags = xstrndup (line + match[2].rm_so,
1126                          match[2].rm_eo - match[2].rm_so);
1127
1128         if (strlen (tags)) {
1129
1130             message = notmuch_database_find_message (notmuch, message_id);
1131             if (message == NULL) {
1132                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
1133                          message_id);
1134                 goto NEXT_LINE;
1135             }
1136
1137             notmuch_message_freeze (message);
1138
1139             notmuch_message_remove_all_tags (message);
1140
1141             next = tags;
1142             while (next) {
1143                 tag = strsep (&next, " ");
1144                 if (*tag == '\0')
1145                     continue;
1146                 status = notmuch_message_add_tag (message, tag);
1147                 if (status) {
1148                     fprintf (stderr,
1149                              "Error applying tag %s to message %s:\n",
1150                              tag, message_id);
1151                     fprintf (stderr, "%s\n",
1152                              notmuch_status_to_string (status));
1153                 }
1154             }
1155
1156             notmuch_message_thaw (message);
1157             notmuch_message_destroy (message);
1158         }
1159       NEXT_LINE:
1160         free (message_id);
1161         free (tags);
1162     }
1163
1164     regfree (&regex);
1165
1166   DONE:
1167     if (line)
1168         free (line);
1169     if (notmuch)
1170         notmuch_database_close (notmuch);
1171     if (input && input != stdin)
1172         fclose (input);
1173
1174     return ret;
1175 }
1176
1177 static int
1178 help_command (int argc, char *argv[]);
1179
1180 command_t commands[] = {
1181     { "setup", setup_command,
1182       "Interactively setup notmuch for first use.",
1183       "\t\tThe setup command is the first command you will run in order\n"
1184       "\t\tto start using notmuch. It will prompt you for the directory\n"
1185       "\t\tcontaining your email archives, and will then proceed to build\n"
1186       "\t\ta database to allow fast searching of that mail.\n\n"
1187       "\t\tInvoking notmuch with no command argument will run setup if\n"
1188       "\t\tthe setup command has not previously been completed." },
1189     { "new", new_command,
1190       "Find and import any new messages.",
1191       "\t\tScans all sub-directories of the database, adding new messages\n"
1192       "\t\tthat are found. Each new message will be tagged as both\n"
1193       "\t\t\"inbox\" and \"unread\".\n"
1194       "\n"
1195       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
1196       "\t\tso you can use that to mark tdirectories that will not\n"
1197       "\t\treceive any new mail (and make \"notmuch new\" faster)." },
1198     { "search", search_command,
1199       "<search-term> [...]\n\n"
1200       "\t\tSearch for threads matching the given search terms.",
1201       "\t\tNote that the individual mail messages will be matched\n"
1202       "\t\tagainst the search terms, but the results will be the\n"
1203       "\t\tthreads containing the matched messages.\n\n"
1204       "\t\tCurrently, the supported search terms are as follows, (where\n"
1205       "\t\t<brackets> indicate user-supplied values):\n\n"
1206       "\t\t\ttag:<tag>\n"
1207       "\t\t\tid:<message-id>\n"
1208       "\t\t\tthread:<thread-id>\n\n"
1209       "\t\tValid tag values include \"inbox\" and \"unread\" by default\n"
1210       "\t\tfor new messages added by \"notmuch new\" as well as any other\n"
1211       "\t\ttag values added manually with \"notmuch tag\".\n\n"
1212       "\t\tMessage ID values are the literal contents of the Message-ID:\n"
1213       "\t\theader of email messages, but without the '<','>' delimiters.\n\n"
1214       "\t\tThread ID values are generated internally by notmuch but can\n"
1215       "\t\tbe seen in the output of \"notmuch search\" for example.\n\n"
1216       "\t\tIn addition to individual terms, multiple terms can be\n"
1217       "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
1218       "\t\tEach term in the query will be implicitly connected by a\n"
1219       "\t\tlogical AND if no explicit operator is provided, (except\n"
1220       "\t\tthat terms with a common prefix will be implicitly combined\n"
1221       "\t\twith OR until we get Xapian defect #402 fixed).\n\n"
1222       "\t\tParentheses can also be used to control the combination of\n"
1223       "\t\tthe Boolean operators, but will have to be protected from\n"
1224       "\t\tinterpretation by the shell, (such as by putting quotation\n"
1225       "\t\tmarks around any parenthesized expression)." },
1226     { "show", show_command,
1227       "<thread-id>\n\n"
1228       "\t\tShow the thread with the given thread ID (see 'search').",
1229       "\t\tThread ID values are given as the first column in the\n"
1230       "\t\toutput of the \"notmuch search\" command. These are the\n"
1231       "\t\trandom-looking strings of 32 characters." },
1232     { "tag", tag_command,
1233       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
1234       "\t\tAdd/remove tags for all messages matching the search terms.",
1235       "\t\tThe search terms are handled exactly as in 'search' so one\n"
1236       "\t\tcan use that command first to see what will be modified.\n\n"
1237       "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n"
1238       "\t\tremoved. For each message, tag removal is before tag addition.\n\n"
1239       "\t\tThe beginning of <search-terms> is recognized by the first\n"
1240       "\t\targument that begins with neither '+' nor '-'. Support for\n"
1241       "\t\tan initial search term beginning with '+' or '-' is provided\n"
1242       "\t\tby allowing the user to specify a \"--\" argument to separate\n"
1243       "\t\tthe tags from the search terms.\n\n"
1244       "\t\tNote: If you run \"notmuch new\" between reading a thread with\n"
1245       "\t\t\"notmuch show\" and removing the \"inbox\" tag for that thread\n"
1246       "\t\twith \"notmuch tag\" then you create the possibility of moving\n"
1247       "\t\tsome messages from that thread out of your inbox without ever\n"
1248       "\t\treading them. The easiest way to avoid this problem is to not\n"
1249       "\t\trun \"notmuch new\" between reading and removing tags." },
1250     { "dump", dump_command,
1251       "[<filename>]\n\n"
1252       "\t\tCreate a plain-text dump of the tags for each message.",
1253       "\t\tOutput is to the given filename, if any, or to stdout.\n"
1254       "\t\tThese tags are the only data in the notmuch database\n"
1255       "\t\tthat can't be recreated from the messages themselves.\n"
1256       "\t\tThe output of notmuch dump is therefore the only\n"
1257       "\t\tcritical thing to backup (and much more friendly to\n"
1258       "\t\tincremental backup than the native database files.)" },
1259     { "restore", restore_command,
1260       "<filename>\n\n"
1261       "\t\tRestore the tags from the given dump file (see 'dump').",
1262       "\t\tNote: The dump file format is specifically chosen to be\n"
1263       "\t\tcompatible with the format of files produced by sup-dump.\n"
1264       "\t\tSo if you've previously been using sup for mail, then the\n"
1265       "\t\t\"notmuch restore\" command provides you a way to import\n"
1266       "\t\tall of your tags (or labels as sup calls them)." },
1267     { "help", help_command,
1268       "[<command>]\n\n"
1269       "\t\tThis message, or more detailed help for the named command.",
1270       "\t\tExcept in this case, where there's not much more detailed\n"
1271       "\t\thelp available." }
1272 };
1273
1274 static void
1275 usage (void)
1276 {
1277     command_t *command;
1278     unsigned int i;
1279
1280     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
1281     fprintf (stderr, "\n");
1282     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
1283     fprintf (stderr, "\n");
1284
1285     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1286         command = &commands[i];
1287
1288         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
1289     }
1290
1291     fprintf (stderr, "Use \"notmuch help <command>\" for more details on each command.\n\n");
1292 }
1293
1294 static int
1295 help_command (int argc, char *argv[])
1296 {
1297     command_t *command;
1298     unsigned int i;
1299
1300     if (argc == 0) {
1301         fprintf (stderr, "The notmuch mail system.\n\n");
1302         usage ();
1303         return 0;
1304     }
1305
1306     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1307         command = &commands[i];
1308
1309         if (strcmp (argv[0], command->name) == 0) {
1310             fprintf (stderr, "Help for \"notmuch %s\":\n\n", argv[0]);
1311             fprintf (stderr, "\t%s\t%s\n\n%s\n\n", command->name,
1312                      command->summary, command->documentation);
1313             return 0;
1314         }
1315     }
1316
1317     fprintf (stderr,
1318              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
1319              argv[0]);
1320     return 1;
1321 }
1322     
1323 int
1324 main (int argc, char *argv[])
1325 {
1326     command_t *command;
1327     unsigned int i;
1328
1329     if (argc == 1)
1330         return setup_command (0, NULL);
1331
1332     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1333         command = &commands[i];
1334
1335         if (strcmp (argv[1], command->name) == 0)
1336             return (command->function) (argc - 2, &argv[2]);
1337     }
1338
1339     /* Don't complain about "help" being an unknown command when we're
1340        about to provide exactly what's wanted anyway. */
1341     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
1342     usage ();
1343
1344     return 1;
1345 }