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