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