]> git.notmuchmail.org Git - notmuch/blobdiff - lib/database.cc
emacs: Add new option notmuch-search-hide-excluded
[notmuch] / lib / database.cc
index a679cbabbac95a2d39885292341370e1d040d8c0..737a3f3060677b4e7fd5a55fbe5268082f0867f2 100644 (file)
@@ -19,9 +19,6 @@
  */
 
 #include "database-private.h"
-#include "parse-time-vrp.h"
-#include "query-fp.h"
-#include "regexp-fields.h"
 #include "string-util.h"
 
 #include <iostream>
 #include <signal.h>
 #include <ftw.h>
 
-#include <glib.h> /* g_free, GPtrArray, GHashTable */
-#include <glib-object.h> /* g_type_init */
+#include <glib.h>               /* g_free, GPtrArray, GHashTable */
+#include <glib-object.h>        /* g_type_init */
 
-#include <gmime/gmime.h> /* g_mime_init */
+#include <gmime/gmime.h>        /* g_mime_init */
 
 using namespace std;
 
-#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
-
 typedef struct {
     const char *name;
     const char *prefix;
@@ -48,28 +43,48 @@ typedef struct {
 
 #define NOTMUCH_DATABASE_VERSION 3
 
-#define STRINGIFY(s) _SUB_STRINGIFY(s)
+#define STRINGIFY(s) _SUB_STRINGIFY (s)
 #define _SUB_STRINGIFY(s) #s
 
-#if HAVE_XAPIAN_DB_RETRY_LOCK
-#define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK)
-#else
-#define DB_ACTION Xapian::DB_CREATE_OR_OPEN
-#endif
+#define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error)
+
+static void
+_log_xapian_exception (const char *where, notmuch_database_t *notmuch,  const Xapian::Error error)
+{
+    _notmuch_database_log (notmuch,
+                          "A Xapian exception occurred at %s: %s\n",
+                          where,
+                          error.get_msg ().c_str ());
+    notmuch->exception_reported = true;
+}
+
+notmuch_database_mode_t
+_notmuch_database_mode (notmuch_database_t *notmuch)
+{
+    if (notmuch->writable_xapian_db)
+       return NOTMUCH_DATABASE_MODE_READ_WRITE;
+    else
+       return NOTMUCH_DATABASE_MODE_READ_ONLY;
+}
 
 /* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
  *
  * We currently have three different types of documents (mail, ghost,
  * and directory) and also some metadata.
  *
+ * There are two kinds of prefixes used in notmuch. There are the
+ * human friendly 'prefix names' like "thread:", which are also used
+ * in the query parser, and the actual prefix terms in the database
+ * (e.g. "G"). The correspondence is maintained in the file scope data
+ * structure 'prefix_table'.
+ *
  * Mail document
  * -------------
  * A mail document is associated with a particular email message. It
- * is stored in one or more files on disk (though only one has its
- * content indexed) and is uniquely identified  by its "id" field
- * (which is generally the message ID). It is indexed with the
- * following prefixed terms which the database uses to construct
- * threads, etc.:
+ * is stored in one or more files on disk and is uniquely identified
+ * by its "id" field (which is generally the message ID). It is
+ * indexed with the following prefixed terms which the database uses
+ * to construct threads, etc.:
  *
  *    Single terms of given prefix:
  *
@@ -116,11 +131,16 @@ typedef struct {
  *     LAST_MOD:       The revision number as of the last tag or
  *                     filename change.
  *
- * In addition, terms from the content of the message are added with
- * "from", "to", "attachment", and "subject" prefixes for use by the
- * user in searching. Similarly, terms from the path of the mail
- * message are added with "folder" and "path" prefixes. But the
- * database doesn't really care itself about any of these.
+ * The prefixed terms described above are also searchable without an
+ * explicit field name, but as of notmuch 0.29 this is due to
+ * query-parser setup, not extra terms in the database.  In addition,
+ * terms from the content of the message are added without a prefix
+ * for use by the user in searching. Note that the prefix name "body"
+ * is used to refer to the empty prefix string in the database.
+ *
+ * The path of the containing folder is added with the "folder" prefix
+ * (see _notmuch_message_add_folder_terms).  Sub-paths of the the path
+ * of the mail message are added with the "path" prefix.
  *
  * The data portion of a mail document is empty.
  *
@@ -234,146 +254,12 @@ typedef struct {
  *                     same thread.
  */
 
-/* With these prefix values we follow the conventions published here:
- *
- * https://xapian.org/docs/omega/termprefixes.html
- *
- * as much as makes sense. Note that I took some liberty in matching
- * the reserved prefix values to notmuch concepts, (for example, 'G'
- * is documented as "newsGroup (or similar entity - e.g. a web forum
- * name)", for which I think the thread is the closest analogue in
- * notmuch. This in spite of the fact that we will eventually be
- * storing mailing-list messages where 'G' for "mailing list name"
- * might be even a closer analogue. I'm treating the single-character
- * prefixes preferentially for core notmuch concepts (which will be
- * nearly universal to all mail messages).
- */
-
-static const
-prefix_t prefix_table[] = {
-    /* name                    term prefix     flags */
-    { "type",                  "T",            NOTMUCH_FIELD_NO_FLAGS },
-    { "reference",             "XREFERENCE",   NOTMUCH_FIELD_NO_FLAGS },
-    { "replyto",               "XREPLYTO",     NOTMUCH_FIELD_NO_FLAGS },
-    { "directory",             "XDIRECTORY",   NOTMUCH_FIELD_NO_FLAGS },
-    { "file-direntry",         "XFDIRENTRY",   NOTMUCH_FIELD_NO_FLAGS },
-    { "directory-direntry",    "XDDIRENTRY",   NOTMUCH_FIELD_NO_FLAGS },
-    { "thread",                        "G",            NOTMUCH_FIELD_EXTERNAL },
-    { "tag",                   "K",            NOTMUCH_FIELD_EXTERNAL },
-    { "is",                    "K",            NOTMUCH_FIELD_EXTERNAL },
-    { "id",                    "Q",            NOTMUCH_FIELD_EXTERNAL },
-    { "mid",                   "Q",            NOTMUCH_FIELD_EXTERNAL },
-    { "path",                  "P",            NOTMUCH_FIELD_EXTERNAL },
-    { "property",              "XPROPERTY",    NOTMUCH_FIELD_EXTERNAL },
-    /*
-     * Unconditionally add ':' to reduce potential ambiguity with
-     * overlapping prefixes and/or terms that start with capital
-     * letters. See Xapian document termprefixes.html for related
-     * discussion.
-     */
-    { "folder",                        "XFOLDER:",     NOTMUCH_FIELD_EXTERNAL },
-#if HAVE_XAPIAN_FIELD_PROCESSOR
-    { "date",                  NULL,           NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROCESSOR },
-    { "query",                 NULL,           NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROCESSOR },
-#endif
-    { "from",                  "XFROM",        NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROBABILISTIC |
-                                               NOTMUCH_FIELD_PROCESSOR },
-    { "to",                    "XTO",          NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROBABILISTIC },
-    { "attachment",            "XATTACHMENT",  NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROBABILISTIC },
-    { "mimetype",              "XMIMETYPE",    NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROBABILISTIC },
-    { "subject",               "XSUBJECT",     NOTMUCH_FIELD_EXTERNAL |
-                                               NOTMUCH_FIELD_PROBABILISTIC |
-                                               NOTMUCH_FIELD_PROCESSOR},
-};
-
-static void
-_setup_query_field_default (const prefix_t *prefix, notmuch_database_t *notmuch)
-{
-    if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC)
-       notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
-    else
-       notmuch->query_parser->add_boolean_prefix (prefix->name, prefix->prefix);
-}
 
-#if HAVE_XAPIAN_FIELD_PROCESSOR
-static void
-_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch)
+notmuch_string_map_iterator_t *
+_notmuch_database_user_headers (notmuch_database_t *notmuch)
 {
-    if (prefix->flags & NOTMUCH_FIELD_PROCESSOR) {
-       Xapian::FieldProcessor *fp;
-
-       if (STRNCMP_LITERAL (prefix->name, "date") == 0)
-           fp = (new DateFieldProcessor())->release ();
-       else if (STRNCMP_LITERAL(prefix->name, "query") == 0)
-           fp = (new QueryFieldProcessor (*notmuch->query_parser, notmuch))->release ();
-       else
-           fp = (new RegexpFieldProcessor (prefix->name, *notmuch->query_parser, notmuch))->release ();
-
-       /* we treat all field-processor fields as boolean in order to get the raw input */
-       notmuch->query_parser->add_boolean_prefix (prefix->name, fp);
-    } else {
-       _setup_query_field_default (prefix, notmuch);
-    }
-}
-#else
-static inline void
-_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch)
-{
-    _setup_query_field_default (prefix, notmuch);
+    return _notmuch_string_map_iterator_create (notmuch->user_header, "", false);
 }
