notmuch show: Trim down header list.
[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     const char *headers[] = {
815         "Subject", "From", "To", "Cc", "Bcc", "Date"
816     };
817     const char *name, *value;
818     unsigned int i;
819
820     if (argc != 1) {
821         fprintf (stderr, "Error: \"notmuch show\" requires exactly one thread-ID argument.\n");
822         ret = 1;
823         goto DONE;
824     }
825
826     notmuch = notmuch_database_open (NULL);
827     if (notmuch == NULL) {
828         ret = 1;
829         goto DONE;
830     }
831
832     query_string = talloc_asprintf (local, "thread:%s", argv[0]);
833     if (query_string == NULL) {
834         fprintf (stderr, "Out of memory\n");
835         ret = 1;
836         goto DONE;
837     }
838
839     query = notmuch_query_create (notmuch, query_string);
840     if (query == NULL) {
841         fprintf (stderr, "Out of memory\n");
842         ret = 1;
843         goto DONE;
844     }
845
846     for (messages = notmuch_query_search_messages (query);
847          notmuch_message_results_has_more (messages);
848          notmuch_message_results_advance (messages))
849     {
850         message = notmuch_message_results_get (messages);
851
852         printf ("%%message{\n");
853
854         printf ("%%header{\n");
855
856         for (i = 0; i < ARRAY_SIZE (headers); i++) {
857             name = headers[i];
858             value = notmuch_message_get_header (message, name);
859             if (value)
860                 printf ("%s: %s\n", name, value);
861         }
862
863         printf ("%%header}\n");
864
865         filename = notmuch_message_get_filename (message);
866
867         file = fopen (filename, "r");
868         if (file) {
869             size_t header_size = notmuch_message_get_header_size (message);
870             fseek (file, header_size + 1, SEEK_SET);
871             while (1) {
872                 c = fgetc (file);
873                 if (c == EOF)
874                     break;
875                 putchar (c);
876             }
877         }
878         fclose (file);
879
880         printf ("%%message}\n");
881
882         notmuch_message_destroy (message);
883     }
884
885   DONE:
886     if (local)
887         talloc_free (local);
888
889     if (query)
890         notmuch_query_destroy (query);
891
892     if (notmuch)
893         notmuch_database_close (notmuch);
894
895     return ret;
896 }
897
898 static int
899 tag_command (unused (int argc), unused (char *argv[]))
900 {
901     void *local;
902     int *add_tags, *remove_tags;
903     int add_tags_count = 0;
904     int remove_tags_count = 0;
905     char *query_string;
906     notmuch_database_t *notmuch = NULL;
907     notmuch_query_t *query;
908     notmuch_message_results_t *results;
909     notmuch_message_t *message;
910     int ret = 0;
911     int i;
912
913     local = talloc_new (NULL);
914     if (local == NULL) {
915         ret = 1;
916         goto DONE;
917     }
918
919     add_tags = talloc_size (local, argc * sizeof (int));
920     if (add_tags == NULL) {
921         ret = 1;
922         goto DONE;
923     }
924
925     remove_tags = talloc_size (local, argc * sizeof (int));
926     if (remove_tags == NULL) {
927         ret = 1;
928         goto DONE;
929     }
930
931     for (i = 0; i < argc; i++) {
932         if (strcmp (argv[i], "--") == 0) {
933             i++;
934             break;
935         }
936         if (argv[i][0] == '+') {
937             add_tags[add_tags_count++] = i;
938         } else if (argv[i][0] == '-') {
939             remove_tags[remove_tags_count++] = i;
940         } else {
941             break;
942         }
943     }
944
945     if (add_tags_count == 0 && remove_tags_count == 0) {
946         fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
947         ret = 1;
948         goto DONE;
949     }
950
951     if (i == argc) {
952         fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
953         ret = 1;
954         goto DONE;
955     }
956
957     notmuch = notmuch_database_open (NULL);
958     if (notmuch == NULL) {
959         ret = 1;
960         goto DONE;
961     }
962
963     query_string = query_string_from_args (local, argc - i, &argv[i]);
964
965     query = notmuch_query_create (notmuch, query_string);
966     if (query == NULL) {
967         fprintf (stderr, "Out of memory.\n");
968         ret = 1;
969         goto DONE;
970     }
971
972     for (results = notmuch_query_search_messages (query);
973          notmuch_message_results_has_more (results);
974          notmuch_message_results_advance (results))
975     {
976         message = notmuch_message_results_get (results);
977
978         notmuch_message_freeze (message);
979
980         for (i = 0; i < remove_tags_count; i++)
981             notmuch_message_remove_tag (message,
982                                         argv[remove_tags[i]] + 1);
983
984         for (i = 0; i < add_tags_count; i++)
985             notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
986
987         notmuch_message_thaw (message);
988
989         notmuch_message_destroy (message);
990     }
991
992     notmuch_query_destroy (query);
993
994   DONE:
995     if (notmuch)
996         notmuch_database_close (notmuch);
997
998     talloc_free (local);
999
1000     return ret;
1001 }
1002
1003 static int
1004 dump_command (int argc, char *argv[])
1005 {
1006     FILE *output = NULL;
1007     notmuch_database_t *notmuch = NULL;
1008     notmuch_query_t *query;
1009     notmuch_message_results_t *results;
1010     notmuch_message_t *message;
1011     notmuch_tags_t *tags;
1012     int ret = 0;
1013
1014     if (argc) {
1015         output = fopen (argv[0], "w");
1016         if (output == NULL) {
1017             fprintf (stderr, "Error opening %s for writing: %s\n",
1018                      argv[0], strerror (errno));
1019             ret = 1;
1020             goto DONE;
1021         }
1022     } else {
1023         output = stdout;
1024     }
1025
1026     notmuch = notmuch_database_open (NULL);
1027     if (notmuch == NULL) {
1028         ret = 1;
1029         goto DONE;
1030     }
1031
1032     query = notmuch_query_create (notmuch, "");
1033     if (query == NULL) {
1034         fprintf (stderr, "Out of memory\n");
1035         ret = 1;
1036         goto DONE;
1037     }
1038
1039     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
1040
1041     for (results = notmuch_query_search_messages (query);
1042          notmuch_message_results_has_more (results);
1043          notmuch_message_results_advance (results))
1044     {
1045         int first = 1;
1046         message = notmuch_message_results_get (results);
1047
1048         fprintf (output,
1049                  "%s (", notmuch_message_get_message_id (message));
1050
1051         for (tags = notmuch_message_get_tags (message);
1052              notmuch_tags_has_more (tags);
1053              notmuch_tags_advance (tags))
1054         {
1055             if (! first)
1056                 fprintf (output, " ");
1057
1058             fprintf (output, "%s", notmuch_tags_get (tags));
1059
1060             first = 0;
1061         }
1062
1063         fprintf (output, ")\n");
1064
1065         notmuch_message_destroy (message);
1066     }
1067
1068     notmuch_query_destroy (query);
1069
1070   DONE:
1071     if (notmuch)
1072         notmuch_database_close (notmuch);
1073     if (output && output != stdout)
1074         fclose (output);
1075
1076     return ret;
1077 }
1078
1079 static int
1080 restore_command (int argc, char *argv[])
1081 {
1082     FILE *input = NULL;
1083     notmuch_database_t *notmuch = NULL;
1084     char *line = NULL;
1085     size_t line_size;
1086     ssize_t line_len;
1087     regex_t regex;
1088     int rerr;
1089     int ret = 0;
1090
1091     if (argc) {
1092         input = fopen (argv[0], "r");
1093         if (input == NULL) {
1094             fprintf (stderr, "Error opening %s for reading: %s\n",
1095                      argv[0], strerror (errno));
1096             ret = 1;
1097             goto DONE;
1098         }
1099     } else {
1100         printf ("No filename given. Reading dump from stdin.\n");
1101         input = stdin;
1102     }
1103
1104     notmuch = notmuch_database_open (NULL);
1105     if (notmuch == NULL) {
1106         ret = 1;
1107         goto DONE;
1108     }
1109
1110     /* Dump output is one line per message. We match a sequence of
1111      * non-space characters for the message-id, then one or more
1112      * spaces, then a list of space-separated tags as a sequence of
1113      * characters within literal '(' and ')'. */
1114     xregcomp (&regex,
1115               "^([^ ]+) \\(([^)]*)\\)$",
1116               REG_EXTENDED);
1117
1118     while ((line_len = getline (&line, &line_size, input)) != -1) {
1119         regmatch_t match[3];
1120         char *message_id, *tags, *tag, *next;
1121         notmuch_message_t *message;
1122         notmuch_status_t status;
1123
1124         chomp_newline (line);
1125
1126         rerr = xregexec (&regex, line, 3, match, 0);
1127         if (rerr == REG_NOMATCH)
1128         {
1129             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
1130                      line);
1131             continue;
1132         }
1133
1134         message_id = xstrndup (line + match[1].rm_so,
1135                                match[1].rm_eo - match[1].rm_so);
1136         tags = xstrndup (line + match[2].rm_so,
1137                          match[2].rm_eo - match[2].rm_so);
1138
1139         if (strlen (tags)) {
1140
1141             message = notmuch_database_find_message (notmuch, message_id);
1142             if (message == NULL) {
1143                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
1144                          message_id);
1145                 goto NEXT_LINE;
1146             }
1147
1148             notmuch_message_freeze (message);
1149
1150             notmuch_message_remove_all_tags (message);
1151
1152             next = tags;
1153             while (next) {
1154                 tag = strsep (&next, " ");
1155                 if (*tag == '\0')
1156                     continue;
1157                 status = notmuch_message_add_tag (message, tag);
1158                 if (status) {
1159                     fprintf (stderr,
1160                              "Error applying tag %s to message %s:\n",
1161                              tag, message_id);
1162                     fprintf (stderr, "%s\n",
1163                              notmuch_status_to_string (status));
1164                 }
1165             }
1166
1167             notmuch_message_thaw (message);
1168             notmuch_message_destroy (message);
1169         }
1170       NEXT_LINE:
1171         free (message_id);
1172         free (tags);
1173     }
1174
1175     regfree (&regex);
1176
1177   DONE:
1178     if (line)
1179         free (line);
1180     if (notmuch)
1181         notmuch_database_close (notmuch);
1182     if (input && input != stdin)
1183         fclose (input);
1184
1185     return ret;
1186 }
1187
1188 static int
1189 help_command (int argc, char *argv[]);
1190
1191 command_t commands[] = {
1192     { "setup", setup_command,
1193       "Interactively setup notmuch for first use.",
1194       "\t\tThe setup command is the first command you will run in order\n"
1195       "\t\tto start using notmuch. It will prompt you for the directory\n"
1196       "\t\tcontaining your email archives, and will then proceed to build\n"
1197       "\t\ta database to allow fast searching of that mail.\n\n"
1198       "\t\tInvoking notmuch with no command argument will run setup if\n"
1199       "\t\tthe setup command has not previously been completed." },
1200     { "new", new_command,
1201       "Find and import any new messages.",
1202       "\t\tScans all sub-directories of the database, adding new messages\n"
1203       "\t\tthat are found. Each new message will be tagged as both\n"
1204       "\t\t\"inbox\" and \"unread\".\n"
1205       "\n"
1206       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
1207       "\t\tso you can use that to mark tdirectories that will not\n"
1208       "\t\treceive any new mail (and make \"notmuch new\" faster)." },
1209     { "search", search_command,
1210       "<search-term> [...]\n\n"
1211       "\t\tSearch for threads matching the given search terms.",
1212       "\t\tNote that the individual mail messages will be matched\n"
1213       "\t\tagainst the search terms, but the results will be the\n"
1214       "\t\tthreads containing the matched messages.\n\n"
1215       "\t\tCurrently, the supported search terms are as follows, (where\n"
1216       "\t\t<brackets> indicate user-supplied values):\n\n"
1217       "\t\t\ttag:<tag>\n"
1218       "\t\t\tid:<message-id>\n"
1219       "\t\t\tthread:<thread-id>\n\n"
1220       "\t\tValid tag values include \"inbox\" and \"unread\" by default\n"
1221       "\t\tfor new messages added by \"notmuch new\" as well as any other\n"
1222       "\t\ttag values added manually with \"notmuch tag\".\n\n"
1223       "\t\tMessage ID values are the literal contents of the Message-ID:\n"
1224       "\t\theader of email messages, but without the '<','>' delimiters.\n\n"
1225       "\t\tThread ID values are generated internally by notmuch but can\n"
1226       "\t\tbe seen in the output of \"notmuch search\" for example.\n\n"
1227       "\t\tIn addition to individual terms, multiple terms can be\n"
1228       "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
1229       "\t\tEach term in the query will be implicitly connected by a\n"
1230       "\t\tlogical AND if no explicit operator is provided, (except\n"
1231       "\t\tthat terms with a common prefix will be implicitly combined\n"
1232       "\t\twith OR until we get Xapian defect #402 fixed).\n\n"
1233       "\t\tParentheses can also be used to control the combination of\n"
1234       "\t\tthe Boolean operators, but will have to be protected from\n"
1235       "\t\tinterpretation by the shell, (such as by putting quotation\n"
1236       "\t\tmarks around any parenthesized expression)." },
1237     { "show", show_command,
1238       "<thread-id>\n\n"
1239       "\t\tShow the thread with the given thread ID (see 'search').",
1240       "\t\tThread ID values are given as the first column in the\n"
1241       "\t\toutput of the \"notmuch search\" command. These are the\n"
1242       "\t\trandom-looking strings of 32 characters." },
1243     { "tag", tag_command,
1244       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
1245       "\t\tAdd/remove tags for all messages matching the search terms.",
1246       "\t\tThe search terms are handled exactly as in 'search' so one\n"
1247       "\t\tcan use that command first to see what will be modified.\n\n"
1248       "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n"
1249       "\t\tremoved. For each message, tag removal is before tag addition.\n\n"
1250       "\t\tThe beginning of <search-terms> is recognized by the first\n"
1251       "\t\targument that begins with neither '+' nor '-'. Support for\n"
1252       "\t\tan initial search term beginning with '+' or '-' is provided\n"
1253       "\t\tby allowing the user to specify a \"--\" argument to separate\n"
1254       "\t\tthe tags from the search terms.\n\n"
1255       "\t\tNote: If you run \"notmuch new\" between reading a thread with\n"
1256       "\t\t\"notmuch show\" and removing the \"inbox\" tag for that thread\n"
1257       "\t\twith \"notmuch tag\" then you create the possibility of moving\n"
1258       "\t\tsome messages from that thread out of your inbox without ever\n"
1259       "\t\treading them. The easiest way to avoid this problem is to not\n"
1260       "\t\trun \"notmuch new\" between reading and removing tags." },
1261     { "dump", dump_command,
1262       "[<filename>]\n\n"
1263       "\t\tCreate a plain-text dump of the tags for each message.",
1264       "\t\tOutput is to the given filename, if any, or to stdout.\n"
1265       "\t\tThese tags are the only data in the notmuch database\n"
1266       "\t\tthat can't be recreated from the messages themselves.\n"
1267       "\t\tThe output of notmuch dump is therefore the only\n"
1268       "\t\tcritical thing to backup (and much more friendly to\n"
1269       "\t\tincremental backup than the native database files.)" },
1270     { "restore", restore_command,
1271       "<filename>\n\n"
1272       "\t\tRestore the tags from the given dump file (see 'dump').",
1273       "\t\tNote: The dump file format is specifically chosen to be\n"
1274       "\t\tcompatible with the format of files produced by sup-dump.\n"
1275       "\t\tSo if you've previously been using sup for mail, then the\n"
1276       "\t\t\"notmuch restore\" command provides you a way to import\n"
1277       "\t\tall of your tags (or labels as sup calls them)." },
1278     { "help", help_command,
1279       "[<command>]\n\n"
1280       "\t\tThis message, or more detailed help for the named command.",
1281       "\t\tExcept in this case, where there's not much more detailed\n"
1282       "\t\thelp available." }
1283 };
1284
1285 static void
1286 usage (void)
1287 {
1288     command_t *command;
1289     unsigned int i;
1290
1291     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
1292     fprintf (stderr, "\n");
1293     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
1294     fprintf (stderr, "\n");
1295
1296     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1297         command = &commands[i];
1298
1299         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
1300     }
1301
1302     fprintf (stderr, "Use \"notmuch help <command>\" for more details on each command.\n\n");
1303 }
1304
1305 static int
1306 help_command (int argc, char *argv[])
1307 {
1308     command_t *command;
1309     unsigned int i;
1310
1311     if (argc == 0) {
1312         fprintf (stderr, "The notmuch mail system.\n\n");
1313         usage ();
1314         return 0;
1315     }
1316
1317     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1318         command = &commands[i];
1319
1320         if (strcmp (argv[0], command->name) == 0) {
1321             fprintf (stderr, "Help for \"notmuch %s\":\n\n", argv[0]);
1322             fprintf (stderr, "\t%s\t%s\n\n%s\n\n", command->name,
1323                      command->summary, command->documentation);
1324             return 0;
1325         }
1326     }
1327
1328     fprintf (stderr,
1329              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
1330              argv[0]);
1331     return 1;
1332 }
1333     
1334 int
1335 main (int argc, char *argv[])
1336 {
1337     command_t *command;
1338     unsigned int i;
1339
1340     if (argc == 1)
1341         return setup_command (0, NULL);
1342
1343     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1344         command = &commands[i];
1345
1346         if (strcmp (argv[1], command->name) == 0)
1347             return (command->function) (argc - 2, &argv[2]);
1348     }
1349
1350     /* Don't complain about "help" being an unknown command when we're
1351        about to provide exactly what's wanted anyway. */
1352     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
1353     usage ();
1354
1355     return 1;
1356 }