3f0202277969d2289d192c61abc461379150af4e
[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     INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
128
129     return "";
130 }
131
132 const char *
133 notmuch_status_to_string (notmuch_status_t status)
134 {
135     switch (status) {
136     case NOTMUCH_STATUS_SUCCESS:
137         return "No error occurred";
138     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
139         return "A Xapian exception occurred";
140     case NOTMUCH_STATUS_FILE_ERROR:
141         return "Something went wrong trying to read or write a file";
142     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
143         return "File is not an email";
144     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
145         return "Message ID is identical to a message in database";
146     case NOTMUCH_STATUS_NULL_POINTER:
147         return "Erroneous NULL pointer";
148     case NOTMUCH_STATUS_TAG_TOO_LONG:
149         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
150     default:
151     case NOTMUCH_STATUS_LAST_STATUS:
152         return "Unknown error status value";
153     }
154 }
155
156 /* XXX: We should drop this function and convert all callers to call
157  * _notmuch_message_add_term instead. */
158 static void
159 add_term (Xapian::Document doc,
160           const char *prefix_name,
161           const char *value)
162 {
163     const char *prefix;
164     char *term;
165
166     if (value == NULL)
167         return;
168
169     prefix = _find_prefix (prefix_name);
170
171     term = g_strdup_printf ("%s%s", prefix, value);
172
173     if (strlen (term) <= NOTMUCH_TERM_MAX)
174         doc.add_term (term);
175
176     g_free (term);
177 }
178
179 static void
180 find_doc_ids (notmuch_database_t *notmuch,
181               const char *prefix_name,
182               const char *value,
183               Xapian::PostingIterator *begin,
184               Xapian::PostingIterator *end)
185 {
186     Xapian::PostingIterator i;
187     char *term;
188
189     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
190
191     *begin = notmuch->xapian_db->postlist_begin (term);
192
193     *end = notmuch->xapian_db->postlist_end (term);
194
195     free (term);
196 }
197
198 static notmuch_private_status_t
199 find_unique_doc_id (notmuch_database_t *notmuch,
200                     const char *prefix_name,
201                     const char *value,
202                     unsigned int *doc_id)
203 {
204     Xapian::PostingIterator i, end;
205
206     find_doc_ids (notmuch, prefix_name, value, &i, &end);
207
208     if (i == end) {
209         *doc_id = 0;
210         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
211     } else {
212         *doc_id = *i;
213         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
214     }
215 }
216
217 static Xapian::Document
218 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
219 {
220     return notmuch->xapian_db->get_document (doc_id);
221 }
222
223 static notmuch_private_status_t
224 find_unique_document (notmuch_database_t *notmuch,
225                       const char *prefix_name,
226                       const char *value,
227                       Xapian::Document *document,
228                       unsigned int *doc_id)
229 {
230     notmuch_private_status_t status;
231
232     status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
233
234     if (status) {
235         *document = Xapian::Document ();
236         return status;
237     }
238
239     *document = find_document_for_doc_id (notmuch, *doc_id);
240     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
241 }
242
243 /* XXX: Should rewrite this to accept a notmuch_message_t* instead of
244  * a Xapian:Document and then we could just use
245  * notmuch_message_get_thread_ids instead of duplicating its logic
246  * here. */
247 static void
248 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
249 {
250     string value_string;
251     Xapian::TermIterator i;
252     const char *prefix_str = _find_prefix ("thread");
253     char prefix;
254
255     assert (strlen (prefix_str) == 1);
256
257     prefix = *prefix_str;
258
259     i = doc.termlist_begin ();
260     i.skip_to (prefix_str);
261
262     while (1) {
263         if (i == doc.termlist_end ())
264             break;
265         value_string = *i;
266         if (value_string.empty () || value_string[0] != prefix)
267             break;
268         g_hash_table_insert (thread_ids,
269                              strdup (value_string.c_str () + 1), NULL);
270         i++;
271     }
272 }
273
274 notmuch_message_t *
275 notmuch_database_find_message (notmuch_database_t *notmuch,
276                                const char *message_id)
277 {
278     notmuch_private_status_t status;
279     unsigned int doc_id;
280
281     status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
282
283     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
284         return NULL;
285
286     return _notmuch_message_create (notmuch, notmuch, doc_id, NULL);
287 }
288
289 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
290  * given message based on looking into the database for any messages
291  * referenced in parents, and also for any messages in the database
292  * referencing message_id.
293  *
294  * Caller should free all strings in the array and the array itself,
295  * (g_ptr_array_free) when done. */
296 static GPtrArray *
297 find_thread_ids (notmuch_database_t *notmuch,
298                  GPtrArray *parents,
299                  const char *message_id)
300 {
301     Xapian::PostingIterator child, children_end;
302     Xapian::Document doc;
303     GHashTable *thread_ids;
304     GList *keys, *l;
305     unsigned int i;
306     const char *parent_message_id;
307     GPtrArray *result;
308
309     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
310                                         free, NULL);
311
312     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
313     for ( ; child != children_end; child++) {
314         doc = find_document_for_doc_id (notmuch, *child);
315         insert_thread_id (thread_ids, doc);
316     }
317
318     for (i = 0; i < parents->len; i++) {
319         notmuch_message_t *parent;
320         notmuch_thread_ids_t *ids;
321
322         parent_message_id = (char *) g_ptr_array_index (parents, i);
323         parent = notmuch_database_find_message (notmuch, parent_message_id);
324         if (parent == NULL)
325             continue;
326
327         for (ids = notmuch_message_get_thread_ids (parent);
328              notmuch_thread_ids_has_more (ids);
329              notmuch_thread_ids_advance (ids))
330         {
331             const char *id;
332
333             id = notmuch_thread_ids_get (ids);
334             g_hash_table_insert (thread_ids, strdup (id), NULL);
335         }
336
337         notmuch_message_destroy (parent);
338     }
339
340     result = g_ptr_array_new ();
341
342     keys = g_hash_table_get_keys (thread_ids);
343     for (l = keys; l; l = l->next) {
344         char *id = (char *) l->data;
345         g_ptr_array_add (result, id);
346     }
347     g_list_free (keys);
348
349     /* We're done with the hash table, but we've taken the pointers to
350      * the allocated strings and put them into our result array, so
351      * tell the hash not to free them on its way out. */
352     g_hash_table_steal_all (thread_ids);
353     g_hash_table_unref (thread_ids);
354
355     return result;
356 }
357
358 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
359  * a (potentially nested) parenthesized sequence with '\' used to
360  * escape any character (including parentheses).
361  *
362  * If the sequence to be skipped continues to the end of the string,
363  * then 'str' will be left pointing at the final terminating '\0'
364  * character.
365  */
366 static void
367 skip_space_and_comments (const char **str)
368 {
369     const char *s;
370
371     s = *str;
372     while (*s && (isspace (*s) || *s == '(')) {
373         while (*s && isspace (*s))
374             s++;
375         if (*s == '(') {
376             int nesting = 1;
377             s++;
378             while (*s && nesting) {
379                 if (*s == '(')
380                     nesting++;
381                 else if (*s == ')')
382                     nesting--;
383                 else if (*s == '\\')
384                     if (*(s+1))
385                         s++;
386                 s++;
387             }
388         }
389     }
390
391     *str = s;
392 }
393
394 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
395  * comments, and the '<' and '>' delimeters.
396  *
397  * If not NULL, then *next will be made to point to the first character
398  * not parsed, (possibly pointing to the final '\0' terminator.
399  *
400  * Returns a newly allocated string which the caller should free()
401  * when done with it.
402  *
403  * Returns NULL if there is any error parsing the message-id. */
404 static char *
405 parse_message_id (const char *message_id, const char **next)
406 {
407     const char *s, *end;
408     char *result;
409
410     if (message_id == NULL)
411         return NULL;
412
413     s = message_id;
414
415     skip_space_and_comments (&s);
416
417     /* Skip any unstructured text as well. */
418     while (*s && *s != '<')
419         s++;
420
421     if (*s == '<') {
422         s++;
423     } else {
424         if (next)
425             *next = s;
426         return NULL;
427     }
428
429     skip_space_and_comments (&s);
430
431     end = s;
432     while (*end && *end != '>')
433         end++;
434     if (next) {
435         if (*end)
436             *next = end + 1;
437         else
438             *next = end;
439     }
440
441     if (end > s && *end == '>')
442         end--;
443     if (end <= s)
444         return NULL;
445
446     result = strndup (s, end - s + 1);
447
448     /* Finally, collapse any whitespace that is within the message-id
449      * itself. */
450     {
451         char *r;
452         int len;
453
454         for (r = result, len = strlen (r); *r; r++, len--)
455             if (*r == ' ' || *r == '\t')
456                 memmove (r, r+1, len);
457     }
458
459     return result;
460 }
461
462 /* Parse a References header value, putting a copy of each referenced
463  * message-id into 'array'. */
464 static void
465 parse_references (GPtrArray *array,
466                   const char *refs)
467 {
468     char *ref;
469
470     if (refs == NULL)
471         return;
472
473     while (*refs) {
474         ref = parse_message_id (refs, &refs);
475
476         if (ref)
477             g_ptr_array_add (array, ref);
478     }
479 }
480
481 char *
482 notmuch_database_default_path (void)
483 {
484     if (getenv ("NOTMUCH_BASE"))
485         return strdup (getenv ("NOTMUCH_BASE"));
486
487     return g_strdup_printf ("%s/mail", getenv ("HOME"));
488 }
489
490 notmuch_database_t *
491 notmuch_database_create (const char *path)
492 {
493     notmuch_database_t *notmuch = NULL;
494     char *notmuch_path = NULL;
495     struct stat st;
496     int err;
497     char *local_path = NULL;
498
499     if (path == NULL)
500         path = local_path = notmuch_database_default_path ();
501
502     err = stat (path, &st);
503     if (err) {
504         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
505                  path, strerror (errno));
506         goto DONE;
507     }
508
509     if (! S_ISDIR (st.st_mode)) {
510         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
511                  path);
512         goto DONE;
513     }
514
515     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
516
517     err = mkdir (notmuch_path, 0755);
518
519     if (err) {
520         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
521                  notmuch_path, strerror (errno));
522         goto DONE;
523     }
524
525     notmuch = notmuch_database_open (path);
526
527   DONE:
528     if (notmuch_path)
529         free (notmuch_path);
530     if (local_path)
531         free (local_path);
532
533     return notmuch;
534 }
535
536 notmuch_database_t *
537 notmuch_database_open (const char *path)
538 {
539     notmuch_database_t *notmuch = NULL;
540     char *notmuch_path = NULL, *xapian_path = NULL;
541     struct stat st;
542     int err;
543     char *local_path = NULL;
544     unsigned int i;
545
546     if (path == NULL)
547         path = local_path = notmuch_database_default_path ();
548
549     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
550
551     err = stat (notmuch_path, &st);
552     if (err) {
553         fprintf (stderr, "Error opening database at %s: %s\n",
554                  notmuch_path, strerror (errno));
555         goto DONE;
556     }
557
558     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
559
560     notmuch = talloc (NULL, notmuch_database_t);
561     notmuch->path = talloc_strdup (notmuch, path);
562
563     try {
564         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
565                                                            Xapian::DB_CREATE_OR_OPEN);
566         notmuch->query_parser = new Xapian::QueryParser;
567         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
568         notmuch->query_parser->set_database (*notmuch->xapian_db);
569
570         for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
571             prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
572             notmuch->query_parser->add_boolean_prefix (prefix->name,
573                                                        prefix->prefix);
574         }
575     } catch (const Xapian::Error &error) {
576         fprintf (stderr, "A Xapian exception occurred: %s\n",
577                  error.get_msg().c_str());
578     }
579     
580   DONE:
581     if (local_path)
582         free (local_path);
583     if (notmuch_path)
584         free (notmuch_path);
585     if (xapian_path)
586         free (xapian_path);
587
588     return notmuch;
589 }
590
591 void
592 notmuch_database_close (notmuch_database_t *notmuch)
593 {
594     delete notmuch->query_parser;
595     delete notmuch->xapian_db;
596     talloc_free (notmuch);
597 }
598
599 const char *
600 notmuch_database_get_path (notmuch_database_t *notmuch)
601 {
602     return notmuch->path;
603 }
604
605 notmuch_private_status_t
606 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
607                          Xapian::Document *doc, unsigned int *doc_id)
608 {
609     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
610 }
611
612 /* We allow the user to use arbitrarily long keys for timestamps,
613  * (they're for filesystem paths after all, which have no limit we
614  * know about). But we have a term-length limit. So if we exceed that,
615  * we'll use the SHA-1 of the user's key as the actual key for
616  * constructing a database term.
617  *
618  * Caution: This function returns a newly allocated string which the
619  * caller should free() when finished.
620  */
621 static char *
622 timestamp_db_key (const char *key)
623 {
624     int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
625
626     if (term_len > NOTMUCH_TERM_MAX)
627         return notmuch_sha1_of_string (key);
628     else
629         return strdup (key);
630 }
631
632 notmuch_status_t
633 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
634                                 const char *key, time_t timestamp)
635 {
636     Xapian::Document doc;
637     unsigned int doc_id;
638     notmuch_private_status_t status;
639     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
640     char *db_key = NULL;
641
642     db_key = timestamp_db_key (key);
643
644     try {
645         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
646
647         doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
648                        Xapian::sortable_serialise (timestamp));
649
650         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
651             char *term = talloc_asprintf (NULL, "%s%s",
652                                           _find_prefix ("timestamp"), db_key);
653             doc.add_term (term);
654             talloc_free (term);
655
656             notmuch->xapian_db->add_document (doc);
657         } else {
658             notmuch->xapian_db->replace_document (doc_id, doc);
659         }
660
661     } catch (Xapian::Error &error) {
662         fprintf (stderr, "A Xapian exception occurred: %s.\n",
663                  error.get_msg().c_str());
664         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
665     }
666
667     if (db_key)
668         free (db_key);
669
670     return ret;
671 }
672
673 time_t
674 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
675 {
676     Xapian::Document doc;
677     unsigned int doc_id;
678     notmuch_private_status_t status;
679     char *db_key = NULL;
680     time_t ret = 0;
681
682     db_key = timestamp_db_key (key);
683
684     try {
685         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
686
687         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
688             goto DONE;
689
690         ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
691     } catch (Xapian::Error &error) {
692         goto DONE;
693     }
694
695   DONE:
696     if (db_key)
697         free (db_key);
698
699     return ret;
700 }
701
702 notmuch_status_t
703 notmuch_database_add_message (notmuch_database_t *notmuch,
704                               const char *filename)
705 {
706     notmuch_message_file_t *message_file;
707     notmuch_message_t *message;
708     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
709
710     GPtrArray *parents, *thread_ids;
711
712     const char *refs, *in_reply_to, *date, *header;
713     const char *from, *to, *subject, *old_filename;
714     char *message_id;
715
716     unsigned int i;
717
718     message_file = notmuch_message_file_open (filename);
719     if (message_file == NULL) {
720         ret = NOTMUCH_STATUS_FILE_ERROR;
721         goto DONE;
722     }
723
724     notmuch_message_file_restrict_headers (message_file,
725                                            "date",
726                                            "from",
727                                            "in-reply-to",
728                                            "message-id",
729                                            "references",
730                                            "subject",
731                                            "to",
732                                            (char *) NULL);
733
734     try {
735         /* The first order of business is to find/create a message ID. */
736
737         header = notmuch_message_file_get_header (message_file, "message-id");
738         if (header) {
739             message_id = parse_message_id (header, NULL);
740             /* So the header value isn't RFC-compliant, but it's
741              * better than no message-id at all. */
742             if (message_id == NULL)
743                 message_id = xstrdup (header);
744         } else {
745             /* No message-id at all, let's generate one by taking a
746              * hash over the file's contents. */
747             char *sha1 = notmuch_sha1_of_file (filename);
748
749             /* If that failed too, something is really wrong. Give up. */
750             if (sha1 == NULL) {
751                 ret = NOTMUCH_STATUS_FILE_ERROR;
752                 goto DONE;
753             }
754
755             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
756             free (sha1);
757         }
758
759         /* Now that we have a message ID, we get a message object,
760          * (which may or may not reference an existing document in the
761          * database). */
762
763         /* Use NULL for owner since we want to free this locally. */
764         message = _notmuch_message_create_for_message_id (NULL,
765                                                           notmuch,
766                                                           message_id,
767                                                           &ret);
768         if (message == NULL)
769             goto DONE;
770
771         /* Has a message previously been added with the same ID? */
772         old_filename = notmuch_message_get_filename (message);
773         if (old_filename && strlen (old_filename)) {
774             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
775             goto DONE;
776         } else {
777             _notmuch_message_set_filename (message, filename);
778             _notmuch_message_add_term (message, "type", "mail");
779         }
780
781         /* Next, find the thread(s) to which this message belongs. */
782         parents = g_ptr_array_new ();
783
784         refs = notmuch_message_file_get_header (message_file, "references");
785         parse_references (parents, refs);
786
787         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
788         parse_references (parents, in_reply_to);
789
790         for (i = 0; i < parents->len; i++)
791             _notmuch_message_add_term (message, "ref",
792                                        (char *) g_ptr_array_index (parents, i));
793
794         thread_ids = find_thread_ids (notmuch, parents, message_id);
795
796         free (message_id);
797
798         for (i = 0; i < parents->len; i++)
799             g_free (g_ptr_array_index (parents, i));
800         g_ptr_array_free (parents, TRUE);
801
802         if (thread_ids->len) {
803             unsigned int i;
804             GString *thread_id;
805             char *id;
806
807             for (i = 0; i < thread_ids->len; i++) {
808                 id = (char *) thread_ids->pdata[i];
809                 _notmuch_message_add_thread_id (message, id);
810                 if (i == 0)
811                     thread_id = g_string_new (id);
812                 else
813                     g_string_append_printf (thread_id, ",%s", id);
814
815                 free (id);
816             }
817             g_string_free (thread_id, TRUE);
818         } else {
819             _notmuch_message_ensure_thread_id (message);
820         }
821
822         g_ptr_array_free (thread_ids, TRUE);
823
824         date = notmuch_message_file_get_header (message_file, "date");
825         _notmuch_message_set_date (message, date);
826
827         from = notmuch_message_file_get_header (message_file, "from");
828         subject = notmuch_message_file_get_header (message_file, "subject");
829         to = notmuch_message_file_get_header (message_file, "to");
830
831         if (from == NULL &&
832             subject == NULL &&
833             to == NULL)
834         {
835             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
836             goto DONE;
837         } else {
838             _notmuch_message_sync (message);
839         }
840     } catch (const Xapian::Error &error) {
841         fprintf (stderr, "A Xapian exception occurred: %s.\n",
842                  error.get_msg().c_str());
843         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
844         goto DONE;
845     }
846
847   DONE:
848     if (message)
849         notmuch_message_destroy (message);
850     if (message_file)
851         notmuch_message_file_close (message_file);
852
853     return ret;
854 }