#include <xapian.h>
-#include <glib.h> /* g_strdup_printf, g_free, GPtrArray, GHashTable */
+#include <glib.h> /* g_free, GPtrArray, GHashTable */
using namespace std;
* 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)
{
switch (status) {
case NOTMUCH_STATUS_SUCCESS:
return "No error occurred";
+ case NOTMUCH_STATUS_OUT_OF_MEMORY:
+ return "Out of memory";
case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
return "A Xapian exception occurred";
case NOTMUCH_STATUS_FILE_ERROR:
}
}
-/* XXX: We should drop this function and convert all callers to call
- * _notmuch_message_add_term instead. */
-static void
-add_term (Xapian::Document doc,
- const char *prefix_name,
- const char *value)
-{
- const char *prefix;
- char *term;
-
- if (value == NULL)
- return;
-
- prefix = _find_prefix (prefix_name);
-
- term = g_strdup_printf ("%s%s", prefix, value);
-
- if (strlen (term) <= NOTMUCH_TERM_MAX)
- doc.add_term (term);
-
- g_free (term);
-}
-
static void
find_doc_ids (notmuch_database_t *notmuch,
const char *prefix_name,
Xapian::PostingIterator i;
char *term;
- term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
+ term = talloc_asprintf (notmuch, "%s%s",
+ _find_prefix (prefix_name), value);
*begin = notmuch->xapian_db->postlist_begin (term);
*end = notmuch->xapian_db->postlist_end (term);
- free (term);
+ talloc_free (term);
}
static notmuch_private_status_t
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).
* If not NULL, then *next will be made to point to the first character
* not parsed, (possibly pointing to the final '\0' terminator.
*
- * Returns a newly allocated string which the caller should free()
- * when done with it.
+ * Returns a newly talloc'ed string belonging to 'ctx'.
*
* Returns NULL if there is any error parsing the message-id. */
static char *
-parse_message_id (const char *message_id, const char **next)
+parse_message_id (void *ctx, const char *message_id, const char **next)
{
const char *s, *end;
char *result;
if (end <= s)
return NULL;
- result = strndup (s, end - s + 1);
+ result = talloc_strndup (ctx, s, end - s + 1);
/* Finally, collapse any whitespace that is within the message-id
* itself. */
return result;
}
-/* Parse a References header value, putting a copy of each referenced
- * message-id into 'array'. */
+/* Parse a References header value, putting a (talloc'ed under 'ctx')
+ * copy of each referenced message-id into 'hash'. */
static void
-parse_references (GPtrArray *array,
+parse_references (void *ctx,
+ GHashTable *hash,
const char *refs)
{
char *ref;
return;
while (*refs) {
- ref = parse_message_id (refs, &refs);
+ ref = parse_message_id (ctx, refs, &refs);
if (ref)
- g_ptr_array_add (array, ref);
+ g_hash_table_insert (hash, ref, NULL);
}
}
char *
notmuch_database_default_path (void)
{
+ char *path;
+
if (getenv ("NOTMUCH_BASE"))
return strdup (getenv ("NOTMUCH_BASE"));
- return g_strdup_printf ("%s/mail", getenv ("HOME"));
+ if (asprintf (&path, "%s/mail", getenv ("HOME")) == -1) {
+ fprintf (stderr, "Out of memory.\n");
+ return xstrdup("");
+ }
+
+ return path;
}
notmuch_database_t *
goto DONE;
}
- notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
+ notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch");
err = mkdir (notmuch_path, 0755);
DONE:
if (notmuch_path)
- free (notmuch_path);
+ talloc_free (notmuch_path);
if (local_path)
free (local_path);
if (path == NULL)
path = local_path = notmuch_database_default_path ();
- notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
+ if (asprintf (¬much_path, "%s/%s", path, ".notmuch") == -1) {
+ notmuch_path = NULL;
+ fprintf (stderr, "Out of memory\n");
+ goto DONE;
+ }
err = stat (notmuch_path, &st);
if (err) {
goto DONE;
}
- xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
+ if (asprintf (&xapian_path, "%s/%s", notmuch_path, "xapian") == -1) {
+ xapian_path = NULL;
+ fprintf (stderr, "Out of memory\n");
+ goto DONE;
+ }
notmuch = talloc (NULL, notmuch_database_t);
notmuch->path = talloc_strdup (notmuch, path);
return notmuch->path;
}
-notmuch_private_status_t
+static notmuch_private_status_t
find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
Xapian::Document *doc, unsigned int *doc_id)
{
return ret;
}
+/* Find the thread ID to which the message with 'message_id' belongs.
+ *
+ * Returns NULL if no message with message ID 'message_id' is in the
+ * database.
+ *
+ * Otherwise, returns a newly talloced string belonging to 'ctx'.
+ */
+static 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
+_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 void
+_my_talloc_free_for_g_hash (void *ptr)
+{
+ talloc_free (ptr);
+}
+
+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)
+{
+ GHashTable *parents = NULL;
+ const char *refs, *in_reply_to;
+ GList *l, *keys = NULL;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+ parents = g_hash_table_new_full (g_str_hash, g_str_equal,
+ _my_talloc_free_for_g_hash, NULL);
+
+ refs = notmuch_message_file_get_header (message_file, "references");
+ parse_references (message, parents, refs);
+
+ in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
+ parse_references (message, parents, in_reply_to);
+
+ 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;
+}
+
+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;
+
+ find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
+
+ for ( ; child != children_end; child++) {
+
+ 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;
+ }
+
+ 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;
+ }
+
+ DONE:
+ if (child_message)
+ notmuch_message_destroy (child_message);
+
+ return ret;
+}
+
/* Given a (mostly empty) 'message' and its corresponding
* 'message_file' link it to existing threads in the database.
*
notmuch_message_t *message,
notmuch_message_file_t *message_file)
{
- GPtrArray *parents, *thread_ids;
- const char *refs, *in_reply_to;
- const char *message_id = notmuch_message_get_message_id (message);
- unsigned int i;
-
- parents = g_ptr_array_new ();
-
- 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);
+ notmuch_status_t status;
+ const char *thread_id = NULL;
- for (i = 0; i < parents->len; i++)
- _notmuch_message_add_term (message, "ref",
- (char *) g_ptr_array_index (parents, i));
-
- thread_ids = find_thread_ids (notmuch, parents, message_id);
-
- for (i = 0; i < parents->len; i++)
- g_free (g_ptr_array_index (parents, i));
- g_ptr_array_free (parents, TRUE);
-
- if (thread_ids->len) {
- GString *thread_id;
- char *id;
+ status = _notmuch_database_link_message_to_parents (notmuch, message,
+ message_file,
+ &thread_id);
+ if (status)
+ return status;
- for (i = 0; i < thread_ids->len; i++) {
- id = (char *) thread_ids->pdata[i];
- _notmuch_message_add_thread_id (message, id);
- if (i == 0)
- thread_id = g_string_new (id);
- else
- g_string_append_printf (thread_id, ",%s", id);
+ status = _notmuch_database_link_message_to_children (notmuch, message,
+ &thread_id);
+ if (status)
+ return status;
- free (id);
- }
- g_string_free (thread_id, TRUE);
- } else {
+ if (thread_id == NULL)
_notmuch_message_ensure_thread_id (message);
- }
-
- g_ptr_array_free (thread_ids, TRUE);
return NOTMUCH_STATUS_SUCCESS;
}
notmuch_status_t
notmuch_database_add_message (notmuch_database_t *notmuch,
- const char *filename)
+ const char *filename,
+ notmuch_message_t **message_ret)
{
notmuch_message_file_t *message_file;
notmuch_message_t *message;
header = notmuch_message_file_get_header (message_file, "message-id");
if (header) {
- message_id = parse_message_id (header, NULL);
+ message_id = parse_message_id (message_file, header, NULL);
/* So the header value isn't RFC-compliant, but it's
* better than no message-id at all. */
if (message_id == NULL)
- message_id = xstrdup (header);
+ message_id = talloc_strdup (message_file, header);
} else {
/* No message-id at all, let's generate one by taking a
* hash over the file's contents. */
goto DONE;
}
- message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
+ message_id = talloc_asprintf (message_file,
+ "notmuch-sha1-%s", sha1);
free (sha1);
}
notmuch,
message_id,
&ret);
- free (message_id);
+
+ talloc_free (message_id);
if (message == NULL)
goto DONE;
}
DONE:
- if (message)
- notmuch_message_destroy (message);
+ if (message) {
+ if (message_ret)
+ *message_ret = message;
+ else
+ notmuch_message_destroy (message);
+ }
+
if (message_file)
notmuch_message_file_close (message_file);