]> git.notmuchmail.org Git - notmuch/blobdiff - lib/message.cc
cli/reindex: destroy stashed session keys when --decrypt=false
[notmuch] / lib / message.cc
index 9d3e80712fd5655f8a5b412b43803df18794af61..d5db89b6606c7da011c2a80c5ce460f769f32650 100644 (file)
@@ -26,7 +26,7 @@
 
 #include <gmime/gmime.h>
 
-struct visible _notmuch_message {
+struct _notmuch_message {
     notmuch_database_t *notmuch;
     Xapian::docid doc_id;
     int frozen;
@@ -36,6 +36,7 @@ struct visible _notmuch_message {
     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;
@@ -47,7 +48,10 @@ struct visible _notmuch_message {
     unsigned long lazy_flags;
 
     /* Message document modified since last sync */
-    notmuch_bool_t modified;
+    bool modified;
+
+    /* last view of database the struct is synced with */
+    unsigned long last_view;
 
     Xapian::Document doc;
     Xapian::termcount termpos;
@@ -58,16 +62,16 @@ struct visible _notmuch_message {
 struct maildir_flag_tag {
     char flag;
     const char *tag;
-    notmuch_bool_t inverse;
+    bool inverse;
 };
 
 /* 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},
-    { 'S', "unread",  TRUE }
+    { 'D', "draft",   false},
+    { 'F', "flagged", false},
+    { 'P', "passed",  false},
+    { 'R', "replied", false},
+    { 'S', "unread",  true }
 };
 
 /* We end up having to call the destructor explicitly because we had
@@ -110,6 +114,9 @@ _notmuch_message_create_for_document (const void *talloc_owner,
     message->flags = 0;
     message->lazy_flags = 0;
 
+    /* the message is initially not synchronized with Xapian */
+    message->last_view = 0;
+
     /* Each of these will be lazily created as needed. */
     message->message_id = NULL;
     message->thread_id = NULL;
@@ -117,6 +124,7 @@ _notmuch_message_create_for_document (const void *talloc_owner,
     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;
@@ -262,7 +270,7 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
     } catch (const Xapian::Error &error) {
        _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred creating message: %s\n",
                 error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
+       notmuch->exception_reported = true;
        *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
        return NULL;
     }
@@ -310,10 +318,14 @@ _notmuch_message_get_term (notmuch_message_t *message,
     return value;
 }
 
-void
-_notmuch_message_ensure_metadata (notmuch_message_t *message)
+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"),
@@ -327,73 +339,88 @@ _notmuch_message_ensure_metadata (notmuch_message_t *message)
      * 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);
+           }
 
-    i = message->doc.termlist_begin ();
-    end = message->doc.termlist_end ();
+           /* 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 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 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 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 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);
 
-    /* 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 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, "");
 
-
-    /* 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);
+           if (status != NOTMUCH_STATUS_SUCCESS)
+               INTERNAL_ERROR ("unhandled error from notmuch_database_reopen: %s\n",
+                               notmuch_status_to_string (status));
+       } catch (const Xapian::Error &error) {
+           INTERNAL_ERROR ("A Xapian exception occurred fetching message metadata: %s\n",
+                           error.get_msg().c_str());
+       }
+    }
+    message->last_view = message->notmuch->view;
 }
 
 void
@@ -448,8 +475,7 @@ _notmuch_message_get_doc_id (notmuch_message_t *message)
 const char *
 notmuch_message_get_message_id (notmuch_message_t *message)
 {
-    if (!message->message_id)
-       _notmuch_message_ensure_metadata (message);
+    _notmuch_message_ensure_metadata (message, message->message_id);
     if (!message->message_id)
        INTERNAL_ERROR ("Message with document ID of %u has no message ID.\n",
                        message->doc_id);
@@ -501,7 +527,7 @@ notmuch_message_get_header (notmuch_message_t *message, const char *header)
        } catch (Xapian::Error &error) {
            _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred when reading header: %s\n",
                     error.get_msg().c_str());
-           message->notmuch->exception_reported = TRUE;
+           message->notmuch->exception_reported = true;
            return NULL;
        }
     }
@@ -524,16 +550,14 @@ notmuch_message_get_header (notmuch_message_t *message, const char *header)
 const char *
 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
 {
-    if (!message->in_reply_to)
-       _notmuch_message_ensure_metadata (message);
+    _notmuch_message_ensure_metadata (message, message->in_reply_to);
     return message->in_reply_to;
 }
 
 const char *
 notmuch_message_get_thread_id (notmuch_message_t *message)
 {
-    if (!message->thread_id)
-       _notmuch_message_ensure_metadata (message);
+    _notmuch_message_ensure_metadata (message, message->thread_id);
     if (!message->thread_id)
        INTERNAL_ERROR ("Message with document ID of %u has no thread ID.\n",
                        message->doc_id);
@@ -557,7 +581,9 @@ void
 _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
 {
     Xapian::TermIterator i;
-    size_t prefix_len = strlen (prefix);
+    size_t prefix_len = 0;
+
+    prefix_len = strlen (prefix);
 
     while (1) {
        i = message->doc.termlist_begin ();
@@ -570,13 +596,66 @@ _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
 
        try {
            message->doc.remove_term ((*i));
-           message->modified = TRUE;
+           message->modified = true;
        } catch (const Xapian::InvalidArgumentError) {
            /* Ignore failure to remove non-existent term. */
        }
     }
 }
 
