]> git.notmuchmail.org Git - notmuch/blobdiff - database.cc
add_message: Pull the thread-stitching portion out into new _notmuch_database_link_me...
[notmuch] / database.cc
index c5d774763307ab8b9c1b862ef7c0b4371d586511..d94755780b88eebdce68303e5980417883807dbe 100644 (file)
 
 using namespace std;
 
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+typedef struct {
+    const char *name;
+    const char *prefix;
+} prefix_t;
+
+/* Here's the current schema for our database:
+ *
+ * We currently have two different types of documents: mail and timestamps.
+ *
+ * Mail document
+ * -------------
+ * A mail document is associated with a particular email message file
+ * on disk. It is indexed with the following prefixed terms:
+ *
+ *    Single terms of given prefix:
+ *
+ *     type:   mail
+ *
+ *     id:     Unique ID of mail, (from Message-ID header or generated
+ *             as "notmuch-sha1-<sha1_sum_of_entire_file>.
+ *
+ *    Multiple terms of given prefix:
+ *
+ *     ref:    The message IDs from all In-Reply-To and References
+ *             headers in the message.
+ *
+ *     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
+ *                     Date header.
+ *
+ *     MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
+ *
+ * Timestamp document
+ * ------------------
+ * A timestamp document is used by a client of the notmuch library to
+ * maintain data necessary to allow for efficient polling of mail
+ * directories. The notmuch library does no interpretation of
+ * timestamps, but merely allows the user to store and retrieve
+ * timestamps as name/value pairs.
+ *
+ * The timestamp document is indexed with a single prefixed term:
+ *
+ *     timestamp:      The user's key value (likely a directory name)
+ *
+ * and has a single value:
+ *
+ *     TIMETAMPS:      The time_t value from the user.
+ */
+
+/* With these prefix values we follow the conventions published here:
+ *
+ * http://xapian.org/docs/omega/termprefixes.html
+ *
+ * as much as makes sense. Note that I took some liberty in matching
+ * the reserved prefix values to notmuch concepts, (for example, 'G'
+ * is documented as "newsGroup (or similar entity - e.g. a web forum
+ * name)", for which I think the thread is the closest analogue in
+ * notmuch. This in spite of the fact that we will eventually be
+ * storing mailing-list messages where 'G' for "mailing list name"
+ * might be even a closer analogue. I'm treating the single-character
+ * prefixes preferentially for core notmuch concepts (which will be
+ * nearly universal to all mail messages).
+ */
+
+prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
+    { "type", "T" },
+    { "thread", "G" },
+    { "ref", "XREFERENCE" },
+    { "timestamp", "XTIMESTAMP" },
+};
+
+prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
+    { "tag", "K" },
+    { "id", "Q" }
+};
+
+const char *
+_find_prefix (const char *name)
+{
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++)
+       if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
+           return BOOLEAN_PREFIX_INTERNAL[i].prefix;
+
+    for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++)
+       if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
+           return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
+
+    INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
+
+    return "";
+}
+
 const char *
 notmuch_status_to_string (notmuch_status_t status)
 {
@@ -40,10 +141,12 @@ notmuch_status_to_string (notmuch_status_t status)
        return "Something went wrong trying to read or write a file";
     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
        return "File is not an email";
+    case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+       return "Message ID is identical to a message in database";
     case NOTMUCH_STATUS_NULL_POINTER:
        return "Erroneous NULL pointer";
     case NOTMUCH_STATUS_TAG_TOO_LONG:
-       return "Tag value is too long";
+       return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
     default:
     case NOTMUCH_STATUS_LAST_STATUS:
        return "Unknown error status value";
@@ -74,51 +177,97 @@ add_term (Xapian::Document doc,
 }
 
 static void
-find_messages_by_term (Xapian::Database *db,
-                      const char *prefix_name,
-                      const char *value,
-                      Xapian::PostingIterator *begin,
-                      Xapian::PostingIterator *end)
+find_doc_ids (notmuch_database_t *notmuch,
+             const char *prefix_name,
+             const char *value,
+             Xapian::PostingIterator *begin,
+             Xapian::PostingIterator *end)
 {
     Xapian::PostingIterator i;
     char *term;
 
     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
 
-    *begin = db->postlist_begin (term);
+    *begin = notmuch->xapian_db->postlist_begin (term);
 
-    if (end)
-       *end = db->postlist_end (term);
+    *end = notmuch->xapian_db->postlist_end (term);
 
     free (term);
 }
 
-Xapian::Document
-find_message_by_docid (Xapian::Database *db, Xapian::docid docid)
+static notmuch_private_status_t
+find_unique_doc_id (notmuch_database_t *notmuch,
+                   const char *prefix_name,
+                   const char *value,
+                   unsigned int *doc_id)
 {
-    return db->get_document (docid);
+    Xapian::PostingIterator i, end;
+
+    find_doc_ids (notmuch, prefix_name, value, &i, &end);
+
+    if (i == end) {
+       *doc_id = 0;
+       return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+    } else {
+       *doc_id = *i;
+       return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+    }
+}
+
+static Xapian::Document
+find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
+{
+    return notmuch->xapian_db->get_document (doc_id);
+}
+
+static notmuch_private_status_t
+find_unique_document (notmuch_database_t *notmuch,
+                     const char *prefix_name,
+                     const char *value,
+                     Xapian::Document *document,
+                     unsigned int *doc_id)
+{
+    notmuch_private_status_t status;
+
+    status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
+
+    if (status) {
+       *document = Xapian::Document ();
+       return status;
+    }
+
+    *document = find_document_for_doc_id (notmuch, *doc_id);
+    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;
-    const char *value, *id, *comma;
-
-    value_string = doc.get_value (NOTMUCH_VALUE_THREAD);
-    value = value_string.c_str();
-    if (strlen (value)) {
-       id = value;
-       while (*id) {
-           comma = strchr (id, ',');
-           if (comma == NULL)
-               comma = id + strlen (id);
-           g_hash_table_insert (thread_ids,
-                                strndup (id, comma - id), NULL);
-           id = comma;
-           if (*id)
-               id++;
-       }
+    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++;
     }
 }
 
@@ -126,15 +275,15 @@ notmuch_message_t *
 notmuch_database_find_message (notmuch_database_t *notmuch,
                               const char *message_id)
 {
-    Xapian::PostingIterator i, end;
+    notmuch_private_status_t status;
+    unsigned int doc_id;
 
-    find_messages_by_term (notmuch->xapian_db,
-                          "msgid", message_id, &i, &end);
+    status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
 
-    if (i == end)
+    if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
        return NULL;
 
-    return _notmuch_message_create (notmuch, notmuch, *i);
+    return _notmuch_message_create (notmuch, notmuch, doc_id, NULL);
 }
 
 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
@@ -149,7 +298,6 @@ find_thread_ids (notmuch_database_t *notmuch,
                 GPtrArray *parents,
                 const char *message_id)
 {
-    Xapian::WritableDatabase *db = notmuch->xapian_db;
     Xapian::PostingIterator child, children_end;
     Xapian::Document doc;
     GHashTable *thread_ids;
@@ -161,9 +309,9 @@ find_thread_ids (notmuch_database_t *notmuch,
     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
                                        free, NULL);
 
-    find_messages_by_term (db, "ref", message_id, &child, &children_end);
+    find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
     for ( ; child != children_end; child++) {
-       doc = find_message_by_docid (db, *child);
+       doc = find_document_for_doc_id (notmuch, *child);
        insert_thread_id (thread_ids, doc);
     }
 
@@ -393,6 +541,7 @@ notmuch_database_open (const char *path)
     struct stat st;
     int err;
     char *local_path = NULL;
+    unsigned int i;
 
     if (path == NULL)
        path = local_path = notmuch_database_default_path ();
@@ -417,6 +566,12 @@ notmuch_database_open (const char *path)
        notmuch->query_parser = new Xapian::QueryParser;
        notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
        notmuch->query_parser->set_database (*notmuch->xapian_db);
+
+       for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
+           prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
+           notmuch->query_parser->add_boolean_prefix (prefix->name,
+                                                      prefix->prefix);
+       }
     } catch (const Xapian::Error &error) {
        fprintf (stderr, "A Xapian exception occurred: %s\n",
                 error.get_msg().c_str());
@@ -447,24 +602,179 @@ notmuch_database_get_path (notmuch_database_t *notmuch)
     return notmuch->path;
 }
 
