]> git.notmuchmail.org Git - notmuch/blobdiff - lib/thread.cc
Merge branch 'release'
[notmuch] / lib / thread.cc
index 8f53e12231f2e3f6a93f70997bb188208a990c76..47c9066406e084c51735b0dc6960055904763659 100644 (file)
@@ -13,7 +13,7 @@
  * 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 <cworth@cworth.org>
  */
 #include <gmime/gmime.h>
 #include <glib.h> /* GHashTable */
 
-struct visible _notmuch_thread {
+#ifdef DEBUG_THREADING
+#define THREAD_DEBUG(format, ...) fprintf(stderr, format " (%s).\n", ##__VA_ARGS__, __location__)
+#else
+#define THREAD_DEBUG(format, ...) do {} while (0) /* ignored */
+#endif
+
+#define EMPTY_STRING(s) ((s)[0] == '\0')
+
+struct _notmuch_thread {
     notmuch_database_t *notmuch;
     char *thread_id;
     char *subject;
@@ -42,6 +50,7 @@ struct visible _notmuch_thread {
 
     GHashTable *message_hash;
     int total_messages;
+    int total_files;
     int matched_messages;
     time_t oldest;
     time_t newest;
@@ -56,12 +65,12 @@ _notmuch_thread_destructor (notmuch_thread_t *thread)
     g_hash_table_unref (thread->message_hash);
 
     if (thread->authors_array) {
-       g_ptr_array_free (thread->authors_array, TRUE);
+       g_ptr_array_free (thread->authors_array, true);
        thread->authors_array = NULL;
     }
 
     if (thread->matched_authors_array) {
-       g_ptr_array_free (thread->matched_authors_array, TRUE);
+       g_ptr_array_free (thread->matched_authors_array, true);
        thread->matched_authors_array = NULL;
     }
 
@@ -153,10 +162,13 @@ _resolve_thread_authors_string (notmuch_thread_t *thread)
        first_non_matched_author = 0;
     }
 
-    g_ptr_array_free (thread->authors_array, TRUE);
+    g_ptr_array_free (thread->authors_array, true);
     thread->authors_array = NULL;
-    g_ptr_array_free (thread->matched_authors_array, TRUE);
+    g_ptr_array_free (thread->matched_authors_array, true);
     thread->matched_authors_array = NULL;
+
+    if (!thread->authors)
+       thread->authors = talloc_strdup(thread, "");
 }
 
 /* clean up the ugly "Lastname, Firstname" format that some mail systems
@@ -236,7 +248,7 @@ _thread_add_message (notmuch_thread_t *thread,
     InternetAddress *address;
     const char *from, *author;
     char *clean_author;
-    notmuch_bool_t message_excluded = FALSE;
+    bool message_excluded = false;
 
     if (omit_exclude != NOTMUCH_EXCLUDE_FALSE) {
        for (tags = notmuch_message_get_tags (message);
@@ -249,9 +261,9 @@ _thread_add_message (notmuch_thread_t *thread,
                 term != NULL;
                 term = term->next)
            {
-               /* We ignore initial 'K'. */
-               if (strcmp(tag, (term->string + 1)) == 0) {
-                   message_excluded = TRUE;
+               /* Check for an empty string, and then ignore initial 'K'. */
+               if (*(term->string) && strcmp(tag, (term->string + 1)) == 0) {
+                   message_excluded = true;
                    break;
                }
            }
@@ -264,6 +276,7 @@ _thread_add_message (notmuch_thread_t *thread,
     _notmuch_message_list_add_message (thread->message_list,
                                       talloc_steal (thread, message));
     thread->total_messages++;
+    thread->total_files += notmuch_message_count_files (message);
 
     g_hash_table_insert (thread->message_hash,
                         xstrdup (notmuch_message_get_message_id (message)),
@@ -277,14 +290,15 @@ _thread_add_message (notmuch_thread_t *thread,
        address = internet_address_list_get_address (list, 0);
        if (address) {
            author = internet_address_get_name (address);
-           if (author == NULL) {
+           /* We treat quoted empty names as if they were empty. */
+           if (author == NULL || author[0] == '\0') {
                InternetAddressMailbox *mailbox;
                mailbox = INTERNET_ADDRESS_MAILBOX (address);
                author = internet_address_mailbox_get_addr (mailbox);
            }
            clean_author = _thread_cleanup_author (thread, author, from);
            _thread_add_author (thread, clean_author);
-           notmuch_message_set_author (message, clean_author);
+           _notmuch_message_set_author (message, clean_author);
        }
        g_object_unref (G_OBJECT (list));
     }
@@ -305,7 +319,7 @@ _thread_add_message (notmuch_thread_t *thread,
 
     /* Mark excluded messages. */
     if (message_excluded)
-       notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, TRUE);
+       notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, true);
 }
 
 static void
@@ -331,10 +345,12 @@ _thread_set_subject_from_message (notmuch_thread_t *thread,
        cleaned_subject = talloc_strdup (thread, subject);
     }
 
-    if (thread->subject)
-       talloc_free (thread->subject);
+    if (! EMPTY_STRING(cleaned_subject)) {
+       if (thread->subject)
+           talloc_free (thread->subject);
 
-    thread->subject = talloc_strdup (thread, cleaned_subject);
+       thread->subject = talloc_strdup (thread, cleaned_subject);
+    }
 }
 
 /* Add a message to this thread which is known to match the original
@@ -359,7 +375,8 @@ _thread_add_matched_message (notmuch_thread_t *thread,
 
     if (date > thread->newest || ! thread->matched_messages) {
        thread->newest = date;
-       if (sort != NOTMUCH_SORT_OLDEST_FIRST)
+       const char *cur_subject = notmuch_thread_get_subject(thread);
+       if (sort != NOTMUCH_SORT_OLDEST_FIRST || EMPTY_STRING(cur_subject))
            _thread_set_subject_from_message (thread, message);
     }
 
@@ -373,38 +390,133 @@ _thread_add_matched_message (notmuch_thread_t *thread,
                                  NOTMUCH_MESSAGE_FLAG_MATCH, 1);
     }
 
-    _thread_add_matched_author (thread, notmuch_message_get_author (hashed_message));
+    _thread_add_matched_author (thread, _notmuch_message_get_author (hashed_message));
+}
+
+static bool
+_parent_via_in_reply_to (notmuch_thread_t *thread, notmuch_message_t *message) {
+    notmuch_message_t *parent;
+    const char *in_reply_to;
+
+    in_reply_to = _notmuch_message_get_in_reply_to (message);
+    THREAD_DEBUG("checking message = %s in_reply_to=%s\n",
+                notmuch_message_get_message_id (message), in_reply_to);
+
+    if (in_reply_to && (! EMPTY_STRING(in_reply_to)) &&
+       g_hash_table_lookup_extended (thread->message_hash,
+                                     in_reply_to, NULL,
+                                     (void **) &parent)) {
+       _notmuch_message_add_reply (parent, message);
+       return true;
+    } else {
+       return false;
+    }
+}
+
+static void
+_parent_or_toplevel (notmuch_thread_t *thread, notmuch_message_t *message)
+{
+    size_t max_depth = 0;
+    notmuch_message_t *new_parent;
+    notmuch_message_t *parent = NULL;
+    const notmuch_string_list_t *references =
+       _notmuch_message_get_references (message);
+
+    THREAD_DEBUG("trying to reparent via references: %s\n",
+                    notmuch_message_get_message_id (message));
+
+    for (notmuch_string_node_t *ref_node = references->head;
+        ref_node; ref_node = ref_node->next) {
+       THREAD_DEBUG("checking reference=%s\n", ref_node->string);
+       if ((g_hash_table_lookup_extended (thread->message_hash,
+                                          ref_node->string, NULL,
+                                          (void **) &new_parent))) {
+           size_t new_depth = _notmuch_message_get_thread_depth (new_parent);
+           THREAD_DEBUG("got depth %lu\n", new_depth);
+           if (new_depth > max_depth || !parent) {
+               THREAD_DEBUG("adding at depth %lu parent=%s\n", new_depth, ref_node->string);
+               max_depth = new_depth;
+               parent = new_parent;
+           }
+       }
+    }
+    if (parent) {
+       THREAD_DEBUG("adding reply %s to parent=%s\n",
+                notmuch_message_get_message_id (message),
+                notmuch_message_get_message_id (parent));
+       _notmuch_message_add_reply (parent, message);
+    } else {
+       THREAD_DEBUG("adding as toplevel %s\n",
+                notmuch_message_get_message_id (message));
+       _notmuch_message_list_add_message (thread->toplevel_list, message);
+    }
 }
 
 static void
 _resolve_thread_relationships (notmuch_thread_t *thread)
 {
-    notmuch_message_node_t *node;
-    notmuch_message_t *message, *parent;
-    const char *in_reply_to;
+    notmuch_message_node_t *node, *first_node;
+    notmuch_message_t *message;
+    void *local;
+    notmuch_message_list_t *maybe_toplevel_list;
+
+    first_node = thread->message_list->head;
+    if (! first_node)
+       return;
+
+    local = talloc_new (thread);
+    maybe_toplevel_list = _notmuch_message_list_create (local);
 
-    for (node = thread->message_list->head; node; node = node->next) {
+    for (node = first_node->next; node; node = node->next) {
        message = node->message;
-       in_reply_to = _notmuch_message_get_in_reply_to (message);
-       if (in_reply_to && strlen (in_reply_to) &&
-           g_hash_table_lookup_extended (thread->message_hash,
-                                         in_reply_to, NULL,
-                                         (void **) &parent))
-           _notmuch_message_add_reply (parent, message);
+       if (! _parent_via_in_reply_to (thread, message))
+           _notmuch_message_list_add_message (maybe_toplevel_list, message);
+    }
+
+    /*
+     * if we reach the end of the list without finding a top-level
+     * message, that means the thread is a cycle (or set of cycles)
+     * and any message can be considered top-level.  Choose the oldest
+     * message, which happens to be first in our list.
+     */
+    if (first_node) {
+       message = first_node->message;
+       THREAD_DEBUG("checking first message  %s\n",
+                    notmuch_message_get_message_id (message));
+
+        if (_notmuch_message_list_empty (maybe_toplevel_list) ||
+           ! _parent_via_in_reply_to (thread, message)) {
+
+           THREAD_DEBUG("adding first message as toplevel = %s\n",
+                        notmuch_message_get_message_id (message));
+           _notmuch_message_list_add_message (maybe_toplevel_list, message);
+       }
+    }
+
+    for (notmuch_messages_t *messages = _notmuch_messages_create (maybe_toplevel_list);
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
+    {
+       notmuch_message_t *message = notmuch_messages_get (messages);
+       _notmuch_message_label_depths (message, 0);
+    }
+
+    for (notmuch_messages_t *roots = _notmuch_messages_create (maybe_toplevel_list);
+        notmuch_messages_valid (roots);
+        notmuch_messages_move_to_next (roots)) {
+       notmuch_message_t *message = notmuch_messages_get (roots);
+       if (_notmuch_messages_has_next (roots) || ! _notmuch_message_list_empty (thread->toplevel_list))
+           _parent_or_toplevel (thread, message);
        else
            _notmuch_message_list_add_message (thread->toplevel_list, message);
     }
 
-    /* XXX: After scanning through the entire list looking for parents
-     * via "In-Reply-To", we should do a second pass that looks at the
-     * list of messages IDs in the "References" header instead. (And
-     * for this the parent would be the "deepest" message of all the
-     * messages found in the "References" list.)
-     *
-     * Doing this will allow messages and sub-threads to be positioned
-     * correctly in the thread even when an intermediate message is
-     * missing from the thread.
+    /* XXX this could be made conditional on messages being inserted
+     * (out of order) in later passes
      */
+    thread->toplevel_list = _notmuch_message_sort_subtrees (thread, thread->toplevel_list);
+
+    talloc_free (local);
 }
 
 /* Create a new notmuch_thread_t object by finding the thread
@@ -441,6 +553,7 @@ _notmuch_thread_create (void *ctx,
 
     notmuch_messages_t *messages;
     notmuch_message_t *message;
+    notmuch_status_t status;
 
     seed_message = _notmuch_message_create (local, notmuch, seed_doc_id, NULL);
     if (! seed_message)
@@ -488,6 +601,7 @@ _notmuch_thread_create (void *ctx,
                                                  free, NULL);
 
     thread->total_messages = 0;
+    thread->total_files = 0;
     thread->matched_messages = 0;
     thread->oldest = 0;
     thread->newest = 0;
@@ -498,7 +612,11 @@ _notmuch_thread_create (void *ctx,
      * oldest or newest subject is desired. */
     notmuch_query_set_sort (thread_id_query, NOTMUCH_SORT_OLDEST_FIRST);
 
-    for (messages = notmuch_query_search_messages (thread_id_query);
+    status = notmuch_query_search_messages (thread_id_query, &messages);
+    if (status)
+       goto DONE;
+
+    for (;
         notmuch_messages_valid (messages);
         notmuch_messages_move_to_next (messages))
     {
@@ -555,6 +673,12 @@ notmuch_thread_get_total_messages (notmuch_thread_t *thread)
     return thread->total_messages;
 }
 
+int
+notmuch_thread_get_total_files (notmuch_thread_t *thread)
+{
+    return thread->total_files;
+}
+
 int
 notmuch_thread_get_matched_messages (notmuch_thread_t *thread)
 {