1 /* notmuch - Not much of an email program, (just index and search)
3 * Copyright © 2009 Carl Worth
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.
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.
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/ .
18 * Author: Carl Worth <cworth@cworth.org>
22 #define _GNU_SOURCE /* for getline */
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).
45 #define unused(x) x __attribute__ ((unused))
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
51 * Note that __location__ comes from talloc.h.
53 #define INTERNAL_ERROR(format, ...) \
56 "Internal error: " format " (%s)\n", \
57 ##__VA_ARGS__, __location__); \
61 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
63 typedef int (*command_function_t) (int argc, char *argv[]);
65 typedef struct command {
67 command_function_t function;
69 const char *documentation;
72 typedef void (*add_files_callback_t) (notmuch_message_t *message);
75 int ignore_read_only_directories;
76 int saw_read_only_directory;
81 struct timeval tv_start;
83 add_files_callback_t callback;
87 chomp_newline (char *str)
89 if (str && str[strlen(str)-1] == '\n')
90 str[strlen(str)-1] = '\0';
93 /* Compute the number of seconds elapsed from start to end. */
95 tv_elapsed (struct timeval start, struct timeval end)
97 return ((end.tv_sec - start.tv_sec) +
98 (end.tv_usec - start.tv_usec) / 1e6);
102 print_formatted_seconds (double seconds)
108 printf ("almost no time");
112 if (seconds > 3600) {
113 hours = (int) seconds / 3600;
114 printf ("%dh ", hours);
115 seconds -= hours * 3600;
119 minutes = (int) seconds / 60;
120 printf ("%dm ", minutes);
121 seconds -= minutes * 60;
124 printf ("%ds", (int) seconds);
127 static volatile sig_atomic_t do_add_files_print_progress = 0;
130 handle_sigalrm (unused (int signal))
132 do_add_files_print_progress = 1;
136 add_files_print_progress (add_files_state_t *state)
138 struct timeval tv_now;
139 double elapsed_overall, rate_overall;
141 gettimeofday (&tv_now, NULL);
143 elapsed_overall = tv_elapsed (state->tv_start, tv_now);
144 rate_overall = (state->processed_files) / elapsed_overall;
146 printf ("Processed %d", state->processed_files);
148 if (state->total_files) {
149 printf (" of %d files (", state->total_files);
150 print_formatted_seconds ((state->total_files - state->processed_files) /
152 printf (" remaining). \r");
154 printf (" files (%d files/sec.) \r", (int) rate_overall);
160 /* Examine 'path' recursively as follows:
162 * o Ask the filesystem for the mtime of 'path' (path_mtime)
164 * o Ask the database for its timestamp of 'path' (path_dbtime)
166 * o If 'path_mtime' > 'path_dbtime'
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
172 * o For each sub-directory of path, recursively call into this
175 * o Tell the database to update its time of 'path' to 'path_mtime'
177 * The 'struct stat *st' must point to a structure that has already
178 * been initialized for 'path' by calling stat().
180 static notmuch_status_t
181 add_files_recursive (notmuch_database_t *notmuch,
184 add_files_state_t *state)
187 struct dirent *e, *entry = NULL;
191 time_t path_mtime, path_dbtime;
192 notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
193 notmuch_message_t *message = NULL, **closure;
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
199 if (state->ignore_read_only_directories &&
200 (st->st_mode & S_IWUSR) == 0)
202 state->saw_read_only_directory = TRUE;
206 path_mtime = st->st_mtime;
208 path_dbtime = notmuch_database_get_timestamp (notmuch, path);
210 dir = opendir (path);
212 fprintf (stderr, "Error opening directory %s: %s\n",
213 path, strerror (errno));
214 ret = NOTMUCH_STATUS_FILE_ERROR;
218 entry_length = offsetof (struct dirent, d_name) +
219 pathconf (path, _PC_NAME_MAX) + 1;
220 entry = malloc (entry_length);
223 err = readdir_r (dir, entry, &e);
225 fprintf (stderr, "Error reading directory: %s\n",
227 ret = NOTMUCH_STATUS_FILE_ERROR;
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)
240 /* Ignore special directories to avoid infinite recursion.
241 * Also ignore the .notmuch directory.
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)
252 next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
254 if (stat (next, st)) {
255 fprintf (stderr, "Error reading %s: %s\n",
256 next, strerror (errno));
257 ret = NOTMUCH_STATUS_FILE_ERROR;
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++;
272 status = notmuch_database_add_message (notmuch, next, closure);
275 case NOTMUCH_STATUS_SUCCESS:
276 state->added_messages++;
278 (state->callback) (message);
280 /* Non-fatal issues (go on to next file) */
281 case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
282 /* Stay silent on this one. */
284 case NOTMUCH_STATUS_FILE_NOT_EMAIL:
285 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
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));
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);
306 notmuch_message_destroy (message);
310 if (do_add_files_print_progress) {
311 do_add_files_print_progress = 0;
312 add_files_print_progress (state);
315 } else if (S_ISDIR (st->st_mode)) {
316 status = add_files_recursive (notmuch, next, st, state);
317 if (status && ret == NOTMUCH_STATUS_SUCCESS)
325 status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
326 if (status && ret == NOTMUCH_STATUS_SUCCESS)
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,
346 add_files_state_t *state)
349 notmuch_status_t status;
350 struct sigaction action;
351 struct itimerval timerval;
353 if (stat (path, &st)) {
354 fprintf (stderr, "Error reading directory %s: %s\n",
355 path, strerror (errno));
356 return NOTMUCH_STATUS_FILE_ERROR;
359 if (! S_ISDIR (st.st_mode)) {
360 fprintf (stderr, "Error: %s is not a directory.\n", path);
361 return NOTMUCH_STATUS_FILE_ERROR;
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);
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);
378 status = add_files_recursive (notmuch, path, &st, state);
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);
387 /* And disable the signal handler. */
388 action.sa_handler = SIG_IGN;
389 sigaction (SIGALRM, &action, NULL);
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
399 count_files (const char *path, int *count)
402 struct dirent *e, *entry = NULL;
408 dir = opendir (path);
411 fprintf (stderr, "Warning: failed to open directory %s: %s\n",
412 path, strerror (errno));
416 entry_length = offsetof (struct dirent, d_name) +
417 pathconf (path, _PC_NAME_MAX) + 1;
418 entry = malloc (entry_length);
421 err = readdir_r (dir, entry, &e);
423 fprintf (stderr, "Error reading directory: %s\n",
432 /* Ignore special directories to avoid infinite recursion.
433 * Also ignore the .notmuch directory.
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)
444 if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
446 fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
447 path, entry->d_name);
453 if (S_ISREG (st.st_mode)) {
455 if (*count % 1000 == 0) {
456 printf ("Found %d files so far.\r", *count);
459 } else if (S_ISDIR (st.st_mode)) {
460 count_files (next, count);
474 setup_command (unused (int argc), unused (char *argv[]))
476 notmuch_database_t *notmuch = NULL;
477 char *default_path, *mail_directory = NULL;
480 add_files_state_t add_files_state;
482 struct timeval tv_now;
483 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
485 printf ("Welcome to notmuch!\n\n");
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");
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");
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");
505 default_path = notmuch_database_default_path ();
506 printf ("Top-level mail directory [%s]: ", default_path);
509 getline (&mail_directory, &line_size, stdin);
510 chomp_newline (mail_directory);
514 if (mail_directory == NULL || strlen (mail_directory) == 0) {
516 free (mail_directory);
517 mail_directory = default_path;
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",
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");
534 /* Coerce th directory into an absolute directory name. */
535 if (*mail_directory != '/') {
536 char *cwd, *absolute_mail_directory;
538 cwd = getcwd (NULL, 0);
540 fprintf (stderr, "Out of memory.\n");
544 if (asprintf (&absolute_mail_directory, "%s/%s",
545 cwd, mail_directory) < 0)
547 fprintf (stderr, "Out of memory.\n");
552 free (mail_directory);
553 mail_directory = absolute_mail_directory;
555 printf ("Abs: %s\n", mail_directory);
558 notmuch = notmuch_database_create (mail_directory);
559 if (notmuch == NULL) {
560 fprintf (stderr, "Failed to create new notmuch database at %s\n",
562 ret = NOTMUCH_STATUS_FILE_ERROR;
566 printf ("OK. Let's take a look at the mail we can find in the directory\n");
567 printf ("%s ...\n", mail_directory);
570 count_files (mail_directory, &count);
572 printf ("Found %d total files. That's not much mail.\n\n", count);
574 printf ("Next, we'll inspect the messages and create a database of threads:\n");
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);
584 ret = add_files (notmuch, mail_directory, &add_files_state);
586 gettimeofday (&tv_now, NULL);
587 elapsed = tv_elapsed (add_files_state.tv_start,
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);
594 printf (" (%d files/sec.). \n",
595 (int) (add_files_state.processed_files / elapsed));
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");
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",
611 printf ("Note: At least one error was encountered: %s\n",
612 notmuch_status_to_string (ret));
617 free (mail_directory);
619 notmuch_database_close (notmuch);
625 tag_inbox_and_unread (notmuch_message_t *message)
627 notmuch_message_add_tag (message, "inbox");
628 notmuch_message_add_tag (message, "unread");
632 new_command (unused (int argc), unused (char *argv[]))
634 notmuch_database_t *notmuch;
635 const char *mail_directory;
636 add_files_state_t add_files_state;
638 struct timeval tv_now;
641 notmuch = notmuch_database_open (NULL);
642 if (notmuch == NULL) {
647 mail_directory = notmuch_database_get_path (notmuch);
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);
657 ret = add_files (notmuch, mail_directory, &add_files_state);
659 gettimeofday (&tv_now, NULL);
660 elapsed = tv_elapsed (add_files_state.tv_start,
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);
668 printf (" (%d files/sec.). \n",
669 (int) (add_files_state.processed_files / elapsed));
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");
680 printf ("No new mail---and that's not much.\n");
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");
691 printf ("\nNote: At least one error was encountered: %s\n",
692 notmuch_status_to_string (ret));
697 notmuch_database_close (notmuch);
702 /* Construct a single query string from the passed arguments, using
703 * 'ctx' as the talloc owner for all allocations.
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).
709 * This function returns NULL in case of insufficient memory.
712 query_string_from_args (void *ctx, int argc, char *argv[])
717 query_string = talloc_strdup (ctx, "");
718 if (query_string == NULL)
721 for (i = 0; i < argc; i++) {
723 query_string = talloc_strdup_append (query_string, " ");
724 if (query_string == NULL)
728 query_string = talloc_strdup_append (query_string, argv[i]);
729 if (query_string == NULL)
736 /* Format a nice representation of 'time' relative to the current time.
740 * 5 minutes ago (For times less than 60 minutes ago)
741 * 12:30 (For times >60 minutes but still today)
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)
748 #define HOUR (60 * MINUTE)
749 #define DAY (24 * HOUR)
750 #define RELATIVE_DATE_MAX 20
752 _format_relative_date (void *ctx, time_t then)
754 struct tm tm_now, tm_then;
755 time_t now = time(NULL);
759 localtime_r (&now, &tm_now);
760 localtime_r (&then, &tm_then);
762 result = talloc_zero_size (ctx, RELATIVE_DATE_MAX);
771 if (delta > 180 * DAY) {
772 strftime (result, RELATIVE_DATE_MAX,
773 "%F", &tm_then); /* 2008-06-30 */
778 snprintf (result, RELATIVE_DATE_MAX,
779 "%d minutes ago", (int) (delta / 60));
783 if (delta <= 7 * DAY) {
784 if (tm_then.tm_wday == tm_now.tm_wday &&
787 strftime (result, RELATIVE_DATE_MAX,
788 "%R", &tm_then); /* 12:30 */
790 } else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
793 if (tm_then.tm_wday != tm_now.tm_wday) {
794 strftime (result, RELATIVE_DATE_MAX,
795 "%A", &tm_then); /* Monday */
801 strftime (result, RELATIVE_DATE_MAX,
802 "%b %d", &tm_then); /* Oct. 12 */
810 search_command (int argc, char *argv[])
812 void *local = talloc_new (NULL);
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;
819 const char *relative_date;
821 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
823 notmuch = notmuch_database_open (NULL);
824 if (notmuch == NULL) {
829 query_str = query_string_from_args (local, argc, argv);
831 query = notmuch_query_create (notmuch, query_str);
833 fprintf (stderr, "Out of memory\n");
838 for (threads = notmuch_query_search_threads (query);
839 notmuch_threads_has_more (threads);
840 notmuch_threads_advance (threads))
844 thread = notmuch_threads_get (threads);
846 date = notmuch_thread_get_oldest_date (thread);
847 relative_date = _format_relative_date (local, date);
849 printf ("%s (%s) %s",
850 notmuch_thread_get_thread_id (thread),
852 notmuch_thread_get_subject (thread));
855 for (tags = notmuch_thread_get_tags (thread);
856 notmuch_tags_has_more (tags);
857 notmuch_tags_advance (tags))
861 printf ("%s", notmuch_tags_get (tags));
866 notmuch_thread_destroy (thread);
869 notmuch_query_destroy (query);
873 notmuch_database_close (notmuch);
879 /* Get a nice, single-line summary of message. */
881 _get_one_line_summary (void *ctx, notmuch_message_t *message)
885 const char *relative_date;
888 from = notmuch_message_get_header (message, "from");
890 date = notmuch_message_get_date (message);
891 relative_date = _format_relative_date (ctx, date);
893 subject = notmuch_message_get_header (message, "subject");
895 return talloc_asprintf (ctx, "%s (%s) %s",
896 from, relative_date, subject);
900 show_command (unused (int argc), unused (char *argv[]))
902 void *local = talloc_new (NULL);
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;
913 const char *headers[] = {
914 "Subject", "From", "To", "Cc", "Bcc", "Date"
916 const char *name, *value;
920 fprintf (stderr, "Error: \"notmuch show\" requires exactly one thread-ID argument.\n");
925 notmuch = notmuch_database_open (NULL);
926 if (notmuch == NULL) {
931 query_string = talloc_asprintf (local, "thread:%s", argv[0]);
932 if (query_string == NULL) {
933 fprintf (stderr, "Out of memory\n");
938 query = notmuch_query_create (notmuch, query_string);
940 fprintf (stderr, "Out of memory\n");
945 for (messages = notmuch_query_search_messages (query);
946 notmuch_messages_has_more (messages);
947 notmuch_messages_advance (messages))
949 message = notmuch_messages_get (messages);
951 printf ("%%message{\n");
953 printf ("%%header{\n");
955 printf ("%s\n", _get_one_line_summary (local, message));
957 for (i = 0; i < ARRAY_SIZE (headers); i++) {
959 value = notmuch_message_get_header (message, name);
961 printf ("%s: %s\n", name, value);
964 printf ("%%header}\n");
966 filename = notmuch_message_get_filename (message);
968 file = fopen (filename, "r");
970 size_t header_size = notmuch_message_get_header_size (message);
971 fseek (file, header_size + 1, SEEK_SET);
981 printf ("%%message}\n");
983 notmuch_message_destroy (message);
991 notmuch_query_destroy (query);
994 notmuch_database_close (notmuch);
1000 tag_command (unused (int argc), unused (char *argv[]))
1003 int *add_tags, *remove_tags;
1004 int add_tags_count = 0;
1005 int remove_tags_count = 0;
1007 notmuch_database_t *notmuch = NULL;
1008 notmuch_query_t *query;
1009 notmuch_messages_t *messages;
1010 notmuch_message_t *message;
1014 local = talloc_new (NULL);
1015 if (local == NULL) {
1020 add_tags = talloc_size (local, argc * sizeof (int));
1021 if (add_tags == NULL) {
1026 remove_tags = talloc_size (local, argc * sizeof (int));
1027 if (remove_tags == NULL) {
1032 for (i = 0; i < argc; i++) {
1033 if (strcmp (argv[i], "--") == 0) {
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;
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");
1053 fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
1058 notmuch = notmuch_database_open (NULL);
1059 if (notmuch == NULL) {
1064 query_string = query_string_from_args (local, argc - i, &argv[i]);
1066 query = notmuch_query_create (notmuch, query_string);
1067 if (query == NULL) {
1068 fprintf (stderr, "Out of memory.\n");
1073 for (messages = notmuch_query_search_messages (query);
1074 notmuch_messages_has_more (messages);
1075 notmuch_messages_advance (messages))
1077 message = notmuch_messages_get (messages);
1079 notmuch_message_freeze (message);
1081 for (i = 0; i < remove_tags_count; i++)
1082 notmuch_message_remove_tag (message,
1083 argv[remove_tags[i]] + 1);
1085 for (i = 0; i < add_tags_count; i++)
1086 notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
1088 notmuch_message_thaw (message);
1090 notmuch_message_destroy (message);
1093 notmuch_query_destroy (query);
1097 notmuch_database_close (notmuch);
1099 talloc_free (local);
1105 dump_command (int argc, char *argv[])
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;
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));
1127 notmuch = notmuch_database_open (NULL);
1128 if (notmuch == NULL) {
1133 query = notmuch_query_create (notmuch, "");
1134 if (query == NULL) {
1135 fprintf (stderr, "Out of memory\n");
1140 notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
1142 for (messages = notmuch_query_search_messages (query);
1143 notmuch_messages_has_more (messages);
1144 notmuch_messages_advance (messages))
1147 message = notmuch_messages_get (messages);
1150 "%s (", notmuch_message_get_message_id (message));
1152 for (tags = notmuch_message_get_tags (message);
1153 notmuch_tags_has_more (tags);
1154 notmuch_tags_advance (tags))
1157 fprintf (output, " ");
1159 fprintf (output, "%s", notmuch_tags_get (tags));
1164 fprintf (output, ")\n");
1166 notmuch_message_destroy (message);
1169 notmuch_query_destroy (query);
1173 notmuch_database_close (notmuch);
1174 if (output && output != stdout)
1181 restore_command (int argc, char *argv[])
1184 notmuch_database_t *notmuch = NULL;
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));
1201 printf ("No filename given. Reading dump from stdin.\n");
1205 notmuch = notmuch_database_open (NULL);
1206 if (notmuch == NULL) {
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 ')'. */
1216 "^([^ ]+) \\(([^)]*)\\)$",
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;
1225 chomp_newline (line);
1227 rerr = xregexec (®ex, line, 3, match, 0);
1228 if (rerr == REG_NOMATCH)
1230 fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
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);
1240 if (strlen (tags)) {
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",
1249 notmuch_message_freeze (message);
1251 notmuch_message_remove_all_tags (message);
1255 tag = strsep (&next, " ");
1258 status = notmuch_message_add_tag (message, tag);
1261 "Error applying tag %s to message %s:\n",
1263 fprintf (stderr, "%s\n",
1264 notmuch_status_to_string (status));
1268 notmuch_message_thaw (message);
1269 notmuch_message_destroy (message);
1282 notmuch_database_close (notmuch);
1283 if (input && input != stdin)
1290 help_command (int argc, char *argv[]);
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"
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"
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,
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,
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,
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,
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." }
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");
1397 for (i = 0; i < ARRAY_SIZE (commands); i++) {
1398 command = &commands[i];
1400 fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
1403 fprintf (stderr, "Use \"notmuch help <command>\" for more details on each command.\n\n");
1407 help_command (int argc, char *argv[])
1413 fprintf (stderr, "The notmuch mail system.\n\n");
1418 for (i = 0; i < ARRAY_SIZE (commands); i++) {
1419 command = &commands[i];
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);
1430 "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
1436 main (int argc, char *argv[])
1442 return setup_command (0, NULL);
1444 for (i = 0; i < ARRAY_SIZE (commands); i++) {
1445 command = &commands[i];
1447 if (strcmp (argv[1], command->name) == 0)
1448 return (command->function) (argc - 2, &argv[2]);
1451 /* Don't complain about "help" being an unknown command when we're
1452 about to provide exactly what's wanted anyway. */
1453 fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",