* id: Unique ID of mail, (from Message-ID header or generated
* as "notmuch-sha1-<sha1_sum_of_entire_file>.
*
+ * thread: The ID of the thread to which the mail belongs
+ *
* Multiple terms of given prefix:
*
- * ref: The message IDs from all In-Reply-To and References
- * headers in the message.
+ * ref: All unresolved message IDs from In-Reply-To and
+ * References headers in the message. (Once a referenced
+ * message is added to the database and the thread IDs
+ * are linked the corresponding "ref" term is dropped
+ * from the message document.)
*
* tag: Any tags associated with this message by the user.
*
- * thread: The thread ID of all threads to which the mail belongs
- *
* A mail document also has two values:
*
* TIMESTAMP: The time_t value corresponding to the message's
{ "id", "Q" }
};
+int
+_internal_error (const char *format, ...)
+{
+ va_list va_args;
+
+ va_start (va_args, format);
+
+ vfprintf (stderr, format, va_args);
+
+ exit (1);
+
+ return 1;
+}
+
const char *
_find_prefix (const char *name)
{
return NOTMUCH_PRIVATE_STATUS_SUCCESS;
}
-/* XXX: Should rewrite this to accept a notmuch_message_t* instead of
- * a Xapian:Document and then we could just use
- * notmuch_message_get_thread_ids instead of duplicating its logic
- * here. */
-static void
-insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
-{
- string value_string;
- Xapian::TermIterator i;
- const char *prefix_str = _find_prefix ("thread");
- char prefix;
-
- assert (strlen (prefix_str) == 1);
-
- prefix = *prefix_str;
-
- i = doc.termlist_begin ();
- i.skip_to (prefix_str);
-
- while (1) {
- if (i == doc.termlist_end ())
- break;
- value_string = *i;
- if (value_string.empty () || value_string[0] != prefix)
- break;
- g_hash_table_insert (thread_ids,
- strdup (value_string.c_str () + 1), NULL);
- i++;
- }
-}
-
notmuch_message_t *
notmuch_database_find_message (notmuch_database_t *notmuch,
const char *message_id)
return _notmuch_message_create (notmuch, notmuch, doc_id, NULL);
}
-/* Return one or more thread_ids, (as a GPtrArray of strings), for the
- * given message based on looking into the database for any messages
- * referenced in parents, and also for any messages in the database
- * referencing message_id.
- *
- * Caller should free all strings in the array and the array itself,
- * (g_ptr_array_free) when done. */
-static GPtrArray *
-find_thread_ids (notmuch_database_t *notmuch,
- GPtrArray *parents,
- const char *message_id)
-{
- Xapian::PostingIterator child, children_end;
- Xapian::Document doc;
- GHashTable *thread_ids;
- GList *keys, *l;
- unsigned int i;
- const char *parent_message_id;
- GPtrArray *result;
-
- thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
- free, NULL);
-
- find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
- for ( ; child != children_end; child++) {
- doc = find_document_for_doc_id (notmuch, *child);
- insert_thread_id (thread_ids, doc);
- }
-
- for (i = 0; i < parents->len; i++) {
- notmuch_message_t *parent;
- notmuch_thread_ids_t *ids;
-
- parent_message_id = (char *) g_ptr_array_index (parents, i);
- parent = notmuch_database_find_message (notmuch, parent_message_id);
- if (parent == NULL)
- continue;
-
- for (ids = notmuch_message_get_thread_ids (parent);
- notmuch_thread_ids_has_more (ids);
- notmuch_thread_ids_advance (ids))
- {
- const char *id;
-
- id = notmuch_thread_ids_get (ids);
- g_hash_table_insert (thread_ids, strdup (id), NULL);
- }
-
- notmuch_message_destroy (parent);
- }
-
- result = g_ptr_array_new ();
-
- keys = g_hash_table_get_keys (thread_ids);
- for (l = keys; l; l = l->next) {
- char *id = (char *) l->data;
- g_ptr_array_add (result, id);
- }
- g_list_free (keys);
-
- /* We're done with the hash table, but we've taken the pointers to
- * the allocated strings and put them into our result array, so
- * tell the hash not to free them on its way out. */
- g_hash_table_steal_all (thread_ids);
- g_hash_table_unref (thread_ids);
-
- return result;
-}
-
/* Advance 'str' past any whitespace or RFC 822 comments. A comment is
* a (potentially nested) parenthesized sequence with '\' used to
* escape any character (including parentheses).
}
/* Parse a References header value, putting a copy of each referenced
- * message-id into 'array'. */
+ * message-id into 'hash'. */
static void
-parse_references (GPtrArray *array,
+parse_references (GHashTable *hash,
const char *refs)
{
char *ref;
ref = parse_message_id (refs, &refs);
if (ref)
- g_ptr_array_add (array, ref);
+ g_hash_table_insert (hash, ref, NULL);
}
}
return ret;
}
-/* Given a (mostly empty) 'message' and its corresponding
- * 'message_file' link it to existing threads in the database.
+/* Find the thread ID to which the message with 'message_id' belongs.
*
- * We first looke at 'message_file' and its link-relevant headers
- * (References and In-Reply-To) for message IDs. We also look in the
- * database for existing message that reference 'message'.p
+ * Returns NULL if no message with message ID 'message_id' is in the
+ * database.
*
- * The end result is to call _notmuch_message_add_thread_id with one
- * or more thread IDs to which this message belongs, (including
- * generating a new thread ID if necessary if the message doesn't
- * connect to any existing threads).
+ * Otherwise, returns a newly talloced string belonging to 'ctx'.
*/
+const char *
+_resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
+ void *ctx,
+ const char *message_id)
+{
+ notmuch_message_t *message;
+ const char *ret = NULL;
+
+ message = notmuch_database_find_message (notmuch, message_id);
+ if (message == NULL)
+ goto DONE;
+
+ ret = talloc_steal (ctx, notmuch_message_get_thread_id (message));
+
+ DONE:
+ if (message)
+ notmuch_message_destroy (message);
+
+ return ret;
+}
+
static notmuch_status_t
-_notmuch_database_link_message (notmuch_database_t *notmuch,
- notmuch_message_t *message,
- notmuch_message_file_t *message_file)
+_merge_threads (notmuch_database_t *notmuch,
+ const char *winner_thread_id,
+ const char *loser_thread_id)
+{
+ Xapian::PostingIterator loser, loser_end;
+ notmuch_message_t *message = NULL;
+ notmuch_private_status_t private_status;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+ find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
+
+ for ( ; loser != loser_end; loser++) {
+ message = _notmuch_message_create (notmuch, notmuch,
+ *loser, &private_status);
+ if (message == NULL) {
+ ret = COERCE_STATUS (private_status,
+ "Cannot find document for doc_id from query");
+ goto DONE;
+ }
+
+ _notmuch_message_remove_term (message, "thread", loser_thread_id);
+ _notmuch_message_add_term (message, "thread", winner_thread_id);
+ _notmuch_message_sync (message);
+
+ notmuch_message_destroy (message);
+ message = NULL;
+ }
+
+ DONE:
+ if (message)
+ notmuch_message_destroy (message);
+
+ return ret;
+}
+
+static notmuch_status_t
+_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
+ notmuch_message_t *message,
+ notmuch_message_file_t *message_file,
+ const char **thread_id)
{
- GPtrArray *parents, *thread_ids;
+ GHashTable *parents = NULL;
const char *refs, *in_reply_to;
- const char *message_id = notmuch_message_get_message_id (message);
- unsigned int i;
+ GList *l, *keys = NULL;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
- parents = g_ptr_array_new ();
+ parents = g_hash_table_new_full (g_str_hash, g_str_equal,
+ free, NULL);
refs = notmuch_message_file_get_header (message_file, "references");
parse_references (parents, refs);
in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
parse_references (parents, in_reply_to);
- for (i = 0; i < parents->len; i++)
- _notmuch_message_add_term (message, "ref",
- (char *) g_ptr_array_index (parents, i));
+ keys = g_hash_table_get_keys (parents);
+ for (l = keys; l; l = l->next) {
+ char *parent_message_id;
+ const char *parent_thread_id;
+
+ parent_message_id = (char *) l->data;
+ parent_thread_id = _resolve_message_id_to_thread_id (notmuch,
+ message,
+ parent_message_id);
+
+ if (parent_thread_id == NULL) {
+ _notmuch_message_add_term (message, "ref", parent_message_id);
+ } else {
+ if (*thread_id == NULL) {
+ *thread_id = talloc_strdup (message, parent_thread_id);
+ _notmuch_message_add_term (message, "thread", *thread_id);
+ } else if (strcmp (*thread_id, parent_thread_id)) {
+ ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
+ if (ret)
+ goto DONE;
+ }
+ }
+ }
+
+ DONE:
+ if (keys)
+ g_list_free (keys);
+ if (parents)
+ g_hash_table_unref (parents);
+
+ return ret;
+}
- thread_ids = find_thread_ids (notmuch, parents, message_id);
+static notmuch_status_t
+_notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
+ notmuch_message_t *message,
+ const char **thread_id)
+{
+ const char *message_id = notmuch_message_get_message_id (message);
+ Xapian::PostingIterator child, children_end;
+ notmuch_message_t *child_message = NULL;
+ const char *child_thread_id;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ notmuch_private_status_t private_status;
- for (i = 0; i < parents->len; i++)
- g_free (g_ptr_array_index (parents, i));
- g_ptr_array_free (parents, TRUE);
+ find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
- if (thread_ids->len) {
- char *id;
+ for ( ; child != children_end; child++) {
- for (i = 0; i < thread_ids->len; i++) {
- id = (char *) thread_ids->pdata[i];
- _notmuch_message_add_thread_id (message, id);
- free (id);
+ child_message = _notmuch_message_create (message, notmuch,
+ *child, &private_status);
+ if (child_message == NULL) {
+ ret = COERCE_STATUS (private_status,
+ "Cannot find document for doc_id from query");
+ goto DONE;
}
- } else {
- _notmuch_message_ensure_thread_id (message);
+
+ child_thread_id = notmuch_message_get_thread_id (child_message);
+ if (*thread_id == NULL) {
+ *thread_id = talloc_strdup (message, child_thread_id);
+ _notmuch_message_add_term (message, "thread", *thread_id);
+ } else if (strcmp (*thread_id, child_thread_id)) {
+ _notmuch_message_remove_term (child_message, "ref",
+ message_id);
+ _notmuch_message_sync (child_message);
+ ret = _merge_threads (notmuch, *thread_id, child_thread_id);
+ if (ret)
+ goto DONE;
+ }
+
+ notmuch_message_destroy (child_message);
+ child_message = NULL;
}
- g_ptr_array_free (thread_ids, TRUE);
+ DONE:
+ if (child_message)
+ notmuch_message_destroy (child_message);
+
+ return ret;
+}
- return NOTMUCH_STATUS_SUCCESS;
+/* Given a (mostly empty) 'message' and its corresponding
+ * 'message_file' link it to existing threads in the database.
+ *
+ * We first looke at 'message_file' and its link-relevant headers
+ * (References and In-Reply-To) for message IDs. We also look in the
+ * database for existing message that reference 'message'.p
+ *
+ * The end result is to call _notmuch_message_add_thread_id with one
+ * or more thread IDs to which this message belongs, (including
+ * generating a new thread ID if necessary if the message doesn't
+ * connect to any existing threads).
+ */
+static notmuch_status_t
+_notmuch_database_link_message (notmuch_database_t *notmuch,
+ notmuch_message_t *message,
+ notmuch_message_file_t *message_file)
+{
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ const char *thread_id = NULL;
+
+ _notmuch_database_link_message_to_parents (notmuch, message,
+ message_file,
+ &thread_id);
+
+ ret = _notmuch_database_link_message_to_children (notmuch, message,
+ &thread_id);
+
+ if (thread_id == NULL)
+ _notmuch_message_ensure_thread_id (message);
+
+ return ret;
}
notmuch_status_t
notmuch_database_t *notmuch;
Xapian::docid doc_id;
char *message_id;
+ char *thread_id;
char *filename;
Xapian::Document doc;
};
notmuch_terms_t terms;
};
-struct _notmuch_thread_ids {
- notmuch_terms_t terms;
-};
-
/* "128 bits of thread-id ought to be enough for anybody" */
#define NOTMUCH_THREAD_ID_BITS 128
#define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
* caller *is* responsible for calling notmuch_message_destroy.
*
* If no document exists in the database with document ID of 'doc_id'
- * then this function returns NULL and sets *status to
+ * then this function returns NULL and optionally sets *status to
* NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
*
* This function can also fail to due lack of available memory,
* returning NULL and optionally setting *status to
- * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY. Caller can pass NULL
- * for status if uninterested in distinguishing these two cases.
+ * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
+ *
+ * The caller can pass NULL for status if uninterested in
+ * distinguishing these two cases.
*/
notmuch_message_t *
_notmuch_message_create (const void *talloc_owner,
message->notmuch = notmuch;
message->doc_id = doc_id;
message->message_id = NULL; /* lazily created */
+ message->thread_id = NULL; /* lazily created */
message->filename = NULL; /* lazily created */
/* This is C++'s creepy "placement new", which is really just an
message = _notmuch_message_create (talloc_owner, notmuch,
doc_id, &private_status);
- if (private_status >= (notmuch_private_status_t) NOTMUCH_STATUS_LAST_STATUS)
- {
- INTERNAL_ERROR ("Failed to find document immediately after adding it.\n");
- }
-
- *status = (notmuch_status_t) private_status;
+ *status = COERCE_STATUS (private_status,
+ "Failed to find dcocument after inserting it.");
return message;
}
i = message->doc.termlist_begin ();
i.skip_to (_find_prefix ("id"));
- if (i == message->doc.termlist_end ()) {
+ if (i == message->doc.termlist_end ())
INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
message->doc_id);
- }
message->message_id = talloc_strdup (message, (*i).c_str () + 1);
+
+#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);
+ }
+#endif
+
return message->message_id;
}
+const char *
+notmuch_message_get_thread_id (notmuch_message_t *message)
+{
+ Xapian::TermIterator i;
+
+ if (message->thread_id)
+ return message->thread_id;
+
+ i = message->doc.termlist_begin ();
+ i.skip_to (_find_prefix ("thread"));
+
+ if (i == message->doc.termlist_end ())
+ INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
+ message->doc_id);
+
+ message->thread_id = talloc_strdup (message, (*i).c_str () + 1);
+
+#if DEBUG_DATABASE_SANITY
+ i++;
+
+ if (i != message->doc.termlist_end () &&
+ strncmp ((*i).c_str (), _find_prefix ("thread"),
+ strlen (_find_prefix ("thread"))) == 0)
+ {
+ INTERNAL_ERROR ("Message with document ID of %d has duplicate thread IDs.\n",
+ message->doc_id);
+ }
+#endif
+
+ return message->thread_id;
+}
+
/* Set the filename for 'message' to 'filename'.
*
- * XXX: We should still figure out what we want to do for multiple
- * files with identical message IDs. We will probably want to store a
- * list of filenames here, (so that this will be "add_filename"
- * instead of "set_filename"). Which would make this very similar to
- * add_thread_ids.
+ * XXX: We should still figure out if we think it's important to store
+ * multiple filenames for email messages with identical message IDs.
*
* This change will not be reflected in the database until the next
* call to _notmuch_message_set_sync. */
notmuch_tags_t);
}
-notmuch_thread_ids_t *
-notmuch_message_get_thread_ids (notmuch_message_t *message)
-{
- return _notmuch_terms_create_type (message, message->doc, "thread",
- notmuch_thread_ids_t);
-}
-
void
_notmuch_message_set_date (notmuch_message_t *message,
const char *date)
Xapian::sortable_serialise (time_value));
}
-void
-_notmuch_message_add_thread_id (notmuch_message_t *message,
- const char *thread_id)
-{
- _notmuch_message_add_term (message, "thread", thread_id);
-}
-
static void
thread_id_generate (thread_id_t *thread_id)
{
{
return _notmuch_terms_destroy (&tags->terms);
}
-
-notmuch_bool_t
-notmuch_thread_ids_has_more (notmuch_thread_ids_t *thread_ids)
-{
- return _notmuch_terms_has_more (&thread_ids->terms);
-}
-
-const char *
-notmuch_thread_ids_get (notmuch_thread_ids_t *thread_ids)
-{
- return _notmuch_terms_get (&thread_ids->terms);
-}
-
-void
-notmuch_thread_ids_advance (notmuch_thread_ids_t *thread_ids)
-{
- return _notmuch_terms_advance (&thread_ids->terms);
-}
-
-void
-notmuch_thread_ids_destroy (notmuch_thread_ids_t *thread_ids)
-{
- return _notmuch_terms_destroy (&thread_ids->terms);
-}
typedef struct _notmuch_results notmuch_results_t;
typedef struct _notmuch_message notmuch_message_t;
typedef struct _notmuch_tags notmuch_tags_t;
-typedef struct _notmuch_thread_ids notmuch_thread_ids_t;
/* Lookup the default database path.
*
const char *
notmuch_message_get_message_id (notmuch_message_t *message);
+
+/* Get the thread ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (for example, until the user calls
+ * notmuch_message_destroy on 'message' or until a query from which it
+ * derived is destroyed).
+ *
+ * This function will not return NULL since Notmuch ensures that every
+ * message belongs to a single thread.
+ */
+const char *
+notmuch_message_get_thread_id (notmuch_message_t *message);
+
/* Get the filename for the email corresponding to 'message'.
*
* The returned filename is relative to the base of the database from
notmuch_tags_t *
notmuch_message_get_tags (notmuch_message_t *message);
-/* Get the thread IDs for 'message', returning a notmuch_thread_ids_t
- * object which can be used to iterate over all thread IDs.
- *
- * The thread_ids object is owned by the message and as such, will
- * only be valid for as long as the message is valid, (which is until
- * the query from which it derived is destroyed).
- *
- * Typical usage might be:
- *
- * notmuch_message_t *message;
- * notmuch_thread_ids_t *thread_ids;
- * const char *thread_id;
- *
- * message = notmuch_database_find_message (database, message_id);
- *
- * for (thread_ids = notmuch_message_get_thread_ids (message);
- * notmuch_thread_ids_has_more (thread_ids);
- * notmuch_thread_ids_advance (thread_ids))
- * {
- * thread_id = notmuch_thread_ids_get (thread_ids);
- * ....
- * }
- *
- * notmuch_message_destroy (message);
- *
- * Note that there's no explicit destructor needed for the
- * notmuch_thread_ids_t object. (For consistency, we do provide a
- * notmuch_thread_ids_destroy function, but there's no good reason to
- * call it if the message is about to be destroyed).
- */
-notmuch_thread_ids_t *
-notmuch_message_get_thread_ids (notmuch_message_t *message);
-
/* The longest possible tag value. */
#define NOTMUCH_TAG_MAX 200
void
notmuch_tags_destroy (notmuch_tags_t *tags);
-/* Does the given notmuch_thread_ids_t object contain any more thread IDs.
- *
- * When this function returns TRUE, notmuch_thread_ids_get will return a
- * valid string. Whereas when this function returns FALSE,
- * notmuch_thread_ids_get will return NULL.
- *
- * See the documentation of notmuch_message_get_thread_ids for example code
- * showing how to iterate over a notmuch_thread_ids_t object.
- */
-notmuch_bool_t
-notmuch_thread_ids_has_more (notmuch_thread_ids_t *thread_ids);
-
-/* Get the current thread ID from 'thread_ids' as a string.
- *
- * Note: The returned string belongs to 'thread_ids' and has a lifetime
- * identical to it (and the query to which it utlimately belongs).
- *
- * See the documentation of notmuch_message_get_thread_ids for example code
- * showing how to iterate over a notmuch_thread_ids_t object.
- */
-const char *
-notmuch_thread_ids_get (notmuch_thread_ids_t *thread_ids);
-
-/* Advance the 'thread_ids' iterator to the next tag.
- *
- * See the documentation of notmuch_message_get_thread_ids for example code
- * showing how to iterate over a notmuch_thread_ids_t object.
- */
-void
-notmuch_thread_ids_advance (notmuch_thread_ids_t *thread_ids);
-
-/* Destroy a notmuch_thread_ids_t object.
- *
- * It's not strictly necessary to call this function. All memory from
- * the notmuch_thread_ids_t object will be reclaimed when the containg
- * message or query objects are destroyed.
- */
-void
-notmuch_thread_ids_destroy (notmuch_thread_ids_t *thread_ids);
-
NOTMUCH_END_DECLS
#endif