-#endif
-
-const char *
-_find_prefix (const char *name)
-{
-    unsigned int i;
-
-    for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
-       if (strcmp (name, prefix_table[i].name) == 0)
-           return prefix_table[i].prefix;
-    }
-
-    INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
-
-    return "";
-}
-
-static const struct {
-    /* NOTMUCH_FEATURE_* value. */
-    _notmuch_features value;
-    /* Feature name as it appears in the database.  This name should
-     * be appropriate for displaying to the user if an older version
-     * of notmuch doesn't support this feature. */
-    const char *name;
-    /* Compatibility flags when this feature is declared. */
-    const char *flags;
-} feature_names[] = {
-    { NOTMUCH_FEATURE_FILE_TERMS,
-      "multiple paths per message", "rw" },
-    { NOTMUCH_FEATURE_DIRECTORY_DOCS,
-      "relative directory paths", "rw" },
-    /* Header values are not required for reading a database because a
-     * reader can just refer to the message file. */
-    { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES,
-      "from/subject/message-ID in database", "w" },
-    { NOTMUCH_FEATURE_BOOL_FOLDER,
-      "exact folder:/path: search", "rw" },
-    { NOTMUCH_FEATURE_GHOSTS,
-      "mail documents for missing messages", "w"},
-    /* Knowledge of the index mime-types are not required for reading
-     * a database because a reader will just be unable to query
-     * them. */
-    { NOTMUCH_FEATURE_INDEXED_MIMETYPES,
-      "indexed MIME types", "w"},
-    { NOTMUCH_FEATURE_LAST_MOD,
-      "modification tracking", "w"},
-};
 
 const char *
 notmuch_status_to_string (notmuch_status_t status)
@@ -407,6 +293,26 @@ notmuch_status_to_string (notmuch_status_t status)
        return "Operation requires a database upgrade";
     case NOTMUCH_STATUS_PATH_ERROR:
        return "Path supplied is illegal for this function";
+    case NOTMUCH_STATUS_IGNORED:
+       return "Argument was ignored";
+    case NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
+       return "Illegal argument for function";
+    case NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL:
+       return "Crypto protocol missing, malformed, or unintelligible";
+    case NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION:
+       return "Crypto engine initialization failure";
+    case NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL:
+       return "Unknown crypto protocol";
+    case NOTMUCH_STATUS_NO_CONFIG:
+       return "No configuration file found";
+    case NOTMUCH_STATUS_NO_DATABASE:
+       return "No database found";
+    case NOTMUCH_STATUS_DATABASE_EXISTS:
+       return "Database exists, not recreated";
+    case NOTMUCH_STATUS_BAD_QUERY_SYNTAX:
+       return "Syntax error in query";
+    case NOTMUCH_STATUS_NO_MAIL_ROOT:
+       return "No mail root found";
     default:
     case NOTMUCH_STATUS_LAST_STATUS:
        return "Unknown error status value";
