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