+
+/* Remove all terms generated by indexing, i.e. not tags or
+ * properties, along with any automatic tags*/
+notmuch_private_status_t
+_notmuch_message_remove_indexed_terms (notmuch_message_t *message)
+{
+    Xapian::TermIterator i;
+
+    const std::string
+       id_prefix = _find_prefix ("id"),
+       property_prefix = _find_prefix ("property"),
+       tag_prefix = _find_prefix ("tag"),
+       type_prefix = _find_prefix ("type");
+
+    for (i = message->doc.termlist_begin ();
+        i != message->doc.termlist_end (); i++) {
+
+       const std::string term = *i;
+
+       if (term.compare (0, type_prefix.size (), type_prefix) == 0)
+           continue;
+
+       if (term.compare (0, id_prefix.size (), id_prefix) == 0)
+           continue;
+
+       if (term.compare (0, property_prefix.size (), property_prefix) == 0)
+           continue;
+
+       if (term.compare (0, tag_prefix.size (), tag_prefix) == 0 &&
+           term.compare (1, strlen("encrypted"), "encrypted") != 0 &&
+           term.compare (1, strlen("signed"), "signed") != 0 &&
+           term.compare (1, strlen("attachment"), "attachment") != 0)
+           continue;
+
+       try {
+           message->doc.remove_term ((*i));
+           message->modified = true;
+       } catch (const Xapian::InvalidArgumentError) {
+           /* Ignore failure to remove non-existent term. */
+       } catch (const Xapian::Error &error) {
+           notmuch_database_t *notmuch = message->notmuch;
+
+           if (!notmuch->exception_reported) {
+               _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred creating message: %s\n",
+                                     error.get_msg().c_str());
+               notmuch->exception_reported = true;
+           }
+           return NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+       }
+    }
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
 /* Return true if p points at "new" or "cur". */
 static bool is_maildir (const char *p)
 {
@@ -624,6 +703,7 @@ _notmuch_message_add_folder_terms (notmuch_message_t *message,
 
     talloc_free (folder);
 
+    message->modified = true;
     return NOTMUCH_STATUS_SUCCESS;
 }
 
@@ -825,7 +905,7 @@ void
 _notmuch_message_clear_data (notmuch_message_t *message)
 {
     message->doc.set_data ("");
-    message->modified = TRUE;
+    message->modified = true;
 }
 
 static void
@@ -836,8 +916,7 @@ _notmuch_message_ensure_filename_list (notmuch_message_t *message)
     if (message->filename_list)
        return;
 
-    if (!message->filename_term_list)
-       _notmuch_message_ensure_metadata (message);
+    _notmuch_message_ensure_metadata (message, message->filename_term_list);
 
     message->filename_list = _notmuch_string_list_create (message);
     node = message->filename_term_list->head;
@@ -849,9 +928,9 @@ _notmuch_message_ensure_filename_list (notmuch_message_t *message)
         *
         * 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");
@@ -925,13 +1004,21 @@ notmuch_message_get_filenames (notmuch_message_t *message)
     return _notmuch_filenames_create (message, message->filename_list);
 }
 
