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