923a7cee53eb366cd91ee822ffb8c7678e157621
[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 <gmime/gmime.h>
27
28 #include "notmuch.h"
29
30 /* This is separate from notmuch-private.h because we're trying to
31  * keep notmuch.c from looking into any internals, (which helps us
32  * develop notmuch.h into a plausible library interface).
33  */
34 #include "xutil.h"
35
36 #include <stddef.h>
37 #include <string.h>
38 #include <sys/stat.h>
39 #include <sys/time.h>
40 #include <unistd.h>
41 #include <dirent.h>
42 #include <errno.h>
43 #include <signal.h>
44
45 #include <talloc.h>
46
47 #define unused(x) x __attribute__ ((unused))
48
49 /* There's no point in continuing when we've detected that we've done
50  * something wrong internally (as opposed to the user passing in a
51  * bogus value).
52  *
53  * Note that __location__ comes from talloc.h.
54  */
55 #define INTERNAL_ERROR(format, ...)                     \
56     do {                                                \
57         fprintf(stderr,                                 \
58                 "Internal error: " format " (%s)\n",    \
59                 ##__VA_ARGS__, __location__);           \
60         exit (1);                                       \
61     } while (0)
62
63 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
64
65 typedef int (*command_function_t) (void *ctx, int argc, char *argv[]);
66
67 typedef struct command {
68     const char *name;
69     command_function_t function;
70     const char *summary;
71     const char *documentation;
72 } command_t;
73
74 typedef void (*add_files_callback_t) (notmuch_message_t *message);
75
76 typedef struct {
77     int ignore_read_only_directories;
78     int saw_read_only_directory;
79
80     int total_files;
81     int processed_files;
82     int added_messages;
83     struct timeval tv_start;
84
85     add_files_callback_t callback;
86 } add_files_state_t;
87
88 static void
89 chomp_newline (char *str)
90 {
91     if (str && str[strlen(str)-1] == '\n')
92         str[strlen(str)-1] = '\0';
93 }
94
95 /* Compute the number of seconds elapsed from start to end. */
96 static double
97 tv_elapsed (struct timeval start, struct timeval end)
98 {
99     return ((end.tv_sec - start.tv_sec) +
100             (end.tv_usec - start.tv_usec) / 1e6);
101 }
102
103 static void
104 print_formatted_seconds (double seconds)
105 {
106     int hours;
107     int minutes;
108
109     if (seconds < 1) {
110         printf ("almost no time");
111         return;
112     }
113
114     if (seconds > 3600) {
115         hours = (int) seconds / 3600;
116         printf ("%dh ", hours);
117         seconds -= hours * 3600;
118     }
119
120     if (seconds > 60) {
121         minutes = (int) seconds / 60;
122         printf ("%dm ", minutes);
123         seconds -= minutes * 60;
124     }
125
126     printf ("%ds", (int) seconds);
127 }
128
129 static volatile sig_atomic_t do_add_files_print_progress = 0;
130
131 static void
132 handle_sigalrm (unused (int signal))
133 {
134     do_add_files_print_progress = 1;
135 }
136
137 static void
138 add_files_print_progress (add_files_state_t *state)
139 {
140     struct timeval tv_now;
141     double elapsed_overall, rate_overall;
142
143     gettimeofday (&tv_now, NULL);
144
145     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
146     rate_overall = (state->processed_files) / elapsed_overall;
147
148     printf ("Processed %d", state->processed_files);
149
150     if (state->total_files) {
151         printf (" of %d files (", state->total_files);
152         print_formatted_seconds ((state->total_files - state->processed_files) /
153                                  rate_overall);
154         printf (" remaining).      \r");
155     } else {
156         printf (" files (%d files/sec.)    \r", (int) rate_overall);
157     }
158
159     fflush (stdout);
160 }
161
162 /* Examine 'path' recursively as follows:
163  *
164  *   o Ask the filesystem for the mtime of 'path' (path_mtime)
165  *
166  *   o Ask the database for its timestamp of 'path' (path_dbtime)
167  *
168  *   o If 'path_mtime' > 'path_dbtime'
169  *
170  *       o For each regular file in 'path' with mtime newer than the
171  *         'path_dbtime' call add_message to add the file to the
172  *         database.
173  *
174  *       o For each sub-directory of path, recursively call into this
175  *         same function.
176  *
177  *   o Tell the database to update its time of 'path' to 'path_mtime'
178  *
179  * The 'struct stat *st' must point to a structure that has already
180  * been initialized for 'path' by calling stat().
181  */
182 static notmuch_status_t
183 add_files_recursive (notmuch_database_t *notmuch,
184                      const char *path,
185                      struct stat *st,
186                      add_files_state_t *state)
187 {
188     DIR *dir = NULL;
189     struct dirent *e, *entry = NULL;
190     int entry_length;
191     int err;
192     char *next = NULL;
193     time_t path_mtime, path_dbtime;
194     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
195     notmuch_message_t *message = NULL, **closure;
196
197     /* If we're told to, we bail out on encountering a read-only
198      * directory, (with this being a clear clue from the user to
199      * Notmuch that new mail won't be arriving there and we need not
200      * look. */
201     if (state->ignore_read_only_directories &&
202         (st->st_mode & S_IWUSR) == 0)
203     {
204         state->saw_read_only_directory = TRUE;
205         goto DONE;
206     }
207
208     path_mtime = st->st_mtime;
209
210     path_dbtime = notmuch_database_get_timestamp (notmuch, path);
211
212     dir = opendir (path);
213     if (dir == NULL) {
214         fprintf (stderr, "Error opening directory %s: %s\n",
215                  path, strerror (errno));
216         ret = NOTMUCH_STATUS_FILE_ERROR;
217         goto DONE;
218     }
219
220     entry_length = offsetof (struct dirent, d_name) +
221         pathconf (path, _PC_NAME_MAX) + 1;
222     entry = malloc (entry_length);
223
224     while (1) {
225         err = readdir_r (dir, entry, &e);
226         if (err) {
227             fprintf (stderr, "Error reading directory: %s\n",
228                      strerror (errno));
229             ret = NOTMUCH_STATUS_FILE_ERROR;
230             goto DONE;
231         }
232
233         if (e == NULL)
234             break;
235
236         /* If this directory hasn't been modified since the last
237          * add_files, then we only need to look further for
238          * sub-directories. */
239         if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
240             continue;
241
242         /* Ignore special directories to avoid infinite recursion.
243          * Also ignore the .notmuch directory.
244          */
245         /* XXX: Eventually we'll want more sophistication to let the
246          * user specify files to be ignored. */
247         if (strcmp (entry->d_name, ".") == 0 ||
248             strcmp (entry->d_name, "..") == 0 ||
249             strcmp (entry->d_name, ".notmuch") ==0)
250         {
251             continue;
252         }
253
254         next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
255
256         if (stat (next, st)) {
257             fprintf (stderr, "Error reading %s: %s\n",
258                      next, strerror (errno));
259             ret = NOTMUCH_STATUS_FILE_ERROR;
260             continue;
261         }
262
263         if (S_ISREG (st->st_mode)) {
264             /* If the file hasn't been modified since the last
265              * add_files, then we need not look at it. */
266             if (st->st_mtime > path_dbtime) {
267                 state->processed_files++;
268
269                 if (state->callback)
270                     closure = &message;
271                 else
272                     closure = NULL;
273
274                 status = notmuch_database_add_message (notmuch, next, closure);
275                 switch (status) {
276                     /* success */
277                     case NOTMUCH_STATUS_SUCCESS:
278                         state->added_messages++;
279                         if (state->callback)
280                             (state->callback) (message);
281                         break;
282                     /* Non-fatal issues (go on to next file) */
283                     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
284                         /* Stay silent on this one. */
285                         break;
286                     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
287                         fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
288                                  next);
289                         break;
290                     /* Fatal issues. Don't process anymore. */
291                     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
292                     case NOTMUCH_STATUS_OUT_OF_MEMORY:
293                         fprintf (stderr, "Error: %s. Halting processing.\n",
294                                  notmuch_status_to_string (status));
295                         ret = status;
296                         goto DONE;
297                     default:
298                     case NOTMUCH_STATUS_FILE_ERROR:
299                     case NOTMUCH_STATUS_NULL_POINTER:
300                     case NOTMUCH_STATUS_TAG_TOO_LONG:
301                     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
302                     case NOTMUCH_STATUS_LAST_STATUS:
303                         INTERNAL_ERROR ("add_message returned unexpected value: %d",  status);
304                         goto DONE;
305                 }
306
307                 if (message) {
308                     notmuch_message_destroy (message);
309                     message = NULL;
310                 }
311
312                 if (do_add_files_print_progress) {
313                     do_add_files_print_progress = 0;
314                     add_files_print_progress (state);
315                 }
316             }
317         } else if (S_ISDIR (st->st_mode)) {
318             status = add_files_recursive (notmuch, next, st, state);
319             if (status && ret == NOTMUCH_STATUS_SUCCESS)
320                 ret = status;
321         }
322
323         talloc_free (next);
324         next = NULL;
325     }
326
327     status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
328     if (status && ret == NOTMUCH_STATUS_SUCCESS)
329         ret = status;
330
331   DONE:
332     if (next)
333         talloc_free (next);
334     if (entry)
335         free (entry);
336     if (dir)
337         closedir (dir);
338
339     return ret;
340 }
341
342 /* This is the top-level entry point for add_files. It does a couple
343  * of error checks, sets up the progress-printing timer and then calls
344  * into the recursive function. */
345 static notmuch_status_t
346 add_files (notmuch_database_t *notmuch,
347            const char *path,
348            add_files_state_t *state)
349 {
350     struct stat st;
351     notmuch_status_t status;
352     struct sigaction action;
353     struct itimerval timerval;
354
355     if (stat (path, &st)) {
356         fprintf (stderr, "Error reading directory %s: %s\n",
357                  path, strerror (errno));
358         return NOTMUCH_STATUS_FILE_ERROR;
359     }
360
361     if (! S_ISDIR (st.st_mode)) {
362         fprintf (stderr, "Error: %s is not a directory.\n", path);
363         return NOTMUCH_STATUS_FILE_ERROR;
364     }
365
366     /* Setup our handler for SIGALRM */
367     memset (&action, 0, sizeof (struct sigaction));
368     action.sa_handler = handle_sigalrm;
369     sigemptyset (&action.sa_mask);
370     action.sa_flags = SA_RESTART;
371     sigaction (SIGALRM, &action, NULL);
372
373     /* Then start a timer to send SIGALRM once per second. */
374     timerval.it_interval.tv_sec = 1;
375     timerval.it_interval.tv_usec = 0;
376     timerval.it_value.tv_sec = 1;
377     timerval.it_value.tv_usec = 0;
378     setitimer (ITIMER_REAL, &timerval, NULL);
379
380     status = add_files_recursive (notmuch, path, &st, state);
381
382     /* Now stop the timer. */
383     timerval.it_interval.tv_sec = 0;
384     timerval.it_interval.tv_usec = 0;
385     timerval.it_value.tv_sec = 0;
386     timerval.it_value.tv_usec = 0;
387     setitimer (ITIMER_REAL, &timerval, NULL);
388
389     /* And disable the signal handler. */
390     action.sa_handler = SIG_IGN;
391     sigaction (SIGALRM, &action, NULL);
392
393     return status;
394 }
395
396 /* Recursively count all regular files in path and all sub-direcotries
397  * of path.  The result is added to *count (which should be
398  * initialized to zero by the top-level caller before calling
399  * count_files). */
400 static void
401 count_files (const char *path, int *count)
402 {
403     DIR *dir;
404     struct dirent *e, *entry = NULL;
405     int entry_length;
406     int err;
407     char *next;
408     struct stat st;
409
410     dir = opendir (path);
411
412     if (dir == NULL) {
413         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
414                  path, strerror (errno));
415         goto DONE;
416     }
417
418     entry_length = offsetof (struct dirent, d_name) +
419         pathconf (path, _PC_NAME_MAX) + 1;
420     entry = malloc (entry_length);
421
422     while (1) {
423         err = readdir_r (dir, entry, &e);
424         if (err) {
425             fprintf (stderr, "Error reading directory: %s\n",
426                      strerror (errno));
427             free (entry);
428             goto DONE;
429         }
430
431         if (e == NULL)
432             break;
433
434         /* Ignore special directories to avoid infinite recursion.
435          * Also ignore the .notmuch directory.
436          */
437         /* XXX: Eventually we'll want more sophistication to let the
438          * user specify files to be ignored. */
439         if (strcmp (entry->d_name, ".") == 0 ||
440             strcmp (entry->d_name, "..") == 0 ||
441             strcmp (entry->d_name, ".notmuch") == 0)
442         {
443             continue;
444         }
445
446         if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
447             next = NULL;
448             fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
449                      path, entry->d_name);
450             continue;
451         }
452
453         stat (next, &st);
454
455         if (S_ISREG (st.st_mode)) {
456             *count = *count + 1;
457             if (*count % 1000 == 0) {
458                 printf ("Found %d files so far.\r", *count);
459                 fflush (stdout);
460             }
461         } else if (S_ISDIR (st.st_mode)) {
462             count_files (next, count);
463         }
464
465         free (next);
466     }
467
468   DONE:
469     if (entry)
470         free (entry);
471
472     closedir (dir);
473 }
474
475 static int
476 setup_command (unused (void *ctx), unused (int argc), unused (char *argv[]))
477 {
478     notmuch_database_t *notmuch = NULL;
479     char *default_path, *mail_directory = NULL;
480     size_t line_size;
481     int count;
482     add_files_state_t add_files_state;
483     double elapsed;
484     struct timeval tv_now;
485     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
486
487     printf ("Welcome to notmuch!\n\n");
488
489     printf ("The goal of notmuch is to help you manage and search your collection of\n"
490             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
491
492     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
493             "(where you already have mail stored and where messages will be delivered\n"
494             "in the future). This directory can contain any number of sub-directories\n"
495             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
496             "archives are perfect). If there are other, non-email files (such as\n"
497             "indexes maintained by other email programs) then notmuch will do its\n"
498             "best to detect those and ignore them.\n\n");
499
500     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
501             "messages), will not work with notmuch. If that's how your mail is currently\n"
502             "stored, we recommend you first convert it to maildir format with a utility\n"
503             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
504             "once the conversion is complete.\n\n");
505
506
507     default_path = notmuch_database_default_path ();
508     printf ("Top-level mail directory [%s]: ", default_path);
509     fflush (stdout);
510
511     getline (&mail_directory, &line_size, stdin);
512     chomp_newline (mail_directory);
513
514     printf ("\n");
515
516     if (mail_directory == NULL || strlen (mail_directory) == 0) {
517         if (mail_directory)
518             free (mail_directory);
519         mail_directory = default_path;
520     } else {
521         /* XXX: Instead of telling the user to use an environment
522          * variable here, we should really be writing out a configuration
523          * file and loading that on the next run. */
524         if (strcmp (mail_directory, default_path)) {
525             printf ("Note: Since you are not using the default path, you will want to set\n"
526                     "the NOTMUCH_BASE environment variable to %s so that\n"
527                     "future calls to notmuch commands will know where to find your mail.\n",
528                     mail_directory);
529             printf ("For example, if you are using bash for your shell, add:\n\n");
530             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
531             printf ("to your ~/.bashrc file.\n\n");
532         }
533         free (default_path);
534     }
535
536     /* Coerce the directory into an absolute directory name. */
537     if (*mail_directory != '/') {
538         char *cwd, *absolute_mail_directory;
539
540         cwd = getcwd (NULL, 0);
541         if (cwd == NULL) {
542             fprintf (stderr, "Out of memory.\n");
543             exit (1);
544         }
545
546         if (asprintf (&absolute_mail_directory, "%s/%s",
547                       cwd, mail_directory) < 0)
548         {
549             fprintf (stderr, "Out of memory.\n");
550             exit (1);
551         }
552
553         free (cwd);
554         free (mail_directory);
555         mail_directory = absolute_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 (void *ctx), 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 /* Format a nice representation of 'time' relative to the current time.
737  *
738  * Examples include:
739  *
740  *      5 mins. ago     (For times less than 60 minutes ago)
741  *      Today 12:30     (For times >60 minutes but still today)
742  *      Yest. 12:30
743  *      Mon.  12:30     (Before yesterday but fewer than 7 days ago)
744  *      October 12      (Between 7 and 180 days ago (about 6 months))
745  *      2008-06-30      (More than 180 days ago)
746  */
747 #define MINUTE (60)
748 #define HOUR (60 * MINUTE)
749 #define DAY (24 * HOUR)
750 #define RELATIVE_DATE_MAX 20
751 static const char *
752 _format_relative_date (void *ctx, time_t then)
753 {
754     struct tm tm_now, tm_then;
755     time_t now = time(NULL);
756     time_t delta;
757     char *result;
758
759     localtime_r (&now, &tm_now);
760     localtime_r (&then, &tm_then);
761
762     result = talloc_zero_size (ctx, RELATIVE_DATE_MAX);
763     if (result == NULL)
764         return "when?";
765
766     if (then > now)
767         return "the future";
768
769     delta = now - then;
770
771     if (delta > 180 * DAY) {
772         strftime (result, RELATIVE_DATE_MAX,
773                   "%F", &tm_then); /* 2008-06-30 */
774         return result;
775     }
776
777     if (delta < 3600) {
778         snprintf (result, RELATIVE_DATE_MAX,
779                   "%d mins. ago", (int) (delta / 60));
780         return result;
781     }
782
783     if (delta <= 7 * DAY) {
784         if (tm_then.tm_wday == tm_now.tm_wday &&
785             delta < DAY)
786         {
787             strftime (result, RELATIVE_DATE_MAX,
788                       "Today %R", &tm_then); /* Today 12:30 */
789             return result;
790         } else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
791             strftime (result, RELATIVE_DATE_MAX,
792                       "Yest. %R", &tm_then); /* Yest. 12:30 */
793             return result;
794         } else {
795             if (tm_then.tm_wday != tm_now.tm_wday) {
796                 strftime (result, RELATIVE_DATE_MAX,
797                           "%a. %R", &tm_then); /* Mon. 12:30 */
798                 return result;
799             }
800         }
801     }
802
803     strftime (result, RELATIVE_DATE_MAX,
804               "%B %d", &tm_then); /* October 12 */
805     return result;
806 }
807 #undef MINUTE
808 #undef HOUR
809 #undef DAY
810
811 static int
812 search_command (void *ctx, int argc, char *argv[])
813 {
814     void *local = talloc_new (ctx);
815     notmuch_database_t *notmuch = NULL;
816     notmuch_query_t *query;
817     notmuch_threads_t *threads;
818     notmuch_thread_t *thread;
819     notmuch_tags_t *tags;
820     char *query_str;
821     const char *relative_date;
822     time_t date;
823     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
824
825     notmuch = notmuch_database_open (NULL);
826     if (notmuch == NULL) {
827         ret = 1;
828         goto DONE;
829     }
830
831     query_str = query_string_from_args (local, argc, argv);
832
833     query = notmuch_query_create (notmuch, query_str);
834     if (query == NULL) {
835         fprintf (stderr, "Out of memory\n");
836         ret = 1;
837         goto DONE;
838     }
839
840     for (threads = notmuch_query_search_threads (query);
841          notmuch_threads_has_more (threads);
842          notmuch_threads_advance (threads))
843     {
844         int first = 1;
845
846         thread = notmuch_threads_get (threads);
847
848         date = notmuch_thread_get_oldest_date (thread);
849         relative_date = _format_relative_date (local, date);
850
851         printf ("thread:%s %12s %s",
852                 notmuch_thread_get_thread_id (thread),
853                 relative_date,
854                 notmuch_thread_get_subject (thread));
855
856         printf (" (");
857         for (tags = notmuch_thread_get_tags (thread);
858              notmuch_tags_has_more (tags);
859              notmuch_tags_advance (tags))
860         {
861             if (! first)
862                 printf (" ");
863             printf ("%s", notmuch_tags_get (tags));
864             first = 0;
865         }
866         printf (")\n");
867
868         notmuch_thread_destroy (thread);
869     }
870
871     notmuch_query_destroy (query);
872
873   DONE:
874     if (notmuch)
875         notmuch_database_close (notmuch);
876     talloc_free (local);
877
878     return ret;
879 }
880
881 static const char *
882 _get_tags_as_string (void *ctx, notmuch_message_t *message)
883 {
884     notmuch_tags_t *tags;
885     int first = 1;
886     const char *tag;
887     char *result;
888
889     result = talloc_strdup (ctx, "");
890     if (result == NULL)
891         return NULL;
892
893     for (tags = notmuch_message_get_tags (message);
894          notmuch_tags_has_more (tags);
895          notmuch_tags_advance (tags))
896     {
897         tag = notmuch_tags_get (tags);
898
899         result = talloc_asprintf_append (result, "%s%s",
900                                          first ? "" : " ", tag);
901         first = 0;
902     }
903
904     return result;
905 }
906
907 /* Get a nice, single-line summary of message. */
908 static const char *
909 _get_one_line_summary (void *ctx, notmuch_message_t *message)
910 {
911     const char *from;
912     time_t date;
913     const char *relative_date;
914     const char *tags;
915
916     from = notmuch_message_get_header (message, "from");
917
918     date = notmuch_message_get_date (message);
919     relative_date = _format_relative_date (ctx, date);
920
921     tags = _get_tags_as_string (ctx, message);
922
923     return talloc_asprintf (ctx, "%s (%s) (%s)",
924                             from, relative_date, tags);
925 }
926
927 static void
928 show_message_part (GMimeObject *part, int *part_count)
929 {
930     GMimeStream *stream;
931     GMimeDataWrapper *wrapper;
932     GMimeContentDisposition *disposition;
933     GMimeContentType *content_type;
934
935     *part_count = *part_count + 1;
936
937     if (GMIME_IS_MULTIPART (part)) {
938         GMimeMultipart *multipart = GMIME_MULTIPART (part);
939         int i;
940
941         for (i = 0; i < g_mime_multipart_get_count (multipart); i++) {
942             if (GMIME_IS_MULTIPART_SIGNED (multipart)) {
943                 /* Don't index the signature. */
944                 if (i == 1)
945                     continue;
946                 if (i > 1)
947                     fprintf (stderr, "Warning: Unexpected extra parts of mutlipart/signed. Continuing.\n");
948             }
949             show_message_part (g_mime_multipart_get_part (multipart, i),
950                                part_count);
951         }
952         return;
953     }
954
955     if (GMIME_IS_MESSAGE_PART (part)) {
956         GMimeMessage *mime_message;
957
958         mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
959
960         show_message_part (g_mime_message_get_mime_part (mime_message),
961                            part_count);
962
963         return;
964     }
965
966     if (! (GMIME_IS_PART (part))) {
967         fprintf (stderr, "Warning: Not displaying unknown mime part: %s.\n",
968                  g_type_name (G_OBJECT_TYPE (part)));
969         return;
970     }
971
972     disposition = g_mime_object_get_content_disposition (part);
973     if (disposition &&
974         strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
975     {
976         const char *filename = g_mime_part_get_filename (GMIME_PART (part));
977         content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
978
979         printf ("\fattachment{ ID: %d, Content-type: %s\n",
980                 *part_count,
981                 g_mime_content_type_to_string (content_type));
982         printf ("Attachment: %s (%s)\n", filename,
983                 g_mime_content_type_to_string (content_type));
984         printf ("\fattachment}\n");
985
986         return;
987     }
988
989     content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
990
991     printf ("\fpart{ ID: %d, Content-type: %s\n",
992             *part_count,
993             g_mime_content_type_to_string (content_type));
994
995     if (g_mime_content_type_is_type (content_type, "text", "*") &&
996         !g_mime_content_type_is_type (content_type, "text", "html"))
997     {
998         stream = g_mime_stream_file_new (stdout);
999         g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
1000
1001         wrapper = g_mime_part_get_content_object (GMIME_PART (part));
1002         if (wrapper)
1003             g_mime_data_wrapper_write_to_stream (wrapper, stream);
1004
1005         g_object_unref (stream);
1006     }
1007     else
1008     {
1009         printf ("Non-text part: %s\n",
1010                 g_mime_content_type_to_string (content_type));
1011     }
1012
1013     printf ("\fpart}\n");
1014 }
1015
1016 static notmuch_status_t
1017 show_message_body (const char *filename)
1018 {
1019     GMimeStream *stream = NULL;
1020     GMimeParser *parser = NULL;
1021     GMimeMessage *mime_message = NULL;
1022     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1023     static int initialized = 0;
1024     FILE *file = NULL;
1025     int part_count = 0;
1026
1027     if (! initialized) {
1028         g_mime_init (0);
1029         initialized = 1;
1030     }
1031
1032     file = fopen (filename, "r");
1033     if (! file) {
1034         fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
1035         ret = NOTMUCH_STATUS_FILE_ERROR;
1036         goto DONE;
1037     }
1038
1039     stream = g_mime_stream_file_new (file);
1040     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
1041
1042     parser = g_mime_parser_new_with_stream (stream);
1043
1044     mime_message = g_mime_parser_construct_message (parser);
1045
1046     show_message_part (g_mime_message_get_mime_part (mime_message),
1047                        &part_count);
1048
1049   DONE:
1050     if (mime_message)
1051         g_object_unref (mime_message);
1052
1053     if (parser)
1054         g_object_unref (parser);
1055
1056     if (stream)
1057         g_object_unref (stream);
1058
1059     if (file)
1060         fclose (file);
1061
1062     return ret;
1063 }
1064
1065 static int
1066 show_command (void *ctx, unused (int argc), unused (char *argv[]))
1067 {
1068     void *local = talloc_new (ctx);
1069     char *query_string;
1070     notmuch_database_t *notmuch = NULL;
1071     notmuch_query_t *query = NULL;
1072     notmuch_messages_t *messages;
1073     notmuch_message_t *message;
1074     int ret = 0;
1075
1076     const char *headers[] = {
1077         "From", "To", "Cc", "Bcc", "Date"
1078     };
1079     const char *name, *value;
1080     unsigned int i;
1081
1082     notmuch = notmuch_database_open (NULL);
1083     if (notmuch == NULL) {
1084         ret = 1;
1085         goto DONE;
1086     }
1087
1088     query_string = query_string_from_args (local, argc, argv);
1089     if (query_string == NULL) {
1090         fprintf (stderr, "Out of memory\n");
1091         ret = 1;
1092         goto DONE;
1093     }
1094
1095     query = notmuch_query_create (notmuch, query_string);
1096     if (query == NULL) {
1097         fprintf (stderr, "Out of memory\n");
1098         ret = 1;
1099         goto DONE;
1100     }
1101
1102     for (messages = notmuch_query_search_messages (query);
1103          notmuch_messages_has_more (messages);
1104          notmuch_messages_advance (messages))
1105     {
1106         message = notmuch_messages_get (messages);
1107
1108         printf ("\fmessage{ id:%s filename:%s\n",
1109                 notmuch_message_get_message_id (message),
1110                 notmuch_message_get_filename (message));
1111
1112         printf ("\fheader{\n");
1113
1114         printf ("%s\n", _get_one_line_summary (local, message));
1115
1116         printf ("%s\n", notmuch_message_get_header (message, "subject"));
1117
1118         for (i = 0; i < ARRAY_SIZE (headers); i++) {
1119             name = headers[i];
1120             value = notmuch_message_get_header (message, name);
1121             if (value)
1122                 printf ("%s: %s\n", name, value);
1123         }
1124
1125         printf ("\fheader}\n");
1126         printf ("\fbody{\n");
1127
1128         show_message_body (notmuch_message_get_filename (message));
1129
1130         printf ("\fbody}\n");
1131
1132         printf ("\fmessage}\n");
1133
1134         notmuch_message_destroy (message);
1135     }
1136
1137   DONE:
1138     if (local)
1139         talloc_free (local);
1140
1141     if (query)
1142         notmuch_query_destroy (query);
1143
1144     if (notmuch)
1145         notmuch_database_close (notmuch);
1146
1147     return ret;
1148 }
1149
1150 static int
1151 tag_command (void *ctx, unused (int argc), unused (char *argv[]))
1152 {
1153     void *local;
1154     int *add_tags, *remove_tags;
1155     int add_tags_count = 0;
1156     int remove_tags_count = 0;
1157     char *query_string;
1158     notmuch_database_t *notmuch = NULL;
1159     notmuch_query_t *query;
1160     notmuch_messages_t *messages;
1161     notmuch_message_t *message;
1162     int ret = 0;
1163     int i;
1164
1165     local = talloc_new (ctx);
1166     if (local == NULL) {
1167         ret = 1;
1168         goto DONE;
1169     }
1170
1171     add_tags = talloc_size (local, argc * sizeof (int));
1172     if (add_tags == NULL) {
1173         ret = 1;
1174         goto DONE;
1175     }
1176
1177     remove_tags = talloc_size (local, argc * sizeof (int));
1178     if (remove_tags == NULL) {
1179         ret = 1;
1180         goto DONE;
1181     }
1182
1183     for (i = 0; i < argc; i++) {
1184         if (strcmp (argv[i], "--") == 0) {
1185             i++;
1186             break;
1187         }
1188         if (argv[i][0] == '+') {
1189             add_tags[add_tags_count++] = i;
1190         } else if (argv[i][0] == '-') {
1191             remove_tags[remove_tags_count++] = i;
1192         } else {
1193             break;
1194         }
1195     }
1196
1197     if (add_tags_count == 0 && remove_tags_count == 0) {
1198         fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
1199         ret = 1;
1200         goto DONE;
1201     }
1202
1203     if (i == argc) {
1204         fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
1205         ret = 1;
1206         goto DONE;
1207     }
1208
1209     notmuch = notmuch_database_open (NULL);
1210     if (notmuch == NULL) {
1211         ret = 1;
1212         goto DONE;
1213     }
1214
1215     query_string = query_string_from_args (local, argc - i, &argv[i]);
1216
1217     query = notmuch_query_create (notmuch, query_string);
1218     if (query == NULL) {
1219         fprintf (stderr, "Out of memory.\n");
1220         ret = 1;
1221         goto DONE;
1222     }
1223
1224     for (messages = notmuch_query_search_messages (query);
1225          notmuch_messages_has_more (messages);
1226          notmuch_messages_advance (messages))
1227     {
1228         message = notmuch_messages_get (messages);
1229
1230         notmuch_message_freeze (message);
1231
1232         for (i = 0; i < remove_tags_count; i++)
1233             notmuch_message_remove_tag (message,
1234                                         argv[remove_tags[i]] + 1);
1235
1236         for (i = 0; i < add_tags_count; i++)
1237             notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
1238
1239         notmuch_message_thaw (message);
1240
1241         notmuch_message_destroy (message);
1242     }
1243
1244     notmuch_query_destroy (query);
1245
1246   DONE:
1247     if (notmuch)
1248         notmuch_database_close (notmuch);
1249
1250     talloc_free (local);
1251
1252     return ret;
1253 }
1254
1255 static int
1256 dump_command (unused (void *ctx), int argc, char *argv[])
1257 {
1258     FILE *output = NULL;
1259     notmuch_database_t *notmuch = NULL;
1260     notmuch_query_t *query;
1261     notmuch_messages_t *messages;
1262     notmuch_message_t *message;
1263     notmuch_tags_t *tags;
1264     int ret = 0;
1265
1266     if (argc) {
1267         output = fopen (argv[0], "w");
1268         if (output == NULL) {
1269             fprintf (stderr, "Error opening %s for writing: %s\n",
1270                      argv[0], strerror (errno));
1271             ret = 1;
1272             goto DONE;
1273         }
1274     } else {
1275         output = stdout;
1276     }
1277
1278     notmuch = notmuch_database_open (NULL);
1279     if (notmuch == NULL) {
1280         ret = 1;
1281         goto DONE;
1282     }
1283
1284     query = notmuch_query_create (notmuch, "");
1285     if (query == NULL) {
1286         fprintf (stderr, "Out of memory\n");
1287         ret = 1;
1288         goto DONE;
1289     }
1290
1291     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
1292
1293     for (messages = notmuch_query_search_messages (query);
1294          notmuch_messages_has_more (messages);
1295          notmuch_messages_advance (messages))
1296     {
1297         int first = 1;
1298         message = notmuch_messages_get (messages);
1299
1300         fprintf (output,
1301                  "%s (", notmuch_message_get_message_id (message));
1302
1303         for (tags = notmuch_message_get_tags (message);
1304              notmuch_tags_has_more (tags);
1305              notmuch_tags_advance (tags))
1306         {
1307             if (! first)
1308                 fprintf (output, " ");
1309
1310             fprintf (output, "%s", notmuch_tags_get (tags));
1311
1312             first = 0;
1313         }
1314
1315         fprintf (output, ")\n");
1316
1317         notmuch_message_destroy (message);
1318     }
1319
1320     notmuch_query_destroy (query);
1321
1322   DONE:
1323     if (notmuch)
1324         notmuch_database_close (notmuch);
1325     if (output && output != stdout)
1326         fclose (output);
1327
1328     return ret;
1329 }
1330
1331 static int
1332 restore_command (unused (void *ctx), int argc, char *argv[])
1333 {
1334     FILE *input = NULL;
1335     notmuch_database_t *notmuch = NULL;
1336     char *line = NULL;
1337     size_t line_size;
1338     ssize_t line_len;
1339     regex_t regex;
1340     int rerr;
1341     int ret = 0;
1342
1343     if (argc) {
1344         input = fopen (argv[0], "r");
1345         if (input == NULL) {
1346             fprintf (stderr, "Error opening %s for reading: %s\n",
1347                      argv[0], strerror (errno));
1348             ret = 1;
1349             goto DONE;
1350         }
1351     } else {
1352         printf ("No filename given. Reading dump from stdin.\n");
1353         input = stdin;
1354     }
1355
1356     notmuch = notmuch_database_open (NULL);
1357     if (notmuch == NULL) {
1358         ret = 1;
1359         goto DONE;
1360     }
1361
1362     /* Dump output is one line per message. We match a sequence of
1363      * non-space characters for the message-id, then one or more
1364      * spaces, then a list of space-separated tags as a sequence of
1365      * characters within literal '(' and ')'. */
1366     xregcomp (&regex,
1367               "^([^ ]+) \\(([^)]*)\\)$",
1368               REG_EXTENDED);
1369
1370     while ((line_len = getline (&line, &line_size, input)) != -1) {
1371         regmatch_t match[3];
1372         char *message_id, *tags, *tag, *next;
1373         notmuch_message_t *message;
1374         notmuch_status_t status;
1375
1376         chomp_newline (line);
1377
1378         rerr = xregexec (&regex, line, 3, match, 0);
1379         if (rerr == REG_NOMATCH)
1380         {
1381             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
1382                      line);
1383             continue;
1384         }
1385
1386         message_id = xstrndup (line + match[1].rm_so,
1387                                match[1].rm_eo - match[1].rm_so);
1388         tags = xstrndup (line + match[2].rm_so,
1389                          match[2].rm_eo - match[2].rm_so);
1390
1391         if (strlen (tags)) {
1392
1393             message = notmuch_database_find_message (notmuch, message_id);
1394             if (message == NULL) {
1395                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
1396                          message_id);
1397                 goto NEXT_LINE;
1398             }
1399
1400             notmuch_message_freeze (message);
1401
1402             notmuch_message_remove_all_tags (message);
1403
1404             next = tags;
1405             while (next) {
1406                 tag = strsep (&next, " ");
1407                 if (*tag == '\0')
1408                     continue;
1409                 status = notmuch_message_add_tag (message, tag);
1410                 if (status) {
1411                     fprintf (stderr,
1412                              "Error applying tag %s to message %s:\n",
1413                              tag, message_id);
1414                     fprintf (stderr, "%s\n",
1415                              notmuch_status_to_string (status));
1416                 }
1417             }
1418
1419             notmuch_message_thaw (message);
1420             notmuch_message_destroy (message);
1421         }
1422       NEXT_LINE:
1423         free (message_id);
1424         free (tags);
1425     }
1426
1427     regfree (&regex);
1428
1429   DONE:
1430     if (line)
1431         free (line);
1432     if (notmuch)
1433         notmuch_database_close (notmuch);
1434     if (input && input != stdin)
1435         fclose (input);
1436
1437     return ret;
1438 }
1439
1440 static int
1441 help_command (void *ctx, int argc, char *argv[]);
1442
1443 command_t commands[] = {
1444     { "setup", setup_command,
1445       "Interactively setup notmuch for first use.",
1446       "\t\tThe setup command is the first command you will run in order\n"
1447       "\t\tto start using notmuch. It will prompt you for the directory\n"
1448       "\t\tcontaining your email archives, and will then proceed to build\n"
1449       "\t\ta database to allow fast searching of that mail.\n\n"
1450       "\t\tInvoking notmuch with no command argument will run setup if\n"
1451       "\t\tthe setup command has not previously been completed." },
1452     { "new", new_command,
1453       "Find and import any new messages.",
1454       "\t\tScans all sub-directories of the database, adding new messages\n"
1455       "\t\tthat are found. Each new message will be tagged as both\n"
1456       "\t\t\"inbox\" and \"unread\".\n"
1457       "\n"
1458       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
1459       "\t\tso you can use that to mark directories that will not\n"
1460       "\t\treceive any new mail (and make \"notmuch new\" faster)." },
1461     { "search", search_command,
1462       "<search-term> [...]\n\n"
1463       "\t\tSearch for threads matching the given search terms.",
1464       "\t\tNote that the individual mail messages will be matched\n"
1465       "\t\tagainst the search terms, but the results will be the\n"
1466       "\t\tthreads containing the matched messages.\n\n"
1467       "\t\tCurrently, in addition to free text (and quoted phrases)\n"
1468       "\t\twhich match terms appearing anywhere within an email,\n"
1469       "\t\tthe following prefixes can be used to search specific\n"
1470       "\t\tportions of an email, (where <brackets> indicate user-\n"
1471       "\t\tsupplied values):\n\n"
1472       "\t\t\tfrom:<name-or-address>\n"
1473       "\t\t\tto:<name-or-address>\n"
1474       "\t\t\tsubject:<word-or-quoted-phrase>\n"
1475       "\t\t\ttag:<tag>\n"
1476       "\t\t\tid:<message-id>\n"
1477       "\t\t\tthread:<thread-id>\n\n"
1478       "\t\tThe from: prefix is used to match the name or address of\n"
1479       "\t\tthe sender of an email message.\n\n"
1480       "\t\tThe to: prefix is used to match the names or addresses of\n"
1481       "\t\tany recipient of an email message, (whether To, Cc, or Bcc).\n\n"
1482       "\t\tAny term prefixed with subject: will match only text from\n"
1483       "\t\tthe subject of an email. Quoted phrases are supported when\n"
1484       "\t\tsearching with: subject:\"this is a phrase\".\n\n"
1485       "\t\tValid tag values include \"inbox\" and \"unread\" by default\n"
1486       "\t\tfor new messages added by \"notmuch new\" as well as any other\n"
1487       "\t\ttag values added manually with \"notmuch tag\".\n\n"
1488       "\t\tMessage ID values are the literal contents of the Message-ID:\n"
1489       "\t\theader of email messages, but without the '<','>' delimiters.\n\n"
1490       "\t\tThread ID values are generated internally by notmuch but can\n"
1491       "\t\tbe seen in the output of \"notmuch search\" for example.\n\n"
1492       "\t\tIn addition to individual terms, multiple terms can be\n"
1493       "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
1494       "\t\tEach term in the query will be implicitly connected by a\n"
1495       "\t\tlogical AND if no explicit operator is provided, (except\n"
1496       "\t\tthat terms with a common prefix will be implicitly combined\n"
1497       "\t\twith OR until we get Xapian defect #402 fixed).\n\n"
1498       "\t\tParentheses can also be used to control the combination of\n"
1499       "\t\tthe Boolean operators, but will have to be protected from\n"
1500       "\t\tinterpretation by the shell, (such as by putting quotation\n"
1501       "\t\tmarks around any parenthesized expression)." },
1502     { "show", show_command,
1503       "<search-terms> [...]\n\n"
1504       "\t\tShows all messages matching the search terms.",
1505       "\t\tSee the documentation of \"notmuch search\" for details\n"
1506       "\t\tof the supported syntax of search terms.\n\n"
1507       "\t\tA common use of \"notmuch show\" is to display a single\n"
1508       "\t\tthread of email messages. For this, use a search term of\n"
1509       "\t\t\"thread:<thread-id>\" as can be seen in the first column\n"
1510       "\t\tof output from the \"notmuch search\" command.\n\n"
1511       "\t\tAll messages will be displayed in date order. The output\n"
1512       "\t\tformat is plain-text, with all text-content MIME parts\n"
1513       "\t\tdecoded. Various components in the output, ('message',\n"
1514       "\t\t'header', 'body', 'attachment', and MIME 'part') will be\n"
1515       "\t\tdelimited by easily-parsed markers. Each marker consists\n"
1516       "\t\tof a Control-L character (ASCII decimal 12), the name of\n"
1517       "\t\tthe marker, and then either an opening or closing brace,\n"
1518       "\t\t'{' or '}' to either open or close the component."},
1519     { "tag", tag_command,
1520       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
1521       "\t\tAdd/remove tags for all messages matching the search terms.",
1522       "\t\tThe search terms are handled exactly as in 'search' so one\n"
1523       "\t\tcan use that command first to see what will be modified.\n\n"
1524       "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n"
1525       "\t\tremoved. For each message, tag removal is before tag addition.\n\n"
1526       "\t\tThe beginning of <search-terms> is recognized by the first\n"
1527       "\t\targument that begins with neither '+' nor '-'. Support for\n"
1528       "\t\tan initial search term beginning with '+' or '-' is provided\n"
1529       "\t\tby allowing the user to specify a \"--\" argument to separate\n"
1530       "\t\tthe tags from the search terms.\n\n"
1531       "\t\tNote: If you run \"notmuch new\" between reading a thread with\n"
1532       "\t\t\"notmuch show\" and removing the \"inbox\" tag for that thread\n"
1533       "\t\twith \"notmuch tag\" then you create the possibility of moving\n"
1534       "\t\tsome messages from that thread out of your inbox without ever\n"
1535       "\t\treading them. The easiest way to avoid this problem is to not\n"
1536       "\t\trun \"notmuch new\" between reading and removing tags." },
1537     { "dump", dump_command,
1538       "[<filename>]\n\n"
1539       "\t\tCreate a plain-text dump of the tags for each message.",
1540       "\t\tOutput is to the given filename, if any, or to stdout.\n"
1541       "\t\tThese tags are the only data in the notmuch database\n"
1542       "\t\tthat can't be recreated from the messages themselves.\n"
1543       "\t\tThe output of notmuch dump is therefore the only\n"
1544       "\t\tcritical thing to backup (and much more friendly to\n"
1545       "\t\tincremental backup than the native database files.)" },
1546     { "restore", restore_command,
1547       "<filename>\n\n"
1548       "\t\tRestore the tags from the given dump file (see 'dump').",
1549       "\t\tNote: The dump file format is specifically chosen to be\n"
1550       "\t\tcompatible with the format of files produced by sup-dump.\n"
1551       "\t\tSo if you've previously been using sup for mail, then the\n"
1552       "\t\t\"notmuch restore\" command provides you a way to import\n"
1553       "\t\tall of your tags (or labels as sup calls them)." },
1554     { "help", help_command,
1555       "[<command>]\n\n"
1556       "\t\tThis message, or more detailed help for the named command.",
1557       "\t\tExcept in this case, where there's not much more detailed\n"
1558       "\t\thelp available." }
1559 };
1560
1561 static void
1562 usage (void)
1563 {
1564     command_t *command;
1565     unsigned int i;
1566
1567     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
1568     fprintf (stderr, "\n");
1569     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
1570     fprintf (stderr, "\n");
1571
1572     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1573         command = &commands[i];
1574
1575         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
1576     }
1577
1578     fprintf (stderr, "Use \"notmuch help <command>\" for more details on each command.\n\n");
1579 }
1580
1581 static int
1582 help_command (unused (void *ctx), int argc, char *argv[])
1583 {
1584     command_t *command;
1585     unsigned int i;
1586
1587     if (argc == 0) {
1588         fprintf (stderr, "The notmuch mail system.\n\n");
1589         usage ();
1590         return 0;
1591     }
1592
1593     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1594         command = &commands[i];
1595
1596         if (strcmp (argv[0], command->name) == 0) {
1597             fprintf (stderr, "Help for \"notmuch %s\":\n\n", argv[0]);
1598             fprintf (stderr, "\t%s\t%s\n\n%s\n\n", command->name,
1599                      command->summary, command->documentation);
1600             return 0;
1601         }
1602     }
1603
1604     fprintf (stderr,
1605              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
1606              argv[0]);
1607     return 1;
1608 }
1609     
1610 int
1611 main (int argc, char *argv[])
1612 {
1613     void *local = talloc_new (NULL);
1614     command_t *command;
1615     unsigned int i;
1616
1617     if (argc == 1)
1618         return setup_command (local, 0, NULL);
1619
1620     for (i = 0; i < ARRAY_SIZE (commands); i++) {
1621         command = &commands[i];
1622
1623         if (strcmp (argv[1], command->name) == 0)
1624             return (command->function) (local, argc - 2, &argv[2]);
1625     }
1626
1627     /* Don't complain about "help" being an unknown command when we're
1628        about to provide exactly what's wanted anyway. */
1629     fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
1630              argv[1]);
1631
1632     talloc_free (local);
1633
1634     return 1;
1635 }