@@ -415,8 +321,8 @@ notmuch_status_to_string (notmuch_status_t status)
 
 void
 _notmuch_database_log (notmuch_database_t *notmuch,
-                     const char *format,
-                     ...)
+                      const char *format,
+                      ...)
 {
     va_list va_args;
 
@@ -431,8 +337,8 @@ _notmuch_database_log (notmuch_database_t *notmuch,
 
 void
 _notmuch_database_log_append (notmuch_database_t *notmuch,
-                     const char *format,
-                     ...)
+                             const char *format,
+                             ...)
 {
     va_list va_args;
 
@@ -457,12 +363,12 @@ find_doc_ids_for_term (notmuch_database_t *notmuch,
     *end = notmuch->xapian_db->postlist_end (term);
 }
 
-static void
-find_doc_ids (notmuch_database_t *notmuch,
-             const char *prefix_name,
-             const char *value,
-             Xapian::PostingIterator *begin,
-             Xapian::PostingIterator *end)
+void
+_notmuch_database_find_doc_ids (notmuch_database_t *notmuch,
+                               const char *prefix_name,
+                               const char *value,
+                               Xapian::PostingIterator *begin,
+                               Xapian::PostingIterator *end)
 {
     char *term;
 
@@ -482,7 +388,7 @@ _notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
 {
     Xapian::PostingIterator i, end;
 
-    find_doc_ids (notmuch, prefix_name, value, &i, &end);
+    _notmuch_database_find_doc_ids (notmuch, prefix_name, value, &i, &end);
 
     if (i == end) {
        *doc_id = 0;
@@ -555,264 +461,26 @@ notmuch_database_find_message (notmuch_database_t *notmuch,
        return NOTMUCH_STATUS_SUCCESS;
     } catch (const Xapian::Error &error) {
        _notmuch_database_log (notmuch, "A Xapian exception occurred finding message: %s.\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
        *message_ret = NULL;
        return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
 }
 
-/* Advance 'str' past any whitespace or RFC 822 comments. A comment is
- * a (potentially nested) parenthesized sequence with '\' used to
- * escape any character (including parentheses).
- *
- * If the sequence to be skipped continues to the end of the string,
- * then 'str' will be left pointing at the final terminating '\0'
- * character.
- */
-static void
-skip_space_and_comments (const char **str)
-{
-    const char *s;
-
-    s = *str;
-    while (*s && (isspace (*s) || *s == '(')) {
-       while (*s && isspace (*s))
-           s++;
-       if (*s == '(') {
-           int nesting = 1;
-           s++;
-           while (*s && nesting) {
-               if (*s == '(') {
-                   nesting++;
-               } else if (*s == ')') {
-                   nesting--;
-               } else if (*s == '\\') {
-                   if (*(s+1))
-                       s++;
-               }
-               s++;
-           }
-       }
-    }
-
-    *str = s;
-}
-
-/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
- * comments, and the '<' and '>' delimiters.
- *
- * If not NULL, then *next will be made to point to the first character
- * not parsed, (possibly pointing to the final '\0' terminator.
- *
- * Returns a newly talloc'ed string belonging to 'ctx'.
- *
- * Returns NULL if there is any error parsing the message-id. */
-static char *
-_parse_message_id (void *ctx, const char *message_id, const char **next)
-{
-    const char *s, *end;
-    char *result;
-
-    if (message_id == NULL || *message_id == '\0')
-       return NULL;
-
-    s = message_id;
-
-    skip_space_and_comments (&s);
-
-    /* Skip any unstructured text as well. */
-    while (*s && *s != '<')
-       s++;
-
-    if (*s == '<') {
-       s++;
-    } else {
-       if (next)
-           *next = s;
-       return NULL;
-    }
-
-    skip_space_and_comments (&s);
-
-    end = s;
-    while (*end && *end != '>')
-       end++;
-    if (next) {
-       if (*end)
-           *next = end + 1;
-       else
-           *next = end;
-    }
-
-    if (end > s && *end == '>')
-       end--;
-    if (end <= s)
-       return NULL;
-
-    result = talloc_strndup (ctx, s, end - s + 1);
-
-    /* Finally, collapse any whitespace that is within the message-id
-     * itself. */
-    {
-       char *r;
-       int len;
-
-       for (r = result, len = strlen (r); *r; r++, len--)
-           if (*r == ' ' || *r == '\t')
-               memmove (r, r+1, len);
-    }
-
-    return result;
-}
-
-/* Parse a References header value, putting a (talloc'ed under 'ctx')
- * copy of each referenced message-id into 'hash'.
- *
- * We explicitly avoid including any reference identical to
- * 'message_id' in the result (to avoid mass confusion when a single
- * message references itself cyclically---and yes, mail messages are
- * not infrequent in the wild that do this---don't ask me why).
- *
- * Return the last reference parsed, if it is not equal to message_id.
- */
-static char *
-parse_references (void *ctx,
-                 const char *message_id,
-                 GHashTable *hash,
-                 const char *refs)
-{
-    char *ref, *last_ref = NULL;
-
-    if (refs == NULL || *refs == '\0')
-       return NULL;
-
-    while (*refs) {
-       ref = _parse_message_id (ctx, refs, &refs);
-
-       if (ref && strcmp (ref, message_id)) {
-           g_hash_table_add (hash, ref);
-           last_ref = ref;
-       }
-    }
-
-    /* The return value of this function is used to add a parent
-     * reference to the database.  We should avoid making a message
-     * its own parent, thus the above check.
-     */
-    return talloc_strdup(ctx, last_ref);
-}
-
-notmuch_status_t
-notmuch_database_create (const char *path, notmuch_database_t **database)
-{
-    char *status_string = NULL;
-    notmuch_status_t status;
-
-    status = notmuch_database_create_verbose (path, database,
-                                             &status_string);
-
-    if (status_string) {
-       fputs (status_string, stderr);
-       free (status_string);
-    }
-
-    return status;
-}
-
-notmuch_status_t
-notmuch_database_create_verbose (const char *path,
-                                notmuch_database_t **database,
-                                char **status_string)
-{
-    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
-    notmuch_database_t *notmuch = NULL;
-    char *notmuch_path = NULL;
-    char *message = NULL;
-    struct stat st;
-    int err;
-
-    if (path == NULL) {
-       message = strdup ("Error: Cannot create a database for a NULL path.\n");
-       status = NOTMUCH_STATUS_NULL_POINTER;
-       goto DONE;
-    }
-
-    if (path[0] != '/') {
-       message = strdup ("Error: Database path must be absolute.\n");
-       status = NOTMUCH_STATUS_PATH_ERROR;
-       goto DONE;
-    }
-
-    err = stat (path, &st);
-    if (err) {
-       IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: %s.\n",
-                               path, strerror (errno)));
-       status = NOTMUCH_STATUS_FILE_ERROR;
-       goto DONE;
-    }
-
-    if (! S_ISDIR (st.st_mode)) {
-       IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: "
-                                "Not a directory.\n",
-                                path));
-       status = NOTMUCH_STATUS_FILE_ERROR;
-       goto DONE;
-    }
-
-    notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch");
-
-    err = mkdir (notmuch_path, 0755);
-
-    if (err) {
-       IGNORE_RESULT (asprintf (&message, "Error: Cannot create directory %s: %s.\n",
-                                notmuch_path, strerror (errno)));
-       status = NOTMUCH_STATUS_FILE_ERROR;
-       goto DONE;
-    }
-
-    status = notmuch_database_open_verbose (path,
-                                           NOTMUCH_DATABASE_MODE_READ_WRITE,
-                                           &notmuch, &message);
-    if (status)
-       goto DONE;
-
-    /* Upgrade doesn't add these feature to existing databases, but
-     * new databases have them. */
-    notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES;
-    notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES;
-
-    status = notmuch_database_upgrade (notmuch, NULL, NULL);
-    if (status) {
-       notmuch_database_close(notmuch);
-       notmuch = NULL;
-    }
-
-  DONE:
-    if (notmuch_path)
-       talloc_free (notmuch_path);
-
-    if (message) {
-       if (status_string)
-           *status_string = message;
-       else
-           free (message);
-    }
-    if (database)
-       *database = notmuch;
-    else
-       talloc_free (notmuch);
-    return status;
-}
-
 notmuch_status_t
 _notmuch_database_ensure_writable (notmuch_database_t *notmuch)
 {
-    if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY) {
        _notmuch_database_log (notmuch, "Cannot write to a read-only database.\n");
        return NOTMUCH_STATUS_READ_ONLY_DATABASE;
     }
 
+    if (! notmuch->open) {
+       _notmuch_database_log (notmuch, "Cannot write to a closed database.\n");
+       return NOTMUCH_STATUS_CLOSED_DATABASE;
+    }
+
     return NOTMUCH_STATUS_SUCCESS;
 }
 
@@ -826,296 +494,13 @@ _notmuch_database_new_revision (notmuch_database_t *notmuch)
      * committed revision number until we commit the atomic section.
      */
     if (notmuch->atomic_nesting)
-       notmuch->atomic_dirty = TRUE;
+       notmuch->atomic_dirty = true;
     else
        notmuch->revision = new_revision;
 
     return new_revision;
 }
 
-/* Parse a database features string from the given database version.
- * Returns the feature bit set.
- *
- * For version < 3, this ignores the features string and returns a
- * hard-coded set of features.
- *
- * If there are unrecognized features that are required to open the
- * database in mode (which should be 'r' or 'w'), return a
- * comma-separated list of unrecognized but required features in
- * *incompat_out suitable for presenting to the user.  *incompat_out
- * will be allocated from ctx.
- */
-static _notmuch_features
-_parse_features (const void *ctx, const char *features, unsigned int version,
-                char mode, char **incompat_out)
-{
-    _notmuch_features res = static_cast<_notmuch_features>(0);
-    unsigned int namelen, i;
-    size_t llen = 0;
-    const char *flags;
-
-    /* Prior to database version 3, features were implied by the
-     * version number. */
-    if (version == 0)
-       return NOTMUCH_FEATURES_V0;
-    else if (version == 1)
-       return NOTMUCH_FEATURES_V1;
-    else if (version == 2)
-       return NOTMUCH_FEATURES_V2;
-
-    /* Parse the features string */
-    while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) {
-       flags = strchr (features, '\t');
-       if (! flags || flags > features + llen)
-           continue;
-       namelen = flags - features;
-
-       for (i = 0; i < ARRAY_SIZE (feature_names); ++i) {
-           if (strlen (feature_names[i].name) == namelen &&
-               strncmp (feature_names[i].name, features, namelen) == 0) {
-               res |= feature_names[i].value;
-               break;
-           }
-       }
-
-       if (i == ARRAY_SIZE (feature_names) && incompat_out) {
-           /* Unrecognized feature */
-           const char *have = strchr (flags, mode);
-           if (have && have < features + llen) {
-               /* This feature is required to access this database in
-                * 'mode', but we don't understand it. */
-               if (! *incompat_out)
-                   *incompat_out = talloc_strdup (ctx, "");
-               *incompat_out = talloc_asprintf_append_buffer (
-                   *incompat_out, "%s%.*s", **incompat_out ? ", " : "",
-                   namelen, features);
-           }
-       }
-    }
-
-    return res;
-}
-
-static char *
-_print_features (const void *ctx, unsigned int features)
-{
-    unsigned int i;
-    char *res = talloc_strdup (ctx, "");
-
-    for (i = 0; i < ARRAY_SIZE (feature_names); ++i)
-       if (features & feature_names[i].value)
-           res = talloc_asprintf_append_buffer (
-               res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags);
-
-    return res;
-}
-
-notmuch_status_t
-notmuch_database_open (const char *path,
-                      notmuch_database_mode_t mode,
-                      notmuch_database_t **database)
-{
-    char *status_string = NULL;
-    notmuch_status_t status;
-
-    status = notmuch_database_open_verbose (path, mode, database,
-                                          &status_string);
-
-    if (status_string) {
-       fputs (status_string, stderr);
-       free (status_string);
-    }
-
-    return status;
-}
-
-notmuch_status_t
-notmuch_database_open_verbose (const char *path,
-                              notmuch_database_mode_t mode,
-                              notmuch_database_t **database,
-                              char **status_string)
-{
-    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
-    void *local = talloc_new (NULL);
-    notmuch_database_t *notmuch = NULL;
-    char *notmuch_path, *xapian_path, *incompat_features;
-    char *message = NULL;
-    struct stat st;
-    int err;
-    unsigned int i, version;
-    static int initialized = 0;
-
-    if (path == NULL) {
-       message = strdup ("Error: Cannot open a database for a NULL path.\n");
-       status = NOTMUCH_STATUS_NULL_POINTER;
-       goto DONE;
-    }
-
-    if (path[0] != '/') {
-       message = strdup ("Error: Database path must be absolute.\n");
-       status = NOTMUCH_STATUS_PATH_ERROR;
-       goto DONE;
-    }
-
-    if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
-       message = strdup ("Out of memory\n");
-       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
-       goto DONE;
-    }
-
-    err = stat (notmuch_path, &st);
-    if (err) {
-       IGNORE_RESULT (asprintf (&message, "Error opening database at %s: %s\n",
-                                notmuch_path, strerror (errno)));
-       status = NOTMUCH_STATUS_FILE_ERROR;
-       goto DONE;
-    }
-
-    if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
-       message = strdup ("Out of memory\n");
-       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
-       goto DONE;
-    }
-
-    /* Initialize the GLib type system and threads */
-#if !GLIB_CHECK_VERSION(2, 35, 1)
-    g_type_init ();
-#endif
-
-    /* Initialize gmime */
-    if (! initialized) {
-       g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
-       initialized = 1;
-    }
-
-    notmuch = talloc_zero (NULL, notmuch_database_t);
-    notmuch->exception_reported = FALSE;
-    notmuch->status_string = NULL;
-    notmuch->path = talloc_strdup (notmuch, path);
-
-    if (notmuch->path[strlen (notmuch->path) - 1] == '/')
-       notmuch->path[strlen (notmuch->path) - 1] = '\0';
-
-    notmuch->mode = mode;
-    notmuch->atomic_nesting = 0;
-    notmuch->view = 1;
-    try {
-       string last_thread_id;
-       string last_mod;
-
-       if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
-           notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
-                                                              DB_ACTION);
-       } else {
-           notmuch->xapian_db = new Xapian::Database (xapian_path);
-       }
-
-       /* Check version.  As of database version 3, we represent
-        * changes in terms of features, so assume a version bump
-        * means a dramatically incompatible change. */
-       version = notmuch_database_get_version (notmuch);
-       if (version > NOTMUCH_DATABASE_VERSION) {
-           IGNORE_RESULT (asprintf (&message,
-                     "Error: Notmuch database at %s\n"
-                     "       has a newer database format version (%u) than supported by this\n"
-                     "       version of notmuch (%u).\n",
-                                    notmuch_path, version, NOTMUCH_DATABASE_VERSION));
-           notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
-           notmuch_database_destroy (notmuch);
-           notmuch = NULL;
-           status = NOTMUCH_STATUS_FILE_ERROR;
-           goto DONE;
-       }
-
-       /* Check features. */
-       incompat_features = NULL;
-       notmuch->features = _parse_features (
-           local, notmuch->xapian_db->get_metadata ("features").c_str (),
-           version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',
-           &incompat_features);
-       if (incompat_features) {
-           IGNORE_RESULT (asprintf (&message,
-               "Error: Notmuch database at %s\n"
-               "       requires features (%s)\n"
-               "       not supported by this version of notmuch.\n",
-                                    notmuch_path, incompat_features));
-           notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
-           notmuch_database_destroy (notmuch);
-           notmuch = NULL;
-           status = NOTMUCH_STATUS_FILE_ERROR;
-           goto DONE;
-       }
-
-       notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
-       last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
-       if (last_thread_id.empty ()) {
-           notmuch->last_thread_id = 0;
-       } else {
-           const char *str;
-           char *end;
-
-           str = last_thread_id.c_str ();
-           notmuch->last_thread_id = strtoull (str, &end, 16);
-           if (*end != '\0')
-               INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
-       }
-
-       /* Get current highest revision number. */
-       last_mod = notmuch->xapian_db->get_value_upper_bound (
-           NOTMUCH_VALUE_LAST_MOD);
-       if (last_mod.empty ())
-           notmuch->revision = 0;
-       else
-           notmuch->revision = Xapian::sortable_unserialise (last_mod);
-       notmuch->uuid = talloc_strdup (
-           notmuch, notmuch->xapian_db->get_uuid ().c_str ());
-
-       notmuch->query_parser = new Xapian::QueryParser;
-       notmuch->term_gen = new Xapian::TermGenerator;
-       notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
-       notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
-       notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
-       notmuch->last_mod_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:");
-
-       notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
-       notmuch->query_parser->set_database (*notmuch->xapian_db);
-       notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
-       notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
-       notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
-       notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor);
-       notmuch->query_parser->add_valuerangeprocessor (notmuch->last_mod_range_processor);
-
-       for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
-           const prefix_t *prefix = &prefix_table[i];
-           if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) {
-               _setup_query_field (prefix, notmuch);
-           }
-       }
-    } catch (const Xapian::Error &error) {
-       IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
-                                error.get_msg().c_str()));
-       notmuch_database_destroy (notmuch);
-       notmuch = NULL;
-       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
-    }
-
-  DONE:
-    talloc_free (local);
-
-    if (message) {
-       if (status_string)
-           *status_string = message;
-       else
-           free (message);
-    }
-
-    if (database)
-       *database = notmuch;
-    else
-       talloc_free (notmuch);
-    return status;
-}
-
 notmuch_status_t
 notmuch_database_close (notmuch_database_t *notmuch)
 {
@@ -1124,68 +509,25 @@ notmuch_database_close (notmuch_database_t *notmuch)
     /* Many Xapian objects (and thus notmuch objects) hold references to
      * the database, so merely deleting the database may not suffice to
      * close it.  Thus, we explicitly close it here. */
-    if (notmuch->xapian_db != NULL) {
+    if (notmuch->open) {
        try {
-           /* If there's an outstanding transaction, it's unclear if
-            * closing the Xapian database commits everything up to
-            * that transaction, or may discard committed (but
-            * unflushed) transactions.  To be certain, explicitly
-            * cancel any outstanding transaction before closing. */
-           if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE &&
-               notmuch->atomic_nesting)
-               (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))
-                   ->cancel_transaction ();
-
            /* Close the database.  This implicitly flushes
-            * outstanding changes. */
-           notmuch->xapian_db->close();
+            * outstanding changes. If there is an open (non-flushed)
+            * transaction, ALL pending changes will be discarded */
+           notmuch->xapian_db->close ();
        } catch (const Xapian::Error &error) {
            status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
            if (! notmuch->exception_reported) {
-               _notmuch_database_log (notmuch, "Error: A Xapian exception occurred closing database: %s\n",
-                        error.get_msg().c_str());
+               _notmuch_database_log (notmuch,
+                                      "Error: A Xapian exception occurred closing database: %s\n",
+                                      error.get_msg ().c_str ());
            }
        }
     }
