X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=notmuch.c;h=c47e64036786afbb19a834c49cc6aedffec7a5f2;hp=4b16a71eb7352321f657fb08f8de7e6d88f0ff3a;hb=f970d8078cc29f876bebe02937307a5a8f2d394f;hpb=450b054245ffa30d447e50c3087fddf0e9716d70 diff --git a/notmuch.c b/notmuch.c index 4b16a71e..c47e6403 100644 --- a/notmuch.c +++ b/notmuch.c @@ -1,6 +1,7 @@ /* notmuch - Not much of an email program, (just index and search) * * Copyright © 2009 Carl Worth + * Copyright © 2009 Keith Packard * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,1052 +16,108 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/ . * - * Author: Carl Worth + * Authors: Carl Worth + * Keith Packard */ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE /* for getline */ -#endif -#include - -#include "notmuch.h" - -/* This is separate from notmuch-private.h because we're trying to - * keep notmuch.c from looking into any internals, (which helps us - * develop notmuch.h into a plausible library interface). - */ -#include "xutil.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#define unused(x) x __attribute__ ((unused)) - -/* There's no point in continuing when we've detected that we've done - * something wrong internally (as opposed to the user passing in a - * bogus value). - * - * Note that __location__ comes from talloc.h. - */ -#define INTERNAL_ERROR(format, ...) \ - do { \ - fprintf(stderr, \ - "Internal error: " format " (%s)\n", \ - ##__VA_ARGS__, __location__); \ - exit (1); \ - } while (0) - -#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) - -typedef int (*command_function_t) (int argc, char *argv[]); - -typedef struct command { - const char *name; - command_function_t function; - const char *summary; - const char *documentation; -} command_t; - -typedef void (*add_files_callback_t) (notmuch_message_t *message); - -typedef struct { - int ignore_read_only_directories; - int saw_read_only_directory; - - int total_files; - int processed_files; - int added_messages; - struct timeval tv_start; - - add_files_callback_t callback; -} add_files_state_t; - -static void -chomp_newline (char *str) -{ - if (str && str[strlen(str)-1] == '\n') - str[strlen(str)-1] = '\0'; -} - -/* Compute the number of seconds elapsed from start to end. */ -static double -tv_elapsed (struct timeval start, struct timeval end) -{ - return ((end.tv_sec - start.tv_sec) + - (end.tv_usec - start.tv_usec) / 1e6); -} - -static void -print_formatted_seconds (double seconds) -{ - int hours; - int minutes; - - if (seconds < 1) { - printf ("almost no time"); - return; - } - - if (seconds > 3600) { - hours = (int) seconds / 3600; - printf ("%dh ", hours); - seconds -= hours * 3600; - } - - if (seconds > 60) { - minutes = (int) seconds / 60; - printf ("%dm ", minutes); - seconds -= minutes * 60; - } - - printf ("%ds", (int) seconds); -} - -static void -add_files_print_progress (add_files_state_t *state) -{ - struct timeval tv_now; - double elapsed_overall, rate_overall; - - gettimeofday (&tv_now, NULL); - - elapsed_overall = tv_elapsed (state->tv_start, tv_now); - rate_overall = (state->processed_files) / elapsed_overall; - - printf ("Processed %d", state->processed_files); - - if (state->total_files) { - printf (" of %d files (", state->total_files); - print_formatted_seconds ((state->total_files - state->processed_files) / - rate_overall); - printf (" remaining). \r"); - } else { - printf (" files (%d files/sec.) \r", (int) rate_overall); - } - - fflush (stdout); -} - -/* Examine 'path' recursively as follows: - * - * o Ask the filesystem for the mtime of 'path' (path_mtime) - * - * o Ask the database for its timestamp of 'path' (path_dbtime) - * - * o If 'path_mtime' > 'path_dbtime' - * - * o For each regular file in 'path' with mtime newer than the - * 'path_dbtime' call add_message to add the file to the - * database. - * - * o For each sub-directory of path, recursively call into this - * same function. - * - * o Tell the database to update its time of 'path' to 'path_mtime' - * - * The 'struct stat *st' must point to a structure that has already - * been initialized for 'path' by calling stat(). - */ -static notmuch_status_t -add_files_recursive (notmuch_database_t *notmuch, - const char *path, - struct stat *st, - add_files_state_t *state) -{ - DIR *dir = NULL; - struct dirent *e, *entry = NULL; - int entry_length; - int err; - char *next = NULL; - time_t path_mtime, path_dbtime; - notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS; - notmuch_message_t *message = NULL, **closure; - - /* If we're told to, we bail out on encountering a read-only - * directory, (with this being a clear clue from the user to - * Notmuch that new mail won't be arriving there and we need not - * look. */ - if (state->ignore_read_only_directories && - (st->st_mode & S_IWUSR) == 0) - { - state->saw_read_only_directory = TRUE; - goto DONE; - } - - path_mtime = st->st_mtime; - - path_dbtime = notmuch_database_get_timestamp (notmuch, path); - - dir = opendir (path); - if (dir == NULL) { - fprintf (stderr, "Error opening directory %s: %s\n", - path, strerror (errno)); - ret = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - entry_length = offsetof (struct dirent, d_name) + - pathconf (path, _PC_NAME_MAX) + 1; - entry = malloc (entry_length); - - while (1) { - err = readdir_r (dir, entry, &e); - if (err) { - fprintf (stderr, "Error reading directory: %s\n", - strerror (errno)); - ret = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - if (e == NULL) - break; - - /* If this directory hasn't been modified since the last - * add_files, then we only need to look further for - * sub-directories. */ - if (path_mtime <= path_dbtime && entry->d_type != DT_DIR) - continue; - - /* Ignore special directories to avoid infinite recursion. - * Also ignore the .notmuch directory. - */ - /* XXX: Eventually we'll want more sophistication to let the - * user specify files to be ignored. */ - if (strcmp (entry->d_name, ".") == 0 || - strcmp (entry->d_name, "..") == 0 || - strcmp (entry->d_name, ".notmuch") ==0) - { - continue; - } - - next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name); - - if (stat (next, st)) { - fprintf (stderr, "Error reading %s: %s\n", - next, strerror (errno)); - ret = NOTMUCH_STATUS_FILE_ERROR; - continue; - } - - if (S_ISREG (st->st_mode)) { - /* If the file hasn't been modified since the last - * add_files, then we need not look at it. */ - if (st->st_mtime > path_dbtime) { - state->processed_files++; - - if (state->callback) - closure = &message; - else - closure = NULL; - - status = notmuch_database_add_message (notmuch, next, closure); - switch (status) { - /* success */ - case NOTMUCH_STATUS_SUCCESS: - state->added_messages++; - if (state->callback) - (state->callback) (message); - break; - /* Non-fatal issues (go on to next file) */ - case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: - /* Stay silent on this one. */ - break; - case NOTMUCH_STATUS_FILE_NOT_EMAIL: - fprintf (stderr, "Note: Ignoring non-mail file: %s\n", - next); - break; - /* Fatal issues. Don't process anymore. */ - case NOTMUCH_STATUS_XAPIAN_EXCEPTION: - case NOTMUCH_STATUS_OUT_OF_MEMORY: - fprintf (stderr, "Error: %s. Halting processing.\n", - notmuch_status_to_string (status)); - ret = status; - goto DONE; - default: - case NOTMUCH_STATUS_FILE_ERROR: - case NOTMUCH_STATUS_NULL_POINTER: - case NOTMUCH_STATUS_TAG_TOO_LONG: - case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: - case NOTMUCH_STATUS_LAST_STATUS: - INTERNAL_ERROR ("add_message returned unexpected value: %d", status); - goto DONE; - } - - if (message) { - notmuch_message_destroy (message); - message = NULL; - } - - if (state->processed_files % 1000 == 0) - add_files_print_progress (state); - } - } else if (S_ISDIR (st->st_mode)) { - status = add_files_recursive (notmuch, next, st, state); - if (status && ret == NOTMUCH_STATUS_SUCCESS) - ret = status; - } - - talloc_free (next); - next = NULL; - } - - status = notmuch_database_set_timestamp (notmuch, path, path_mtime); - if (status && ret == NOTMUCH_STATUS_SUCCESS) - ret = status; - - DONE: - if (next) - talloc_free (next); - if (entry) - free (entry); - if (dir) - closedir (dir); - - return ret; -} - -/* This is the top-level entry point for add_files. It does a couple - * of error checks, and then calls into the recursive function, - * (avoiding the repeating of these error checks at every - * level---which would be useless becaues we already do a stat() at - * the level above). */ -static notmuch_status_t -add_files (notmuch_database_t *notmuch, - const char *path, - add_files_state_t *state) -{ - struct stat st; - - if (stat (path, &st)) { - fprintf (stderr, "Error reading directory %s: %s\n", - path, strerror (errno)); - return NOTMUCH_STATUS_FILE_ERROR; - } - - if (! S_ISDIR (st.st_mode)) { - fprintf (stderr, "Error: %s is not a directory.\n", path); - return NOTMUCH_STATUS_FILE_ERROR; - } - - return add_files_recursive (notmuch, path, &st, state); -} - -/* Recursively count all regular files in path and all sub-direcotries - * of path. The result is added to *count (which should be - * initialized to zero by the top-level caller before calling - * count_files). */ -static void -count_files (const char *path, int *count) -{ - DIR *dir; - struct dirent *e, *entry = NULL; - int entry_length; - int err; - char *next; - struct stat st; - - dir = opendir (path); - - if (dir == NULL) { - fprintf (stderr, "Warning: failed to open directory %s: %s\n", - path, strerror (errno)); - goto DONE; - } - - entry_length = offsetof (struct dirent, d_name) + - pathconf (path, _PC_NAME_MAX) + 1; - entry = malloc (entry_length); - - while (1) { - err = readdir_r (dir, entry, &e); - if (err) { - fprintf (stderr, "Error reading directory: %s\n", - strerror (errno)); - free (entry); - goto DONE; - } - - if (e == NULL) - break; - - /* Ignore special directories to avoid infinite recursion. - * Also ignore the .notmuch directory. - */ - /* XXX: Eventually we'll want more sophistication to let the - * user specify files to be ignored. */ - if (strcmp (entry->d_name, ".") == 0 || - strcmp (entry->d_name, "..") == 0 || - strcmp (entry->d_name, ".notmuch") == 0) - { - continue; - } - - if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) { - next = NULL; - fprintf (stderr, "Error descending from %s to %s: Out of memory\n", - path, entry->d_name); - continue; - } - - stat (next, &st); - - if (S_ISREG (st.st_mode)) { - *count = *count + 1; - if (*count % 1000 == 0) { - printf ("Found %d files so far.\r", *count); - fflush (stdout); - } - } else if (S_ISDIR (st.st_mode)) { - count_files (next, count); - } - - free (next); - } - - DONE: - if (entry) - free (entry); - - closedir (dir); -} - -static int -setup_command (unused (int argc), unused (char *argv[])) -{ - notmuch_database_t *notmuch = NULL; - char *default_path, *mail_directory = NULL; - size_t line_size; - int count; - add_files_state_t add_files_state; - double elapsed; - struct timeval tv_now; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - - printf ("Welcome to notmuch!\n\n"); - - printf ("The goal of notmuch is to help you manage and search your collection of\n" - "email, and to efficiently keep up with the flow of email as it comes in.\n\n"); - - printf ("Notmuch needs to know the top-level directory of your email archive,\n" - "(where you already have mail stored and where messages will be delivered\n" - "in the future). This directory can contain any number of sub-directories\n" - "and primarily just files with indvidual email messages (eg. maildir or mh\n" - "archives are perfect). If there are other, non-email files (such as\n" - "indexes maintained by other email programs) then notmuch will do its\n" - "best to detect those and ignore them.\n\n"); - - printf ("Mail storage that uses mbox format, (where one mbox file contains many\n" - "messages), will not work with notmuch. If that's how your mail is currently\n" - "stored, we recommend you first convert it to maildir format with a utility\n" - "such as mb2md. In that case, press Control-C now and run notmuch again\n" - "once the conversion is complete.\n\n"); - - - default_path = notmuch_database_default_path (); - printf ("Top-level mail directory [%s]: ", default_path); - fflush (stdout); - - getline (&mail_directory, &line_size, stdin); - chomp_newline (mail_directory); - - printf ("\n"); - - if (mail_directory == NULL || strlen (mail_directory) == 0) { - if (mail_directory) - free (mail_directory); - mail_directory = default_path; - } else { - /* XXX: Instead of telling the user to use an environment - * variable here, we should really be writing out a configuration - * file and loading that on the next run. */ - if (strcmp (mail_directory, default_path)) { - printf ("Note: Since you are not using the default path, you will want to set\n" - "the NOTMUCH_BASE environment variable to %s so that\n" - "future calls to notmuch commands will know where to find your mail.\n", - mail_directory); - printf ("For example, if you are using bash for your shell, add:\n\n"); - printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory); - printf ("to your ~/.bashrc file.\n\n"); - } - free (default_path); - } - - notmuch = notmuch_database_create (mail_directory); - if (notmuch == NULL) { - fprintf (stderr, "Failed to create new notmuch database at %s\n", - mail_directory); - ret = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - printf ("OK. Let's take a look at the mail we can find in the directory\n"); - printf ("%s ...\n", mail_directory); - - count = 0; - count_files (mail_directory, &count); - - printf ("Found %d total files. That's not much mail.\n\n", count); - - printf ("Next, we'll inspect the messages and create a database of threads:\n"); - - add_files_state.ignore_read_only_directories = FALSE; - add_files_state.saw_read_only_directory = FALSE; - add_files_state.total_files = count; - add_files_state.processed_files = 0; - add_files_state.added_messages = 0; - add_files_state.callback = NULL; - gettimeofday (&add_files_state.tv_start, NULL); - - ret = add_files (notmuch, mail_directory, &add_files_state); - - gettimeofday (&tv_now, NULL); - elapsed = tv_elapsed (add_files_state.tv_start, - tv_now); - printf ("Processed %d %s in ", add_files_state.processed_files, - add_files_state.processed_files == 1 ? - "file" : "total files"); - print_formatted_seconds (elapsed); - if (elapsed > 1) { - printf (" (%d files/sec.). \n", - (int) (add_files_state.processed_files / elapsed)); - } else { - printf (". \n"); - } - if (add_files_state.added_messages) { - printf ("Added %d %s to the database.\n\n", - add_files_state.added_messages, - add_files_state.added_messages == 1 ? - "message" : "unique messages"); - } - - printf ("When new mail is delivered to %s in the future,\n" - "run \"notmuch new\" to add it to the database.\n\n", - mail_directory); - - if (ret) { - printf ("Note: At least one error was encountered: %s\n", - notmuch_status_to_string (ret)); - } - - DONE: - if (mail_directory) - free (mail_directory); - if (notmuch) - notmuch_database_close (notmuch); - - return ret; -} - -static void -tag_inbox_and_unread (notmuch_message_t *message) -{ - notmuch_message_add_tag (message, "inbox"); - notmuch_message_add_tag (message, "unread"); -} - -static int -new_command (unused (int argc), unused (char *argv[])) -{ - notmuch_database_t *notmuch; - const char *mail_directory; - add_files_state_t add_files_state; - double elapsed; - struct timeval tv_now; - int ret = 0; - - notmuch = notmuch_database_open (NULL); - if (notmuch == NULL) { - ret = 1; - goto DONE; - } - - mail_directory = notmuch_database_get_path (notmuch); - - add_files_state.ignore_read_only_directories = TRUE; - add_files_state.saw_read_only_directory = FALSE; - add_files_state.total_files = 0; - add_files_state.processed_files = 0; - add_files_state.added_messages = 0; - add_files_state.callback = tag_inbox_and_unread; - gettimeofday (&add_files_state.tv_start, NULL); - - ret = add_files (notmuch, mail_directory, &add_files_state); - - gettimeofday (&tv_now, NULL); - elapsed = tv_elapsed (add_files_state.tv_start, - tv_now); - if (add_files_state.processed_files) { - printf ("Processed %d %s in ", add_files_state.processed_files, - add_files_state.processed_files == 1 ? - "file" : "total files"); - print_formatted_seconds (elapsed); - if (elapsed > 1) { - printf (" (%d files/sec.). \n", - (int) (add_files_state.processed_files / elapsed)); - } else { - printf (". \n"); - } - } - if (add_files_state.added_messages) { - printf ("Added %d new %s to the database (not much, really).\n", - add_files_state.added_messages, - add_files_state.added_messages == 1 ? - "message" : "messages"); - } else { - printf ("No new mail---and that's not much.\n"); - } - - if (elapsed > 1 && ! add_files_state.saw_read_only_directory) { - printf ("\nTip: If you have any sub-directories that are archives (that is,\n" - "they will never receive new mail), marking these directores as\n" - "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n" - "much more efficient (it won't even look in those directories).\n"); - } - - if (ret) { - printf ("\nNote: At least one error was encountered: %s\n", - notmuch_status_to_string (ret)); - } - - DONE: - if (notmuch) - notmuch_database_close (notmuch); - - return ret; -} - -/* Construct a single query string from the passed arguments, using - * 'ctx' as the talloc owner for all allocations. - * - * Currently, the arguments are just connected with space characters, - * but we might do more processing in the future, (such as inserting - * any AND operators needed to work around Xapian QueryParser bugs). - * - * This function returns NULL in case of insufficient memory. - */ -static char * -query_string_from_args (void *ctx, int argc, char *argv[]) -{ - char *query_string; - int i; - - query_string = talloc_strdup (ctx, ""); - if (query_string == NULL) - return NULL; - - for (i = 0; i < argc; i++) { - if (i != 0) { - query_string = talloc_strdup_append (query_string, " "); - if (query_string == NULL) - return NULL; - } - - query_string = talloc_strdup_append (query_string, argv[i]); - if (query_string == NULL) - return NULL; - } - - return query_string; -} - -static int -search_command (int argc, char *argv[]) -{ - void *local = talloc_new (NULL); - notmuch_database_t *notmuch = NULL; - notmuch_query_t *query; - notmuch_thread_results_t *results; - notmuch_thread_t *thread; - notmuch_tags_t *tags; - char *query_str; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - - notmuch = notmuch_database_open (NULL); - if (notmuch == NULL) { - ret = 1; - goto DONE; - } - - query_str = query_string_from_args (local, argc, argv); - - query = notmuch_query_create (notmuch, query_str); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); - ret = 1; - goto DONE; - } - - for (results = notmuch_query_search_threads (query); - notmuch_thread_results_has_more (results); - notmuch_thread_results_advance (results)) - { - int first = 1; - - thread = notmuch_thread_results_get (results); - - printf ("%s %s", - notmuch_thread_get_thread_id (thread), - notmuch_thread_get_subject (thread)); - - printf (" ("); - for (tags = notmuch_thread_get_tags (thread); - notmuch_tags_has_more (tags); - notmuch_tags_advance (tags)) - { - if (! first) - printf (" "); - printf ("%s", notmuch_tags_get (tags)); - first = 0; - } - printf (")\n"); - - notmuch_thread_destroy (thread); - } - - notmuch_query_destroy (query); - - DONE: - if (notmuch) - notmuch_database_close (notmuch); - talloc_free (local); - - return ret; -} - -static int -show_command (unused (int argc), unused (char *argv[])) -{ - fprintf (stderr, "Error: show is not implemented yet.\n"); - return 1; -} +#include "notmuch-client.h" static int -tag_command (unused (int argc), unused (char *argv[])) -{ - void *local; - int *add_tags, *remove_tags; - int add_tags_count = 0; - int remove_tags_count = 0; - char *query_string; - notmuch_database_t *notmuch = NULL; - notmuch_query_t *query; - notmuch_message_results_t *results; - notmuch_message_t *message; - int ret = 0; - int i; - - local = talloc_new (NULL); - if (local == NULL) { - ret = 1; - goto DONE; - } - - add_tags = talloc_size (local, argc * sizeof (int)); - if (add_tags == NULL) { - ret = 1; - goto DONE; - } - - remove_tags = talloc_size (local, argc * sizeof (int)); - if (remove_tags == NULL) { - ret = 1; - goto DONE; - } - - for (i = 0; i < argc; i++) { - if (strcmp (argv[i], "--") == 0) { - i++; - break; - } - if (argv[i][0] == '+') { - add_tags[add_tags_count++] = i; - } else if (argv[i][0] == '-') { - remove_tags[remove_tags_count++] = i; - } else { - break; - } - } - - if (add_tags_count == 0 && remove_tags_count == 0) { - fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n"); - ret = 1; - goto DONE; - } - - if (i == argc) { - fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n"); - ret = 1; - goto DONE; - } - - notmuch = notmuch_database_open (NULL); - if (notmuch == NULL) { - ret = 1; - goto DONE; - } - - query_string = query_string_from_args (local, argc - i, &argv[i]); - - query = notmuch_query_create (notmuch, query_string); - if (query == NULL) { - fprintf (stderr, "Out of memory.\n"); - ret = 1; - goto DONE; - } - - for (results = notmuch_query_search_messages (query); - notmuch_message_results_has_more (results); - notmuch_message_results_advance (results)) - { - message = notmuch_message_results_get (results); - - notmuch_message_freeze (message); - - for (i = 0; i < remove_tags_count; i++) - notmuch_message_remove_tag (message, - argv[remove_tags[i]] + 1); - - for (i = 0; i < add_tags_count; i++) - notmuch_message_add_tag (message, argv[add_tags[i]] + 1); - - notmuch_message_thaw (message); - - notmuch_message_destroy (message); - } - - notmuch_query_destroy (query); - - DONE: - if (notmuch) - notmuch_database_close (notmuch); - - talloc_free (local); - - return ret; -} - -static int -dump_command (int argc, char *argv[]) -{ - FILE *output = NULL; - notmuch_database_t *notmuch = NULL; - notmuch_query_t *query; - notmuch_message_results_t *results; - notmuch_message_t *message; - notmuch_tags_t *tags; - int ret = 0; - - if (argc) { - output = fopen (argv[0], "w"); - if (output == NULL) { - fprintf (stderr, "Error opening %s for writing: %s\n", - argv[0], strerror (errno)); - ret = 1; - goto DONE; - } - } else { - output = stdout; - } - - notmuch = notmuch_database_open (NULL); - if (notmuch == NULL) { - ret = 1; - goto DONE; - } - - query = notmuch_query_create (notmuch, ""); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); - ret = 1; - goto DONE; - } - - notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID); - - for (results = notmuch_query_search_messages (query); - notmuch_message_results_has_more (results); - notmuch_message_results_advance (results)) - { - int first = 1; - message = notmuch_message_results_get (results); - - fprintf (output, - "%s (", notmuch_message_get_message_id (message)); - - for (tags = notmuch_message_get_tags (message); - notmuch_tags_has_more (tags); - notmuch_tags_advance (tags)) - { - if (! first) - fprintf (output, " "); - - fprintf (output, "%s", notmuch_tags_get (tags)); - - first = 0; - } - - fprintf (output, ")\n"); - - notmuch_message_destroy (message); - } - - notmuch_query_destroy (query); - - DONE: - if (notmuch) - notmuch_database_close (notmuch); - if (output && output != stdout) - fclose (output); - - return ret; -} - -static int -restore_command (int argc, char *argv[]) -{ - FILE *input = NULL; - notmuch_database_t *notmuch = NULL; - char *line = NULL; - size_t line_size; - ssize_t line_len; - regex_t regex; - int rerr; - int ret = 0; - - if (argc) { - input = fopen (argv[0], "r"); - if (input == NULL) { - fprintf (stderr, "Error opening %s for reading: %s\n", - argv[0], strerror (errno)); - ret = 1; - goto DONE; - } - } else { - printf ("No filename given. Reading dump from stdin.\n"); - input = stdin; - } - - notmuch = notmuch_database_open (NULL); - if (notmuch == NULL) { - ret = 1; - goto DONE; - } - - /* Dump output is one line per message. We match a sequence of - * non-space characters for the message-id, then one or more - * spaces, then a list of space-separated tags as a sequence of - * characters within literal '(' and ')'. */ - xregcomp (®ex, - "^([^ ]+) \\(([^)]*)\\)$", - REG_EXTENDED); - - while ((line_len = getline (&line, &line_size, input)) != -1) { - regmatch_t match[3]; - char *message_id, *tags, *tag, *next; - notmuch_message_t *message; - notmuch_status_t status; - - chomp_newline (line); - - rerr = xregexec (®ex, line, 3, match, 0); - if (rerr == REG_NOMATCH) - { - fprintf (stderr, "Warning: Ignoring invalid input line: %s\n", - line); - continue; - } - - message_id = xstrndup (line + match[1].rm_so, - match[1].rm_eo - match[1].rm_so); - tags = xstrndup (line + match[2].rm_so, - match[2].rm_eo - match[2].rm_so); - - if (strlen (tags)) { - - message = notmuch_database_find_message (notmuch, message_id); - if (message == NULL) { - fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n", - message_id); - goto NEXT_LINE; - } - - notmuch_message_freeze (message); - - notmuch_message_remove_all_tags (message); - - next = tags; - while (next) { - tag = strsep (&next, " "); - if (*tag == '\0') - continue; - status = notmuch_message_add_tag (message, tag); - if (status) { - fprintf (stderr, - "Error applying tag %s to message %s:\n", - tag, message_id); - fprintf (stderr, "%s\n", - notmuch_status_to_string (status)); - } - } - - notmuch_message_thaw (message); - notmuch_message_destroy (message); - } - NEXT_LINE: - free (message_id); - free (tags); - } - - regfree (®ex); - - DONE: - if (line) - free (line); - if (notmuch) - notmuch_database_close (notmuch); - if (input && input != stdin) - fclose (input); - - return ret; -} - -static int -help_command (int argc, char *argv[]); +notmuch_help_command (void *ctx, int argc, char *argv[]); command_t commands[] = { - { "setup", setup_command, + { "setup", notmuch_setup_command, "Interactively setup notmuch for first use.", + "\t\tThe setup command is the first command you will run in order\n" + "\t\tto start using notmuch. It will prompt you for the directory\n" + "\t\tcontaining your email archives, and will then proceed to build\n" + "\t\ta database to allow fast searching of that mail.\n\n" "\t\tInvoking notmuch with no command argument will run setup if\n" "\t\tthe setup command has not previously been completed." }, - { "new", new_command, + { "new", notmuch_new_command, "Find and import any new messages.", "\t\tScans all sub-directories of the database, adding new messages\n" - "\t\tthat are found. Each new message will be tagges as both\n" + "\t\tthat are found. Each new message will be tagged as both\n" "\t\t\"inbox\" and \"unread\".\n" "\n" "\t\tNote: \"notmuch new\" will skip any read-only directories,\n" - "\t\tso you can use that to mark tdirectories that will not\n" + "\t\tso you can use that to mark directories that will not\n" "\t\treceive any new mail (and make \"notmuch new\" faster)." }, - { "search", search_command, + { "search", notmuch_search_command, " [...]\n\n" "\t\tSearch for threads matching the given search terms.", - "\t\tOnce we actually implement search we'll document the\n" - "\t\tsyntax here." }, - { "show", show_command, - "\n\n" - "\t\tShow the thread with the given thread ID (see 'search').", - "" }, - { "tag", tag_command, + "\t\tNote that the individual mail messages will be matched\n" + "\t\tagainst the search terms, but the results will be the\n" + "\t\tthreads containing the matched messages.\n\n" + "\t\tCurrently, in addition to free text (and quoted phrases)\n" + "\t\twhich match terms appearing anywhere within an email,\n" + "\t\tthe following prefixes can be used to search specific\n" + "\t\tportions of an email, (where indicate user-\n" + "\t\tsupplied values):\n\n" + "\t\t\tfrom:\n" + "\t\t\tto:\n" + "\t\t\tsubject:\n" + "\t\t\ttag:\n" + "\t\t\tid:\n" + "\t\t\tthread:\n\n" + "\t\tThe from: prefix is used to match the name or address of\n" + "\t\tthe sender of an email message.\n\n" + "\t\tThe to: prefix is used to match the names or addresses of\n" + "\t\tany recipient of an email message, (whether To, Cc, or Bcc).\n\n" + "\t\tAny term prefixed with subject: will match only text from\n" + "\t\tthe subject of an email. Quoted phrases are supported when\n" + "\t\tsearching with: subject:\"this is a phrase\".\n\n" + "\t\tValid tag values include \"inbox\" and \"unread\" by default\n" + "\t\tfor new messages added by \"notmuch new\" as well as any other\n" + "\t\ttag values added manually with \"notmuch tag\".\n\n" + "\t\tMessage ID values are the literal contents of the Message-ID:\n" + "\t\theader of email messages, but without the '<','>' delimiters.\n\n" + "\t\tThread ID values are generated internally by notmuch but can\n" + "\t\tbe seen in the output of \"notmuch search\" for example.\n\n" + "\t\tIn addition to individual terms, multiple terms can be\n" + "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n" + "\t\tEach term in the query will be implicitly connected by a\n" + "\t\tlogical AND if no explicit operator is provided, (except\n" + "\t\tthat terms with a common prefix will be implicitly combined\n" + "\t\twith OR until we get Xapian defect #402 fixed).\n\n" + "\t\tParentheses can also be used to control the combination of\n" + "\t\tthe Boolean operators, but will have to be protected from\n" + "\t\tinterpretation by the shell, (such as by putting quotation\n" + "\t\tmarks around any parenthesized expression)." }, + { "reply", notmuch_reply_command, + " [...]\n\n" + "\t\tFormats a reply from a set of existing messages.", + "\t\tConstructs a new message as a reply to a set of existing\n" + "\t\tmessages. The From: address is used as a To: address\n" + "\t\talong with all old To: addresses. All of the Cc: addresses\n" + "\t\tare copied as new Cc: addresses. An In-Reply-To: header\n" + "\t\twill be constructed from the name and date of the original\n" + "\t\tmessage, and the original Message-ID will be added to the\n" + "\t\tlist of References in the new message. The text of each\n" + "\t\tmessage (as described in the \"show\" command) will be\n" + "\t\tpresented, each line prefixed with \"> \" The resulting\n" + "\t\tmessage will be dumped to stdout." }, + { "show", notmuch_show_command, + " [...]\n\n" + "\t\tShows all messages matching the search terms.", + "\t\tSee the documentation of \"notmuch search\" for details\n" + "\t\tof the supported syntax of search terms.\n\n" + "\t\tA common use of \"notmuch show\" is to display a single\n" + "\t\tthread of email messages. For this, use a search term of\n" + "\t\t\"thread:\" as can be seen in the first column\n" + "\t\tof output from the \"notmuch search\" command.\n\n" + "\t\tAll messages will be displayed in date order. The output\n" + "\t\tformat is plain-text, with all text-content MIME parts\n" + "\t\tdecoded. Various components in the output, ('message',\n" + "\t\t'header', 'body', 'attachment', and MIME 'part') will be\n" + "\t\tdelimited by easily-parsed markers. Each marker consists\n" + "\t\tof a Control-L character (ASCII decimal 12), the name of\n" + "\t\tthe marker, and then either an opening or closing brace,\n" + "\t\t'{' or '}' to either open or close the component."}, + { "tag", notmuch_tag_command, "+|- [...] [--] [...]\n\n" "\t\tAdd/remove tags for all messages matching the search terms.", - "\t\tThe search terms are handled texactly as in 'search' so one\n" + "\t\tThe search terms are handled exactly as in 'search' so one\n" "\t\tcan use that command first to see what will be modified.\n\n" "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n" "\t\tremoved. For each message, tag removal is before tag addition.\n\n" @@ -1068,8 +125,14 @@ command_t commands[] = { "\t\targument that begins with neither '+' nor '-'. Support for\n" "\t\tan initial search term beginning with '+' or '-' is provided\n" "\t\tby allowing the user to specify a \"--\" argument to separate\n" - "\t\tthe tags from the search terms." }, - { "dump", dump_command, + "\t\tthe tags from the search terms.\n\n" + "\t\tNote: If you run \"notmuch new\" between reading a thread with\n" + "\t\t\"notmuch show\" and removing the \"inbox\" tag for that thread\n" + "\t\twith \"notmuch tag\" then you create the possibility of moving\n" + "\t\tsome messages from that thread out of your inbox without ever\n" + "\t\treading them. The easiest way to avoid this problem is to not\n" + "\t\trun \"notmuch new\" between reading and removing tags." }, + { "dump", notmuch_dump_command, "[]\n\n" "\t\tCreate a plain-text dump of the tags for each message.", "\t\tOutput is to the given filename, if any, or to stdout.\n" @@ -1077,15 +140,20 @@ command_t commands[] = { "\t\tthat can't be recreated from the messages themselves.\n" "\t\tThe output of notmuch dump is therefore the only\n" "\t\tcritical thing to backup (and much more friendly to\n" - "\t\tincremental backup than the native database files." }, - { "restore", restore_command, + "\t\tincremental backup than the native database files.)" }, + { "restore", notmuch_restore_command, "\n\n" "\t\tRestore the tags from the given dump file (see 'dump').", - "" }, - { "help", help_command, + "\t\tNote: The dump file format is specifically chosen to be\n" + "\t\tcompatible with the format of files produced by sup-dump.\n" + "\t\tSo if you've previously been using sup for mail, then the\n" + "\t\t\"notmuch restore\" command provides you a way to import\n" + "\t\tall of your tags (or labels as sup calls them)." }, + { "help", notmuch_help_command, "[]\n\n" "\t\tThis message, or more detailed help for the named command.", - "" } + "\t\tExcept in this case, where there's not much more detailed\n" + "\t\thelp available." } }; static void @@ -1109,7 +177,7 @@ usage (void) } static int -help_command (int argc, char *argv[]) +notmuch_help_command (unused (void *ctx), int argc, char *argv[]) { command_t *command; unsigned int i; @@ -1136,27 +204,98 @@ help_command (int argc, char *argv[]) argv[0]); return 1; } - + +/* Handle the case of "notmuch" being invoked with no command + * argument. For now we just call notmuch_setup_command, but we plan + * to be more clever about this in the future. + */ +static int +notmuch (void *ctx) +{ + notmuch_config_t *config; + notmuch_bool_t is_new; + char *db_path; + struct stat st; + + config = notmuch_config_open (ctx, NULL, &is_new); + + /* If the user has never configured notmuch, then run + * notmuch_setup_command which will give a nice welcome message, + * and interactively guide the user through the configuration. */ + if (is_new) { + notmuch_config_close (config); + return notmuch_setup_command (ctx, 0, NULL); + } + + /* Notmuch is already configured, but is there a database? */ + db_path = talloc_asprintf (ctx, "%s/%s", + notmuch_config_get_database_path (config), + ".notmuch"); + if (stat (db_path, &st)) { + notmuch_config_close (config); + if (errno != ENOENT) { + fprintf (stderr, "Error looking for notmuch database at %s: %s\n", + db_path, strerror (errno)); + return 1; + } + printf ("Notmuch is configured, but there's not yet a database at\n\n\t%s\n\n", + db_path); + printf ("You probably want to run \"notmuch new\" now to create that database.\n\n" + "Note that the first run of \"notmuch new\" can take a very long time\n" + "and that the resulting database will use roughly the same amount of\n" + "storage space as the email being indexed.\n\n"); + return 0; + } + + printf ("Notmuch is configured and appears to have a database. Excellent!\n\n" + "At this point you can start exploring the functionality of notmuch by\n" + "using commands such as:\n\n" + "\tnotmuch search tag:inbox\n\n" + "\tnotmuch search to:\"%s\"\n\n" + "\tnotmuch search from:\"%s\"\n\n" + "\tnotmuch search subject:\"my favorite things\"\n\n" + "See \"notmuch help search\" for more details.\n\n" + "You can also use \"notmuch show\" with any of the thread IDs resulting\n" + "from a search. Finally, you may want to explore using a more sophisticated\n" + "interface to notmuch such as the emacs interface implemented in notmuch.el\n" + "or any other interface described at http://notmuchmail.org\n\n" + "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n" + "Have fun, and may your inbox never have much mail.\n\n", + notmuch_config_get_user_name (config), + notmuch_config_get_user_primary_email (config)); + + notmuch_config_close (config); + + return 0; +} + int main (int argc, char *argv[]) { + void *local; command_t *command; unsigned int i; + local = talloc_new (NULL); + + g_mime_init (0); + if (argc == 1) - return setup_command (0, NULL); + return notmuch (local); for (i = 0; i < ARRAY_SIZE (commands); i++) { command = &commands[i]; if (strcmp (argv[1], command->name) == 0) - return (command->function) (argc - 2, &argv[2]); + return (command->function) (local, argc - 2, &argv[2]); } /* Don't complain about "help" being an unknown command when we're about to provide exactly what's wanted anyway. */ - fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]); - usage (); + fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", + argv[1]); + + talloc_free (local); return 1; }