]> git.notmuchmail.org Git - notmuch/blob - notmuch.c
5a0ca5c946c69e12adadca235969463e98983f91
[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     notmuch = notmuch_database_create (mail_directory);
535     if (notmuch == NULL) {
536         fprintf (stderr, "Failed to create new notmuch database at %s\n",
537                  mail_directory);
538         ret = NOTMUCH_STATUS_FILE_ERROR;
539         goto DONE;
540     }
541
542     printf ("OK. Let's take a look at the mail we can find in the directory\n");
543     printf ("%s ...\n", mail_directory);
544
545     count = 0;
546     count_files (mail_directory, &count);
547
548     printf ("Found %d total files. That's not much mail.\n\n", count);
549
550     printf ("Next, we'll inspect the messages and create a database of threads:\n");
551
552     add_files_state.ignore_read_only_directories = FALSE;
553     add_files_state.saw_read_only_directory = FALSE;
554     add_files_state.total_files = count;
555     add_files_state.processed_files = 0;
556     add_files_state.added_messages = 0;
557     add_files_state.callback = NULL;
558     gettimeofday (&add_files_state.tv_start, NULL);
559
560     ret = add_files (notmuch, mail_directory, &add_files_state);
561
562     gettimeofday (&tv_now, NULL);
563     elapsed = tv_elapsed (add_files_state.tv_start,
564                           tv_now);
565     printf ("Processed %d %s in ", add_files_state.processed_files,
566             add_files_state.processed_files == 1 ?
567             "file" : "total files");
568     print_formatted_seconds (elapsed);
569     if (elapsed > 1) {
570         printf (" (%d files/sec.).                 \n",
571                 (int) (add_files_state.processed_files / elapsed));
572     } else {
573         printf (".                    \n");
574     }
575     if (add_files_state.added_messages) {
576         printf ("Added %d %s to the database.\n\n",
577                 add_files_state.added_messages,
578                 add_files_state.added_messages == 1 ?
579                 "message" : "unique messages");
580     }
581
582     printf ("When new mail is delivered to %s in the future,\n"
583             "run \"notmuch new\" to add it to the database.\n\n",
584             mail_directory);
585
586     if (ret) {
587         printf ("Note: At least one error was encountered: %s\n",
588                 notmuch_status_to_string (ret));
589     }
590
591   DONE:
592     if (mail_directory)
593         free (mail_directory);
594     if (notmuch)
595         notmuch_database_close (notmuch);
596
597     return ret;
598 }
599
600 static void
601 tag_inbox_and_unread (notmuch_message_t *message)
602 {
603     notmuch_message_add_tag (message, "inbox");
604     notmuch_message_add_tag (message, "unread");
605 }
606
607 static int
608 new_command (unused (int argc), unused (char *argv[]))
609 {
610     notmuch_database_t *notmuch;
611     const char *mail_directory;
612     add_files_state_t add_files_state;
613     double elapsed;
614     struct timeval tv_now;
615     int ret = 0;
616
617     notmuch = notmuch_database_open (NULL);
618     if (notmuch == NULL) {
619         ret = 1;
620         goto DONE;
621     }
622
623     mail_directory = notmuch_database_get_path (notmuch);
624
625     add_files_state.ignore_read_only_directories = TRUE;
626     add_files_state.saw_read_only_directory = FALSE;
627     add_files_state.total_files = 0;
628     add_files_state.processed_files = 0;
629     add_files_state.added_messages = 0;
630     add_files_state.callback = tag_inbox_and_unread;
631     gettimeofday (&add_files_state.tv_start, NULL);
632
633     ret = add_files (notmuch, mail_directory, &add_files_state);
634
635     gettimeofday (&tv_now, NULL);
636     elapsed = tv_elapsed (add_files_state.tv_start,
637                           tv_now);
638     if (add_files_state.processed_files) {
639         printf ("Processed %d %s in ", add_files_state.processed_files,
640                 add_files_state.processed_files == 1 ?
641                 "file" : "total files");
642         print_formatted_seconds (elapsed);
643         if (elapsed > 1) {
644             printf (" (%d files/sec.).                 \n",
645                     (int) (add_files_state.processed_files / elapsed));
646         } else {
647             printf (".                    \n");
648         }
649     }
650     if (add_files_state.added_messages) {
651         printf ("Added %d new %s to the database (not much, really).\n",
652                 add_files_state.added_messages,
653                 add_files_state.added_messages == 1 ?
654                 "message" : "messages");
655     } else {
656         printf ("No new mail---and that's not much.\n");
657     }
658
659     if (elapsed > 1 && ! add_files_state.saw_read_only_directory) {
660         printf ("\nTip: If you have any sub-directories that are archives (that is,\n"
661                 "they will never receive new mail), marking these directores as\n"
662                 "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
663                 "much more efficient (it won't even look in those directories).\n");
664     }
665
666     if (ret) {
667         printf ("\nNote: At least one error was encountered: %s\n",
668                 notmuch_status_to_string (ret));
669     }
670
671   DONE:
672     if (notmuch)
673         notmuch_database_close (notmuch);
674
675     return ret;
676 }
677
678 /* Construct a single query string from the passed arguments, using
679  * 'ctx' as the talloc owner for all allocations.
680  *
681  * Currently, the arguments are just connected with space characters,
682  * but we might do more processing in the future, (such as inserting
683  * any AND operators needed to work around Xapian QueryParser bugs).
684  *
685  * This function returns NULL in case of insufficient memory.
686  */
687 static char *
688 query_string_from_args (void *ctx, int argc, char *argv[])
689 {
690     char *query_string;
691     int i;
692
693     query_string = talloc_strdup (ctx, "");
694     if (query_string == NULL)
695         return NULL;
696
697     for (i = 0; i < argc; i++) {
698         if (i != 0) {
699             query_string = talloc_strdup_append (query_string, " ");
700             if (query_string == NULL)
701                 return NULL;
702         }
703
704         query_string = talloc_strdup_append (query_string, argv[i]);
705         if (query_string == NULL)
706             return NULL;
707     }
708
709     return query_string;
710 }
711
712 static int
713 search_command (int argc, char *argv[])
714 {
715     void *local = talloc_new (NULL);
716     notmuch_database_t *notmuch = NULL;
717     notmuch_query_t *query;
718     notmuch_thread_results_t *results;
719     notmuch_thread_t *thread;
720     notmuch_tags_t *tags;
721     char *query_str;
722     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
723
724     notmuch = notmuch_database_open (NULL);
725     if (notmuch == NULL) {
726         ret = 1;
727         goto DONE;
728     }
729
730     query_str = query_string_from_args (local, argc, argv);
731
732     query = notmuch_query_create (notmuch, query_str);
733     if (query == NULL) {
734         fprintf (stderr, "Out of memory\n");
735         ret = 1;
736         goto DONE;
737     }
738
739     for (results = notmuch_query_search_threads (query);
740          notmuch_thread_results_has_more (results);
741          notmuch_thread_results_advance (results))
742     {
743         int first = 1;
744
745         thread = notmuch_thread_results_get (results);
746
747         printf ("%s %s",
748                 notmuch_thread_get_thread_id (thread),
749                 notmuch_thread_get_subject (thread));
750
751         printf (" (");
752         for (tags = notmuch_thread_get_tags (thread);
753              notmuch_tags_has_more (tags);
754              notmuch_tags_advance (tags))
755         {
756             if (! first)
757                 printf (" ");
758             printf ("%s", notmuch_tags_get (tags));
759             first = 0;
760         }
761         printf (")\n");
762
763         notmuch_thread_destroy (thread);
764     }
765
766     notmuch_query_destroy (query);
767
768   DONE:
769     if (notmuch)
770         notmuch_database_close (notmuch);
771     talloc_free (local);
772
773     return ret;
774 }
775
776 static int
777 show_command (unused (int argc), unused (char *argv[]))
778 {
779     fprintf (stderr, "Error: show is not implemented yet.\n");
780     return 1;
781 }
782
783 static int
784 tag_command (unused (int argc), unused (char *argv[]))
785 {
786     void *local;
787     int *add_tags, *remove_tags;
788     int add_tags_count = 0;
789     int remove_tags_count = 0;
790     char *query_string;
791     notmuch_database_t *notmuch = NULL;
792     notmuch_query_t *query;
793     notmuch_message_results_t *results;
794     notmuch_message_t *message;
795     int ret = 0;
796     int i;
797
798     local = talloc_new (NULL);
799     if (local == NULL) {
800         ret = 1;
801         goto DONE;
802     }
803
804     add_tags = talloc_size (local, argc * sizeof (int));
805     if (add_tags == NULL) {
806         ret = 1;
807         goto DONE;
808     }
809
810     remove_tags = talloc_size (local, argc * sizeof (int));
811     if (remove_tags == NULL) {
812         ret = 1;
813         goto DONE;
814     }
815
816     for (i = 0; i < argc; i++) {
817         if (strcmp (argv[i], "--") == 0) {
818             i++;
819             break;
820         }
821         if (argv[i][0] == '+') {
822             add_tags[add_tags_count++] = i;
823         } else if (argv[i][0] == '-') {
824             remove_tags[remove_tags_count++] = i;
825         } else {
826             break;
827         }
828     }
829
830     if (add_tags_count == 0 && remove_tags_count == 0) {
831         fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
832         ret = 1;
833         goto DONE;
834     }
835
836     if (i == argc) {
837         fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
838         ret = 1;
839         goto DONE;
840     }
841
842     notmuch = notmuch_database_open (NULL);
843     if (notmuch == NULL) {
844         ret = 1;
845         goto DONE;
846     }
847
848     query_string = query_string_from_args (local, argc - i, &argv[i]);
849
850     query = notmuch_query_create (notmuch, query_string);
851     if (query == NULL) {
852         fprintf (stderr, "Out of memory.\n");
853         ret = 1;
854         goto DONE;
855     }
856
857     for (results = notmuch_query_search_messages (query);
858          notmuch_message_results_has_more (results);
859          notmuch_message_results_advance (results))
860     {
861         message = notmuch_message_results_get (results);
862
863         notmuch_message_freeze (message);
864
865         for (i = 0; i < remove_tags_count; i++)
866             notmuch_message_remove_tag (message,
867                                         argv[remove_tags[i]] + 1);
868
869         for (i = 0; i < add_tags_count; i++)
870             notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
871
872         notmuch_message_thaw (message);
873
874         notmuch_message_destroy (message);
875     }
876
877     notmuch_query_destroy (query);
878
879   DONE:
880     if (notmuch)
881         notmuch_database_close (notmuch);
882
883     talloc_free (local);
884
885     return ret;
886 }
887
888 static int
889 dump_command (int argc, char *argv[])
890 {
891     FILE *output = NULL;
892     notmuch_database_t *notmuch = NULL;
893     notmuch_query_t *query;
894     notmuch_message_results_t *results;
895     notmuch_message_t *message;
896     notmuch_tags_t *tags;
897     int ret = 0;
898
899     if (argc) {
900         output = fopen (argv[0], "w");
901         if (output == NULL) {
902             fprintf (stderr, "Error opening %s for writing: %s\n",
903                      argv[0], strerror (errno));
904             ret = 1;
905             goto DONE;
906         }
907     } else {
908         output = stdout;
909     }
910
911     notmuch = notmuch_database_open (NULL);
912     if (notmuch == NULL) {
913         ret = 1;
914         goto DONE;
915     }
916
917     query = notmuch_query_create (notmuch, "");
918     if (query == NULL) {
919         fprintf (stderr, "Out of memory\n");
920         ret = 1;
921         goto DONE;
922     }
923
924     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
925
926     for (results = notmuch_query_search_messages (query);
927          notmuch_message_results_has_more (results);
928          notmuch_message_results_advance (results))
929     {
930         int first = 1;
931         message = notmuch_message_results_get (results);
932
933         fprintf (output,
934                  "%s (", notmuch_message_get_message_id (message));
935
936         for (tags = notmuch_message_get_tags (message);
937              notmuch_tags_has_more (tags);
938              notmuch_tags_advance (tags))
939         {
940             if (! first)
941                 fprintf (output, " ");
942
943             fprintf (output, "%s", notmuch_tags_get (tags));
944
945             first = 0;
946         }
947
948         fprintf (output, ")\n");
949
950         notmuch_message_destroy (message);
951     }
952
953     notmuch_query_destroy (query);
954
955   DONE:
956     if (notmuch)
957         notmuch_database_close (notmuch);
958     if (output && output != stdout)
959         fclose (output);
960
961     return ret;
962 }
963
964 static int
965 restore_command (int argc, char *argv[])
966 {
967     FILE *input = NULL;
968     notmuch_database_t *notmuch = NULL;
969     char *line = NULL;
970     size_t line_size;
971     ssize_t line_len;
972     regex_t regex;
973     int rerr;
974     int ret = 0;
975
976     if (argc) {
977         input = fopen (argv[0], "r");
978         if (input == NULL) {
979             fprintf (stderr, "Error opening %s for reading: %s\n",
980                      argv[0], strerror (errno));
981             ret = 1;
982             goto DONE;
983         }
984     } else {
985         printf ("No filename given. Reading dump from stdin.\n");
986         input = stdin;
987     }
988
989     notmuch = notmuch_database_open (NULL);
990     if (notmuch == NULL) {
991         ret = 1;
992         goto DONE;
993     }
994
995     /* Dump output is one line per message. We match a sequence of
996      * non-space characters for the message-id, then one or more
997      * spaces, then a list of space-separated tags as a sequence of
998      * characters within literal '(' and ')'. */
999     xregcomp (&regex,
1000               "^([^ ]+) \\(([^)]*)\\)$",
1001               REG_EXTENDED);
1002
1003     while ((line_len = getline (&line, &line_size, input)) != -1) {
1004         regmatch_t match[3];
1005         char *message_id, *tags, *tag, *next;
1006         notmuch_message_t *message;
1007         notmuch_status_t status;
1008
1009         chomp_newline (line);
1010
1011         rerr = xregexec (&regex, line, 3, match, 0);
1012         if (rerr == REG_NOMATCH)
1013         {
1014             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
1015                      line);
1016             continue;
1017         }
1018
1019         message_id = xstrndup (line + match[1].rm_so,
1020                                match[1].rm_eo - match[1].rm_so);
1021         tags = xstrndup (line + match[2].rm_so,
1022                          match[2].rm_eo - match[2].rm_so);
1023
1024         if (strlen (tags)) {
1025
1026             message = notmuch_database_find_message (notmuch, message_id);
1027             if (message == NULL) {
1028                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
1029                          message_id);
1030                 goto NEXT_LINE;
1031             }
1032
1033             notmuch_message_freeze (message);
1034
1035             notmuch_message_remove_all_tags (message);
1036
1037             next = tags;
1038             while (next) {
1039                 tag = strsep (&next, " ");
1040                 if (*tag == '\0')
1041                     continue;
1042                 status = notmuch_message_add_tag (message, tag);
1043                 if (status) {
1044                     fprintf (stderr,
1045                              "Error applying tag %s to message %s:\n",
1046                              tag, message_id);
1047                     fprintf (stderr, "%s\n",
1048                              notmuch_status_to_string (status));
1049                 }
1050             }
1051
1052             notmuch_message_thaw (message);
1053             notmuch_message_destroy (message);
1054         }
1055       NEXT_LINE:
1056         free (message_id);
1057         free (tags);
1058     }
1059
1060     regfree (&regex);
1061
1062   DONE:
1063     if (line)
1064         free (line);
1065     if (notmuch)
1066         notmuch_database_close (notmuch);
1067     if (input && input != stdin)
1068         fclose (input);
1069
1070     return ret;
1071 }
1072
1073 static int
1074 help_command (int argc, char *argv[]);
1075
1076 command_t commands[] = {
1077     { "setup", setup_command,
1078       "Interactively setup notmuch for first use.",
1079       "\t\tThe setup command is the first command you will run in order\n"
1080       "\t\tto start using notmuch. It will prompt you for the directory\n"
1081       "\t\tcontaining your email archives, and will then proceed to build\n"
1082       "\t\ta database to allow fast searching of that mail.\n\n"
1083       "\t\tInvoking notmuch with no command argument will run setup if\n"
1084       "\t\tthe setup command has not previously been completed." },
1085     { "new", new_command,
1086       "Find and import any new messages.",
1087       "\t\tScans all sub-directories of the database, adding new messages\n"
1088       "\t\tthat are found. Each new message will be tagged as both\n"
1089       "\t\t\"inbox\" and \"unread\".\n"
1090       "\n"
1091       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
1092       "\t\tso you can use that to mark tdirectories that will not\n"
1093       "\t\treceive any new mail (and make \"notmuch new\" faster)." },
1094     { "search", search_command,
1095       "<search-term> [...]\n\n"
1096       "\t\tSearch for threads matching the given search terms.",
1097       "\t\tNote that the individual mail messages will be matched\n"
1098       "\t\tagainst the search terms, but the results will be the\n"
1099       "\t\tthreads containing the matched messages.\n\n"
1100       "\t\tCurrently, the supported search terms are as follows, (where\n"
1101       "\t\t<brackets> indicate user-supplied values):\n\n"
1102       "\t\t\ttag:<tag>\n"
1103       "\t\t\tid:<message-id>\n"
1104       "\t\t\tthread:<thread-id>\n\n"
1105       "\t\tValid tag values include \"inbox\" and \"unread\" by default\n"
1106       "\t\tfor new messages added by \"notmuch new\" as well as any other\n"
1107       "\t\ttag values added manually with \"notmuch tag\".\n\n"
1108       "\t\tMessage ID values are the literal contents of the Message-ID:\n"
1109       "\t\theader of email messages, but without the '<','>' delimiters.\n\n"
1110       "\t\tThread ID values are generated internally by notmuch but can\n"
1111       "\t\tbe seen in the output of \"notmuch search\" for example.\n\n"
1112       "\t\tIn addition to individual terms, multiple terms can be\n"
1113       "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
1114       "\t\tEach term in the query will be implicitly connected by a\n"
1115       "\t\tlogical AND if no explicit operator is provided, (except\n"
1116       "\t\tthat terms with a common prefix will be implicitly combined\n"
1117       "\t\twith OR until we get Xapian defect #402 fixed).\n\n"
1118       "\t\tParentheses can also be used to control the combination of\n"
1119       "\t\tthe Boolean operators, but will have to be protected from\n"
1120       "\t\tinterpretation by the shell, (such as by putting quotation\n"
1121       "\t\tmarks around any parenthesized expression)." },
1122     { "show", show_command,
1123       "<thread-id>\n\n"
1124       "\t\tNote: The \"notmuch show\" command is not implemented yet.\n\n"
1125       "\t\tShow the thread with the given thread ID (see 'search').",
1126       "\t\tThread ID values are given as the first column in the\n"
1127       "\t\toutput of the \"notmuch search\" command. These are the\n"
1128       "\t\trandom-looking strings of 32 characters." },
1129     { "tag", tag_command,
1130       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
1131       "\t\tAdd/remove tags for all messages matching the search terms.",
1132       "\t\tThe search terms are handled exactly as in 'search' so one\n"
1133       "\t\tcan use that command first to see what will be modified.\n\n"
1134       "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n"
1135       "\t\tremoved. For each message, tag removal is before tag addition.\n\n"
1136       "\t\tThe beginning of <search-terms> is recognized by the first\n"
1137       "\t\targument that begins with neither '+' nor '-'. Support for\n"
1138       "\t\tan initial search term beginning with '+' or '-' is provided\n"
1139       "\t\tby allowing the user to specify a \"--\" argument to separate\n"
1140       "\t\tthe tags from the search terms.\n\n"
1141       "\t\tNote: If you run \"notmuch new\" between reading a thread with\n"
1142       "\t\t\"notmuch show\" and removing the \"inbox\" tag for that thread\n"
1143       "\t\twith \"notmuch tag\" then you create the possibility of moving\n"
1144       "\t\tsome messages from that thread out of your inbox without ever\n"
1145       "\t\treading them. The easiest way to avoid this problem is to not\n"
1146       "\t\trun \"notmuch new\" between reading and removing tags." },
1147     { "dump", dump_command,
1148       "[<filename>]\n\n"
1149       "\t\tCreate a plain-text dump of the tags for each message.",
1150       "\t\tOutput is to the given filename, if any, or to stdout.\n"
1151       "\t\tThese tags are the only data in the notmuch database\n"
1152       "\t\tthat can't be recreated from the messages themselves.\n"
1153       "\t\tThe output of notmuch dump is therefore the only\n"
1154       "\t\tcritical thing to backup (and much more friendly to\n"
1155       "\t\tincremental backup than the native database files.)" },
1156     { "restore", restore_command,
1157       "<filename>\n\n"
1158       "\t\tRestore the tags from the given dump file (see 'dump').",
1159       "\t\tNote: The dump file format is specifically chosen to be\n"
1160       "\t\tcompatible with the format of files produced by sup-dump.\n"
1161       "\t\tSo if you've previously been using sup for mail, then the\n"
1162       "\t\t\"notmuch restore\" command provides you a way to import\n"
1163       "\t\tall of your tags (or labels as sup calls them)." },
1164     { "help", help_command,
1165       "[<command>]\n\n"
1166       "\t\tThis message, or more detailed help for the named command.",
1167       "\t\tExcept in this case, where there's not much more detailed\n"
1168       "\t\thelp available." }
1169 };
1170
1171 static void
1172 usage (void)
1173 {
1174     command_t *command;
1175     unsigned int i;
1176
1177     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
1178     fprintf (stderr, "\n");
1179     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
1180     fprintf (stderr, "\n");
1181
1182     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1183         command = &commands[i];
1184
1185         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
1186     }
1187
1188     fprintf (stderr, "Use \"notmuch help <command>\" for more details on each command.\n\n");
1189 }
1190
1191 static int
1192 help_command (int argc, char *argv[])
1193 {
1194     command_t *command;
1195     unsigned int i;
1196
1197     if (argc == 0) {
1198         fprintf (stderr, "The notmuch mail system.\n\n");
1199         usage ();
1200         return 0;
1201     }
1202
1203     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1204         command = &commands[i];
1205
1206         if (strcmp (argv[0], command->name) == 0) {
1207             fprintf (stderr, "Help for \"notmuch %s\":\n\n", argv[0]);
1208             fprintf (stderr, "\t%s\t%s\n\n%s\n\n", command->name,
1209                      command->summary, command->documentation);
1210             return 0;
1211         }
1212     }
1213
1214     fprintf (stderr,
1215              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
1216              argv[0]);
1217     return 1;
1218 }
1219     
1220 int
1221 main (int argc, char *argv[])
1222 {
1223     command_t *command;
1224     unsigned int i;
1225
1226     if (argc == 1)
1227         return setup_command (0, NULL);
1228
1229     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1230         command = &commands[i];
1231
1232         if (strcmp (argv[1], command->name) == 0)
1233             return (command->function) (argc - 2, &argv[2]);
1234     }
1235
1236     /* Don't complain about "help" being an unknown command when we're
1237        about to provide exactly what's wanted anyway. */
1238     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
1239     usage ();
1240
1241     return 1;
1242 }