-
-    delete notmuch->term_gen;
-    notmuch->term_gen = NULL;
-    delete notmuch->query_parser;
-    notmuch->query_parser = NULL;
-    delete notmuch->xapian_db;
-    notmuch->xapian_db = NULL;
-    delete notmuch->value_range_processor;
-    notmuch->value_range_processor = NULL;
-    delete notmuch->date_range_processor;
-    notmuch->date_range_processor = NULL;
-    delete notmuch->last_mod_range_processor;
-    notmuch->last_mod_range_processor = NULL;
-
+    notmuch->open = false;
     return status;
 }
 
-notmuch_status_t
-_notmuch_database_reopen (notmuch_database_t *notmuch)
-{
-    if (notmuch->mode != NOTMUCH_DATABASE_MODE_READ_ONLY)
-       return NOTMUCH_STATUS_UNSUPPORTED_OPERATION;
-
-    try {
-       notmuch->xapian_db->reopen ();
-    } catch (const Xapian::Error &error) {
-       if (! notmuch->exception_reported) {
-           _notmuch_database_log (notmuch, "Error: A Xapian exception reopening database: %s\n",
-                                  error.get_msg ().c_str ());
-           notmuch->exception_reported = TRUE;
-       }
-       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
-    }
-
-    notmuch->view++;
-
-    return NOTMUCH_STATUS_SUCCESS;
-}
-
 static int
 unlink_cb (const char *path,
           unused (const struct stat *sb),
@@ -1208,7 +550,9 @@ class NotmuchCompactor : public Xapian::Compactor
 
 public:
     NotmuchCompactor(notmuch_compact_status_cb_t cb, void *closure) :
-       status_cb (cb), status_closure (closure) { }
+       status_cb (cb), status_closure (closure)
+    {
+    }
 
     virtual void
     set_status (const std::string &table, const std::string &status)
@@ -1219,9 +563,9 @@ public:
            return;
 
        if (status.length () == 0)
-           msg = talloc_asprintf (NULL, "compacting table %s", table.c_str());
+           msg = talloc_asprintf (NULL, "compacting table %s", table.c_str ());
        else
-           msg = talloc_asprintf (NULL, "     %s", status.c_str());
+           msg = talloc_asprintf (NULL, "     %s", status.c_str ());
 
        if (msg == NULL) {
            return;
@@ -1249,36 +593,58 @@ notmuch_database_compact (const char *path,
                          notmuch_compact_status_cb_t status_cb,
                          void *closure)
 {
-    void *local;
-    char *notmuch_path, *xapian_path, *compact_xapian_path;
     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
     notmuch_database_t *notmuch = NULL;
-    struct stat statbuf;
-    notmuch_bool_t keep_backup;
     char *message = NULL;
 
-    local = talloc_new (NULL);
-    if (! local)
-       return NOTMUCH_STATUS_OUT_OF_MEMORY;
-
-    ret = notmuch_database_open_verbose (path,
-                                        NOTMUCH_DATABASE_MODE_READ_WRITE,
-                                        &notmuch,
-                                        &message);
+    ret = notmuch_database_open_with_config (path,
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            "",
+                                            NULL,
+                                            &notmuch,
+                                            &message);
     if (ret) {
        if (status_cb) status_cb (message, closure);
-       goto DONE;
+       return ret;
     }
 
-    if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
-       ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
-       goto DONE;
-    }
+    _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path);
 
-    if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
-       ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+    return notmuch_database_compact_db (notmuch,
+                                       backup_path,
+                                       status_cb,
+                                       closure);
+}
+
+notmuch_status_t
+notmuch_database_compact_db (notmuch_database_t *notmuch,
+                            const char *backup_path,
+                            notmuch_compact_status_cb_t status_cb,
+                            void *closure)
+{
+    void *local;
+    const char *xapian_path, *compact_xapian_path;
+    const char *path;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    struct stat statbuf;
+    bool keep_backup;
+    char *message;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
+    if (! path)
+       return NOTMUCH_STATUS_PATH_ERROR;
+
+    local = talloc_new (NULL);
+    if (! local)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    ret = _notmuch_choose_xapian_path (local, path, &xapian_path, &message);
+    if (ret)
        goto DONE;
-    }
 
     if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) {
        ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
@@ -1290,10 +656,9 @@ notmuch_database_compact (const char *path,
            ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
            goto DONE;
        }
-       keep_backup = FALSE;
-    }
-    else {
-       keep_backup = TRUE;
+       keep_backup = false;
+    } else {
+       keep_backup = true;
     }
 
     if (stat (backup_path, &statbuf) != -1) {
@@ -1303,7 +668,7 @@ notmuch_database_compact (const char *path,
     }
     if (errno != ENOENT) {
        _notmuch_database_log (notmuch, "Unknown error while stat()ing path: %s\n",
-                strerror (errno));
+                              strerror (errno));
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
     }
