X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=lib%2Fdatabase.cc;h=f0bfe5667fba4736996583830047d7529cd390fc;hp=4173b68162ac8be100ece4ac7497f56bee3ada18;hb=af8903df34647d9456e8f3fe26a164959969b451;hpb=84d3b15d251623cbb66e5eca7ddb8d61aa596d33 diff --git a/lib/database.cc b/lib/database.cc index 4173b681..f0bfe566 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -13,13 +13,14 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . + * along with this program. If not, see https://www.gnu.org/licenses/ . * * Author: Carl Worth */ #include "database-private.h" #include "parse-time-vrp.h" +#include "query-fp.h" #include "string-util.h" #include @@ -48,6 +49,12 @@ typedef struct { #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 + /* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION): * * We currently have three different types of documents (mail, ghost, @@ -90,6 +97,9 @@ typedef struct { * STRING is the name of a file within that * directory for this mail message. * + * property: Has a property with key=value + * FIXME: if no = is present, should match on any value + * * A mail document also has four values: * * TIMESTAMP: The time_t value corresponding to the message's @@ -101,6 +111,9 @@ typedef struct { * * SUBJECT: The value of the "Subject" header * + * 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 @@ -181,6 +194,14 @@ typedef struct { * generated is 1 and the value will be * incremented for each thread ID. * + * C* metadata keys starting with C indicate + * configuration data. It can be managed with the + * n_database_*config* API. There is a convention + * of hierarchical keys separated by '.' (e.g. + * query.notmuch stores the value for the named + * query 'notmuch'), but it is not enforced by the + * API. + * * Obsolete metadata * ----------------- * @@ -213,7 +234,7 @@ typedef struct { /* With these prefix values we follow the conventions published here: * - * http://xapian.org/docs/omega/termprefixes.html + * 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' @@ -241,11 +262,12 @@ static prefix_t BOOLEAN_PREFIX_EXTERNAL[] = { { "is", "K" }, { "id", "Q" }, { "path", "P" }, + { "property", "XPROPERTY" }, /* - * Without the ":", since this is a multi-letter prefix, Xapian - * will add a colon itself if the first letter of the path is - * upper-case ASCII. Including the ":" forces there to always be a - * colon, which keeps our own logic simpler. + * 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:" }, }; @@ -310,6 +332,8 @@ static const struct { * them. */ { NOTMUCH_FEATURE_INDEXED_MIMETYPES, "indexed MIME types", "w"}, + { NOTMUCH_FEATURE_LAST_MOD, + "modification tracking", "w"}, }; const char * @@ -342,12 +366,47 @@ notmuch_status_to_string (notmuch_status_t status) return "Unsupported operation"; case NOTMUCH_STATUS_UPGRADE_REQUIRED: return "Operation requires a database upgrade"; + case NOTMUCH_STATUS_PATH_ERROR: + return "Path supplied is illegal for this function"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; } } +void +_notmuch_database_log (notmuch_database_t *notmuch, + const char *format, + ...) +{ + va_list va_args; + + va_start (va_args, format); + + if (notmuch->status_string) + talloc_free (notmuch->status_string); + + notmuch->status_string = talloc_vasprintf (notmuch, format, va_args); + va_end (va_args); +} + +void +_notmuch_database_log_append (notmuch_database_t *notmuch, + const char *format, + ...) +{ + va_list va_args; + + va_start (va_args, format); + + if (notmuch->status_string) + notmuch->status_string = talloc_vasprintf_append (notmuch->status_string, format, va_args); + else + notmuch->status_string = talloc_vasprintf (notmuch, format, va_args); + + va_end (va_args); +} + static void find_doc_ids_for_term (notmuch_database_t *notmuch, const char *term, @@ -456,7 +515,7 @@ notmuch_database_find_message (notmuch_database_t *notmuch, return NOTMUCH_STATUS_SUCCESS; } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred finding message: %s.\n", + _notmuch_database_log (notmuch, "A Xapian exception occurred finding message: %s.\n", error.get_msg().c_str()); notmuch->exception_reported = TRUE; *message_ret = NULL; @@ -640,6 +699,12 @@ notmuch_database_create_verbose (const char *path, 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", @@ -705,13 +770,30 @@ notmuch_status_t _notmuch_database_ensure_writable (notmuch_database_t *notmuch) { if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) { - fprintf (stderr, "Cannot write to a read-only database.\n"); + _notmuch_database_log (notmuch, "Cannot write to a read-only database.\n"); return NOTMUCH_STATUS_READ_ONLY_DATABASE; } return NOTMUCH_STATUS_SUCCESS; } +/* Allocate a revision number for the next change. */ +unsigned long +_notmuch_database_new_revision (notmuch_database_t *notmuch) +{ + unsigned long new_revision = notmuch->revision + 1; + + /* If we're in an atomic section, hold off on updating the + * committed revision number until we commit the atomic section. + */ + if (notmuch->atomic_nesting) + 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. * @@ -830,6 +912,12 @@ notmuch_database_open_verbose (const char *path, 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; @@ -863,6 +951,7 @@ notmuch_database_open_verbose (const char *path, 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] == '/') @@ -872,10 +961,11 @@ notmuch_database_open_verbose (const char *path, notmuch->atomic_nesting = 0; try { string last_thread_id; + string last_mod; if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) { notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path, - Xapian::DB_CREATE_OR_OPEN); + DB_ACTION); } else { notmuch->xapian_db = new Xapian::Database (xapian_path); } @@ -930,11 +1020,30 @@ notmuch_database_open_verbose (const char *path, 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); +#if HAVE_XAPIAN_FIELD_PROCESSOR + /* This currently relies on the query parser to pass anything + * with a .. to the range processor */ + notmuch->date_field_processor = new DateFieldProcessor(); + notmuch->query_parser->add_boolean_prefix("date", notmuch->date_field_processor); + notmuch->query_field_processor = new QueryFieldProcessor (*notmuch->query_parser, notmuch); + notmuch->query_parser->add_boolean_prefix("query", notmuch->query_field_processor); +#endif + 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); @@ -942,6 +1051,7 @@ notmuch_database_open_verbose (const char *path, 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 (BOOLEAN_PREFIX_EXTERNAL); i++) { prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i]; @@ -1004,7 +1114,7 @@ notmuch_database_close (notmuch_database_t *notmuch) } catch (const Xapian::Error &error) { status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; if (! notmuch->exception_reported) { - fprintf (stderr, "Error: A Xapian exception occurred closing database: %s\n", + _notmuch_database_log (notmuch, "Error: A Xapian exception occurred closing database: %s\n", error.get_msg().c_str()); } } @@ -1020,11 +1130,19 @@ notmuch_database_close (notmuch_database_t *notmuch) 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; + +#if HAVE_XAPIAN_FIELD_PROCESSOR + delete notmuch->date_field_processor; + notmuch->date_field_processor = NULL; + delete notmuch->query_field_processor; + notmuch->query_field_processor = NULL; +#endif return status; } -#if HAVE_XAPIAN_COMPACT static int unlink_cb (const char *path, unused (const struct stat *sb), @@ -1136,12 +1254,12 @@ notmuch_database_compact (const char *path, } if (stat (backup_path, &statbuf) != -1) { - fprintf (stderr, "Path already exists: %s\n", backup_path); + _notmuch_database_log (notmuch, "Path already exists: %s\n", backup_path); ret = NOTMUCH_STATUS_FILE_ERROR; goto DONE; } if (errno != ENOENT) { - fprintf (stderr, "Unknown error while stat()ing path: %s\n", + _notmuch_database_log (notmuch, "Unknown error while stat()ing path: %s\n", strerror (errno)); ret = NOTMUCH_STATUS_FILE_ERROR; goto DONE; @@ -1161,20 +1279,20 @@ notmuch_database_compact (const char *path, compactor.set_destdir (compact_xapian_path); compactor.compact (); } catch (const Xapian::Error &error) { - fprintf (stderr, "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)) { - fprintf (stderr, "Error moving %s to %s: %s\n", + _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n", xapian_path, backup_path, strerror (errno)); ret = NOTMUCH_STATUS_FILE_ERROR; goto DONE; } if (rename (compact_xapian_path, xapian_path)) { - fprintf (stderr, "Error moving %s to %s: %s\n", + _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n", compact_xapian_path, xapian_path, strerror (errno)); ret = NOTMUCH_STATUS_FILE_ERROR; goto DONE; @@ -1182,7 +1300,7 @@ notmuch_database_compact (const char *path, if (! keep_backup) { if (rmtree (backup_path)) { - fprintf (stderr, "Error removing old database %s: %s\n", + _notmuch_database_log (notmuch, "Error removing old database %s: %s\n", backup_path, strerror (errno)); ret = NOTMUCH_STATUS_FILE_ERROR; goto DONE; @@ -1193,6 +1311,10 @@ notmuch_database_compact (const char *path, if (notmuch) { notmuch_status_t ret2; + const char *str = notmuch_database_status_string (notmuch); + if (status_cb && str) + status_cb (str, closure); + ret2 = notmuch_database_destroy (notmuch); /* don't clobber previous error status */ @@ -1204,17 +1326,6 @@ notmuch_database_compact (const char *path, return ret; } -#else -notmuch_status_t -notmuch_database_compact (unused (const char *path), - unused (const char *backup_path), - unused (notmuch_compact_status_cb_t status_cb), - unused (void *closure)) -{ - fprintf (stderr, "notmuch was compiled against a xapian version lacking compaction support.\n"); - return NOTMUCH_STATUS_UNSUPPORTED_OPERATION; -} -#endif notmuch_status_t notmuch_database_destroy (notmuch_database_t *notmuch) @@ -1299,6 +1410,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, 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); @@ -1314,7 +1426,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, return NOTMUCH_STATUS_SUCCESS; if (progress_notify) { - /* Setup our handler for SIGALRM */ + /* Set up our handler for SIGALRM */ memset (&action, 0, sizeof (struct sigaction)); action.sa_handler = handle_sigalrm; sigemptyset (&action.sa_mask); @@ -1333,10 +1445,18 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, /* Figure out how much total work we need to do. */ if (new_features & - (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) { - notmuch_query_t *query = notmuch_query_create (notmuch, ""); - total += notmuch_query_count_messages (query); + (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER | + NOTMUCH_FEATURE_LAST_MOD)) { + query = notmuch_query_create (notmuch, ""); + unsigned msg_count; + + status = notmuch_query_count_messages_st (query, &msg_count); + if (status) + goto DONE; + + total += msg_count; notmuch_query_destroy (query); + query = NULL; } if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) { t_end = db->allterms_end ("XTIMESTAMP"); @@ -1360,13 +1480,18 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, /* Perform per-message upgrades. */ if (new_features & - (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) { - notmuch_query_t *query = notmuch_query_create (notmuch, ""); + (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER | + NOTMUCH_FEATURE_LAST_MOD)) { notmuch_messages_t *messages; notmuch_message_t *message; char *filename; - for (messages = notmuch_query_search_messages (query); + query = notmuch_query_create (notmuch, ""); + + status = notmuch_query_search_messages_st (query, &messages); + if (status) + goto DONE; + for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { @@ -1397,6 +1522,14 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, if (new_features & NOTMUCH_FEATURE_BOOL_FOLDER) _notmuch_message_upgrade_folder (message); + /* Prior to NOTMUCH_FEATURE_LAST_MOD, messages did not + * track modification revisions. Give all messages the + * next available revision; since we just started tracking + * revisions for this database, that will be 1. + */ + if (new_features & NOTMUCH_FEATURE_LAST_MOD) + _notmuch_message_upgrade_last_mod (message); + _notmuch_message_sync (message); notmuch_message_destroy (message); @@ -1405,6 +1538,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, } notmuch_query_destroy (query); + query = NULL; } /* Perform per-directory upgrades. */ @@ -1489,7 +1623,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, } if (private_status) { - fprintf (stderr, + _notmuch_database_log (notmuch, "Upgrade failed while creating ghost messages.\n"); status = COERCE_STATUS (private_status, "Unexpected status from _notmuch_message_initialize_ghost"); goto DONE; @@ -1525,6 +1659,9 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, sigaction (SIGALRM, &action, NULL); } + if (query) + notmuch_query_destroy (query); + talloc_free (local); return status; } @@ -1536,10 +1673,13 @@ notmuch_database_begin_atomic (notmuch_database_t *notmuch) notmuch->atomic_nesting > 0) goto DONE; + if (notmuch_database_needs_upgrade (notmuch)) + return NOTMUCH_STATUS_UPGRADE_REQUIRED; + try { (static_cast (notmuch->xapian_db))->begin_transaction (false); } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred beginning transaction: %s.\n", + _notmuch_database_log (notmuch, "A Xapian exception occurred beginning transaction: %s.\n", error.get_msg().c_str()); notmuch->exception_reported = TRUE; return NOTMUCH_STATUS_XAPIAN_EXCEPTION; @@ -1573,17 +1713,31 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch) if (thresh && atoi (thresh) == 1) db->flush (); } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred committing transaction: %s.\n", + _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n", error.get_msg().c_str()); notmuch->exception_reported = TRUE; return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } + if (notmuch->atomic_dirty) { + ++notmuch->revision; + notmuch->atomic_dirty = FALSE; + } + DONE: notmuch->atomic_nesting--; return NOTMUCH_STATUS_SUCCESS; } +unsigned long +notmuch_database_get_revision (notmuch_database_t *notmuch, + const char **uuid) +{ + if (uuid) + *uuid = notmuch->uuid; + return notmuch->revision; +} + /* We allow the user to use arbitrarily long paths for directories. But * we have a term-length limit. So if we exceed that, we'll use the * SHA-1 of the path for the database term. @@ -1645,18 +1799,11 @@ _notmuch_database_split_path (void *ctx, slash = path + strlen (path) - 1; /* First, skip trailing slashes. */ - while (slash != path) { - if (*slash != '/') - break; - + while (slash != path && *slash == '/') --slash; - } /* Then, find a slash. */ - while (slash != path) { - if (*slash == '/') - break; - + while (slash != path && *slash != '/') { if (basename) *basename = slash; @@ -1664,12 +1811,8 @@ _notmuch_database_split_path (void *ctx, } /* Finally, skip multiple slashes. */ - while (slash != path) { - if (*slash != '/') - break; - + while (slash != path && *(slash - 1) == '/') --slash; - } if (slash == path) { if (directory) @@ -1678,7 +1821,7 @@ _notmuch_database_split_path (void *ctx, *basename = path; } else { if (directory) - *directory = talloc_strndup (ctx, path, slash - path + 1); + *directory = talloc_strndup (ctx, path, slash - path); } return NOTMUCH_STATUS_SUCCESS; @@ -1819,7 +1962,7 @@ notmuch_database_get_directory (notmuch_database_t *notmuch, *directory = _notmuch_directory_create (notmuch, path, NOTMUCH_FIND_LOOKUP, &status); } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred getting directory: %s.\n", + _notmuch_database_log (notmuch, "A Xapian exception occurred getting directory: %s.\n", error.get_msg().c_str()); notmuch->exception_reported = TRUE; status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; @@ -2063,8 +2206,8 @@ _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch, * 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); + _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); @@ -2173,15 +2316,15 @@ _consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch, if (stored_id.empty ()) { return NULL; } else { - Xapian::WritableDatabase *db; + Xapian::WritableDatabase *db; db = static_cast (notmuch->xapian_db); /* Clear the metadata for this message ID. We don't need it * anymore. */ - db->set_metadata (metadata_key, ""); + db->set_metadata (metadata_key, ""); - return talloc_strdup (ctx, stored_id.c_str ()); + return talloc_strdup (ctx, stored_id.c_str ()); } } @@ -2292,7 +2435,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch, if (ret) return ret; - message_file = _notmuch_message_file_open (filename); + message_file = _notmuch_message_file_open (notmuch, filename); if (message_file == NULL) return NOTMUCH_STATUS_FILE_ERROR; @@ -2401,7 +2544,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch, _notmuch_message_sync (message); } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred adding message: %s.\n", + _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; @@ -2493,7 +2636,7 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch, status = NOTMUCH_STATUS_OUT_OF_MEMORY; } } catch (const Xapian::Error &error) { - fprintf (stderr, "Error: A Xapian exception occurred finding message by filename: %s\n", + _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; @@ -2546,9 +2689,15 @@ notmuch_database_get_all_tags (notmuch_database_t *db) _notmuch_string_list_sort (tags); return _notmuch_tags_create (db, tags); } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred getting tags: %s.\n", + _notmuch_database_log (db, "A Xapian exception occurred getting tags: %s.\n", error.get_msg().c_str()); db->exception_reported = TRUE; return NULL; } } + +const char * +notmuch_database_status_string (const notmuch_database_t *notmuch) +{ + return notmuch->status_string; +}