X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=lib%2Fmessage.cc;h=0fa0eb3aef531237a01a2a19d73847a38fe49fff;hp=ad2208976729d653fc5162ec8b6d0823e6b5e1a5;hb=HEAD;hpb=36161181df961bb09da8090c13c234145270e5bf diff --git a/lib/message.cc b/lib/message.cc index ad220897..46638f80 100644 --- a/lib/message.cc +++ b/lib/message.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 "notmuch-private.h" #include "database-private.h" +#include "message-private.h" #include @@ -31,14 +32,31 @@ struct _notmuch_message { int frozen; char *message_id; char *thread_id; + size_t thread_depth; char *in_reply_to; - notmuch_filename_list_t *filename_list; + notmuch_string_list_t *tag_list; + notmuch_string_list_t *filename_term_list; + notmuch_string_list_t *filename_list; + char *maildir_flags; char *author; notmuch_message_file_t *message_file; + notmuch_string_list_t *property_term_list; + notmuch_string_map_t *property_map; + notmuch_string_list_t *reference_list; notmuch_message_list_t *replies; unsigned long flags; + /* For flags that are initialized on-demand, lazy_flags indicates + * if each flag has been initialized. */ + unsigned long lazy_flags; + + /* Message document modified since last sync */ + bool modified; + + /* last view of database the struct is synced with */ + unsigned long last_view; Xapian::Document doc; + Xapian::termcount termpos; }; #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) @@ -50,11 +68,11 @@ struct maildir_flag_tag { }; /* ASCII ordered table of Maildir flags and associated tags */ -static struct maildir_flag_tag flag2tag[] = { - { 'D', "draft", false}, - { 'F', "flagged", false}, - { 'P', "passed", false}, - { 'R', "replied", false}, +static const struct maildir_flag_tag flag2tag[] = { + { 'D', "draft", false }, + { 'F', "flagged", false }, + { 'P', "passed", false }, + { 'R', "replied", false }, { 'S', "unread", true } }; @@ -72,6 +90,20 @@ _notmuch_message_destructor (notmuch_message_t *message) return 0; } +#define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error) + +static void +_log_xapian_exception (const char *where, notmuch_message_t *message, const Xapian::Error error) +{ + notmuch_database_t *notmuch = notmuch_message_get_database (message); + + _notmuch_database_log (notmuch, + "A Xapian exception occurred at %s: %s\n", + where, + error.get_msg ().c_str ()); + notmuch->exception_reported = true; +} + static notmuch_message_t * _notmuch_message_create_for_document (const void *talloc_owner, notmuch_database_t *notmuch, @@ -96,14 +128,27 @@ _notmuch_message_create_for_document (const void *talloc_owner, message->frozen = 0; message->flags = 0; + message->lazy_flags = 0; + + /* the message is initially not synchronized with Xapian */ + message->last_view = 0; + + /* Calculated after the thread structure is computed */ + message->thread_depth = 0; /* Each of these will be lazily created as needed. */ message->message_id = NULL; message->thread_id = NULL; message->in_reply_to = NULL; + message->tag_list = NULL; + message->filename_term_list = NULL; message->filename_list = NULL; + message->maildir_flags = NULL; message->message_file = NULL; message->author = NULL; + message->property_term_list = NULL; + message->property_map = NULL; + message->reference_list = NULL; message->replies = _notmuch_message_list_create (message); if (unlikely (message->replies == NULL)) { @@ -123,6 +168,8 @@ _notmuch_message_create_for_document (const void *talloc_owner, talloc_set_destructor (message, _notmuch_message_destructor); message->doc = doc; + message->termpos = 0; + message->modified = false; return message; } @@ -186,15 +233,17 @@ _notmuch_message_create (const void *talloc_owner, * * There is already a document with message ID 'message_id' in the * database. The returned message can be used to query/modify the - * document. + * document. The message may be a ghost message. + * * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND: * * No document with 'message_id' exists in the database. The * returned message contains a newly created document (not yet * added to the database) and a document ID that is known not to - * exist in the database. The caller can modify the message, and a - * call to _notmuch_message_sync will add * the document to the - * database. + * exist in the database. This message is "blank"; that is, it + * contains only a message ID and no other metadata. The caller + * can modify the message, and a call to _notmuch_message_sync + * will add the document to the database. * * If an error occurs, this function will return NULL and *status * will be set as appropriate. (The status pointer argument must @@ -207,15 +256,20 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch, { notmuch_message_t *message; Xapian::Document doc; - Xapian::WritableDatabase *db; unsigned int doc_id; char *term; - *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS; - - message = notmuch_database_find_message (notmuch, message_id); + *status_ret = (notmuch_private_status_t) notmuch_database_find_message (notmuch, + message_id, + &message); if (message) return talloc_steal (notmuch, message); + else if (*status_ret) + return NULL; + + /* If the message ID is too long, substitute its sha1 instead. */ + if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) + message_id = _notmuch_message_id_compressed (message, message_id); term = talloc_asprintf (NULL, "%s%s", _find_prefix ("id"), message_id); @@ -224,10 +278,9 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch, return NULL; } - if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) + if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY) INTERNAL_ERROR ("Failure to ensure database is writable."); - db = static_cast (notmuch->xapian_db); try { doc.add_term (term, 0); talloc_free (term); @@ -236,9 +289,10 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch, doc_id = _notmuch_database_generate_doc_id (notmuch); } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred creating message: %s\n", - error.get_msg().c_str()); - notmuch->exception_reported = TRUE; + _notmuch_database_log (notmuch, + "A Xapian exception occurred creating message: %s\n", + error.get_msg ().c_str ()); + notmuch->exception_reported = true; *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; return NULL; } @@ -254,41 +308,213 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch, return message; } -unsigned int -_notmuch_message_get_doc_id (notmuch_message_t *message) +static char * +_notmuch_message_get_term (notmuch_message_t *message, + Xapian::TermIterator &i, Xapian::TermIterator &end, + const char *prefix) { - return message->doc_id; -} + int prefix_len = strlen (prefix); + char *value; -const char * -notmuch_message_get_message_id (notmuch_message_t *message) -{ - Xapian::TermIterator i; + i.skip_to (prefix); - if (message->message_id) - return message->message_id; + if (i == end) + return NULL; - i = message->doc.termlist_begin (); - i.skip_to (_find_prefix ("id")); + const std::string &term = *i; - if (i == message->doc.termlist_end ()) - INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n", - message->doc_id); + if (strncmp (term.c_str (), prefix, prefix_len)) + return NULL; - message->message_id = talloc_strdup (message, (*i).c_str () + 1); + value = talloc_strdup (message, term.c_str () + prefix_len); #if DEBUG_DATABASE_SANITY i++; - if (i != message->doc.termlist_end () && - strncmp ((*i).c_str (), _find_prefix ("id"), - strlen (_find_prefix ("id"))) == 0) - { - INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs", - message->doc_id); + if (i != end && strncmp ((*i).c_str (), prefix, prefix_len) == 0) { + INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate %s terms: %s and %s\n", + message->doc_id, prefix, value, + (*i).c_str () + prefix_len); } #endif + return value; +} + +static void +_notmuch_message_ensure_metadata (notmuch_message_t *message, void *field) +{ + Xapian::TermIterator i, end; + + if (field && (message->last_view >= message->notmuch->view)) + return; + + const char *thread_prefix = _find_prefix ("thread"), + *tag_prefix = _find_prefix ("tag"), + *id_prefix = _find_prefix ("id"), + *type_prefix = _find_prefix ("type"), + *filename_prefix = _find_prefix ("file-direntry"), + *property_prefix = _find_prefix ("property"), + *reference_prefix = _find_prefix ("reference"), + *replyto_prefix = _find_prefix ("replyto"); + + /* We do this all in a single pass because Xapian decompresses the + * term list every time you iterate over it. Thus, while this is + * slightly more costly than looking up individual fields if only + * one field of the message object is actually used, it's a huge + * win as more fields are used. */ + for (int count = 0; count < 3; count++) { + try { + i = message->doc.termlist_begin (); + end = message->doc.termlist_end (); + + /* Get thread */ + if (! message->thread_id) + message->thread_id = + _notmuch_message_get_term (message, i, end, thread_prefix); + + /* Get tags */ + assert (strcmp (thread_prefix, tag_prefix) < 0); + if (! message->tag_list) { + message->tag_list = + _notmuch_database_get_terms_with_prefix (message, i, end, + tag_prefix); + _notmuch_string_list_sort (message->tag_list); + } + + /* Get id */ + assert (strcmp (tag_prefix, id_prefix) < 0); + if (! message->message_id) + message->message_id = + _notmuch_message_get_term (message, i, end, id_prefix); + + /* Get document type */ + assert (strcmp (id_prefix, type_prefix) < 0); + if (! NOTMUCH_TEST_BIT (message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST)) { + i.skip_to (type_prefix); + /* "T" is the prefix "type" fields. See + * BOOLEAN_PREFIX_INTERNAL. */ + if (*i == "Tmail") + NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST); + else if (*i == "Tghost") + NOTMUCH_SET_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST); + else + INTERNAL_ERROR ("Message without type term"); + NOTMUCH_SET_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST); + } + + /* Get filename list. Here we get only the terms. We lazily + * expand them to full file names when needed in + * _notmuch_message_ensure_filename_list. */ + assert (strcmp (type_prefix, filename_prefix) < 0); + if (! message->filename_term_list && ! message->filename_list) + message->filename_term_list = + _notmuch_database_get_terms_with_prefix (message, i, end, + filename_prefix); + + + /* Get property terms. Mimic the setup with filenames above */ + assert (strcmp (filename_prefix, property_prefix) < 0); + if (! message->property_map && ! message->property_term_list) + message->property_term_list = + _notmuch_database_get_terms_with_prefix (message, i, end, + property_prefix); + + /* get references */ + assert (strcmp (property_prefix, reference_prefix) < 0); + if (! message->reference_list) { + message->reference_list = + _notmuch_database_get_terms_with_prefix (message, i, end, + reference_prefix); + } + + /* Get reply to */ + assert (strcmp (property_prefix, replyto_prefix) < 0); + if (! message->in_reply_to) + message->in_reply_to = + _notmuch_message_get_term (message, i, end, replyto_prefix); + + + /* It's perfectly valid for a message to have no In-Reply-To + * header. For these cases, we return an empty string. */ + if (! message->in_reply_to) + message->in_reply_to = talloc_strdup (message, ""); + + /* all the way without an exception */ + break; + } catch (const Xapian::DatabaseModifiedError &error) { + notmuch_status_t status = notmuch_database_reopen (message->notmuch, + NOTMUCH_DATABASE_MODE_READ_ONLY); + if (status != NOTMUCH_STATUS_SUCCESS) + INTERNAL_ERROR ("unhandled error from notmuch_database_reopen: %s\n", + notmuch_status_to_string (status)); + } + } + message->last_view = message->notmuch->view; +} + +void +_notmuch_message_invalidate_metadata (notmuch_message_t *message, + const char *prefix_name) +{ + if (strcmp ("thread", prefix_name) == 0) { + talloc_free (message->thread_id); + message->thread_id = NULL; + } + + if (strcmp ("tag", prefix_name) == 0) { + talloc_unlink (message, message->tag_list); + message->tag_list = NULL; + } + + if (strcmp ("type", prefix_name) == 0) { + NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST); + NOTMUCH_CLEAR_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST); + } + + if (strcmp ("file-direntry", prefix_name) == 0) { + talloc_free (message->filename_term_list); + talloc_free (message->filename_list); + message->filename_term_list = message->filename_list = NULL; + } + + if (strcmp ("property", prefix_name) == 0) { + + if (message->property_term_list) + talloc_free (message->property_term_list); + message->property_term_list = NULL; + + if (message->property_map) + talloc_unlink (message, message->property_map); + + message->property_map = NULL; + } + + if (strcmp ("replyto", prefix_name) == 0) { + talloc_free (message->in_reply_to); + message->in_reply_to = NULL; + } +} + +unsigned int +_notmuch_message_get_doc_id (notmuch_message_t *message) +{ + return message->doc_id; +} + +const char * +notmuch_message_get_message_id (notmuch_message_t *message) +{ + try { + _notmuch_message_ensure_metadata (message, message->message_id); + } catch (const Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NULL; + } + + if (! message->message_id) + INTERNAL_ERROR ("Message with document ID of %u has no message ID.\n", + message->doc_id); return message->message_id; } @@ -304,17 +530,48 @@ _notmuch_message_ensure_message_file (notmuch_message_t *message) if (unlikely (filename == NULL)) return; - message->message_file = _notmuch_message_file_open_ctx (message, filename); + message->message_file = _notmuch_message_file_open_ctx ( + notmuch_message_get_database (message), message, filename); } const char * notmuch_message_get_header (notmuch_message_t *message, const char *header) { + Xapian::valueno slot = Xapian::BAD_VALUENO; + + /* Fetch header from the appropriate xapian value field if + * available */ + if (strcasecmp (header, "from") == 0) + slot = NOTMUCH_VALUE_FROM; + else if (strcasecmp (header, "subject") == 0) + slot = NOTMUCH_VALUE_SUBJECT; + else if (strcasecmp (header, "message-id") == 0) + slot = NOTMUCH_VALUE_MESSAGE_ID; + + if (slot != Xapian::BAD_VALUENO) { + try { + std::string value = message->doc.get_value (slot); + + /* If we have NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, then + * empty values indicate empty headers. If we don't, then + * it could just mean we didn't record the header. */ + if ((message->notmuch->features & + NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES) || + ! value.empty ()) + return talloc_strdup (message, value.c_str ()); + + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NULL; + } + } + + /* Otherwise fall back to parsing the file */ _notmuch_message_ensure_message_file (message); if (message->message_file == NULL) return NULL; - return notmuch_message_file_get_header (message->message_file, header); + return _notmuch_message_file_get_header (message->message_file, header); } /* Return the message ID from the In-Reply-To header of 'message'. @@ -327,103 +584,353 @@ notmuch_message_get_header (notmuch_message_t *message, const char *header) const char * _notmuch_message_get_in_reply_to (notmuch_message_t *message) { - const char *prefix = _find_prefix ("replyto"); - int prefix_len = strlen (prefix); - Xapian::TermIterator i; - std::string in_reply_to; + _notmuch_message_ensure_metadata (message, message->in_reply_to); + return message->in_reply_to; +} - if (message->in_reply_to) - return message->in_reply_to; +const char * +notmuch_message_get_thread_id (notmuch_message_t *message) +{ + try { + _notmuch_message_ensure_metadata (message, message->thread_id); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NULL; + } + if (! message->thread_id) + INTERNAL_ERROR ("Message with document ID of %u has no thread ID.\n", + message->doc_id); + return message->thread_id; +} - i = message->doc.termlist_begin (); - i.skip_to (prefix); +void +_notmuch_message_add_reply (notmuch_message_t *message, + notmuch_message_t *reply) +{ + _notmuch_message_list_add_message (message->replies, reply); +} - if (i != message->doc.termlist_end ()) - in_reply_to = *i; +size_t +_notmuch_message_get_thread_depth (notmuch_message_t *message) +{ + return message->thread_depth; +} - /* It's perfectly valid for a message to have no In-Reply-To - * header. For these cases, we return an empty string. */ - if (i == message->doc.termlist_end () || - strncmp (in_reply_to.c_str (), prefix, prefix_len)) - { - message->in_reply_to = talloc_strdup (message, ""); - return message->in_reply_to; +void +_notmuch_message_label_depths (notmuch_message_t *message, + size_t depth) +{ + message->thread_depth = depth; + + for (notmuch_messages_t *messages = _notmuch_messages_create (message->replies); + notmuch_messages_valid (messages); + notmuch_messages_move_to_next (messages)) { + notmuch_message_t *child = notmuch_messages_get (messages); + _notmuch_message_label_depths (child, depth + 1); } +} - message->in_reply_to = talloc_strdup (message, - in_reply_to.c_str () + prefix_len); +const notmuch_string_list_t * +_notmuch_message_get_references (notmuch_message_t *message) +{ + _notmuch_message_ensure_metadata (message, message->reference_list); + return message->reference_list; +} -#if DEBUG_DATABASE_SANITY - i++; +static int +_cmpmsg (const void *pa, const void *pb) +{ + notmuch_message_t **a = (notmuch_message_t **) pa; + notmuch_message_t **b = (notmuch_message_t **) pb; + time_t time_a = notmuch_message_get_date (*a); + time_t time_b = notmuch_message_get_date (*b); - in_reply_to = *i; + if (time_a == time_b) + return 0; + else if (time_a < time_b) + return -1; + else + return 1; +} - if (i != message->doc.termlist_end () && - strncmp ((*i).c_str (), prefix, prefix_len) == 0) - { - INTERNAL_ERROR ("Message %s has duplicate In-Reply-To IDs: %s and %s\n", - notmuch_message_get_message_id (message), - message->in_reply_to, - (*i).c_str () + prefix_len); +notmuch_message_list_t * +_notmuch_message_sort_subtrees (void *ctx, notmuch_message_list_t *list) +{ + + size_t count = 0; + size_t capacity = 16; + + if (! list) + return list; + + void *local = talloc_new (NULL); + notmuch_message_list_t *new_list = _notmuch_message_list_create (ctx); + notmuch_message_t **message_array = talloc_zero_array (local, notmuch_message_t *, capacity); + + for (notmuch_messages_t *messages = _notmuch_messages_create (list); + notmuch_messages_valid (messages); + notmuch_messages_move_to_next (messages)) { + notmuch_message_t *root = notmuch_messages_get (messages); + if (count >= capacity) { + capacity *= 2; + message_array = talloc_realloc (local, message_array, notmuch_message_t *, capacity); + } + message_array[count++] = root; + root->replies = _notmuch_message_sort_subtrees (root, root->replies); } -#endif - return message->in_reply_to; + qsort (message_array, count, sizeof (notmuch_message_t *), _cmpmsg); + for (size_t i = 0; i < count; i++) { + _notmuch_message_list_add_message (new_list, message_array[i]); + } + + talloc_free (local); + talloc_free (list); + return new_list; } -const char * -notmuch_message_get_thread_id (notmuch_message_t *message) +notmuch_messages_t * +notmuch_message_get_replies (notmuch_message_t *message) +{ + return _notmuch_messages_create (message->replies); +} + +void +_notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix) { - const char *prefix = _find_prefix ("thread"); Xapian::TermIterator i; - std::string id; + size_t prefix_len = 0; - /* This code is written with the assumption that "thread" has a - * single-character prefix. */ - assert (strlen (prefix) == 1); + prefix_len = strlen (prefix); - if (message->thread_id) - return message->thread_id; + while (1) { + i = message->doc.termlist_begin (); + i.skip_to (prefix); - i = message->doc.termlist_begin (); - i.skip_to (prefix); + /* Terminate loop when no terms remain with desired prefix. */ + if (i == message->doc.termlist_end () || + strncmp ((*i).c_str (), prefix, prefix_len)) + break; + + try { + message->doc.remove_term ((*i)); + message->modified = true; + } catch (const Xapian::InvalidArgumentError) { + /* Ignore failure to remove non-existent term. */ + } + } - if (i != message->doc.termlist_end ()) - id = *i; + _notmuch_message_invalidate_metadata (message, "property"); +} - if (i == message->doc.termlist_end () || id[0] != *prefix) - INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n", - message->doc_id); - message->thread_id = talloc_strdup (message, id.c_str () + 1); +/* Remove all terms generated by indexing, i.e. not tags or + * properties, along with any automatic tags*/ +/* According to Xapian API docs, none of these calls throw + * exceptions */ +static notmuch_private_status_t +_notmuch_message_remove_indexed_terms (notmuch_message_t *message) +{ + Xapian::TermIterator i; -#if DEBUG_DATABASE_SANITY - i++; - id = *i; + const std::string + id_prefix = _find_prefix ("id"), + property_prefix = _find_prefix ("property"), + tag_prefix = _find_prefix ("tag"), + type_prefix = _find_prefix ("type"); + + /* Make sure we have the data to restore to Xapian*/ + _notmuch_message_ensure_metadata (message, NULL); + + /* Empirically, it turns out to be faster to remove all the terms, + * and add back the ones we want. */ + message->doc.clear_terms (); + message->modified = true; + + /* still a mail message */ + message->doc.add_term (type_prefix + "mail"); - if (i != message->doc.termlist_end () && id[0] == *prefix) - { - INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n", - notmuch_message_get_message_id (message), - message->thread_id, - id.c_str () + 1); + /* Put back message-id */ + message->doc.add_term (id_prefix + message->message_id); + + /* Put back non-automatic tags */ + for (notmuch_tags_t *tags = notmuch_message_get_tags (message); + notmuch_tags_valid (tags); + notmuch_tags_move_to_next (tags)) { + + const char *tag = notmuch_tags_get (tags); + + if (strcmp (tag, "encrypted") != 0 && + strcmp (tag, "signed") != 0 && + strcmp (tag, "attachment") != 0) { + std::string term = tag_prefix + tag; + message->doc.add_term (term); + } } -#endif - return message->thread_id; + /* Put back properties */ + notmuch_message_properties_t *list; + + for (list = notmuch_message_get_properties (message, "", false); + notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) { + std::string term = property_prefix + + notmuch_message_properties_key (list) + "=" + + notmuch_message_properties_value (list); + + message->doc.add_term (term); + } + + notmuch_message_properties_destroy (list); + + return NOTMUCH_PRIVATE_STATUS_SUCCESS; } -void -_notmuch_message_add_reply (notmuch_message_t *message, - notmuch_message_node_t *reply) + +/* Return true if p points at "new" or "cur". */ +static bool +is_maildir (const char *p) { - _notmuch_message_list_append (message->replies, reply); + return strcmp (p, "cur") == 0 || strcmp (p, "new") == 0; } -notmuch_messages_t * -notmuch_message_get_replies (notmuch_message_t *message) +/* Add "folder:" term for directory. */ +NODISCARD static notmuch_status_t +_notmuch_message_add_folder_terms (notmuch_message_t *message, + const char *directory) { - return _notmuch_messages_create (message->replies); + char *folder, *last; + + folder = talloc_strdup (NULL, directory); + if (! folder) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + /* + * If the message file is in a leaf directory named "new" or + * "cur", presume maildir and index the parent directory. Thus a + * "folder:" prefix search matches messages in the specified + * maildir folder, i.e. in the specified directory and its "new" + * and "cur" subdirectories. + * + * Note that this means the "folder:" prefix can't be used for + * distinguishing between message files in "new" or "cur". The + * "path:" prefix needs to be used for that. + * + * Note the deliberate difference to _filename_is_in_maildir(). We + * don't want to index different things depending on the existence + * or non-existence of all maildir sibling directories "new", + * "cur", and "tmp". Doing so would be surprising, and difficult + * for the user to fix in case all subdirectories were not in + * place during indexing. + */ + last = strrchr (folder, '/'); + if (last) { + if (is_maildir (last + 1)) + *last = '\0'; + } else if (is_maildir (folder)) { + *folder = '\0'; + } + + if (notmuch_status_t status = COERCE_STATUS (_notmuch_message_add_term (message, "folder", + folder), + "adding folder term")) + return status; + + talloc_free (folder); + + message->modified = true; + return NOTMUCH_STATUS_SUCCESS; +} + +#define RECURSIVE_SUFFIX "/**" + +/* Add "path:" terms for directory. */ +NODISCARD static notmuch_status_t +_notmuch_message_add_path_terms (notmuch_message_t *message, + const char *directory) +{ + notmuch_status_t status; + + /* Add exact "path:" term. */ + status = COERCE_STATUS (_notmuch_message_add_term (message, "path", directory), + "adding path term"); + if (status) + return status; + + if (strlen (directory)) { + char *path, *p; + + path = talloc_asprintf (NULL, "%s%s", directory, RECURSIVE_SUFFIX); + if (! path) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + /* Add recursive "path:" terms for directory and all parents. */ + for (p = path + strlen (path) - 1; p > path; p--) { + if (*p == '/') { + strcpy (p, RECURSIVE_SUFFIX); + status = COERCE_STATUS (_notmuch_message_add_term (message, "path", path), + "adding path term"); + if (status) + return status; + } + } + + talloc_free (path); + } + + /* Recursive all-matching path:** for consistency. */ + status = COERCE_STATUS (_notmuch_message_add_term (message, "path", "**"), + "adding path term"); + if (status) + return status; + + return NOTMUCH_STATUS_SUCCESS; +} + +/* Add directory based terms for all filenames of the message. */ +static notmuch_status_t +_notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message) +{ + const char *direntry_prefix = _find_prefix ("file-direntry"); + int direntry_prefix_len = strlen (direntry_prefix); + Xapian::TermIterator i = message->doc.termlist_begin (); + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + + for (i.skip_to (direntry_prefix); i != message->doc.termlist_end (); i++) { + unsigned int directory_id; + const char *direntry, *directory; + char *colon; + const std::string &term = *i; + notmuch_status_t term_status; + + /* Terminate loop at first term without desired prefix. */ + if (strncmp (term.c_str (), direntry_prefix, direntry_prefix_len)) + break; + + /* Indicate that there are filenames remaining. */ + status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; + + direntry = term.c_str (); + direntry += direntry_prefix_len; + + directory_id = strtol (direntry, &colon, 10); + + if (colon == NULL || *colon != ':') + INTERNAL_ERROR ("malformed direntry"); + + directory = _notmuch_database_get_directory_path (ctx, + message->notmuch, + directory_id); + + term_status = _notmuch_message_add_folder_terms (message, directory); + if (term_status) + return term_status; + + term_status = _notmuch_message_add_path_terms (message, directory); + if (term_status) + return term_status; + } + + return status; } /* Add an additional 'filename' for 'message'. @@ -434,66 +941,127 @@ notmuch_status_t _notmuch_message_add_filename (notmuch_message_t *message, const char *filename) { + const char *relative, *directory; notmuch_status_t status; + notmuch_private_status_t private_status; void *local = talloc_new (message); char *direntry; - if (message->filename_list) { - _notmuch_filename_list_destroy (message->filename_list); - message->filename_list = NULL; - } - if (filename == NULL) INTERNAL_ERROR ("Message filename cannot be NULL."); - status = _notmuch_database_filename_to_direntry (local, - message->notmuch, - filename, &direntry); + if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) || + ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER)) + return NOTMUCH_STATUS_UPGRADE_REQUIRED; + + relative = _notmuch_database_relative_path (message->notmuch, filename); + + status = _notmuch_database_split_path (local, relative, &directory, NULL); + if (status) + return status; + + status = _notmuch_database_filename_to_direntry ( + local, message->notmuch, filename, NOTMUCH_FIND_CREATE, &direntry); + if (status) + return status; + + /* New file-direntry allows navigating to this message with + * notmuch_directory_get_child_files() . */ + private_status = _notmuch_message_add_term (message, "file-direntry", direntry); + switch (private_status) { + case NOTMUCH_PRIVATE_STATUS_SUCCESS: + break; + case NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG: + _notmuch_database_log (message->notmuch, "filename too long for file-direntry term: %s\n", + filename); + return NOTMUCH_STATUS_PATH_ERROR; + default: + return COERCE_STATUS (private_status, "adding file-direntry term"); + } + + status = _notmuch_message_add_folder_terms (message, directory); if (status) return status; - _notmuch_message_add_term (message, "file-direntry", direntry); + status = _notmuch_message_add_path_terms (message, directory); + if (status) + return status; talloc_free (local); return NOTMUCH_STATUS_SUCCESS; } -/* Change a particular filename for 'message' from 'old_filename' to - * 'new_filename' +/* Remove a particular 'filename' from 'message'. * * This change will not be reflected in the database until the next * call to _notmuch_message_sync. - */ -static notmuch_status_t -_notmuch_message_rename (notmuch_message_t *message, - const char *old_filename, - const char *new_filename) + * + * If this message still has other filenames, returns + * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID. + * + * Note: This function does not remove a document from the database, + * even if the specified filename is the only filename for this + * message. For that functionality, see + * notmuch_database_remove_message. */ +notmuch_status_t +_notmuch_message_remove_filename (notmuch_message_t *message, + const char *filename) { void *local = talloc_new (message); char *direntry; notmuch_private_status_t private_status; notmuch_status_t status; - status = _notmuch_message_add_filename (message, new_filename); - if (status) - return status; + if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) || + ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER)) + return NOTMUCH_STATUS_UPGRADE_REQUIRED; - status = _notmuch_database_filename_to_direntry (local, message->notmuch, - old_filename, &direntry); - if (status) + status = _notmuch_database_filename_to_direntry ( + local, message->notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry); + if (status || ! direntry) return status; + /* Unlink this file from its parent directory. */ private_status = _notmuch_message_remove_term (message, "file-direntry", direntry); status = COERCE_STATUS (private_status, "Unexpected error from _notmuch_message_remove_term"); + if (status) + return status; + + /* Re-synchronize "folder:" and "path:" terms for this message. */ + + /* Remove all "folder:" terms. */ + _notmuch_message_remove_terms (message, _find_prefix ("folder")); + + /* Remove all "path:" terms. */ + _notmuch_message_remove_terms (message, _find_prefix ("path")); + + /* Add back terms for all remaining filenames of the message. */ + status = _notmuch_message_add_directory_terms (local, message); talloc_free (local); return status; } +/* Upgrade the "folder:" prefix from V1 to V2. */ +#define FOLDER_PREFIX_V1 "XFOLDER" +#define ZFOLDER_PREFIX_V1 "Z" FOLDER_PREFIX_V1 +void +_notmuch_message_upgrade_folder (notmuch_message_t *message) +{ + /* Remove all old "folder:" terms. */ + _notmuch_message_remove_terms (message, FOLDER_PREFIX_V1); + + /* Remove all old "folder:" stemmed terms. */ + _notmuch_message_remove_terms (message, ZFOLDER_PREFIX_V1); + + /* Add new boolean "folder:" and "path:" terms. */ + _notmuch_message_add_directory_terms (message, message); +} + char * _notmuch_message_talloc_copy_data (notmuch_message_t *message) { @@ -504,57 +1072,48 @@ void _notmuch_message_clear_data (notmuch_message_t *message) { message->doc.set_data (""); + message->modified = true; } static void _notmuch_message_ensure_filename_list (notmuch_message_t *message) { - const char *prefix = _find_prefix ("file-direntry"); - int prefix_len = strlen (prefix); - Xapian::TermIterator i; + notmuch_string_node_t *node; if (message->filename_list) return; - message->filename_list = _notmuch_filename_list_create (message); + _notmuch_message_ensure_metadata (message, message->filename_term_list); - i = message->doc.termlist_begin (); - i.skip_to (prefix); + message->filename_list = _notmuch_string_list_create (message); + node = message->filename_term_list->head; - if (i == message->doc.termlist_end () || - strncmp ((*i).c_str (), prefix, prefix_len)) - { + if (! node) { /* A message document created by an old version of notmuch * (prior to rename support) will have the filename in the * data of the document rather than as a file-direntry term. * * It would be nice to do the upgrade of the document directly * here, but the database is likely open in read-only mode. */ - const char *data; - data = message->doc.get_data ().c_str (); + std::string datastr = message->doc.get_data (); + const char *data = datastr.c_str (); if (data == NULL) INTERNAL_ERROR ("message with no filename"); - _notmuch_filename_list_add_filename (message->filename_list, data); + _notmuch_string_list_append (message->filename_list, data); return; } - for (; i != message->doc.termlist_end (); i++) { + for (; node; node = node->next) { void *local = talloc_new (message); const char *db_path, *directory, *basename, *filename; char *colon, *direntry = NULL; unsigned int directory_id; - /* Terminate loop at first term without desired prefix. */ - if (strncmp ((*i).c_str (), prefix, prefix_len)) - break; - - direntry = talloc_strdup (local, (*i).c_str ()); - - direntry += prefix_len; + direntry = node->string; directory_id = strtol (direntry, &colon, 10); @@ -565,7 +1124,7 @@ _notmuch_message_ensure_filename_list (notmuch_message_t *message) *colon = '\0'; - db_path = notmuch_database_get_path (message->notmuch); + db_path = notmuch_config_get (message->notmuch, NOTMUCH_CONFIG_MAIL_ROOT); directory = _notmuch_database_get_directory_path (local, message->notmuch, @@ -578,43 +1137,96 @@ _notmuch_message_ensure_filename_list (notmuch_message_t *message) filename = talloc_asprintf (message, "%s/%s", db_path, basename); - _notmuch_filename_list_add_filename (message->filename_list, - filename); + _notmuch_string_list_append (message->filename_list, filename); talloc_free (local); } + + talloc_free (message->filename_term_list); + message->filename_term_list = NULL; } const char * notmuch_message_get_filename (notmuch_message_t *message) { - _notmuch_message_ensure_filename_list (message); + try { + _notmuch_message_ensure_filename_list (message); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NULL; + } if (message->filename_list == NULL) return NULL; if (message->filename_list->head == NULL || - message->filename_list->head->filename == NULL) - { + message->filename_list->head->string == NULL) { INTERNAL_ERROR ("message with no filename"); } - return message->filename_list->head->filename; + return message->filename_list->head->string; } notmuch_filenames_t * notmuch_message_get_filenames (notmuch_message_t *message) { - _notmuch_message_ensure_filename_list (message); + try { + _notmuch_message_ensure_filename_list (message); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NULL; + } return _notmuch_filenames_create (message, message->filename_list); } +int +notmuch_message_count_files (notmuch_message_t *message) +{ + try { + _notmuch_message_ensure_filename_list (message); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return -1; + } + + return _notmuch_string_list_length (message->filename_list); +} + +notmuch_status_t +notmuch_message_get_flag_st (notmuch_message_t *message, + notmuch_message_flag_t flag, + notmuch_bool_t *is_set) +{ + if (! is_set) + return NOTMUCH_STATUS_NULL_POINTER; + + try { + if (flag == NOTMUCH_MESSAGE_FLAG_GHOST && + ! NOTMUCH_TEST_BIT (message->lazy_flags, flag)) + _notmuch_message_ensure_metadata (message, NULL); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + *is_set = NOTMUCH_TEST_BIT (message->flags, flag); + return NOTMUCH_STATUS_SUCCESS; +} + notmuch_bool_t notmuch_message_get_flag (notmuch_message_t *message, notmuch_message_flag_t flag) { - return message->flags & (1 << flag); + notmuch_bool_t is_set; + notmuch_status_t status; + + status = notmuch_message_get_flag_st (message, flag, &is_set); + + if (status) + return FALSE; + else + return is_set; } void @@ -622,9 +1234,10 @@ notmuch_message_set_flag (notmuch_message_t *message, notmuch_message_flag_t flag, notmuch_bool_t enable) { if (enable) - message->flags |= (1 << flag); + NOTMUCH_SET_BIT (&message->flags, flag); else - message->flags &= ~(1 << flag); + NOTMUCH_CLEAR_BIT (&message->flags, flag); + NOTMUCH_SET_BIT (&message->lazy_flags, flag); } time_t @@ -635,66 +1248,241 @@ notmuch_message_get_date (notmuch_message_t *message) try { value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP); } catch (Xapian::Error &error) { - INTERNAL_ERROR ("Failed to read timestamp value from document."); + LOG_XAPIAN_EXCEPTION (message, error); return 0; } + if (value.empty ()) + /* sortable_unserialise is undefined on empty string */ + return 0; return Xapian::sortable_unserialise (value); } notmuch_tags_t * notmuch_message_get_tags (notmuch_message_t *message) { - Xapian::TermIterator i, end; - i = message->doc.termlist_begin(); - end = message->doc.termlist_end(); - return _notmuch_convert_tags(message, i, end); + notmuch_tags_t *tags; + + try { + _notmuch_message_ensure_metadata (message, message->tag_list); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NULL; + } + + tags = _notmuch_tags_create (message, message->tag_list); + /* _notmuch_tags_create steals the reference to the tag_list, but + * in this case it's still used by the message, so we add an + * *additional* talloc reference to the list. As a result, it's + * possible to modify the message tags (which talloc_unlink's the + * current list from the message) while still iterating because + * the iterator will keep the current list alive. */ + if (! talloc_reference (message, message->tag_list)) + return NULL; + + return tags; } const char * -notmuch_message_get_author (notmuch_message_t *message) +_notmuch_message_get_author (notmuch_message_t *message) { return message->author; } void -notmuch_message_set_author (notmuch_message_t *message, - const char *author) +_notmuch_message_set_author (notmuch_message_t *message, + const char *author) { if (message->author) - talloc_free(message->author); - message->author = talloc_strdup(message, author); + talloc_free (message->author); + message->author = talloc_strdup (message, author); return; } void -_notmuch_message_set_date (notmuch_message_t *message, - const char *date) +_notmuch_message_set_header_values (notmuch_message_t *message, + const char *date, + const char *from, + const char *subject) { time_t time_value; /* GMime really doesn't want to see a NULL date, so protect its * sensibilities. */ - if (date == NULL || *date == '\0') + if (date == NULL || *date == '\0') { time_value = 0; - else - time_value = g_mime_utils_header_decode_date (date, NULL); + } else { + time_value = g_mime_utils_header_decode_date_unix (date); + /* + * Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=779923 + */ + if (time_value < 0) + time_value = 0; + } message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP, Xapian::sortable_serialise (time_value)); + message->doc.add_value (NOTMUCH_VALUE_FROM, from); + message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject); + message->modified = true; +} + +void +_notmuch_message_update_subject (notmuch_message_t *message, + const char *subject) +{ + message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject); + message->modified = true; +} + +/* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD. The caller + * must call _notmuch_message_sync. */ +void +_notmuch_message_upgrade_last_mod (notmuch_message_t *message) +{ + /* _notmuch_message_sync will update the last modification + * revision; we just have to ask it to. */ + message->modified = true; } /* Synchronize changes made to message->doc out into the database. */ void _notmuch_message_sync (notmuch_message_t *message) { - Xapian::WritableDatabase *db; + if (_notmuch_database_mode (message->notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY) + return; - if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) + if (! message->modified) return; - db = static_cast (message->notmuch->xapian_db); - db->replace_document (message->doc_id, message->doc); + /* Update the last modification of this message. */ + if (message->notmuch->features & NOTMUCH_FEATURE_LAST_MOD) + /* sortable_serialise gives a reasonably compact encoding, + * which directly translates to reduced IO when scanning the + * value stream. Since it's built for doubles, we only get 53 + * effective bits, but that's still enough for the database to + * last a few centuries at 1 million revisions per second. */ + message->doc.add_value (NOTMUCH_VALUE_LAST_MOD, + Xapian::sortable_serialise ( + _notmuch_database_new_revision ( + message->notmuch))); + + message->notmuch->writable_xapian_db-> + replace_document (message->doc_id, message->doc); + message->modified = false; +} + +/* Delete a message document from the database, leaving a ghost + * message in its place */ +notmuch_status_t +_notmuch_message_delete (notmuch_message_t *message) +{ + notmuch_status_t status; + const char *mid, *tid; + notmuch_message_t *ghost; + notmuch_private_status_t private_status; + notmuch_database_t *notmuch; + unsigned int count = 0; + bool is_ghost; + + mid = notmuch_message_get_message_id (message); + tid = notmuch_message_get_thread_id (message); + notmuch = message->notmuch; + + status = _notmuch_database_ensure_writable (message->notmuch); + if (status) + return status; + + try { + Xapian::PostingIterator thread_doc, thread_doc_end; + Xapian::PostingIterator mail_doc, mail_doc_end; + + /* look for a non-ghost message in the same thread */ + /* if this was a ghost to begin with, we are done */ + private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost); + if (private_status) + return COERCE_STATUS (private_status, + "Error trying to determine whether message was a ghost"); + + message->notmuch->writable_xapian_db->delete_document (message->doc_id); + + if (is_ghost) + return NOTMUCH_STATUS_SUCCESS; + + _notmuch_database_find_doc_ids (message->notmuch, "thread", tid, &thread_doc, + &thread_doc_end); + _notmuch_database_find_doc_ids (message->notmuch, "type", "mail", &mail_doc, &mail_doc_end); + + while (count == 0 && + thread_doc != thread_doc_end && + mail_doc != mail_doc_end) { + thread_doc.skip_to (*mail_doc); + if (thread_doc != thread_doc_end) { + if (*thread_doc == *mail_doc) { + count++; + } else { + mail_doc.skip_to (*thread_doc); + if (mail_doc != mail_doc_end && *thread_doc == *mail_doc) + count++; + } + } + } + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + if (count > 0) { + /* reintroduce a ghost in its place because there are still + * other active messages in this thread: */ + ghost = _notmuch_message_create_for_message_id (notmuch, mid, &private_status); + if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) { + private_status = _notmuch_message_initialize_ghost (ghost, tid); + if (! private_status) + _notmuch_message_sync (ghost); + } else if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) { + /* this is deeply weird, and we should not have gotten + * into this state. is there a better error message to + * return here? */ + status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; + } + + notmuch_message_destroy (ghost); + status = COERCE_STATUS (private_status, "Error converting to ghost message"); + } else { + /* the thread now contains only ghosts: delete them */ + try { + Xapian::PostingIterator doc, doc_end; + + _notmuch_database_find_doc_ids (message->notmuch, "thread", tid, &doc, &doc_end); + + for (; doc != doc_end; doc++) { + message->notmuch->writable_xapian_db->delete_document (*doc); + } + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + } + return status; +} + +/* Transform a blank message into a ghost message. The caller must + * _notmuch_message_sync the message. */ +notmuch_private_status_t +_notmuch_message_initialize_ghost (notmuch_message_t *message, + const char *thread_id) +{ + notmuch_private_status_t status; + + status = _notmuch_message_add_term (message, "type", "ghost"); + if (status) + return status; + status = _notmuch_message_add_term (message, "thread", thread_id); + if (status) + return status; + + return NOTMUCH_PRIVATE_STATUS_SUCCESS; } /* Ensure that 'message' is not holding any file object open. Future @@ -705,7 +1493,7 @@ void _notmuch_message_close (notmuch_message_t *message) { if (message->message_file) { - notmuch_message_file_close (message->message_file); + _notmuch_message_file_close (message->message_file); message->message_file = NULL; } } @@ -717,33 +1505,43 @@ _notmuch_message_close (notmuch_message_t *message) * * This change will not be reflected in the database until the next * call to _notmuch_message_sync. */ -notmuch_private_status_t +NODISCARD notmuch_private_status_t _notmuch_message_add_term (notmuch_message_t *message, const char *prefix_name, const char *value) { char *term; + notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS; if (value == NULL) return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; term = talloc_asprintf (message, "%s%s", _find_prefix (prefix_name), value); + if (strlen (term) > NOTMUCH_TERM_MAX) { + status = NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; + goto DONE; + } - if (strlen (term) > NOTMUCH_TERM_MAX) - return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; - - message->doc.add_term (term, 0); + try { + message->doc.add_term (term, 0); + message->modified = true; + _notmuch_message_invalidate_metadata (message, prefix_name); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; + } + DONE: talloc_free (term); - - return NOTMUCH_PRIVATE_STATUS_SUCCESS; + return status; } /* Parse 'text' and add a term to 'message' for each parsed word. Each - * term will be added both prefixed (if prefix_name is not NULL) and - * also unprefixed). */ + * term will be added with the appropriate prefix if prefix_name is + * non-NULL. + */ notmuch_private_status_t _notmuch_message_gen_terms (notmuch_message_t *message, const char *prefix_name, @@ -755,14 +1553,22 @@ _notmuch_message_gen_terms (notmuch_message_t *message, return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; term_gen->set_document (message->doc); + term_gen->set_termpos (message->termpos); if (prefix_name) { - const char *prefix = _find_prefix (prefix_name); + const char *prefix = _notmuch_database_prefix (message->notmuch, prefix_name); + if (prefix == NULL) + return NOTMUCH_PRIVATE_STATUS_BAD_PREFIX; + _notmuch_message_invalidate_metadata (message, prefix_name); term_gen->index_text (text, 1, prefix); + } else { + term_gen->index_text (text); } - term_gen->index_text (text); + /* Create a gap between this an the next terms so they don't + * appear to be a phrase. */ + message->termpos = term_gen->get_termpos () + 100; return NOTMUCH_PRIVATE_STATUS_SUCCESS; } @@ -774,7 +1580,7 @@ _notmuch_message_gen_terms (notmuch_message_t *message, * * This change will not be reflected in the database until the next * call to _notmuch_message_sync. */ -notmuch_private_status_t +NODISCARD notmuch_private_status_t _notmuch_message_remove_term (notmuch_message_t *message, const char *prefix_name, const char *value) @@ -792,128 +1598,124 @@ _notmuch_message_remove_term (notmuch_message_t *message, try { message->doc.remove_term (term); - } catch (const Xapian::InvalidArgumentError) { - /* We'll let the philosopher's try to wrestle with the + message->modified = true; + } catch (const Xapian::InvalidArgumentError &error) { + /* We'll let the philosophers try to wrestle with the * question of whether failing to remove that which was not * there in the first place is failure. For us, we'll silently * consider it all good. */ + LOG_XAPIAN_EXCEPTION (message, error); } talloc_free (term); + _notmuch_message_invalidate_metadata (message, prefix_name); + return NOTMUCH_PRIVATE_STATUS_SUCCESS; } -notmuch_status_t -notmuch_message_add_tag (notmuch_message_t *message, const char *tag) +notmuch_private_status_t +_notmuch_message_has_term (notmuch_message_t *message, + const char *prefix_name, + const char *value, + bool *result) { - notmuch_private_status_t private_status; - notmuch_status_t status; + char *term; + bool out = false; + notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS; - status = _notmuch_database_ensure_writable (message->notmuch); - if (status) - return status; + if (value == NULL) + return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; - if (tag == NULL) - return NOTMUCH_STATUS_NULL_POINTER; + term = talloc_asprintf (message, "%s%s", + _find_prefix (prefix_name), value); - if (strlen (tag) > NOTMUCH_TAG_MAX) - return NOTMUCH_STATUS_TAG_TOO_LONG; + if (strlen (term) > NOTMUCH_TERM_MAX) + return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; - private_status = _notmuch_message_add_term (message, "tag", tag); - if (private_status) { - INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n", - private_status); + try { + /* Look for the exact term */ + Xapian::TermIterator i = message->doc.termlist_begin (); + i.skip_to (term); + if (i != message->doc.termlist_end () && + ! strcmp ((*i).c_str (), term)) + out = true; + } catch (Xapian::Error &error) { + status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; } + talloc_free (term); - if (! message->frozen) - _notmuch_message_sync (message); - - return NOTMUCH_STATUS_SUCCESS; + *result = out; + return status; } notmuch_status_t -notmuch_message_remove_tag (notmuch_message_t *message, const char *tag) +notmuch_message_add_tag (notmuch_message_t *message, const char *tag) { notmuch_private_status_t private_status; notmuch_status_t status; - status = _notmuch_database_ensure_writable (message->notmuch); - if (status) - return status; + try { + status = _notmuch_database_ensure_writable (message->notmuch); + if (status) + return status; - if (tag == NULL) - return NOTMUCH_STATUS_NULL_POINTER; + if (tag == NULL) + return NOTMUCH_STATUS_NULL_POINTER; - if (strlen (tag) > NOTMUCH_TAG_MAX) - return NOTMUCH_STATUS_TAG_TOO_LONG; + if (strlen (tag) > NOTMUCH_TAG_MAX) + return NOTMUCH_STATUS_TAG_TOO_LONG; - private_status = _notmuch_message_remove_term (message, "tag", tag); - if (private_status) { - INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n", - private_status); - } + private_status = _notmuch_message_add_term (message, "tag", tag); + if (private_status) { + return COERCE_STATUS (private_status, + "_notmuch_message_remove_term return unexpected value: %d\n", + private_status); + } - if (! message->frozen) - _notmuch_message_sync (message); + if (! message->frozen) + _notmuch_message_sync (message); + + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } return NOTMUCH_STATUS_SUCCESS; } notmuch_status_t -notmuch_message_maildir_flags_to_tags (notmuch_message_t *message) +notmuch_message_remove_tag (notmuch_message_t *message, const char *tag) { - const char *flags; + notmuch_private_status_t private_status; notmuch_status_t status; - notmuch_filenames_t *filenames; - const char *filename; - char *combined_flags = talloc_strdup (message, ""); - unsigned i; - int seen_maildir_info = 0; - - for (filenames = notmuch_message_get_filenames (message); - notmuch_filenames_valid (filenames); - notmuch_filenames_move_to_next (filenames)) - { - filename = notmuch_filenames_get (filenames); - - flags = strstr (filename, ":2,"); - if (! flags) - continue; - seen_maildir_info = 1; - flags += 3; - - combined_flags = talloc_strdup_append (combined_flags, flags); - } + try { + status = _notmuch_database_ensure_writable (message->notmuch); + if (status) + return status; - /* If none of the filenames have any maildir info field (not even - * an empty info with no flags set) then there's no information to - * go on, so do nothing. */ - if (! seen_maildir_info) - return NOTMUCH_STATUS_SUCCESS; + if (tag == NULL) + return NOTMUCH_STATUS_NULL_POINTER; - status = notmuch_message_freeze (message); - if (status) - return status; + if (strlen (tag) > NOTMUCH_TAG_MAX) + return NOTMUCH_STATUS_TAG_TOO_LONG; - for (i = 0; i < ARRAY_SIZE(flag2tag); i++) { - if ((strchr (combined_flags, flag2tag[i].flag) != NULL) - ^ - flag2tag[i].inverse) - { - status = notmuch_message_add_tag (message, flag2tag[i].tag); - } else { - status = notmuch_message_remove_tag (message, flag2tag[i].tag); + private_status = _notmuch_message_remove_term (message, "tag", tag); + if (private_status) { + return COERCE_STATUS (private_status, + "_notmuch_message_remove_term return unexpected value: %d\n", + private_status); } - if (status) - return status; - } - status = notmuch_message_thaw (message); - talloc_free (combined_flags); + if (! message->frozen) + _notmuch_message_sync (message); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } - return status; + return NOTMUCH_STATUS_SUCCESS; } /* Is the given filename within a maildir directory? @@ -949,14 +1751,126 @@ _filename_is_in_maildir (const char *filename) dir = slash + 1; if (STRNCMP_LITERAL (dir, "cur/") == 0 || - STRNCMP_LITERAL (dir, "new/") == 0) - { + STRNCMP_LITERAL (dir, "new/") == 0) { return dir; } return NULL; } +static notmuch_status_t +_ensure_maildir_flags (notmuch_message_t *message, bool force) +{ + const char *flags; + notmuch_filenames_t *filenames; + const char *filename, *dir; + char *combined_flags = talloc_strdup (message, ""); + int seen_maildir_info = 0; + + if (message->maildir_flags) { + if (force) { + talloc_free (message->maildir_flags); + message->maildir_flags = NULL; + } + } + filenames = notmuch_message_get_filenames (message); + if (! filenames) + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + for (; + notmuch_filenames_valid (filenames); + notmuch_filenames_move_to_next (filenames)) { + filename = notmuch_filenames_get (filenames); + dir = _filename_is_in_maildir (filename); + + if (! dir) + continue; + + flags = strstr (filename, ":2,"); + if (flags) { + seen_maildir_info = 1; + flags += 3; + combined_flags = talloc_strdup_append (combined_flags, flags); + } else if (STRNCMP_LITERAL (dir, "new/") == 0) { + /* Messages are delivered to new/ with no "info" part, but + * they effectively have default maildir flags. According + * to the spec, we should ignore the info part for + * messages in new/, but some MUAs (mutt) can set maildir + * flags on messages in new/, so we're liberal in what we + * accept. */ + seen_maildir_info = 1; + } + } + if (seen_maildir_info) + message->maildir_flags = combined_flags; + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_bool_t +notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag) +{ + notmuch_status_t status; + notmuch_bool_t ret; + + status = notmuch_message_has_maildir_flag_st (message, flag, &ret); + if (status) + return FALSE; + + return ret; +} + +notmuch_status_t +notmuch_message_has_maildir_flag_st (notmuch_message_t *message, + char flag, + notmuch_bool_t *is_set) +{ + notmuch_status_t status; + + if (! is_set) + return NOTMUCH_STATUS_NULL_POINTER; + + status = _ensure_maildir_flags (message, false); + if (status) + return status; + + *is_set = message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL); + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_message_maildir_flags_to_tags (notmuch_message_t *message) +{ + notmuch_status_t status; + unsigned i; + + status = _ensure_maildir_flags (message, true); + if (status) + return status; + /* If none of the filenames have any maildir info field (not even + * an empty info with no flags set) then there's no information to + * go on, so do nothing. */ + if (! message->maildir_flags) + return NOTMUCH_STATUS_SUCCESS; + + status = notmuch_message_freeze (message); + if (status) + return status; + + for (i = 0; i < ARRAY_SIZE (flag2tag); i++) { + if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL) + ^ + flag2tag[i].inverse) { + status = notmuch_message_add_tag (message, flag2tag[i].tag); + } else { + status = notmuch_message_remove_tag (message, flag2tag[i].tag); + } + if (status) + return status; + } + status = notmuch_message_thaw (message); + + return status; +} + /* From the set of tags on 'message' and the flag2tag table, compute a * set of maildir-flag actions to be taken, (flags that should be * either set or cleared). @@ -979,8 +1893,7 @@ _get_maildir_flag_actions (notmuch_message_t *message, /* First, find flags for all set tags. */ for (tags = notmuch_message_get_tags (message); notmuch_tags_valid (tags); - notmuch_tags_move_to_next (tags)) - { + notmuch_tags_move_to_next (tags)) { tag = notmuch_tags_get (tags); for (i = 0; i < ARRAY_SIZE (flag2tag); i++) { @@ -1016,7 +1929,9 @@ _get_maildir_flag_actions (notmuch_message_t *message, * compute the new maildir filename. * * If the existing filename is in the directory "new", the new - * filename will be in the directory "cur". + * filename will be in the directory "cur", except for the case when + * no flags are changed and the existing filename does not contain + * maildir info (starting with ",2:"). * * After a sequence of ":2," in the filename, any subsequent * single-character flags will be added or removed according to the @@ -1028,7 +1943,7 @@ _get_maildir_flag_actions (notmuch_message_t *message, * non-ASCII ordering of flags), this function will return NULL * (meaning that renaming would not be safe and should not occur). */ -static char* +static char * _new_maildir_filename (void *ctx, const char *filename, const char *flags_to_set, @@ -1039,6 +1954,7 @@ _new_maildir_filename (void *ctx, char *filename_new, *dir; char flag_map[128]; int flags_in_map = 0; + bool flags_changed = false; unsigned int i; char *s; @@ -1047,15 +1963,12 @@ _new_maildir_filename (void *ctx, info = strstr (filename, ":2,"); if (info == NULL) { - info = filename + strlen(filename); + info = filename + strlen (filename); } else { - flags = info + 3; - /* Loop through existing flags in filename. */ for (flags = info + 3, last_flag = 0; *flags; - last_flag = flag, flags++) - { + last_flag = flag, flags++) { flag = *flags; /* Original flags not in ASCII order. Abort. */ @@ -1063,7 +1976,7 @@ _new_maildir_filename (void *ctx, return NULL; /* Non-ASCII flag. Abort. */ - if (flag > sizeof(flag_map) - 1) + if (flag > sizeof (flag_map) - 1) return NULL; /* Repeated flag value. Abort. */ @@ -1081,6 +1994,7 @@ _new_maildir_filename (void *ctx, if (flag_map[flag] == 0) { flag_map[flag] = 1; flags_in_map++; + flags_changed = true; } } @@ -1089,9 +2003,16 @@ _new_maildir_filename (void *ctx, if (flag_map[flag]) { flag_map[flag] = 0; flags_in_map--; + flags_changed = true; } } + /* Messages in new/ without maildir info can be kept in new/ if no + * flags have changed. */ + dir = (char *) _filename_is_in_maildir (filename); + if (dir && STRNCMP_LITERAL (dir, "new/") == 0 && ! *info && ! flags_changed) + return talloc_strdup (ctx, filename); + filename_new = (char *) talloc_size (ctx, info - filename + strlen (":2,") + flags_in_map + 1); @@ -1104,8 +2025,7 @@ _new_maildir_filename (void *ctx, strcat (filename_new, ":2,"); s = filename_new + strlen (filename_new); - for (i = 0; i < sizeof (flag_map); i++) - { + for (i = 0; i < sizeof (flag_map); i++) { if (flag_map[i]) { *s = i; s++; @@ -1130,12 +2050,15 @@ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message) char *to_set, *to_clear; notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + status = _notmuch_database_ensure_writable (message->notmuch); + if (status) + return status; + _get_maildir_flag_actions (message, &to_set, &to_clear); for (filenames = notmuch_message_get_filenames (message); notmuch_filenames_valid (filenames); - notmuch_filenames_move_to_next (filenames)) - { + notmuch_filenames_move_to_next (filenames)) { filename = notmuch_filenames_get (filenames); if (! _filename_is_in_maildir (filename)) @@ -1154,8 +2077,17 @@ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message) if (err) continue; - new_status = _notmuch_message_rename (message, - filename, filename_new); + new_status = _notmuch_message_remove_filename (message, + filename); + /* Hold on to only the first error. */ + if (! status && new_status + && new_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) { + status = new_status; + continue; + } + + new_status = _notmuch_message_add_filename (message, + filename_new); /* Hold on to only the first error. */ if (! status && new_status) { status = new_status; @@ -1171,7 +2103,7 @@ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message) talloc_free (to_set); talloc_free (to_clear); - return NOTMUCH_STATUS_SUCCESS; + return status; } notmuch_status_t @@ -1185,23 +2117,27 @@ notmuch_message_remove_all_tags (notmuch_message_t *message) status = _notmuch_database_ensure_writable (message->notmuch); if (status) return status; + tags = notmuch_message_get_tags (message); + if (! tags) + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; - for (tags = notmuch_message_get_tags (message); + for (; notmuch_tags_valid (tags); - notmuch_tags_move_to_next (tags)) - { + notmuch_tags_move_to_next (tags)) { tag = notmuch_tags_get (tags); private_status = _notmuch_message_remove_term (message, "tag", tag); if (private_status) { - INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n", - private_status); + return COERCE_STATUS (private_status, + "_notmuch_message_remove_term return unexpected value: %d\n", + private_status); } } if (! message->frozen) _notmuch_message_sync (message); + talloc_free (tags); return NOTMUCH_STATUS_SUCCESS; } @@ -1243,3 +2179,180 @@ notmuch_message_destroy (notmuch_message_t *message) { talloc_free (message); } + +notmuch_database_t * +notmuch_message_get_database (const notmuch_message_t *message) +{ + return message->notmuch; +} + +static void +_notmuch_message_ensure_property_map (notmuch_message_t *message) +{ + notmuch_string_node_t *node; + + if (message->property_map) + return; + + _notmuch_message_ensure_metadata (message, message->property_term_list); + + message->property_map = _notmuch_string_map_create (message); + + for (node = message->property_term_list->head; node; node = node->next) { + const char *key; + char *value; + + value = strchr (node->string, '='); + if (! value) + INTERNAL_ERROR ("malformed property term"); + + *value = '\0'; + value++; + key = node->string; + + _notmuch_string_map_append (message->property_map, key, value); + + } + + talloc_free (message->property_term_list); + message->property_term_list = NULL; +} + +notmuch_string_map_t * +_notmuch_message_property_map (notmuch_message_t *message) +{ + _notmuch_message_ensure_property_map (message); + + return message->property_map; +} + +bool +_notmuch_message_frozen (notmuch_message_t *message) +{ + return message->frozen; +} + +notmuch_status_t +notmuch_message_reindex (notmuch_message_t *message, + notmuch_indexopts_t *indexopts) +{ + notmuch_database_t *notmuch = NULL; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + notmuch_private_status_t private_status; + notmuch_filenames_t *orig_filenames = NULL; + const char *orig_thread_id = NULL; + notmuch_message_file_t *message_file = NULL; + + int found = 0; + + if (message == NULL) + return NOTMUCH_STATUS_NULL_POINTER; + + /* Save in case we need to delete message */ + orig_thread_id = notmuch_message_get_thread_id (message); + if (! orig_thread_id) { + /* the following is correct as long as there is only one reason + * n_m_get_thread_id returns NULL + */ + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + /* strdup it because the metadata may be invalidated */ + orig_thread_id = talloc_strdup (message, orig_thread_id); + + notmuch = notmuch_message_get_database (message); + + ret = _notmuch_database_ensure_writable (notmuch); + if (ret) + return ret; + + orig_filenames = notmuch_message_get_filenames (message); + + private_status = _notmuch_message_remove_indexed_terms (message); + if (private_status) { + ret = COERCE_STATUS (private_status, "error removing terms"); + goto DONE; + } + + ret = notmuch_message_remove_all_properties_with_prefix (message, "index."); + if (ret) + goto DONE; /* XXX TODO: distinguish from other error returns above? */ + if (indexopts && notmuch_indexopts_get_decrypt_policy (indexopts) == NOTMUCH_DECRYPT_FALSE) { + ret = notmuch_message_remove_all_properties (message, "session-key"); + if (ret) + goto DONE; + } + + /* re-add the filenames with the associated indexopts */ + for (; notmuch_filenames_valid (orig_filenames); + notmuch_filenames_move_to_next (orig_filenames)) { + + const char *date; + const char *from, *to, *subject; + char *message_id = NULL; + const char *thread_id = NULL; + + const char *filename = notmuch_filenames_get (orig_filenames); + + message_file = _notmuch_message_file_open (notmuch, filename); + if (message_file == NULL) + continue; + + ret = _notmuch_message_file_get_headers (message_file, + &from, &subject, &to, &date, + &message_id); + if (ret) + goto DONE; + + /* XXX TODO: deal with changing message id? */ + + _notmuch_message_add_filename (message, filename); + + ret = _notmuch_database_link_message_to_parents (notmuch, message, + message_file, + &thread_id); + if (ret) + goto DONE; + + if (thread_id == NULL) + thread_id = orig_thread_id; + + ret = COERCE_STATUS (_notmuch_message_add_term (message, "thread", thread_id), + "adding thread term"); + if (ret) + goto DONE; + + /* Take header values only from first filename */ + if (found == 0) + _notmuch_message_set_header_values (message, date, from, subject); + + ret = _notmuch_message_index_file (message, indexopts, message_file); + + if (ret == NOTMUCH_STATUS_FILE_ERROR) + continue; + if (ret) + goto DONE; + + found++; + _notmuch_message_file_close (message_file); + message_file = NULL; + } + if (found == 0) { + /* put back thread id to help cleanup */ + ret = COERCE_STATUS (_notmuch_message_add_term (message, "thread", orig_thread_id), + "adding thread term"); + if (ret) + goto DONE; + + ret = _notmuch_message_delete (message); + } else { + _notmuch_message_sync (message); + } + + DONE: + if (message_file) + _notmuch_message_file_close (message_file); + + /* XXX TODO destroy orig_filenames? */ + return ret; +}