]> git.notmuchmail.org Git - notmuch/blob - database.cc
add_message: Propagate error status from notmuch_message_create_for_message_id
[notmuch] / database.cc
1 /* database.cc - The database interfaces of the notmuch mail library
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "database-private.h"
22
23 #include <iostream>
24
25 #include <xapian.h>
26
27 #include <glib.h> /* g_strdup_printf, g_free, GPtrArray, GHashTable */
28
29 using namespace std;
30
31 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
32
33 typedef struct {
34     const char *name;
35     const char *prefix;
36 } prefix_t;
37
38 /* Here's the current schema for our database:
39  *
40  * We currently have two different types of documents: mail and timestamps.
41  *
42  * Mail document
43  * -------------
44  * A mail document is associated with a particular email message file
45  * on disk. It is indexed with the following prefixed terms:
46  *
47  *    Single terms of given prefix:
48  *
49  *      type:   mail
50  *
51  *      id:     Unique ID of mail, (from Message-ID header or generated
52  *              as "notmuch-sha1-<sha1_sum_of_entire_file>.
53  *
54  *    Multiple terms of given prefix:
55  *
56  *      ref:    The message IDs from all In-Reply-To and References
57  *              headers in the message.
58  *
59  *      tag:    Any tags associated with this message by the user.
60  *
61  *      thread: The thread ID of all threads to which the mail belongs
62  *
63  *    A mail document also has two values:
64  *
65  *      TIMESTAMP:      The time_t value corresponding to the message's
66  *                      Date header.
67  *
68  *      MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
69  *
70  * Timestamp document
71  * ------------------
72  * A timestamp document is used by a client of the notmuch library to
73  * maintain data necessary to allow for efficient polling of mail
74  * directories. The notmuch library does no interpretation of
75  * timestamps, but merely allows the user to store and retrieve
76  * timestamps as name/value pairs.
77  *
78  * The timestamp document is indexed with a single prefixed term:
79  *
80  *      timestamp:      The user's key value (likely a directory name)
81  *
82  * and has a single value:
83  *
84  *      TIMETAMPS:      The time_t value from the user.
85  */
86
87 /* With these prefix values we follow the conventions published here:
88  *
89  * http://xapian.org/docs/omega/termprefixes.html
90  *
91  * as much as makes sense. Note that I took some liberty in matching
92  * the reserved prefix values to notmuch concepts, (for example, 'G'
93  * is documented as "newsGroup (or similar entity - e.g. a web forum
94  * name)", for which I think the thread is the closest analogue in
95  * notmuch. This in spite of the fact that we will eventually be
96  * storing mailing-list messages where 'G' for "mailing list name"
97  * might be even a closer analogue. I'm treating the single-character
98  * prefixes preferentially for core notmuch concepts (which will be
99  * nearly universal to all mail messages).
100  */
101
102 prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
103     { "type", "T" },
104     { "thread", "G" },
105     { "ref", "XREFERENCE" },
106     { "timestamp", "XTIMESTAMP" },
107 };
108
109 prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
110     { "tag", "K" },
111     { "id", "Q" }
112 };
113
114 const char *
115 _find_prefix (const char *name)
116 {
117     unsigned int i;
118
119     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++)
120         if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
121             return BOOLEAN_PREFIX_INTERNAL[i].prefix;
122
123     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++)
124         if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
125             return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
126
127     fprintf (stderr, "Internal error: No prefix exists for '%s'\n", name);
128     exit (1);
129
130     return "";
131 }
132
133 const char *
134 notmuch_status_to_string (notmuch_status_t status)
135 {
136     switch (status) {
137     case NOTMUCH_STATUS_SUCCESS:
138         return "No error occurred";
139     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
140         return "A Xapian exception occurred";
141     case NOTMUCH_STATUS_FILE_ERROR:
142         return "Something went wrong trying to read or write a file";
143     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
144         return "File is not an email";
145     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
146         return "Message ID is identical to a message in database";
147     case NOTMUCH_STATUS_NULL_POINTER:
148         return "Erroneous NULL pointer";
149     case NOTMUCH_STATUS_TAG_TOO_LONG:
150         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
151     default:
152     case NOTMUCH_STATUS_LAST_STATUS:
153         return "Unknown error status value";
154     }
155 }
156
157 /* XXX: We should drop this function and convert all callers to call
158  * _notmuch_message_add_term instead. */
159 static void
160 add_term (Xapian::Document doc,
161           const char *prefix_name,
162           const char *value)
163 {
164     const char *prefix;
165     char *term;
166
167     if (value == NULL)
168         return;
169
170     prefix = _find_prefix (prefix_name);
171
172     term = g_strdup_printf ("%s%s", prefix, value);
173
174     if (strlen (term) <= NOTMUCH_TERM_MAX)
175         doc.add_term (term);
176
177     g_free (term);
178 }
179
180 static void
181 find_doc_ids (notmuch_database_t *notmuch,
182               const char *prefix_name,
183               const char *value,
184               Xapian::PostingIterator *begin,
185               Xapian::PostingIterator *end)
186 {
187     Xapian::PostingIterator i;
188     char *term;
189
190     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
191
192     *begin = notmuch->xapian_db->postlist_begin (term);
193
194     *end = notmuch->xapian_db->postlist_end (term);
195
196     free (term);
197 }
198
199 static notmuch_private_status_t
200 find_unique_doc_id (notmuch_database_t *notmuch,
201                     const char *prefix_name,
202                     const char *value,
203                     unsigned int *doc_id)
204 {
205     Xapian::PostingIterator i, end;
206
207     find_doc_ids (notmuch, prefix_name, value, &i, &end);
208
209     if (i == end) {
210         *doc_id = 0;
211         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
212     } else {
213         *doc_id = *i;
214         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
215     }
216 }
217
218 static Xapian::Document
219 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
220 {
221     return notmuch->xapian_db->get_document (doc_id);
222 }
223
224 static notmuch_private_status_t
225 find_unique_document (notmuch_database_t *notmuch,
226                       const char *prefix_name,
227                       const char *value,
228                       Xapian::Document *document,
229                       unsigned int *doc_id)
230 {
231     notmuch_private_status_t status;
232
233     status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
234
235     if (status) {
236         *document = Xapian::Document ();
237         return status;
238     }
239
240     *document = find_document_for_doc_id (notmuch, *doc_id);
241     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
242 }
243
244 /* XXX: Should rewrite this to accept a notmuch_message_t* instead of
245  * a Xapian:Document and then we could just use
246  * notmuch_message_get_thread_ids instead of duplicating its logic
247  * here. */
248 static void
249 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
250 {
251     string value_string;
252     Xapian::TermIterator i;
253     const char *prefix_str = _find_prefix ("thread");
254     char prefix;
255
256     assert (strlen (prefix_str) == 1);
257
258     prefix = *prefix_str;
259
260     i = doc.termlist_begin ();
261     i.skip_to (prefix_str);
262
263     while (1) {
264         if (i == doc.termlist_end ())
265             break;
266         value_string = *i;
267         if (value_string.empty () || value_string[0] != prefix)
268             break;
269         g_hash_table_insert (thread_ids,
270                              strdup (value_string.c_str () + 1), NULL);
271         i++;
272     }
273 }
274
275 notmuch_message_t *
276 notmuch_database_find_message (notmuch_database_t *notmuch,
277                                const char *message_id)
278 {
279     notmuch_private_status_t status;
280     unsigned int doc_id;
281
282     status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
283
284     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
285         return NULL;
286
287     return _notmuch_message_create (notmuch, notmuch, doc_id, NULL);
288 }
289
290 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
291  * given message based on looking into the database for any messages
292  * referenced in parents, and also for any messages in the database
293  * referencing message_id.
294  *
295  * Caller should free all strings in the array and the array itself,
296  * (g_ptr_array_free) when done. */
297 static GPtrArray *
298 find_thread_ids (notmuch_database_t *notmuch,
299                  GPtrArray *parents,
300                  const char *message_id)
301 {
302     Xapian::PostingIterator child, children_end;
303     Xapian::Document doc;
304     GHashTable *thread_ids;
305     GList *keys, *l;
306     unsigned int i;
307     const char *parent_message_id;
308     GPtrArray *result;
309
310     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
311                                         free, NULL);
312
313     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
314     for ( ; child != children_end; child++) {
315         doc = find_document_for_doc_id (notmuch, *child);
316         insert_thread_id (thread_ids, doc);
317     }
318
319     for (i = 0; i < parents->len; i++) {
320         notmuch_message_t *parent;
321         notmuch_thread_ids_t *ids;
322
323         parent_message_id = (char *) g_ptr_array_index (parents, i);
324         parent = notmuch_database_find_message (notmuch, parent_message_id);
325         if (parent == NULL)
326             continue;
327
328         for (ids = notmuch_message_get_thread_ids (parent);
329              notmuch_thread_ids_has_more (ids);
330              notmuch_thread_ids_advance (ids))
331         {
332             const char *id;
333
334             id = notmuch_thread_ids_get (ids);
335             g_hash_table_insert (thread_ids, strdup (id), NULL);
336         }
337
338         notmuch_message_destroy (parent);
339     }
340
341     result = g_ptr_array_new ();
342
343     keys = g_hash_table_get_keys (thread_ids);
344     for (l = keys; l; l = l->next) {
345         char *id = (char *) l->data;
346         g_ptr_array_add (result, id);
347     }
348     g_list_free (keys);
349
350     /* We're done with the hash table, but we've taken the pointers to
351      * the allocated strings and put them into our result array, so
352      * tell the hash not to free them on its way out. */
353     g_hash_table_steal_all (thread_ids);
354     g_hash_table_unref (thread_ids);
355
356     return result;
357 }
358
359 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
360  * a (potentially nested) parenthesized sequence with '\' used to
361  * escape any character (including parentheses).
362  *
363  * If the sequence to be skipped continues to the end of the string,
364  * then 'str' will be left pointing at the final terminating '\0'
365  * character.
366  */
367 static void
368 skip_space_and_comments (const char **str)
369 {
370     const char *s;
371
372     s = *str;
373     while (*s && (isspace (*s) || *s == '(')) {
374         while (*s && isspace (*s))
375             s++;
376         if (*s == '(') {
377             int nesting = 1;
378             s++;
379             while (*s && nesting) {
380                 if (*s == '(')
381                     nesting++;
382                 else if (*s == ')')
383                     nesting--;
384                 else if (*s == '\\')
385                     if (*(s+1))
386                         s++;
387                 s++;
388             }
389         }
390     }
391
392     *str = s;
393 }
394
395 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
396  * comments, and the '<' and '>' delimeters.
397  *
398  * If not NULL, then *next will be made to point to the first character
399  * not parsed, (possibly pointing to the final '\0' terminator.
400  *
401  * Returns a newly allocated string which the caller should free()
402  * when done with it.
403  *
404  * Returns NULL if there is any error parsing the message-id. */
405 static char *
406 parse_message_id (const char *message_id, const char **next)
407 {
408     const char *s, *end;
409     char *result;
410
411     if (message_id == NULL)
412         return NULL;
413
414     s = message_id;
415
416     skip_space_and_comments (&s);
417
418     /* Skip any unstructured text as well. */
419     while (*s && *s != '<')
420         s++;
421
422     if (*s == '<') {
423         s++;
424     } else {
425         if (next)
426             *next = s;
427         return NULL;
428     }
429
430     skip_space_and_comments (&s);
431
432     end = s;
433     while (*end && *end != '>')
434         end++;
435     if (next) {
436         if (*end)
437             *next = end + 1;
438         else
439             *next = end;
440     }
441
442     if (end > s && *end == '>')
443         end--;
444     if (end <= s)
445         return NULL;
446
447     result = strndup (s, end - s + 1);
448
449     /* Finally, collapse any whitespace that is within the message-id
450      * itself. */
451     {
452         char *r;
453         int len;
454
455         for (r = result, len = strlen (r); *r; r++, len--)
456             if (*r == ' ' || *r == '\t')
457                 memmove (r, r+1, len);
458     }
459
460     return result;
461 }
462
463 /* Parse a References header value, putting a copy of each referenced
464  * message-id into 'array'. */
465 static void
466 parse_references (GPtrArray *array,
467                   const char *refs)
468 {
469     char *ref;
470
471     if (refs == NULL)
472         return;
473
474     while (*refs) {
475         ref = parse_message_id (refs, &refs);
476
477         if (ref)
478             g_ptr_array_add (array, ref);
479     }
480 }
481
482 char *
483 notmuch_database_default_path (void)
484 {
485     if (getenv ("NOTMUCH_BASE"))
486         return strdup (getenv ("NOTMUCH_BASE"));
487
488     return g_strdup_printf ("%s/mail", getenv ("HOME"));
489 }
490
491 notmuch_database_t *
492 notmuch_database_create (const char *path)
493 {
494     notmuch_database_t *notmuch = NULL;
495     char *notmuch_path = NULL;
496     struct stat st;
497     int err;
498     char *local_path = NULL;
499
500     if (path == NULL)
501         path = local_path = notmuch_database_default_path ();
502
503     err = stat (path, &st);
504     if (err) {
505         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
506                  path, strerror (errno));
507         goto DONE;
508     }
509
510     if (! S_ISDIR (st.st_mode)) {
511         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
512                  path);
513         goto DONE;
514     }
515
516     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
517
518     err = mkdir (notmuch_path, 0755);
519
520     if (err) {
521         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
522                  notmuch_path, strerror (errno));
523         goto DONE;
524     }
525
526     notmuch = notmuch_database_open (path);
527
528   DONE:
529     if (notmuch_path)
530         free (notmuch_path);
531     if (local_path)
532         free (local_path);
533
534     return notmuch;
535 }
536
537 notmuch_database_t *
538 notmuch_database_open (const char *path)
539 {
540     notmuch_database_t *notmuch = NULL;
541     char *notmuch_path = NULL, *xapian_path = NULL;
542     struct stat st;
543     int err;
544     char *local_path = NULL;
545     unsigned int i;
546
547     if (path == NULL)
548         path = local_path = notmuch_database_default_path ();
549
550     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
551
552     err = stat (notmuch_path, &st);
553     if (err) {
554         fprintf (stderr, "Error opening database at %s: %s\n",
555                  notmuch_path, strerror (errno));
556         goto DONE;
557     }
558
559     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
560
561     notmuch = talloc (NULL, notmuch_database_t);
562     notmuch->path = talloc_strdup (notmuch, path);
563
564     try {
565         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
566                                                            Xapian::DB_CREATE_OR_OPEN);
567         notmuch->query_parser = new Xapian::QueryParser;
568         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
569         notmuch->query_parser->set_database (*notmuch->xapian_db);
570
571         for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
572             prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
573             notmuch->query_parser->add_boolean_prefix (prefix->name,
574                                                        prefix->prefix);
575         }
576     } catch (const Xapian::Error &error) {
577         fprintf (stderr, "A Xapian exception occurred: %s\n",
578                  error.get_msg().c_str());
579     }
580     
581   DONE:
582     if (local_path)
583         free (local_path);
584     if (notmuch_path)
585         free (notmuch_path);
586     if (xapian_path)
587         free (xapian_path);
588
589     return notmuch;
590 }
591
592 void
593 notmuch_database_close (notmuch_database_t *notmuch)
594 {
595     delete notmuch->query_parser;
596     delete notmuch->xapian_db;
597     talloc_free (notmuch);
598 }
599
600 const char *
601 notmuch_database_get_path (notmuch_database_t *notmuch)
602 {
603     return notmuch->path;
604 }
605
606 notmuch_private_status_t
607 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
608                          Xapian::Document *doc, unsigned int *doc_id)
609 {
610     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
611 }
612
613 /* We allow the user to use arbitrarily long keys for timestamps,
614  * (they're for filesystem paths after all, which have no limit we
615  * know about). But we have a term-length limit. So if we exceed that,
616  * we'll use the SHA-1 of the user's key as the actual key for
617  * constructing a database term.
618  *
619  * Caution: This function returns a newly allocated string which the
620  * caller should free() when finished.
621  */
622 static char *
623 timestamp_db_key (const char *key)
624 {
625     int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
626
627     if (term_len > NOTMUCH_TERM_MAX)
628         return notmuch_sha1_of_string (key);
629     else
630         return strdup (key);
631 }
632
633 notmuch_status_t
634 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
635                                 const char *key, time_t timestamp)
636 {
637     Xapian::Document doc;
638     unsigned int doc_id;
639     notmuch_private_status_t status;
640     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
641     char *db_key = NULL;
642
643     db_key = timestamp_db_key (key);
644
645     try {
646         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
647
648         doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
649                        Xapian::sortable_serialise (timestamp));
650
651         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
652             char *term = talloc_asprintf (NULL, "%s%s",
653                                           _find_prefix ("timestamp"), db_key);
654             doc.add_term (term);
655             talloc_free (term);
656
657             notmuch->xapian_db->add_document (doc);
658         } else {
659             notmuch->xapian_db->replace_document (doc_id, doc);
660         }
661
662     } catch (Xapian::Error &error) {
663         fprintf (stderr, "A Xapian exception occurred: %s.\n",
664                  error.get_msg().c_str());
665         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
666     }
667
668     if (db_key)
669         free (db_key);
670
671     return ret;
672 }
673
674 time_t
675 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
676 {
677     Xapian::Document doc;
678     unsigned int doc_id;
679     notmuch_private_status_t status;
680     char *db_key = NULL;
681     time_t ret = 0;
682
683     db_key = timestamp_db_key (key);
684
685     try {
686         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
687
688         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
689             goto DONE;
690
691         ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
692     } catch (Xapian::Error &error) {
693         goto DONE;
694     }
695
696   DONE:
697     if (db_key)
698         free (db_key);
699
700     return ret;
701 }
702
703 notmuch_status_t
704 notmuch_database_add_message (notmuch_database_t *notmuch,
705                               const char *filename)
706 {
707     notmuch_message_file_t *message_file;
708     notmuch_message_t *message;
709     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
710
711     GPtrArray *parents, *thread_ids;
712
713     const char *refs, *in_reply_to, *date, *header;
714     const char *from, *to, *subject, *old_filename;
715     char *message_id;
716
717     unsigned int i;
718
719     message_file = notmuch_message_file_open (filename);
720     if (message_file == NULL) {
721         ret = NOTMUCH_STATUS_FILE_ERROR;
722         goto DONE;
723     }
724
725     notmuch_message_file_restrict_headers (message_file,
726                                            "date",
727                                            "from",
728                                            "in-reply-to",
729                                            "message-id",
730                                            "references",
731                                            "subject",
732                                            "to",
733                                            (char *) NULL);
734
735     try {
736         /* The first order of business is to find/create a message ID. */
737
738         header = notmuch_message_file_get_header (message_file, "message-id");
739         if (header) {
740             message_id = parse_message_id (header, NULL);
741             /* So the header value isn't RFC-compliant, but it's
742              * better than no message-id at all. */
743             if (message_id == NULL)
744                 message_id = xstrdup (header);
745         } else {
746             /* No message-id at all, let's generate one by taking a
747              * hash over the file's contents. */
748             char *sha1 = notmuch_sha1_of_file (filename);
749
750             /* If that failed too, something is really wrong. Give up. */
751             if (sha1 == NULL) {
752                 ret = NOTMUCH_STATUS_FILE_ERROR;
753                 goto DONE;
754             }
755
756             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
757             free (sha1);
758         }
759
760         /* Now that we have a message ID, we get a message object,
761          * (which may or may not reference an existing document in the
762          * database). */
763
764         /* Use NULL for owner since we want to free this locally. */
765         message = _notmuch_message_create_for_message_id (NULL,
766                                                           notmuch,
767                                                           message_id,
768                                                           &ret);
769         if (message == NULL)
770             goto DONE;
771
772         /* Has a message previously been added with the same ID? */
773         old_filename = notmuch_message_get_filename (message);
774         if (old_filename && strlen (old_filename)) {
775             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
776             goto DONE;
777         } else {
778             _notmuch_message_set_filename (message, filename);
779             _notmuch_message_add_term (message, "type", "mail");
780         }
781
782         /* Next, find the thread(s) to which this message belongs. */
783         parents = g_ptr_array_new ();
784
785         refs = notmuch_message_file_get_header (message_file, "references");
786         parse_references (parents, refs);
787
788         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
789         parse_references (parents, in_reply_to);
790
791         for (i = 0; i < parents->len; i++)
792             _notmuch_message_add_term (message, "ref",
793                                        (char *) g_ptr_array_index (parents, i));
794
795         thread_ids = find_thread_ids (notmuch, parents, message_id);
796
797         free (message_id);
798
799         for (i = 0; i < parents->len; i++)
800             g_free (g_ptr_array_index (parents, i));
801         g_ptr_array_free (parents, TRUE);
802
803         if (thread_ids->len) {
804             unsigned int i;
805             GString *thread_id;
806             char *id;
807
808             for (i = 0; i < thread_ids->len; i++) {
809                 id = (char *) thread_ids->pdata[i];
810                 _notmuch_message_add_thread_id (message, id);
811                 if (i == 0)
812                     thread_id = g_string_new (id);
813                 else
814                     g_string_append_printf (thread_id, ",%s", id);
815
816                 free (id);
817             }
818             g_string_free (thread_id, TRUE);
819         } else {
820             _notmuch_message_ensure_thread_id (message);
821         }
822
823         g_ptr_array_free (thread_ids, TRUE);
824
825         date = notmuch_message_file_get_header (message_file, "date");
826         _notmuch_message_set_date (message, date);
827
828         from = notmuch_message_file_get_header (message_file, "from");
829         subject = notmuch_message_file_get_header (message_file, "subject");
830         to = notmuch_message_file_get_header (message_file, "to");
831
832         if (from == NULL &&
833             subject == NULL &&
834             to == NULL)
835         {
836             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
837             goto DONE;
838         } else {
839             _notmuch_message_sync (message);
840         }
841     } catch (const Xapian::Error &error) {
842         fprintf (stderr, "A Xapian exception occurred: %s.\n",
843                  error.get_msg().c_str());
844         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
845         goto DONE;
846     }
847
848   DONE:
849     if (message)
850         notmuch_message_destroy (message);
851     if (message_file)
852         notmuch_message_file_close (message_file);
853
854     return ret;
855 }