]> git.notmuchmail.org Git - notmuch/blob - lib/database.cc
dbfec63031b367bab636c6c83180703ca6c0cbd5
[notmuch] / lib / 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_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 directory.
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 which the
46  * database uses to construct threads, etc.:
47  *
48  *    Single terms of given prefix:
49  *
50  *      type:   mail
51  *
52  *      id:     Unique ID of mail, (from Message-ID header or generated
53  *              as "notmuch-sha1-<sha1_sum_of_entire_file>.
54  *
55  *      thread: The ID of the thread to which the mail belongs
56  *
57  *      replyto: The ID from the In-Reply-To header of the mail (if any).
58  *
59  *    Multiple terms of given prefix:
60  *
61  *      reference: All message IDs from In-Reply-To and Re ferences
62  *                 headers in the message.
63  *
64  *      tag:       Any tags associated with this message by the user.
65  *
66  *    A mail document also has two values:
67  *
68  *      TIMESTAMP:      The time_t value corresponding to the message's
69  *                      Date header.
70  *
71  *      MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
72  *
73  * In addition, terms from the content of the message are added with
74  * "from", "to", "attachment", and "subject" prefixes for use by the
75  * user in searching. But the database doesn't really care itself
76  * about any of these.
77  *
78  * Finally, the data portion of a mail document contains the path name
79  * of the mail message (relative to the database path).
80  *
81  * Directory document
82  * ------------------
83  * A directory document is used by a client of the notmuch library to
84  * maintain data necessary to allow for efficient polling of mail
85  * directories.
86  *
87  * The directory document contains the following terms:
88  *
89  *      directory:      The directory path (relative to the database path)
90  *                      Or the SHA1 sum of the directory path (if the
91  *                      path itself is too long to fit in a Xapian
92  *                      term).
93  *
94  *      parent:         The document ID of the parent directory document.
95  *                      Top-level directories will have a parent value of 0.
96  *
97  * and has a single value:
98  *
99  *      TIMESTAMP:      The mtime of the directory (at last scan)
100  *
101  * The data portion of a directory document contains the path of the
102  * directory (relative to the datbase path).
103  */
104
105 /* With these prefix values we follow the conventions published here:
106  *
107  * http://xapian.org/docs/omega/termprefixes.html
108  *
109  * as much as makes sense. Note that I took some liberty in matching
110  * the reserved prefix values to notmuch concepts, (for example, 'G'
111  * is documented as "newsGroup (or similar entity - e.g. a web forum
112  * name)", for which I think the thread is the closest analogue in
113  * notmuch. This in spite of the fact that we will eventually be
114  * storing mailing-list messages where 'G' for "mailing list name"
115  * might be even a closer analogue. I'm treating the single-character
116  * prefixes preferentially for core notmuch concepts (which will be
117  * nearly universal to all mail messages).
118  */
119
120 prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
121     { "type", "T" },
122     { "reference", "XREFERENCE" },
123     { "replyto", "XREPLYTO" },
124     { "directory", "XDIRECTORY" },
125     { "parent", "XPARENT" },
126 };
127
128 prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
129     { "thread", "G" },
130     { "tag", "K" },
131     { "id", "Q" }
132 };
133
134 prefix_t PROBABILISTIC_PREFIX[]= {
135     { "from", "XFROM" },
136     { "to", "XTO" },
137     { "attachment", "XATTACHMENT" },
138     { "subject", "XSUBJECT"}
139 };
140
141 int
142 _internal_error (const char *format, ...)
143 {
144     va_list va_args;
145
146     va_start (va_args, format);
147
148     fprintf (stderr, "Internal error: ");
149     vfprintf (stderr, format, va_args);
150
151     exit (1);
152
153     return 1;
154 }
155
156 const char *
157 _find_prefix (const char *name)
158 {
159     unsigned int i;
160
161     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++) {
162         if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
163             return BOOLEAN_PREFIX_INTERNAL[i].prefix;
164     }
165
166     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
167         if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
168             return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
169     }
170
171     for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
172         if (strcmp (name, PROBABILISTIC_PREFIX[i].name) == 0)
173             return PROBABILISTIC_PREFIX[i].prefix;
174     }
175
176     INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
177
178     return "";
179 }
180
181 const char *
182 notmuch_status_to_string (notmuch_status_t status)
183 {
184     switch (status) {
185     case NOTMUCH_STATUS_SUCCESS:
186         return "No error occurred";
187     case NOTMUCH_STATUS_OUT_OF_MEMORY:
188         return "Out of memory";
189     case NOTMUCH_STATUS_READONLY_DATABASE:
190         return "The database is read-only";
191     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
192         return "A Xapian exception occurred";
193     case NOTMUCH_STATUS_FILE_ERROR:
194         return "Something went wrong trying to read or write a file";
195     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
196         return "File is not an email";
197     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
198         return "Message ID is identical to a message in database";
199     case NOTMUCH_STATUS_NULL_POINTER:
200         return "Erroneous NULL pointer";
201     case NOTMUCH_STATUS_TAG_TOO_LONG:
202         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
203     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
204         return "Unbalanced number of calls to notmuch_message_freeze/thaw";
205     default:
206     case NOTMUCH_STATUS_LAST_STATUS:
207         return "Unknown error status value";
208     }
209 }
210
211 static void
212 find_doc_ids (notmuch_database_t *notmuch,
213               const char *prefix_name,
214               const char *value,
215               Xapian::PostingIterator *begin,
216               Xapian::PostingIterator *end)
217 {
218     Xapian::PostingIterator i;
219     char *term;
220
221     term = talloc_asprintf (notmuch, "%s%s",
222                             _find_prefix (prefix_name), value);
223
224     *begin = notmuch->xapian_db->postlist_begin (term);
225
226     *end = notmuch->xapian_db->postlist_end (term);
227
228     talloc_free (term);
229 }
230
231 static notmuch_private_status_t
232 find_unique_doc_id (notmuch_database_t *notmuch,
233                     const char *prefix_name,
234                     const char *value,
235                     unsigned int *doc_id)
236 {
237     Xapian::PostingIterator i, end;
238
239     find_doc_ids (notmuch, prefix_name, value, &i, &end);
240
241     if (i == end) {
242         *doc_id = 0;
243         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
244     } else {
245         *doc_id = *i;
246         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
247     }
248 }
249
250 static Xapian::Document
251 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
252 {
253     return notmuch->xapian_db->get_document (doc_id);
254 }
255
256 static notmuch_private_status_t
257 find_unique_document (notmuch_database_t *notmuch,
258                       const char *prefix_name,
259                       const char *value,
260                       Xapian::Document *document,
261                       unsigned int *doc_id)
262 {
263     notmuch_private_status_t status;
264
265     status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
266
267     if (status) {
268         *document = Xapian::Document ();
269         return status;
270     }
271
272     *document = find_document_for_doc_id (notmuch, *doc_id);
273     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
274 }
275
276 notmuch_message_t *
277 notmuch_database_find_message (notmuch_database_t *notmuch,
278                                const char *message_id)
279 {
280     notmuch_private_status_t status;
281     unsigned int doc_id;
282
283     status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
284
285     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
286         return NULL;
287
288     return _notmuch_message_create (notmuch, notmuch, doc_id, NULL);
289 }
290
291 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
292  * a (potentially nested) parenthesized sequence with '\' used to
293  * escape any character (including parentheses).
294  *
295  * If the sequence to be skipped continues to the end of the string,
296  * then 'str' will be left pointing at the final terminating '\0'
297  * character.
298  */
299 static void
300 skip_space_and_comments (const char **str)
301 {
302     const char *s;
303
304     s = *str;
305     while (*s && (isspace (*s) || *s == '(')) {
306         while (*s && isspace (*s))
307             s++;
308         if (*s == '(') {
309             int nesting = 1;
310             s++;
311             while (*s && nesting) {
312                 if (*s == '(') {
313                     nesting++;
314                 } else if (*s == ')') {
315                     nesting--;
316                 } else if (*s == '\\') {
317                     if (*(s+1))
318                         s++;
319                 }
320                 s++;
321             }
322         }
323     }
324
325     *str = s;
326 }
327
328 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
329  * comments, and the '<' and '>' delimeters.
330  *
331  * If not NULL, then *next will be made to point to the first character
332  * not parsed, (possibly pointing to the final '\0' terminator.
333  *
334  * Returns a newly talloc'ed string belonging to 'ctx'.
335  *
336  * Returns NULL if there is any error parsing the message-id. */
337 static char *
338 _parse_message_id (void *ctx, const char *message_id, const char **next)
339 {
340     const char *s, *end;
341     char *result;
342
343     if (message_id == NULL || *message_id == '\0')
344         return NULL;
345
346     s = message_id;
347
348     skip_space_and_comments (&s);
349
350     /* Skip any unstructured text as well. */
351     while (*s && *s != '<')
352         s++;
353
354     if (*s == '<') {
355         s++;
356     } else {
357         if (next)
358             *next = s;
359         return NULL;
360     }
361
362     skip_space_and_comments (&s);
363
364     end = s;
365     while (*end && *end != '>')
366         end++;
367     if (next) {
368         if (*end)
369             *next = end + 1;
370         else
371             *next = end;
372     }
373
374     if (end > s && *end == '>')
375         end--;
376     if (end <= s)
377         return NULL;
378
379     result = talloc_strndup (ctx, s, end - s + 1);
380
381     /* Finally, collapse any whitespace that is within the message-id
382      * itself. */
383     {
384         char *r;
385         int len;
386
387         for (r = result, len = strlen (r); *r; r++, len--)
388             if (*r == ' ' || *r == '\t')
389                 memmove (r, r+1, len);
390     }
391
392     return result;
393 }
394
395 /* Parse a References header value, putting a (talloc'ed under 'ctx')
396  * copy of each referenced message-id into 'hash'.
397  *
398  * We explicitly avoid including any reference identical to
399  * 'message_id' in the result (to avoid mass confusion when a single
400  * message references itself cyclically---and yes, mail messages are
401  * not infrequent in the wild that do this---don't ask me why).
402 */
403 static void
404 parse_references (void *ctx,
405                   const char *message_id,
406                   GHashTable *hash,
407                   const char *refs)
408 {
409     char *ref;
410
411     if (refs == NULL || *refs == '\0')
412         return;
413
414     while (*refs) {
415         ref = _parse_message_id (ctx, refs, &refs);
416
417         if (ref && strcmp (ref, message_id))
418             g_hash_table_insert (hash, ref, NULL);
419     }
420 }
421
422 notmuch_database_t *
423 notmuch_database_create (const char *path)
424 {
425     notmuch_database_t *notmuch = NULL;
426     char *notmuch_path = NULL;
427     struct stat st;
428     int err;
429
430     if (path == NULL) {
431         fprintf (stderr, "Error: Cannot create a database for a NULL path.\n");
432         goto DONE;
433     }
434
435     err = stat (path, &st);
436     if (err) {
437         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
438                  path, strerror (errno));
439         goto DONE;
440     }
441
442     if (! S_ISDIR (st.st_mode)) {
443         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
444                  path);
445         goto DONE;
446     }
447
448     notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch");
449
450     err = mkdir (notmuch_path, 0755);
451
452     if (err) {
453         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
454                  notmuch_path, strerror (errno));
455         goto DONE;
456     }
457
458     notmuch = notmuch_database_open (path,
459                                      NOTMUCH_DATABASE_MODE_READ_WRITE);
460
461   DONE:
462     if (notmuch_path)
463         talloc_free (notmuch_path);
464
465     return notmuch;
466 }
467
468 notmuch_database_t *
469 notmuch_database_open (const char *path,
470                        notmuch_database_mode_t mode)
471 {
472     notmuch_database_t *notmuch = NULL;
473     char *notmuch_path = NULL, *xapian_path = NULL;
474     struct stat st;
475     int err;
476     unsigned int i;
477
478     if (asprintf (&notmuch_path, "%s/%s", path, ".notmuch") == -1) {
479         notmuch_path = NULL;
480         fprintf (stderr, "Out of memory\n");
481         goto DONE;
482     }
483
484     err = stat (notmuch_path, &st);
485     if (err) {
486         fprintf (stderr, "Error opening database at %s: %s\n",
487                  notmuch_path, strerror (errno));
488         goto DONE;
489     }
490
491     if (asprintf (&xapian_path, "%s/%s", notmuch_path, "xapian") == -1) {
492         xapian_path = NULL;
493         fprintf (stderr, "Out of memory\n");
494         goto DONE;
495     }
496
497     notmuch = talloc (NULL, notmuch_database_t);
498     notmuch->exception_reported = FALSE;
499     notmuch->path = talloc_strdup (notmuch, path);
500
501     if (notmuch->path[strlen (notmuch->path) - 1] == '/')
502         notmuch->path[strlen (notmuch->path) - 1] = '\0';
503
504     notmuch->mode = mode;
505     try {
506         if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
507             notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
508                                                                Xapian::DB_CREATE_OR_OPEN);
509         } else {
510             notmuch->xapian_db = new Xapian::Database (xapian_path);
511         }
512         notmuch->query_parser = new Xapian::QueryParser;
513         notmuch->term_gen = new Xapian::TermGenerator;
514         notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
515         notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
516
517         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
518         notmuch->query_parser->set_database (*notmuch->xapian_db);
519         notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
520         notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
521         notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
522
523         for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
524             prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
525             notmuch->query_parser->add_boolean_prefix (prefix->name,
526                                                        prefix->prefix);
527         }
528
529         for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
530             prefix_t *prefix = &PROBABILISTIC_PREFIX[i];
531             notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
532         }
533     } catch (const Xapian::Error &error) {
534         fprintf (stderr, "A Xapian exception occurred opening database: %s\n",
535                  error.get_msg().c_str());
536         notmuch = NULL;
537     }
538
539   DONE:
540     if (notmuch_path)
541         free (notmuch_path);
542     if (xapian_path)
543         free (xapian_path);
544
545     return notmuch;
546 }
547
548 void
549 notmuch_database_close (notmuch_database_t *notmuch)
550 {
551     try {
552         if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE)
553             (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->flush ();
554     } catch (const Xapian::Error &error) {
555         if (! notmuch->exception_reported) {
556             fprintf (stderr, "Error: A Xapian exception occurred flushing database: %s\n",
557                      error.get_msg().c_str());
558         }
559     }
560
561     delete notmuch->term_gen;
562     delete notmuch->query_parser;
563     delete notmuch->xapian_db;
564     delete notmuch->value_range_processor;
565     talloc_free (notmuch);
566 }
567
568 const char *
569 notmuch_database_get_path (notmuch_database_t *notmuch)
570 {
571     return notmuch->path;
572 }
573
574 static notmuch_private_status_t
575 find_directory_document (notmuch_database_t *notmuch, const char *db_path,
576                          Xapian::Document *doc, unsigned int *doc_id)
577 {
578     return find_unique_document (notmuch, "directory", db_path, doc, doc_id);
579 }
580
581 /* We allow the user to use arbitrarily long paths for directories. But
582  * we have a term-length limit. So if we exceed that, we'll use the
583  * SHA-1 of the path for the database term.
584  *
585  * Note: This function may return the original value of 'path'. If it
586  * does not, then the caller is responsible to free() the returned
587  * value.
588  */
589 static const char *
590 directory_db_path (const char *path)
591 {
592     int term_len = strlen (_find_prefix ("directory")) + strlen (path);
593
594     if (term_len > NOTMUCH_TERM_MAX)
595         return notmuch_sha1_of_string (path);
596     else
597         return path;
598 }
599
600 /* Given a 'path' (relative to the database path) return the document
601  * ID of the directory document corresponding to the parent directory
602  * of 'path' in 'parent_id'.
603  *
604  * The original 'path' can represent either a regular file or a
605  * directory, (in either case, the document ID of the parent will be
606  * returned). Trailing slashes on 'path' will be ignored, and any
607  * cases of multiple '/' characters appearing in series will be
608  * treated as a single '/'.
609  *
610  * If no directory document exists in the database for the parent, (or
611  * for any of its parents up to the top-level database path), then
612  * directory documents will be created for these (each with an mtime
613  * of 0).
614  *
615  * Return value:
616  *
617  * NOTMUCH_STATUS_SUCCESS: Valid value available in parent_id.
618  *
619  * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception
620  *      occurred and parent_id will be set to (unsigned) -1.
621  */
622 notmuch_status_t
623 _notmuch_database_find_parent_id (notmuch_database_t *notmuch,
624                                   const char *path,
625                                   unsigned int *parent_id)
626 {
627     const char *slash, *parent_db_path;
628     char *parent_path;
629     notmuch_private_status_t private_status;
630     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
631
632     if (path == NULL || *path == '\0') {
633         *parent_id = 0;
634         return NOTMUCH_STATUS_SUCCESS;
635     }
636
637     /* Find the last slash (not counting a trailing slash), if any. */
638
639     slash = path + strlen (path) - 1;
640
641     /* First, skip trailing slashes. */
642     while (slash != path) {
643         if (*slash != '/')
644             break;
645
646         --slash;
647     }
648
649     /* Then, find a slash. */
650     while (slash != path) {
651         if (*slash == '/')
652             break;
653
654         --slash;
655     }
656
657     /* Finally, skip multiple slashes. */
658     while (slash != path) {
659         if (*slash != '/')
660             break;
661
662         --slash;
663     }
664
665     if (slash == path)
666         parent_path = talloc_strdup (notmuch, "");
667     else
668         parent_path = talloc_strndup (notmuch, path, slash - path + 1);
669
670     parent_db_path = directory_db_path (parent_path);
671
672     private_status = find_unique_doc_id (notmuch, "directory",
673                                          parent_db_path, parent_id);
674     if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
675         status = notmuch_database_set_directory_mtime (notmuch,
676                                                        parent_path, 0);
677         if (status)
678             return status;
679         private_status = find_unique_doc_id (notmuch, "directory",
680                                              parent_db_path, parent_id);
681         status = COERCE_STATUS (private_status, "_find_parent_id");
682     }
683
684     if (parent_db_path != parent_path)
685         free ((char *) parent_db_path);
686
687     talloc_free (parent_path);
688
689     if (status)
690         *parent_id = -1;
691
692     return status;
693 }
694
695 /* Given a legal 'path' for the database, return the relative path.
696  *
697  * The return value will be a pointer to the originl path contents,
698  * and will be either the original string (if 'path' was relative) or
699  * a portion of the string (if path was absolute and begins with the
700  * database path).
701  */
702 const char *
703 _notmuch_database_relative_path (notmuch_database_t *notmuch,
704                                  const char *path)
705 {
706     const char *db_path, *relative;
707     unsigned int db_path_len;
708
709     db_path = notmuch_database_get_path (notmuch);
710     db_path_len = strlen (db_path);
711
712     relative = path;
713
714     if (*relative == '/') {
715         while (*relative == '/' && *(relative+1) == '/')
716             relative++;
717
718         if (strncmp (relative, db_path, db_path_len) == 0)
719         {
720             relative += db_path_len;
721             while (*relative == '/')
722                 relative++;
723         }
724     }
725
726     return relative;
727 }
728
729 notmuch_status_t
730 notmuch_database_set_directory_mtime (notmuch_database_t *notmuch,
731                                       const char *path,
732                                       time_t mtime)
733 {
734     Xapian::Document doc;
735     Xapian::WritableDatabase *db;
736     unsigned int doc_id;
737     notmuch_private_status_t status;
738     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
739     const char *db_path = NULL;
740     unsigned int parent_id;
741
742     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
743         fprintf (stderr, "Attempted to update a read-only database.\n");
744         return NOTMUCH_STATUS_READONLY_DATABASE;
745     }
746
747     path = _notmuch_database_relative_path (notmuch, path);
748
749     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
750     db_path = directory_db_path (path);
751
752     try {
753         status = find_directory_document (notmuch, db_path, &doc, &doc_id);
754
755         doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
756                        Xapian::sortable_serialise (mtime));
757
758         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
759             char *term = talloc_asprintf (NULL, "%s%s",
760                                           _find_prefix ("directory"), db_path);
761             doc.add_term (term);
762             talloc_free (term);
763
764             doc.set_data (path);
765
766             ret = _notmuch_database_find_parent_id (notmuch, path,
767                                                     &parent_id);
768             if (ret)
769                 return ret;
770
771             term = talloc_asprintf (NULL, "%s%u",
772                                     _find_prefix ("parent"),
773                                     parent_id);
774             doc.add_term (term);
775             talloc_free (term);
776
777             db->add_document (doc);
778         } else {
779             db->replace_document (doc_id, doc);
780         }
781
782     } catch (const Xapian::Error &error) {
783         fprintf (stderr, "A Xapian exception occurred setting directory mtime: %s.\n",
784                  error.get_msg().c_str());
785         notmuch->exception_reported = TRUE;
786         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
787     }
788
789     if (db_path != path)
790         free ((char *) db_path);
791
792     return ret;
793 }
794
795 time_t
796 notmuch_database_get_directory_mtime (notmuch_database_t *notmuch,
797                                       const char *path)
798 {
799     Xapian::Document doc;
800     unsigned int doc_id;
801     notmuch_private_status_t status;
802     const char *db_path = NULL;
803     time_t ret = 0;
804
805     db_path = directory_db_path (path);
806
807     try {
808         status = find_directory_document (notmuch, db_path, &doc, &doc_id);
809
810         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
811             goto DONE;
812
813         ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
814     } catch (Xapian::Error &error) {
815         ret = 0;
816         goto DONE;
817     }
818
819   DONE:
820     if (db_path != path)
821         free ((char *) db_path);
822
823     return ret;
824 }
825
826 /* Find the thread ID to which the message with 'message_id' belongs.
827  *
828  * Returns NULL if no message with message ID 'message_id' is in the
829  * database.
830  *
831  * Otherwise, returns a newly talloced string belonging to 'ctx'.
832  */
833 static const char *
834 _resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
835                                   void *ctx,
836                                   const char *message_id)
837 {
838     notmuch_message_t *message;
839     const char *ret = NULL;
840
841     message = notmuch_database_find_message (notmuch, message_id);
842     if (message == NULL)
843         goto DONE;
844
845     ret = talloc_steal (ctx, notmuch_message_get_thread_id (message));
846
847   DONE:
848     if (message)
849         notmuch_message_destroy (message);
850
851     return ret;
852 }
853
854 static notmuch_status_t
855 _merge_threads (notmuch_database_t *notmuch,
856                 const char *winner_thread_id,
857                 const char *loser_thread_id)
858 {
859     Xapian::PostingIterator loser, loser_end;
860     notmuch_message_t *message = NULL;
861     notmuch_private_status_t private_status;
862     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
863
864     find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
865
866     for ( ; loser != loser_end; loser++) {
867         message = _notmuch_message_create (notmuch, notmuch,
868                                            *loser, &private_status);
869         if (message == NULL) {
870             ret = COERCE_STATUS (private_status,
871                                  "Cannot find document for doc_id from query");
872             goto DONE;
873         }
874
875         _notmuch_message_remove_term (message, "thread", loser_thread_id);
876         _notmuch_message_add_term (message, "thread", winner_thread_id);
877         _notmuch_message_sync (message);
878
879         notmuch_message_destroy (message);
880         message = NULL;
881     }
882
883   DONE:
884     if (message)
885         notmuch_message_destroy (message);
886
887     return ret;
888 }
889
890 static void
891 _my_talloc_free_for_g_hash (void *ptr)
892 {
893     talloc_free (ptr);
894 }
895
896 static notmuch_status_t
897 _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
898                                            notmuch_message_t *message,
899                                            notmuch_message_file_t *message_file,
900                                            const char **thread_id)
901 {
902     GHashTable *parents = NULL;
903     const char *refs, *in_reply_to, *in_reply_to_message_id;
904     GList *l, *keys = NULL;
905     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
906
907     parents = g_hash_table_new_full (g_str_hash, g_str_equal,
908                                      _my_talloc_free_for_g_hash, NULL);
909
910     refs = notmuch_message_file_get_header (message_file, "references");
911     parse_references (message, notmuch_message_get_message_id (message),
912                       parents, refs);
913
914     in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
915     parse_references (message, notmuch_message_get_message_id (message),
916                       parents, in_reply_to);
917
918     /* Carefully avoid adding any self-referential in-reply-to term. */
919     in_reply_to_message_id = _parse_message_id (message, in_reply_to, NULL);
920     if (in_reply_to_message_id &&
921         strcmp (in_reply_to_message_id,
922                 notmuch_message_get_message_id (message)))
923     {
924         _notmuch_message_add_term (message, "replyto",
925                              _parse_message_id (message, in_reply_to, NULL));
926     }
927
928     keys = g_hash_table_get_keys (parents);
929     for (l = keys; l; l = l->next) {
930         char *parent_message_id;
931         const char *parent_thread_id;
932
933         parent_message_id = (char *) l->data;
934         parent_thread_id = _resolve_message_id_to_thread_id (notmuch,
935                                                              message,
936                                                              parent_message_id);
937
938         if (parent_thread_id == NULL) {
939             _notmuch_message_add_term (message, "reference",
940                                        parent_message_id);
941         } else {
942             if (*thread_id == NULL) {
943                 *thread_id = talloc_strdup (message, parent_thread_id);
944                 _notmuch_message_add_term (message, "thread", *thread_id);
945             } else if (strcmp (*thread_id, parent_thread_id)) {
946                 ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
947                 if (ret)
948                     goto DONE;
949             }
950         }
951     }
952
953   DONE:
954     if (keys)
955         g_list_free (keys);
956     if (parents)
957         g_hash_table_unref (parents);
958
959     return ret;
960 }
961
962 static notmuch_status_t
963 _notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
964                                             notmuch_message_t *message,
965                                             const char **thread_id)
966 {
967     const char *message_id = notmuch_message_get_message_id (message);
968     Xapian::PostingIterator child, children_end;
969     notmuch_message_t *child_message = NULL;
970     const char *child_thread_id;
971     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
972     notmuch_private_status_t private_status;
973
974     find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
975
976     for ( ; child != children_end; child++) {
977
978         child_message = _notmuch_message_create (message, notmuch,
979                                                  *child, &private_status);
980         if (child_message == NULL) {
981             ret = COERCE_STATUS (private_status,
982                                  "Cannot find document for doc_id from query");
983             goto DONE;
984         }
985
986         child_thread_id = notmuch_message_get_thread_id (child_message);
987         if (*thread_id == NULL) {
988             *thread_id = talloc_strdup (message, child_thread_id);
989             _notmuch_message_add_term (message, "thread", *thread_id);
990         } else if (strcmp (*thread_id, child_thread_id)) {
991             _notmuch_message_remove_term (child_message, "reference",
992                                           message_id);
993             _notmuch_message_sync (child_message);
994             ret = _merge_threads (notmuch, *thread_id, child_thread_id);
995             if (ret)
996                 goto DONE;
997         }
998
999         notmuch_message_destroy (child_message);
1000         child_message = NULL;
1001     }
1002
1003   DONE:
1004     if (child_message)
1005         notmuch_message_destroy (child_message);
1006
1007     return ret;
1008 }
1009
1010 /* Given a (mostly empty) 'message' and its corresponding
1011  * 'message_file' link it to existing threads in the database.
1012  *
1013  * We first look at 'message_file' and its link-relevant headers
1014  * (References and In-Reply-To) for message IDs. We also look in the
1015  * database for existing message that reference 'message'.
1016  *
1017  * The end result is to call _notmuch_message_ensure_thread_id which
1018  * generates a new thread ID if the message doesn't connect to any
1019  * existing threads.
1020  */
1021 static notmuch_status_t
1022 _notmuch_database_link_message (notmuch_database_t *notmuch,
1023                                 notmuch_message_t *message,
1024                                 notmuch_message_file_t *message_file)
1025 {
1026     notmuch_status_t status;
1027     const char *thread_id = NULL;
1028
1029     status = _notmuch_database_link_message_to_parents (notmuch, message,
1030                                                         message_file,
1031                                                         &thread_id);
1032     if (status)
1033         return status;
1034
1035     status = _notmuch_database_link_message_to_children (notmuch, message,
1036                                                          &thread_id);
1037     if (status)
1038         return status;
1039
1040     if (thread_id == NULL)
1041         _notmuch_message_ensure_thread_id (message);
1042
1043     return NOTMUCH_STATUS_SUCCESS;
1044 }
1045
1046 notmuch_status_t
1047 notmuch_database_add_message (notmuch_database_t *notmuch,
1048                               const char *filename,
1049                               notmuch_message_t **message_ret)
1050 {
1051     notmuch_message_file_t *message_file;
1052     notmuch_message_t *message = NULL;
1053     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1054     notmuch_private_status_t private_status;
1055
1056     const char *date, *header;
1057     const char *from, *to, *subject;
1058     char *message_id = NULL;
1059
1060     if (message_ret)
1061         *message_ret = NULL;
1062
1063     message_file = notmuch_message_file_open (filename);
1064     if (message_file == NULL) {
1065         ret = NOTMUCH_STATUS_FILE_ERROR;
1066         goto DONE;
1067     }
1068
1069     notmuch_message_file_restrict_headers (message_file,
1070                                            "date",
1071                                            "from",
1072                                            "in-reply-to",
1073                                            "message-id",
1074                                            "references",
1075                                            "subject",
1076                                            "to",
1077                                            (char *) NULL);
1078
1079     try {
1080         /* Before we do any real work, (especially before doing a
1081          * potential SHA-1 computation on the entire file's contents),
1082          * let's make sure that what we're looking at looks like an
1083          * actual email message.
1084          */
1085         from = notmuch_message_file_get_header (message_file, "from");
1086         subject = notmuch_message_file_get_header (message_file, "subject");
1087         to = notmuch_message_file_get_header (message_file, "to");
1088
1089         if ((from == NULL || *from == '\0') &&
1090             (subject == NULL || *subject == '\0') &&
1091             (to == NULL || *to == '\0'))
1092         {
1093             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
1094             goto DONE;
1095         }
1096
1097         /* Now that we're sure it's mail, the first order of business
1098          * is to find a message ID (or else create one ourselves). */
1099
1100         header = notmuch_message_file_get_header (message_file, "message-id");
1101         if (header && *header != '\0') {
1102             message_id = _parse_message_id (message_file, header, NULL);
1103
1104             /* So the header value isn't RFC-compliant, but it's
1105              * better than no message-id at all. */
1106             if (message_id == NULL)
1107                 message_id = talloc_strdup (message_file, header);
1108
1109             /* Reject a Message ID that's too long. */
1110             if (message_id && strlen (message_id) + 1 > NOTMUCH_TERM_MAX) {
1111                 talloc_free (message_id);
1112                 message_id = NULL;
1113             }
1114         }
1115
1116         if (message_id == NULL ) {
1117             /* No message-id at all, let's generate one by taking a
1118              * hash over the file's contents. */
1119             char *sha1 = notmuch_sha1_of_file (filename);
1120
1121             /* If that failed too, something is really wrong. Give up. */
1122             if (sha1 == NULL) {
1123                 ret = NOTMUCH_STATUS_FILE_ERROR;
1124                 goto DONE;
1125             }
1126
1127             message_id = talloc_asprintf (message_file,
1128                                           "notmuch-sha1-%s", sha1);
1129             free (sha1);
1130         }
1131
1132         /* Now that we have a message ID, we get a message object,
1133          * (which may or may not reference an existing document in the
1134          * database). */
1135
1136         message = _notmuch_message_create_for_message_id (notmuch,
1137                                                           message_id,
1138                                                           &private_status);
1139
1140         talloc_free (message_id);
1141
1142         if (message == NULL) {
1143             ret = COERCE_STATUS (private_status,
1144                                  "Unexpected status value from _notmuch_message_create_for_message_id");
1145             goto DONE;
1146         }
1147
1148         /* Is this a newly created message object? */
1149         if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1150             _notmuch_message_set_filename (message, filename);
1151             _notmuch_message_add_term (message, "type", "mail");
1152         } else {
1153             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
1154             goto DONE;
1155         }
1156
1157         ret = _notmuch_database_link_message (notmuch, message, message_file);
1158         if (ret)
1159             goto DONE;
1160
1161         date = notmuch_message_file_get_header (message_file, "date");
1162         _notmuch_message_set_date (message, date);
1163
1164         _notmuch_message_index_file (message, filename);
1165
1166         _notmuch_message_sync (message);
1167     } catch (const Xapian::Error &error) {
1168         fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
1169                  error.get_description().c_str());
1170         notmuch->exception_reported = TRUE;
1171         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1172         goto DONE;
1173     }
1174
1175   DONE:
1176     if (message) {
1177         if (ret == NOTMUCH_STATUS_SUCCESS && message_ret)
1178             *message_ret = message;
1179         else
1180             notmuch_message_destroy (message);
1181     }
1182
1183     if (message_file)
1184         notmuch_message_file_close (message_file);
1185
1186     return ret;
1187 }
1188
1189 notmuch_tags_t *
1190 _notmuch_convert_tags (void *ctx, Xapian::TermIterator &i,
1191                        Xapian::TermIterator &end)
1192 {
1193     const char *prefix = _find_prefix ("tag");
1194     notmuch_tags_t *tags;
1195     std::string tag;
1196
1197     /* Currently this iteration is written with the assumption that
1198      * "tag" has a single-character prefix. */
1199     assert (strlen (prefix) == 1);
1200
1201     tags = _notmuch_tags_create (ctx);
1202     if (unlikely (tags == NULL))
1203         return NULL;
1204
1205     i.skip_to (prefix);
1206
1207     while (i != end) {
1208         tag = *i;
1209
1210         if (tag.empty () || tag[0] != *prefix)
1211             break;
1212
1213         _notmuch_tags_add_tag (tags, tag.c_str () + 1);
1214
1215         i++;
1216     }
1217
1218     _notmuch_tags_prepare_iterator (tags);
1219
1220     return tags;
1221 }
1222
1223 notmuch_tags_t *
1224 notmuch_database_get_all_tags (notmuch_database_t *db)
1225 {
1226     Xapian::TermIterator i, end;
1227     i = db->xapian_db->allterms_begin();
1228     end = db->xapian_db->allterms_end();
1229     return _notmuch_convert_tags(db, i, end);
1230 }