+notmuch_private_status_t
+find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
+                        Xapian::Document *doc, unsigned int *doc_id)
+{
+    return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
+}
+
+/* We allow the user to use arbitrarily long keys for timestamps,
+ * (they're for filesystem paths after all, which have no limit we
+ * know about). But we have a term-length limit. So if we exceed that,
+ * we'll use the SHA-1 of the user's key as the actual key for
+ * constructing a database term.
+ *
+ * Caution: This function returns a newly allocated string which the
+ * caller should free() when finished.
+ */
+static char *
+timestamp_db_key (const char *key)
+{
+    int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
+
+    if (term_len > NOTMUCH_TERM_MAX)
+       return notmuch_sha1_of_string (key);
+    else
+       return strdup (key);
+}
+
 notmuch_status_t
-notmuch_database_add_message (notmuch_database_t *notmuch,
-                             const char *filename)
+notmuch_database_set_timestamp (notmuch_database_t *notmuch,
+                               const char *key, time_t timestamp)
 {
-    Xapian::WritableDatabase *db = notmuch->xapian_db;
     Xapian::Document doc;
-    notmuch_message_file_t *message_file;
+    unsigned int doc_id;
+    notmuch_private_status_t status;
     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    char *db_key = NULL;
 
-    GPtrArray *parents, *thread_ids;
+    db_key = timestamp_db_key (key);
 
-    const char *refs, *in_reply_to, *date, *header;
-    const char *from, *to, *subject;
-    char *message_id;
+    try {
+       status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
 
-    time_t time_value;
+       doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+                      Xapian::sortable_serialise (timestamp));
+
+       if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           char *term = talloc_asprintf (NULL, "%s%s",
+                                         _find_prefix ("timestamp"), db_key);
+           doc.add_term (term);
+           talloc_free (term);
+
+           notmuch->xapian_db->add_document (doc);
+       } else {
+           notmuch->xapian_db->replace_document (doc_id, doc);
+       }
+
+    } catch (Xapian::Error &error) {
+       fprintf (stderr, "A Xapian exception occurred: %s.\n",
+                error.get_msg().c_str());
+       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    if (db_key)
+       free (db_key);
+
+    return ret;
+}
+
+time_t
+notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
+{
+    Xapian::Document doc;
+    unsigned int doc_id;
+    notmuch_private_status_t status;
+    char *db_key = NULL;
+    time_t ret = 0;
+
+    db_key = timestamp_db_key (key);
+
+    try {
+       status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
+
+       if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
+           goto DONE;
+
+       ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
+    } catch (Xapian::Error &error) {
+       goto DONE;
+    }
+
+  DONE:
+    if (db_key)
+       free (db_key);
+
+    return ret;
+}
+
+/* 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)
+{
+    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);
+
+    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;
+
+       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);
+
+           free (id);
+       }
+       g_string_free (thread_id, TRUE);
+    } else {
+       _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)
+{
+    notmuch_message_file_t *message_file;
+    notmuch_message_t *message;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    const char *date, *header;
+    const char *from, *to, *subject, *old_filename;
+    char *message_id;
+
     message_file = notmuch_message_file_open (filename);
     if (message_file == NULL) {
        ret = NOTMUCH_STATUS_FILE_ERROR;
@@ -482,20 +792,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
                                           (char *) NULL);
 
     try {
-       doc.set_data (filename);
-
-       add_term (doc, "type", "mail");
-
-       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);
-
-       for (i = 0; i < parents->len; i++)
-           add_term (doc, "ref", (char *) g_ptr_array_index (parents, i));
+       /* The first order of business is to find/create a message ID. */
 
        header = notmuch_message_file_get_header (message_file, "message-id");
        if (header) {
@@ -519,50 +816,36 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
            free (sha1);
        }
 