@@ -1316,27 +681,24 @@ notmuch_database_compact (const char *path,
 
     try {
        NotmuchCompactor compactor (status_cb, closure);
-
-       compactor.set_renumber (false);
-       compactor.add_source (xapian_path);
-       compactor.set_destdir (compact_xapian_path);
-       compactor.compact ();
+       notmuch->xapian_db->compact (compact_xapian_path, Xapian::DBCOMPACT_NO_RENUMBER, 0,
+                                    compactor);
     } catch (const Xapian::Error &error) {
-       _notmuch_database_log (notmuch, "Error while compacting: %s\n", error.get_msg().c_str());
+       _notmuch_database_log (notmuch, "Error while compacting: %s\n", error.get_msg ().c_str ());
        ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
        goto DONE;
     }
 
     if (rename (xapian_path, backup_path)) {
        _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
-                xapian_path, backup_path, strerror (errno));
+                              xapian_path, backup_path, strerror (errno));
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
     }
 
     if (rename (compact_xapian_path, xapian_path)) {
        _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
-                compact_xapian_path, xapian_path, strerror (errno));
+                              compact_xapian_path, xapian_path, strerror (errno));
        ret = NOTMUCH_STATUS_FILE_ERROR;
        goto DONE;
     }
@@ -1344,7 +706,7 @@ notmuch_database_compact (const char *path,
     if (! keep_backup) {
        if (rmtree (backup_path)) {
            _notmuch_database_log (notmuch, "Error removing old database %s: %s\n",
-                    backup_path, strerror (errno));
+                                  backup_path, strerror (errno));
            ret = NOTMUCH_STATUS_FILE_ERROR;
            goto DONE;
        }
@@ -1374,8 +736,33 @@ notmuch_status_t
 notmuch_database_destroy (notmuch_database_t *notmuch)
 {
     notmuch_status_t status;
+    const char *talloc_report;
+
+    talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
+    if (talloc_report && strcmp (talloc_report, "") != 0) {
+       FILE *report = fopen (talloc_report, "a");
+       if (report) {
+           talloc_report_full (notmuch, report);
+       }
+    }
 
     status = notmuch_database_close (notmuch);
+
+    delete notmuch->term_gen;
+    notmuch->term_gen = NULL;
+    delete notmuch->query_parser;
+    notmuch->query_parser = NULL;
+    delete notmuch->xapian_db;
+    notmuch->xapian_db = NULL;
+    delete notmuch->value_range_processor;
+    notmuch->value_range_processor = NULL;
+    delete notmuch->date_range_processor;
+    notmuch->date_range_processor = NULL;
+    delete notmuch->last_mod_range_processor;
+    notmuch->last_mod_range_processor = NULL;
+    delete notmuch->stemmer;
+    notmuch->stemmer = NULL;
+
     talloc_free (notmuch);
 
     return status;
@@ -1384,7 +771,7 @@ notmuch_database_destroy (notmuch_database_t *notmuch)
 const char *
 notmuch_database_get_path (notmuch_database_t *notmuch)
 {
-    return notmuch->path;
+    return notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
 }
 
 unsigned int
@@ -1395,7 +782,13 @@ notmuch_database_get_version (notmuch_database_t *notmuch)
     const char *str;
     char *end;
 
-    version_string = notmuch->xapian_db->get_metadata ("version");
+    try {
+       version_string = notmuch->xapian_db->get_metadata ("version");
+    } catch (const Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (notmuch, error);
+       return 0;
+    }
+
     if (version_string.empty ())
        return 0;
 
@@ -1413,9 +806,17 @@ notmuch_database_get_version (notmuch_database_t *notmuch)
 notmuch_bool_t
 notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
 {
-    return notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE &&
-       ((NOTMUCH_FEATURES_CURRENT & ~notmuch->features) ||
-        (notmuch_database_get_version (notmuch) < NOTMUCH_DATABASE_VERSION));
+    unsigned int version;
+
+    if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE)
+       return FALSE;
+
+    if (NOTMUCH_FEATURES_CURRENT & ~notmuch->features)
+       return TRUE;
+
+    version = notmuch_database_get_version (notmuch);
+
+    return (version > 0 && version < NOTMUCH_DATABASE_VERSION);
 }
 
 static volatile sig_atomic_t do_progress_notify = 0;
@@ -1440,8 +841,8 @@ handle_sigalrm (unused (int signal))
  */
 notmuch_status_t
 notmuch_database_upgrade (notmuch_database_t *notmuch,
-                         void (*progress_notify) (void *closure,
-                                                  double progress),
+                         void (*progress_notify)(void *closure,
+                                                 double progress),
                          void *closure)
 {
     void *local = talloc_new (NULL);
@@ -1449,18 +850,17 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
     Xapian::WritableDatabase *db;
     struct sigaction action;
     struct itimerval timerval;
-    notmuch_bool_t timer_is_active = FALSE;
+    bool timer_is_active = false;
     enum _notmuch_features target_features, new_features;
     notmuch_status_t status;
     notmuch_private_status_t private_status;
     notmuch_query_t *query = NULL;
     unsigned int count = 0, total = 0;
 
-    status = _notmuch_database_ensure_writable (notmuch);
-    if (status)
-       return status;
+    if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE)
+       return NOTMUCH_STATUS_READ_ONLY_DATABASE;
 
-    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+    db = notmuch->writable_xapian_db;
 
     target_features = notmuch->features | NOTMUCH_FEATURES_CURRENT;
     new_features = NOTMUCH_FEATURES_CURRENT & ~notmuch->features;
@@ -1483,7 +883,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        timerval.it_value.tv_usec = 0;
        setitimer (ITIMER_REAL, &timerval, NULL);
 
-       timer_is_active = TRUE;
+       timer_is_active = true;
     }
 
     /* Figure out how much total work we need to do. */
@@ -1493,7 +893,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        query = notmuch_query_create (notmuch, "");
        unsigned msg_count;
 
-       status = notmuch_query_count_messages_st (query, &msg_count);
+       status = notmuch_query_count_messages (query, &msg_count);
        if (status)
            goto DONE;
 
@@ -1531,13 +931,12 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 
        query = notmuch_query_create (notmuch, "");
 