+int
+notmuch_message_count_files (notmuch_message_t *message)
+{
+    _notmuch_message_ensure_filename_list (message);
+
+    return _notmuch_string_list_length (message->filename_list);
+}
+
 notmuch_bool_t
 notmuch_message_get_flag (notmuch_message_t *message,
                          notmuch_message_flag_t flag)
 {
     if (flag == NOTMUCH_MESSAGE_FLAG_GHOST &&
        ! NOTMUCH_TEST_BIT (message->lazy_flags, flag))
-       _notmuch_message_ensure_metadata (message);
+       _notmuch_message_ensure_metadata (message, NULL);
 
     return NOTMUCH_TEST_BIT (message->flags, flag);
 }
@@ -957,7 +1044,7 @@ notmuch_message_get_date (notmuch_message_t *message)
     } catch (Xapian::Error &error) {
        _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred when reading date: %s\n",
                 error.get_msg().c_str());
-       message->notmuch->exception_reported = TRUE;
+       message->notmuch->exception_reported = true;
        return 0;
     }
 
@@ -972,8 +1059,7 @@ notmuch_message_get_tags (notmuch_message_t *message)
 {
     notmuch_tags_t *tags;
 
-    if (!message->tag_list)
-       _notmuch_message_ensure_metadata (message);
+    _notmuch_message_ensure_metadata (message, message->tag_list);
 
     tags = _notmuch_tags_create (message, message->tag_list);
     /* _notmuch_tags_create steals the reference to the tag_list, but
@@ -1014,16 +1100,22 @@ _notmuch_message_set_header_values (notmuch_message_t *message,
 
     /* 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;
+    message->modified = true;
 }
 
 /* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD.  The caller
@@ -1033,7 +1125,7 @@ _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;
+    message->modified = true;
 }
 
 /* Synchronize changes made to message->doc out into the database. */
@@ -1062,7 +1154,7 @@ _notmuch_message_sync (notmuch_message_t *message)
 
     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
     db->replace_document (message->doc_id, message->doc);
-    message->modified = FALSE;
+    message->modified = false;
 }
 
 /* Delete a message document from the database, leaving a ghost
@@ -1078,7 +1170,7 @@ _notmuch_message_delete (notmuch_message_t *message)
     notmuch_database_t *notmuch;
     notmuch_query_t *query;
     unsigned int count = 0;
-    notmuch_bool_t is_ghost;
+    bool is_ghost;
 
     mid = notmuch_message_get_message_id (message);
     tid = notmuch_message_get_thread_id (message);
@@ -1103,7 +1195,7 @@ _notmuch_message_delete (notmuch_message_t *message)
     query = notmuch_query_create (notmuch, query_string);
     if (query == NULL)
        return NOTMUCH_STATUS_OUT_OF_MEMORY;
-    status = notmuch_query_count_messages_st (query, &count);
+    status = notmuch_query_count_messages (query, &count);
     if (status) {
        notmuch_query_destroy (query);
        return status;
@@ -1207,7 +1299,7 @@ _notmuch_message_add_term (notmuch_message_t *message,
        return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
 
     message->doc.add_term (term, 0);
-    message->modified = TRUE;
+    message->modified = true;
 
     talloc_free (term);
 
@@ -1276,7 +1368,7 @@ _notmuch_message_remove_term (notmuch_message_t *message,
 
     try {
        message->doc.remove_term (term);
-       message->modified = TRUE;
+       message->modified = true;
     } catch (const Xapian::InvalidArgumentError) {
        /* We'll let the philosophers try to wrestle with the
         * question of whether failing to remove that which was not
@@ -1295,10 +1387,10 @@ notmuch_private_status_t
 _notmuch_message_has_term (notmuch_message_t *message,
                           const char *prefix_name,
                           const char *value,
-                          notmuch_bool_t *result)
+                          bool *result)
 {
     char *term;
-    notmuch_bool_t out = FALSE;
+    bool out = false;
     notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
 
     if (value == NULL)
@@ -1316,7 +1408,7 @@ _notmuch_message_has_term (notmuch_message_t *message,
        i.skip_to (term);
        if (i != message->doc.termlist_end () &&
            !strcmp ((*i).c_str (), term))
-           out = TRUE;
+           out = true;
     } catch (Xapian::Error &error) {
        status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
     }
@@ -1423,17 +1515,22 @@ _filename_is_in_maildir (const char *filename)
     return NULL;
 }
 
-notmuch_status_t
-notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
+static void
+_ensure_maildir_flags (notmuch_message_t *message, bool force)
 {
     const char *flags;
-    notmuch_status_t status;
     notmuch_filenames_t *filenames;
     const char *filename, *dir;
     char *combined_flags = talloc_strdup (message, "");
-    unsigned i;
     int seen_maildir_info = 0;
 
+    if (message->maildir_flags) {
+       if (force) {
+           talloc_free (message->maildir_flags);
+           message->maildir_flags = NULL;
+       }
+    }
+
     for (filenames = notmuch_message_get_filenames (message);
         notmuch_filenames_valid (filenames);
         notmuch_filenames_move_to_next (filenames))
@@ -1459,11 +1556,28 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
            seen_maildir_info = 1;
        }
     }
+    if (seen_maildir_info)
+       message->maildir_flags = combined_flags;
+}
+
+notmuch_bool_t
+notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag)
+{
+    _ensure_maildir_flags (message, false);
+    return message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL);
+}
+
+notmuch_status_t
+notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
+{
+    notmuch_status_t status;
+    unsigned i;
 
+    _ensure_maildir_flags (message, true);
     /* 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)
+    if (! message->maildir_flags)
        return NOTMUCH_STATUS_SUCCESS;
 
     status = notmuch_message_freeze (message);
@@ -1471,7 +1585,7 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
        return status;
 
     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
-       if ((strchr (combined_flags, flag2tag[i].flag) != NULL)
+       if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL)
            ^
            flag2tag[i].inverse)
        {
@@ -1484,8 +1598,6 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
     }
     status = notmuch_message_thaw (message);
 
-    talloc_free (combined_flags);
-
     return status;
 }
 
@@ -1573,7 +1685,7 @@ _new_maildir_filename (void *ctx,
     char *filename_new, *dir;
     char flag_map[128];
     int flags_in_map = 0;
-    notmuch_bool_t flags_changed = FALSE;
+    bool flags_changed = false;
     unsigned int i;
     char *s;
 
@@ -1614,7 +1726,7 @@ _new_maildir_filename (void *ctx,
        if (flag_map[flag] == 0) {
            flag_map[flag] = 1;
            flags_in_map++;
-           flags_changed = TRUE;
+           flags_changed = true;
        }
     }
 
@@ -1623,7 +1735,7 @@ _new_maildir_filename (void *ctx,
        if (flag_map[flag]) {
            flag_map[flag] = 0;
            flags_in_map--;
-           flags_changed = TRUE;
+           flags_changed = true;
        }
     }
 
@@ -1801,7 +1913,7 @@ _notmuch_message_database (notmuch_message_t *message)
     return message->notmuch;
 }
 
-void
+static void
 _notmuch_message_ensure_property_map (notmuch_message_t *message)
 {
     notmuch_string_node_t *node;
@@ -1809,8 +1921,7 @@ _notmuch_message_ensure_property_map (notmuch_message_t *message)
     if (message->property_map)
        return;
 
-    if (!message->property_term_list)
-       _notmuch_message_ensure_metadata (message);
+    _notmuch_message_ensure_metadata (message, message->property_term_list);
 
     message->property_map = _notmuch_string_map_create (message);
 
@@ -1818,7 +1929,7 @@ _notmuch_message_ensure_property_map (notmuch_message_t *message)
        const char *key;
        char *value;
 
-       value = index(node->string, '=');
+       value = strchr(node->string, '=');
        if (!value)
            INTERNAL_ERROR ("malformed property term");
 
@@ -1842,8 +1953,123 @@ _notmuch_message_property_map (notmuch_message_t *message)
     return message->property_map;
 }
 
-notmuch_bool_t
+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) {
+       /* XXX TODO: make up new error return? */
+       INTERNAL_ERROR ("message without thread-id");
+    }
+
+    /* strdup it because the metadata may be invalidated */
+    orig_thread_id = talloc_strdup (message, orig_thread_id);
+
+    notmuch = _notmuch_message_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;
+
+       _notmuch_message_add_term (message, "thread", thread_id);
+       /* 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 */
+       _notmuch_message_add_term (message, "thread", orig_thread_id);
+       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;
+}