-       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);
-
-       add_term (doc, "msgid", message_id);
-       doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
+       /* Now that we have a message ID, we get a message object,
+        * (which may or may not reference an existing document in the
+        * database). */
 
+       /* Use NULL for owner since we want to free this locally. */
+       message = _notmuch_message_create_for_message_id (NULL,
+                                                         notmuch,
+                                                         message_id,
+                                                         &ret);
        free (message_id);
 
-       if (thread_ids->len) {
-           unsigned int i;
-           GString *thread_id;
-           char *id;
-
-           for (i = 0; i < thread_ids->len; i++) {
-               id = (char *) thread_ids->pdata[i];
-               add_term (doc, "thread", id);
-               if (i == 0)
-                   thread_id = g_string_new (id);
-               else
-                   g_string_append_printf (thread_id, ",%s", id);
+       if (message == NULL)
+           goto DONE;
 
-               free (id);
-           }
-           doc.add_value (NOTMUCH_VALUE_THREAD, thread_id->str);
-           g_string_free (thread_id, TRUE);
+       /* Has a message previously been added with the same ID? */
+       old_filename = notmuch_message_get_filename (message);
+       if (old_filename && strlen (old_filename)) {
+           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+           goto DONE;
        } else {
-           /* If not part of any existing thread, generate a new thread_id. */
-           thread_id_t thread_id;
-
-           thread_id_generate (&thread_id);
-           add_term (doc, "thread", thread_id.str);
-           doc.add_value (NOTMUCH_VALUE_THREAD, thread_id.str);
+           _notmuch_message_set_filename (message, filename);
+           _notmuch_message_add_term (message, "type", "mail");
        }
 
-       g_ptr_array_free (thread_ids, TRUE);
+       ret = _notmuch_database_link_message (notmuch, message, message_file);
+       if (ret)
+           goto DONE;
 
        date = notmuch_message_file_get_header (message_file, "date");
-       time_value = notmuch_parse_date (date, NULL);
-
-       doc.add_value (NOTMUCH_VALUE_DATE,
-                      Xapian::sortable_serialise (time_value));
+       _notmuch_message_set_date (message, date);
 
        from = notmuch_message_file_get_header (message_file, "from");
        subject = notmuch_message_file_get_header (message_file, "subject");
@@ -575,7 +858,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
            ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
            goto DONE;
        } else {
-           db->add_document (doc);
+           _notmuch_message_sync (message);
        }
     } catch (const Xapian::Error &error) {
        fprintf (stderr, "A Xapian exception occurred: %s.\n",
@@ -585,6 +868,8 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
     }
 
   DONE:
+    if (message)
+       notmuch_message_destroy (message);
     if (message_file)
        notmuch_message_file_close (message_file);