-       status = notmuch_query_search_messages_st (query, &messages);
+       status = notmuch_query_search_messages (query, &messages);
        if (status)
            goto DONE;
        for (;
             notmuch_messages_valid (messages);
-            notmuch_messages_move_to_next (messages))
-       {
+            notmuch_messages_move_to_next (messages)) {
            if (do_progress_notify) {
                progress_notify (closure, (double) count / total);
                do_progress_notify = 0;
@@ -1594,8 +993,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 
        for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
             t != t_end;
-            t++)
-       {
+            t++) {
            Xapian::PostingIterator p, p_end;
            std::string term = *t;
 
@@ -1603,8 +1001,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 
            for (p = notmuch->xapian_db->postlist_begin (term);
                 p != p_end;
-                p++)
-           {
+                p++) {
                Xapian::Document document;
                time_t mtime;
                notmuch_directory_t *directory;
@@ -1618,8 +1015,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
                mtime = Xapian::sortable_unserialise (
                    document.get_value (NOTMUCH_VALUE_TIMESTAMP));
 
-               directory = _notmuch_directory_create (notmuch, term.c_str() + 10,
-                                                      NOTMUCH_FIND_CREATE, &status);
+               directory = _notmuch_directory_find_or_create (notmuch, term.c_str () + 10,
+                                                              NOTMUCH_FIND_CREATE, &status);
                notmuch_directory_set_mtime (directory, mtime);
                notmuch_directory_destroy (directory);
 
@@ -1667,8 +1064,9 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 
            if (private_status) {
                _notmuch_database_log (notmuch,
-                        "Upgrade failed while creating ghost messages.\n");
-               status = COERCE_STATUS (private_status, "Unexpected status from _notmuch_message_initialize_ghost");
+                                      "Upgrade failed while creating ghost messages.\n");
+               status = COERCE_STATUS (private_status,
+                                       "Unexpected status from _notmuch_message_initialize_ghost");
                goto DONE;
            }
 
@@ -1680,10 +1078,10 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
     }
 
     status = NOTMUCH_STATUS_SUCCESS;
-    db->set_metadata ("features", _print_features (local, notmuch->features));
+    db->set_metadata ("features", _notmuch_database_print_features (local, notmuch->features));
     db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
 
- DONE:
 DONE:
     if (status == NOTMUCH_STATUS_SUCCESS)
        db->commit_transaction ();
     else
@@ -1712,7 +1110,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
 notmuch_status_t
 notmuch_database_begin_atomic (notmuch_database_t *notmuch)
 {
-    if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY ||
        notmuch->atomic_nesting > 0)
        goto DONE;
 
@@ -1720,15 +1118,15 @@ notmuch_database_begin_atomic (notmuch_database_t *notmuch)
        return NOTMUCH_STATUS_UPGRADE_REQUIRED;
 
     try {
-       (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
+       notmuch->writable_xapian_db->begin_transaction (false);
     } catch (const Xapian::Error &error) {
        _notmuch_database_log (notmuch, "A Xapian exception occurred beginning transaction: %s.\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
        return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
 
-DONE:
+  DONE:
     notmuch->atomic_nesting++;
     return NOTMUCH_STATUS_SUCCESS;
 }
@@ -1741,40 +1139,48 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch)
     if (notmuch->atomic_nesting == 0)
        return NOTMUCH_STATUS_UNBALANCED_ATOMIC;
 
-    if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY ||
        notmuch->atomic_nesting > 1)
        goto DONE;
 
-    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+    db = notmuch->writable_xapian_db;
     try {
        db->commit_transaction ();
-
-       /* This is a hack for testing.  Xapian never flushes on a
-        * non-flushed commit, even if the flush threshold is 1.
-        * However, we rely on flushing to test atomicity. */
+       notmuch->transaction_count++;
+
+       /* Xapian never flushes on a non-flushed commit, even if the
+        * flush threshold is 1.  However, we rely on flushing to test
+        * atomicity. On the other hand, we can't straight replace
+        * XAPIAN_FLUSH_THRESHOLD with our autocommit counter, because
+        * the former also applies outside notmuch atomic
+        * commits. Hence the follow complicated  test */
        const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
-       if (thresh && atoi (thresh) == 1)
+       if ((notmuch->transaction_threshold > 0 &&
+            notmuch->transaction_count >= notmuch->transaction_threshold) ||
+           (thresh && atoi (thresh) == 1)) {
            db->commit ();
+           notmuch->transaction_count = 0;
+       }
     } catch (const Xapian::Error &error) {
        _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
        return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
 
     if (notmuch->atomic_dirty) {
        ++notmuch->revision;
-       notmuch->atomic_dirty = FALSE;
+       notmuch->atomic_dirty = false;
     }
 
-DONE:
+  DONE:
     notmuch->atomic_nesting--;
     return NOTMUCH_STATUS_SUCCESS;
 }
 
 unsigned long
 notmuch_database_get_revision (notmuch_database_t *notmuch,
-                               const char **uuid)
+                              const char **uuid)
 {
     if (uuid)
        *uuid = notmuch->uuid;
@@ -1891,8 +1297,8 @@ _notmuch_database_find_directory_id (notmuch_database_t *notmuch,
        return NOTMUCH_STATUS_SUCCESS;
     }
 
-    directory = _notmuch_directory_create (notmuch, path, flags, &status);
-    if (status || !directory) {
+    directory = _notmuch_directory_find_or_create (notmuch, path, flags, &status);
+    if (status || ! directory) {
        *directory_id = -1;
        return status;
     }
@@ -1946,7 +1352,7 @@ _notmuch_database_filename_to_direntry (void *ctx,
 
     status = _notmuch_database_find_directory_id (notmuch, directory, flags,
                                                  &directory_id);
-    if (status || directory_id == (unsigned int)-1) {
+    if (status || directory_id == (unsigned int) -1) {
        *direntry = NULL;
        return status;
     }
@@ -1970,17 +1376,16 @@ _notmuch_database_relative_path (notmuch_database_t *notmuch,
     const char *db_path, *relative;
     unsigned int db_path_len;
 
-    db_path = notmuch_database_get_path (notmuch);
+    db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
     db_path_len = strlen (db_path);
 
     relative = path;
 
     if (*relative == '/') {
-       while (*relative == '/' && *(relative+1) == '/')
+       while (*relative == '/' && *(relative + 1) == '/')
            relative++;
 
-       if (strncmp (relative, db_path, db_path_len) == 0)
-       {
+       if (strncmp (relative, db_path, db_path_len) == 0) {
            relative += db_path_len;
            while (*relative == '/')
                relative++;
@@ -2002,12 +1407,12 @@ notmuch_database_get_directory (notmuch_database_t *notmuch,
     *directory = NULL;
 
     try {
-       *directory = _notmuch_directory_create (notmuch, path,
-                                               NOTMUCH_FIND_LOOKUP, &status);
+       *directory = _notmuch_directory_find_or_create (notmuch, path,
+                                                       NOTMUCH_FIND_LOOKUP, &status);
     } catch (const Xapian::Error &error) {
        _notmuch_database_log (notmuch, "A Xapian exception occurred getting directory: %s.\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
        status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
     return status;
@@ -2038,583 +1443,6 @@ _notmuch_database_generate_doc_id (notmuch_database_t *notmuch)
     return notmuch->last_doc_id;
 }
 
-static const char *
-_notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
-{
-    /* 16 bytes (+ terminator) for hexadecimal representation of
-     * a 64-bit integer. */
-    static char thread_id[17];
-    Xapian::WritableDatabase *db;
-
-    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-
-    notmuch->last_thread_id++;
-
-    sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
-
-    db->set_metadata ("last_thread_id", thread_id);
-
-    return thread_id;
-}
-
-static char *
-_get_metadata_thread_id_key (void *ctx, const char *message_id)
-{
-    if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
-       message_id = _notmuch_message_id_compressed (ctx, message_id);
-
-    return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
-                           message_id);
-}
-
-static notmuch_status_t
-_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
-                                     void *ctx,
-                                     const char *message_id,
-                                     const char **thread_id_ret);
-
-/* Find the thread ID to which the message with 'message_id' belongs.
- *
- * Note: 'thread_id_ret' must not be NULL!
- * On success '*thread_id_ret' is set to a newly talloced string belonging to
- * 'ctx'.
- *
- * Note: If there is no message in the database with the given
- * 'message_id' then a new thread_id will be allocated for this
- * message ID and stored in the database metadata so that the
- * thread ID can be looked up if the message is added to the database
- * later.
- */
-static notmuch_status_t
-_resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
-                                 void *ctx,
-                                 const char *message_id,
-                                 const char **thread_id_ret)
-{
-    notmuch_private_status_t status;
-    notmuch_message_t *message;
-
-    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS))
-       return _resolve_message_id_to_thread_id_old (notmuch, ctx, message_id,
-                                                    thread_id_ret);
-
-    /* Look for this message (regular or ghost) */
-    message = _notmuch_message_create_for_message_id (
-       notmuch, message_id, &status);
-    if (status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
-       /* Message exists */
-       *thread_id_ret = talloc_steal (
-           ctx, notmuch_message_get_thread_id (message));
-    } else if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
-       /* Message did not exist.  Give it a fresh thread ID and
-        * populate this message as a ghost message. */
-       *thread_id_ret = talloc_strdup (
-           ctx, _notmuch_database_generate_thread_id (notmuch));
-       if (! *thread_id_ret) {
-           status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
-       } else {
-           status = _notmuch_message_initialize_ghost (message, *thread_id_ret);
-           if (status == 0)
-               /* Commit the new ghost message */
-               _notmuch_message_sync (message);
-       }
-    } else {
-       /* Create failed. Fall through. */
-    }
-
-    notmuch_message_destroy (message);
-
-    return COERCE_STATUS (status, "Error creating ghost message");
-}
-
-/* Pre-ghost messages _resolve_message_id_to_thread_id */
-static notmuch_status_t
-_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
-                                     void *ctx,
-                                     const char *message_id,
-                                     const char **thread_id_ret)
-{
-    notmuch_status_t status;
-    notmuch_message_t *message;
-    string thread_id_string;
-    char *metadata_key;
-    Xapian::WritableDatabase *db;
-
-    status = notmuch_database_find_message (notmuch, message_id, &message);
-
-    if (status)
-       return status;
-
-    if (message) {
-       *thread_id_ret = talloc_steal (ctx,
-                                      notmuch_message_get_thread_id (message));
-
-       notmuch_message_destroy (message);
-
-       return NOTMUCH_STATUS_SUCCESS;
-    }
-
-    /* Message has not been seen yet.
-     *
-     * We may have seen a reference to it already, in which case, we
-     * can return the thread ID stored in the metadata. Otherwise, we
-     * generate a new thread ID and store it there.
-     */
-    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
-    thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
-
-    if (thread_id_string.empty()) {
-       *thread_id_ret = talloc_strdup (ctx,
-                                       _notmuch_database_generate_thread_id (notmuch));
-       db->set_metadata (metadata_key, *thread_id_ret);
-    } else {
-       *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str());
-    }
-
-    talloc_free (metadata_key);
-
-    return NOTMUCH_STATUS_SUCCESS;
-}
-
-static notmuch_status_t
-_merge_threads (notmuch_database_t *notmuch,
-               const char *winner_thread_id,
-               const char *loser_thread_id)
-{
-    Xapian::PostingIterator loser, loser_end;
-    notmuch_message_t *message = NULL;
-    notmuch_private_status_t private_status;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
-
-    find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
-
-    for ( ; loser != loser_end; loser++) {
-       message = _notmuch_message_create (notmuch, notmuch,
-                                          *loser, &private_status);
-       if (message == NULL) {
-           ret = COERCE_STATUS (private_status,
-                                "Cannot find document for doc_id from query");
-           goto DONE;
-       }
-
-       _notmuch_message_remove_term (message, "thread", loser_thread_id);
-       _notmuch_message_add_term (message, "thread", winner_thread_id);
-       _notmuch_message_sync (message);
-
-       notmuch_message_destroy (message);
-       message = NULL;
-    }
-
-  DONE:
-    if (message)
-       notmuch_message_destroy (message);
-
-    return ret;
-}
-
-static void
-_my_talloc_free_for_g_hash (void *ptr)
-{
-    talloc_free (ptr);
-}
-
-static notmuch_status_t
-_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
-                                          notmuch_message_t *message,
-                                          notmuch_message_file_t *message_file,
-                                          const char **thread_id)
-{
-    GHashTable *parents = NULL;
-    const char *refs, *in_reply_to, *in_reply_to_message_id;
-    const char *last_ref_message_id, *this_message_id;
-    GList *l, *keys = NULL;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
-
-    parents = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                    _my_talloc_free_for_g_hash, NULL);
-    this_message_id = notmuch_message_get_message_id (message);
-
-    refs = _notmuch_message_file_get_header (message_file, "references");
-    last_ref_message_id = parse_references (message,
-                                           this_message_id,
-                                           parents, refs);
-
-    in_reply_to = _notmuch_message_file_get_header (message_file, "in-reply-to");
-    in_reply_to_message_id = parse_references (message,
-                                              this_message_id,
-                                              parents, in_reply_to);
-
-    /* For the parent of this message, use the last message ID of the
-     * References header, if available.  If not, fall back to the
-     * first message ID in the In-Reply-To header. */
-    if (last_ref_message_id) {
-       _notmuch_message_add_term (message, "replyto",
-                                  last_ref_message_id);
-    } else if (in_reply_to_message_id) {
-       _notmuch_message_add_term (message, "replyto",
-                            in_reply_to_message_id);
-    }
-
-    keys = g_hash_table_get_keys (parents);
-    for (l = keys; l; l = l->next) {
-       char *parent_message_id;
-       const char *parent_thread_id = NULL;
-
-       parent_message_id = (char *) l->data;
-
-       _notmuch_message_add_term (message, "reference",
-                                  parent_message_id);
-
-       ret = _resolve_message_id_to_thread_id (notmuch,
-                                               message,
-                                               parent_message_id,
-                                               &parent_thread_id);
-       if (ret)
-           goto DONE;
-
-       if (*thread_id == NULL) {
-           *thread_id = talloc_strdup (message, parent_thread_id);
-           _notmuch_message_add_term (message, "thread", *thread_id);
-       } else if (strcmp (*thread_id, parent_thread_id)) {
-           ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
-           if (ret)
-               goto DONE;
-       }
-    }
-
-  DONE:
-    if (keys)
-       g_list_free (keys);
-    if (parents)
-       g_hash_table_unref (parents);
-
-    return ret;
-}
-
-static notmuch_status_t
-_notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
-                                           notmuch_message_t *message,
-                                           const char **thread_id)
-{
-    const char *message_id = notmuch_message_get_message_id (message);
-    Xapian::PostingIterator child, children_end;
-    notmuch_message_t *child_message = NULL;
-    const char *child_thread_id;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
-    notmuch_private_status_t private_status;
-
-    find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
-
-    for ( ; child != children_end; child++) {
-
-       child_message = _notmuch_message_create (message, notmuch,
-                                                *child, &private_status);
-       if (child_message == NULL) {
-           ret = COERCE_STATUS (private_status,
-                                "Cannot find document for doc_id from query");
-           goto DONE;
-       }
-
-       child_thread_id = notmuch_message_get_thread_id (child_message);
-       if (*thread_id == NULL) {
-           *thread_id = talloc_strdup (message, child_thread_id);
-           _notmuch_message_add_term (message, "thread", *thread_id);
-       } else if (strcmp (*thread_id, child_thread_id)) {
-           _notmuch_message_remove_term (child_message, "reference",
-                                         message_id);
-           _notmuch_message_sync (child_message);
-           ret = _merge_threads (notmuch, *thread_id, child_thread_id);
-           if (ret)
-               goto DONE;
-       }
-
-       notmuch_message_destroy (child_message);
-       child_message = NULL;
-    }
-
-  DONE:
-    if (child_message)
-       notmuch_message_destroy (child_message);
-
-    return ret;
-}
-
-/* Fetch and clear the stored thread_id for message, or NULL if none. */
-static char *
-_consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch,
-                            notmuch_message_t *message)
-{
-    const char *message_id;
-    string stored_id;
-    char *metadata_key;
-
-    message_id = notmuch_message_get_message_id (message);
-    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
-
-    /* Check if we have already seen related messages to this one.
-     * If we have then use the thread_id that we stored at that time.
-     */
-    stored_id = notmuch->xapian_db->get_metadata (metadata_key);
-    if (stored_id.empty ()) {
-       return NULL;
-    } else {
-       Xapian::WritableDatabase *db;
-
-       db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-
-       /* Clear the metadata for this message ID. We don't need it
-        * anymore. */
-       db->set_metadata (metadata_key, "");
-
-       return talloc_strdup (ctx, stored_id.c_str ());
-    }
-}
-
-/* Given a blank or ghost 'message' and its corresponding
- * 'message_file' link it to existing threads in the database.
- *
- * First, if is_ghost, this retrieves the thread ID already stored in
- * the message (which will be the case if a message was previously
- * added that referenced this one).  If the message is blank
- * (!is_ghost), it doesn't have a thread ID yet (we'll generate one
- * later in this function).  If the database does not support ghost
- * messages, this checks for a thread ID stored in database metadata
- * for this message ID.
- *
- * Second, we look at 'message_file' and its link-relevant headers
- * (References and In-Reply-To) for message IDs.
- *
- * Finally, we look in the database for existing message that
- * reference 'message'.
- *
- * In all cases, we assign to the current message the first thread ID
- * found. We will also merge any existing, distinct threads where this
- * message belongs to both, (which is not uncommon when messages are
- * processed out of order).
- *
- * Finally, if no thread ID has been found through referenced messages, we
- * call _notmuch_message_generate_thread_id to generate a new thread
- * ID. This should only happen for new, top-level messages, (no
- * References or In-Reply-To header in this message, and no previously
- * added message refers to this message).
- */
-static notmuch_status_t
-_notmuch_database_link_message (notmuch_database_t *notmuch,
-                               notmuch_message_t *message,
-                               notmuch_message_file_t *message_file,
-                               notmuch_bool_t is_ghost)
-{
-    void *local = talloc_new (NULL);
-    notmuch_status_t status;
-    const char *thread_id = NULL;
-
-    /* Check if the message already had a thread ID */
-    if (notmuch->features & NOTMUCH_FEATURE_GHOSTS) {
-       if (is_ghost)
-           thread_id = notmuch_message_get_thread_id (message);
-    } else {
-       thread_id = _consume_metadata_thread_id (local, notmuch, message);
-       if (thread_id)
-           _notmuch_message_add_term (message, "thread", thread_id);
-    }
-
-    status = _notmuch_database_link_message_to_parents (notmuch, message,
-                                                       message_file,
-                                                       &thread_id);
-    if (status)
-       goto DONE;
-
-    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS)) {
-       /* In general, it shouldn't be necessary to link children,
-        * since the earlier indexing of those children will have
-        * stored a thread ID for the missing parent.  However, prior
-        * to ghost messages, these stored thread IDs were NOT
-        * rewritten during thread merging (and there was no
-        * performant way to do so), so if indexed children were
-        * pulled into a different thread ID by a merge, it was
-        * necessary to pull them *back* into the stored thread ID of
-        * the parent.  With ghost messages, we just rewrite the
-        * stored thread IDs during merging, so this workaround isn't
-        * necessary. */
-       status = _notmuch_database_link_message_to_children (notmuch, message,
-                                                            &thread_id);
-       if (status)
-           goto DONE;
-    }
-
-    /* If not part of any existing thread, generate a new thread ID. */
-    if (thread_id == NULL) {
-       thread_id = _notmuch_database_generate_thread_id (notmuch);
-
-       _notmuch_message_add_term (message, "thread", thread_id);
-    }
-
- DONE:
-    talloc_free (local);
-
-    return status;
-}
-
-notmuch_status_t
-notmuch_database_add_message (notmuch_database_t *notmuch,
-                             const char *filename,
-                             notmuch_message_t **message_ret)
-{
-    notmuch_message_file_t *message_file;
-    notmuch_message_t *message = NULL;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
-    notmuch_private_status_t private_status;
-    notmuch_bool_t is_ghost = false;
-
-    const char *date, *header;
-    const char *from, *to, *subject;
-    char *message_id = NULL;
-
-    if (message_ret)
-       *message_ret = NULL;
-
-    ret = _notmuch_database_ensure_writable (notmuch);
-    if (ret)
-       return ret;
-
-    message_file = _notmuch_message_file_open (notmuch, filename);
-    if (message_file == NULL)
-       return NOTMUCH_STATUS_FILE_ERROR;
-
-    /* Adding a message may change many documents.  Do this all
-     * atomically. */
-    ret = notmuch_database_begin_atomic (notmuch);
-    if (ret)
-       goto DONE;
-
-    /* Parse message up front to get better error status. */
-    ret = _notmuch_message_file_parse (message_file);
-    if (ret)
-       goto DONE;
-
-    /* Before we do any real work, (especially before doing a
-     * potential SHA-1 computation on the entire file's contents),
-     * let's make sure that what we're looking at looks like an
-     * actual email message.
-     */
-    from = _notmuch_message_file_get_header (message_file, "from");
-    subject = _notmuch_message_file_get_header (message_file, "subject");
-    to = _notmuch_message_file_get_header (message_file, "to");
-
-    if ((from == NULL || *from == '\0') &&
-       (subject == NULL || *subject == '\0') &&
-       (to == NULL || *to == '\0')) {
-       ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
-       goto DONE;
-    }
-
-    /* Now that we're sure it's mail, the first order of business
-     * is to find a message ID (or else create one ourselves).
-     */
-    header = _notmuch_message_file_get_header (message_file, "message-id");
-    if (header && *header != '\0') {
-       message_id = _parse_message_id (message_file, header, NULL);
-
-       /* So the header value isn't RFC-compliant, but it's
-        * better than no message-id at all.
-        */
-       if (message_id == NULL)
-           message_id = talloc_strdup (message_file, header);
-    }
-
-    if (message_id == NULL ) {
-       /* No message-id at all, let's generate one by taking a
-        * hash over the file's contents.
-        */
-       char *sha1 = _notmuch_sha1_of_file (filename);
-
-       /* If that failed too, something is really wrong. Give up. */
-       if (sha1 == NULL) {
-           ret = NOTMUCH_STATUS_FILE_ERROR;
-           goto DONE;
-       }
-
-       message_id = talloc_asprintf (message_file, "notmuch-sha1-%s", sha1);
-       free (sha1);
-    }
-
-    try {
-       /* Now that we have a message ID, we get a message object,
-        * (which may or may not reference an existing document in the
-        * database). */
-
-       message = _notmuch_message_create_for_message_id (notmuch,
-                                                         message_id,
-                                                         &private_status);
-
-       talloc_free (message_id);
-
-       if (message == NULL) {
-           ret = COERCE_STATUS (private_status,
-                                "Unexpected status value from _notmuch_message_create_for_message_id");
-           goto DONE;
-       }
-
-       _notmuch_message_add_filename (message, filename);
-
-       /* Is this a newly created message object or a ghost
-        * message?  We have to be slightly careful: if this is a
-        * blank message, it's not safe to call
-        * notmuch_message_get_flag yet. */
-       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND ||
-           (is_ghost = notmuch_message_get_flag (
-               message, NOTMUCH_MESSAGE_FLAG_GHOST))) {
-           _notmuch_message_add_term (message, "type", "mail");
-           if (is_ghost)
-               /* Convert ghost message to a regular message */
-               _notmuch_message_remove_term (message, "type", "ghost");
-
-           ret = _notmuch_database_link_message (notmuch, message,
-                                                 message_file, is_ghost);
-           if (ret)
-               goto DONE;
-
-           date = _notmuch_message_file_get_header (message_file, "date");
-           _notmuch_message_set_header_values (message, date, from, subject);
-
-           ret = _notmuch_message_index_file (message, message_file);
-           if (ret)
-               goto DONE;
-       } else {
-           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
-       }
-
-       _notmuch_message_sync (message);
-    } catch (const Xapian::Error &error) {
-       _notmuch_database_log (notmuch, "A Xapian exception occurred adding message: %s.\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
-       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
-       goto DONE;
-    }
-
-  DONE:
-    if (message) {
-       if ((ret == NOTMUCH_STATUS_SUCCESS ||
-            ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
-           *message_ret = message;
-       else
-           notmuch_message_destroy (message);
-    }
-
-    if (message_file)
-       _notmuch_message_file_close (message_file);
-
-    ret2 = notmuch_database_end_atomic (notmuch);
-    if ((ret == NOTMUCH_STATUS_SUCCESS ||
-        ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
-       ret2 != NOTMUCH_STATUS_SUCCESS)
-       ret = ret2;
-
-    return ret;
-}
-
 notmuch_status_t
 notmuch_database_remove_message (notmuch_database_t *notmuch,
                                 const char *filename)
@@ -2626,13 +1454,15 @@ notmuch_database_remove_message (notmuch_database_t *notmuch,
                                                        &message);
 
     if (status == NOTMUCH_STATUS_SUCCESS && message) {
+       if (notmuch_message_count_files (message) > 1) {
            status = _notmuch_message_remove_filename (message, filename);
-           if (status == NOTMUCH_STATUS_SUCCESS)
-               _notmuch_message_delete (message);
-           else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
-               _notmuch_message_sync (message);
+       }
+       if (status == NOTMUCH_STATUS_SUCCESS)
+           status = _notmuch_message_delete (message);
+       else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+           _notmuch_message_sync (message);
 
-           notmuch_message_destroy (message);
+       notmuch_message_destroy (message);
     }
 
     return status;
@@ -2663,7 +1493,7 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
     try {
        status = _notmuch_database_filename_to_direntry (
            local, notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
-       if (status || !direntry)
+       if (status || ! direntry)
            goto DONE;
 
        term = talloc_asprintf (local, "%s%s", prefix, direntry);
@@ -2679,9 +1509,10 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
                status = NOTMUCH_STATUS_OUT_OF_MEMORY;
        }
     } catch (const Xapian::Error &error) {
-       _notmuch_database_log (notmuch, "Error: A Xapian exception occurred finding message by filename: %s\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
+       _notmuch_database_log (notmuch,
+                              "Error: A Xapian exception occurred finding message by filename: %s\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
        status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
 
@@ -2725,16 +1556,16 @@ notmuch_database_get_all_tags (notmuch_database_t *db)
     notmuch_string_list_t *tags;
 
     try {
-       i = db->xapian_db->allterms_begin();
-       end = db->xapian_db->allterms_end();
+       i = db->xapian_db->allterms_begin ();
+       end = db->xapian_db->allterms_end ();
        tags = _notmuch_database_get_terms_with_prefix (db, i, end,
                                                        _find_prefix ("tag"));
        _notmuch_string_list_sort (tags);
        return _notmuch_tags_create (db, tags);
     } catch (const Xapian::Error &error) {
        _notmuch_database_log (db, "A Xapian exception occurred getting tags: %s.\n",
-                error.get_msg().c_str());
-       db->exception_reported = TRUE;
+                              error.get_msg ().c_str ());
+       db->exception_reported = true;
        return NULL;
     }
 }
@@ -2744,3 +1575,15 @@ notmuch_database_status_string (const notmuch_database_t *notmuch)
 {
     return notmuch->status_string;
 }
+
+bool
+_notmuch_database_indexable_as_text (notmuch_database_t *notmuch, const char *mime_string)
+{
+    for (size_t i = 0; i < notmuch->index_as_text_length; i++) {
+       if (regexec (&notmuch->index_as_text[i], mime_string, 0, NULL, 0) == 0) {
+           return true;
+       }
+    }
+
+    return false;
+}