]> git.notmuchmail.org Git - notmuch/blob - lib/database.cc
database: Split _find_parent_id into _split_path and _find_directory_id
[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, split it into two parts: the directory part is all
601  * components except for the last, and the basename is that last
602  * component. Getting the return-value for either part is optional
603  * (the caller can pass NULL).
604  *
605  * The original 'path' can represent either a regular file or a
606  * directory---the splitting will be carried out in the same way in
607  * either case. Trailing slashes on 'path' will be ignored, and any
608  * cases of multiple '/' characters appearing in series will be
609  * treated as a single '/'.
610  *
611  * Allocation (if any) will have 'ctx' as the talloc owner. But
612  * pointers will be returned within the original path string whenever
613  * possible.
614  *
615  * Note: If 'path' is non-empty and contains no non-trailing slash,
616  * (that is, consists of a filename with no parent directory), then
617  * the directory returned will be an empty string. However, if 'path'
618  * is an empty string, then both directory and basename will be
619  * returned as NULL.
620  */
621 notmuch_status_t
622 _notmuch_database_split_path (void *ctx,
623                               const char *path,
624                               const char **directory,
625                               const char **basename)
626 {
627     const char *slash;
628
629     if (path == NULL || *path == '\0') {
630         if (directory)
631             *directory = NULL;
632         if (basename)
633             *basename = NULL;
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         if (basename)
655             *basename = slash;
656
657         --slash;
658     }
659
660     /* Finally, skip multiple slashes. */
661     while (slash != path) {
662         if (*slash != '/')
663             break;
664
665         --slash;
666     }
667
668     if (slash == path) {
669         if (directory)
670             *directory = talloc_strdup (ctx, "");
671         if (basename)
672             *basename = path;
673     } else {
674         if (directory)
675             *directory = talloc_strndup (ctx, path, slash - path + 1);
676     }
677
678     return NOTMUCH_STATUS_SUCCESS;
679 }
680
681 notmuch_status_t
682 _notmuch_database_find_directory_id (notmuch_database_t *notmuch,
683                                      const char *path,
684                                      unsigned int *directory_id)
685 {
686     notmuch_private_status_t private_status;
687     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
688     const char *db_path;
689
690     if (path == NULL) {
691         *directory_id = 0;
692         return NOTMUCH_STATUS_SUCCESS;
693     }
694
695     db_path = directory_db_path (path);
696
697     private_status = find_unique_doc_id (notmuch, "directory",
698                                          db_path, directory_id);
699     if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
700         status = notmuch_database_set_directory_mtime (notmuch,
701                                                        path, 0);
702         if (status)
703             goto DONE;
704
705         private_status = find_unique_doc_id (notmuch, "directory",
706                                              db_path, directory_id);
707         status = COERCE_STATUS (private_status, "_find_directory_id");
708     }
709
710   DONE:
711     if (db_path != path)
712         free ((char *) db_path);
713
714     if (status)
715         *directory_id = -1;
716
717     return status;
718 }
719
720 /* Given a legal 'path' for the database, return the relative path.
721  *
722  * The return value will be a pointer to the originl path contents,
723  * and will be either the original string (if 'path' was relative) or
724  * a portion of the string (if path was absolute and begins with the
725  * database path).
726  */
727 const char *
728 _notmuch_database_relative_path (notmuch_database_t *notmuch,
729                                  const char *path)
730 {
731     const char *db_path, *relative;
732     unsigned int db_path_len;
733
734     db_path = notmuch_database_get_path (notmuch);
735     db_path_len = strlen (db_path);
736
737     relative = path;
738
739     if (*relative == '/') {
740         while (*relative == '/' && *(relative+1) == '/')
741             relative++;
742
743         if (strncmp (relative, db_path, db_path_len) == 0)
744         {
745             relative += db_path_len;
746             while (*relative == '/')
747                 relative++;
748         }
749     }
750
751     return relative;
752 }
753
754 notmuch_status_t
755 notmuch_database_set_directory_mtime (notmuch_database_t *notmuch,
756                                       const char *path,
757                                       time_t mtime)
758 {
759     Xapian::Document doc;
760     Xapian::WritableDatabase *db;
761     unsigned int doc_id;
762     notmuch_private_status_t status;
763     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
764     const char *parent, *db_path = NULL;
765     unsigned int parent_id;
766     void *local = talloc_new (notmuch);
767
768     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
769         fprintf (stderr, "Attempted to update a read-only database.\n");
770         return NOTMUCH_STATUS_READONLY_DATABASE;
771     }
772
773     path = _notmuch_database_relative_path (notmuch, path);
774
775     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
776     db_path = directory_db_path (path);
777
778     try {
779         status = find_directory_document (notmuch, db_path, &doc, &doc_id);
780
781         doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
782                        Xapian::sortable_serialise (mtime));
783
784         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
785             char *term = talloc_asprintf (local, "%s%s",
786                                           _find_prefix ("directory"), db_path);
787             doc.add_term (term);
788
789             doc.set_data (path);
790
791             _notmuch_database_split_path (local, path, &parent, NULL);
792
793             _notmuch_database_find_directory_id (notmuch, parent, &parent_id);
794
795             term = talloc_asprintf (local, "%s%u",
796                                     _find_prefix ("parent"),
797                                     parent_id);
798             doc.add_term (term);
799
800             db->add_document (doc);
801         } else {
802             db->replace_document (doc_id, doc);
803         }
804
805     } catch (const Xapian::Error &error) {
806         fprintf (stderr, "A Xapian exception occurred setting directory mtime: %s.\n",
807                  error.get_msg().c_str());
808         notmuch->exception_reported = TRUE;
809         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
810     }
811
812     if (db_path != path)
813         free ((char *) db_path);
814
815     return ret;
816 }
817
818 time_t
819 notmuch_database_get_directory_mtime (notmuch_database_t *notmuch,
820                                       const char *path)
821 {
822     Xapian::Document doc;
823     unsigned int doc_id;
824     notmuch_private_status_t status;
825     const char *db_path = NULL;
826     time_t ret = 0;
827     void *local = talloc_new (notmuch);
828
829     db_path = directory_db_path (path);
830
831     try {
832         status = find_directory_document (notmuch, db_path, &doc, &doc_id);
833
834         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
835             goto DONE;
836
837         ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
838     } catch (Xapian::Error &error) {
839         ret = 0;
840         goto DONE;
841     }
842
843   DONE:
844     if (db_path != path)
845         free ((char *) db_path);
846
847     talloc_free (local);
848
849     return ret;
850 }
851
852 /* Find the thread ID to which the message with 'message_id' belongs.
853  *
854  * Returns NULL if no message with message ID 'message_id' is in the
855  * database.
856  *
857  * Otherwise, returns a newly talloced string belonging to 'ctx'.
858  */
859 static const char *
860 _resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
861                                   void *ctx,
862                                   const char *message_id)
863 {
864     notmuch_message_t *message;
865     const char *ret = NULL;
866
867     message = notmuch_database_find_message (notmuch, message_id);
868     if (message == NULL)
869         goto DONE;
870
871     ret = talloc_steal (ctx, notmuch_message_get_thread_id (message));
872
873   DONE:
874     if (message)
875         notmuch_message_destroy (message);
876
877     return ret;
878 }
879
880 static notmuch_status_t
881 _merge_threads (notmuch_database_t *notmuch,
882                 const char *winner_thread_id,
883                 const char *loser_thread_id)
884 {
885     Xapian::PostingIterator loser, loser_end;
886     notmuch_message_t *message = NULL;
887     notmuch_private_status_t private_status;
888     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
889
890     find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
891
892     for ( ; loser != loser_end; loser++) {
893         message = _notmuch_message_create (notmuch, notmuch,
894                                            *loser, &private_status);
895         if (message == NULL) {
896             ret = COERCE_STATUS (private_status,
897                                  "Cannot find document for doc_id from query");
898             goto DONE;
899         }
900
901         _notmuch_message_remove_term (message, "thread", loser_thread_id);
902         _notmuch_message_add_term (message, "thread", winner_thread_id);
903         _notmuch_message_sync (message);
904
905         notmuch_message_destroy (message);
906         message = NULL;
907     }
908
909   DONE:
910     if (message)
911         notmuch_message_destroy (message);
912
913     return ret;
914 }
915
916 static void
917 _my_talloc_free_for_g_hash (void *ptr)
918 {
919     talloc_free (ptr);
920 }
921
922 static notmuch_status_t
923 _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
924                                            notmuch_message_t *message,
925                                            notmuch_message_file_t *message_file,
926                                            const char **thread_id)
927 {
928     GHashTable *parents = NULL;
929     const char *refs, *in_reply_to, *in_reply_to_message_id;
930     GList *l, *keys = NULL;
931     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
932
933     parents = g_hash_table_new_full (g_str_hash, g_str_equal,
934                                      _my_talloc_free_for_g_hash, NULL);
935
936     refs = notmuch_message_file_get_header (message_file, "references");
937     parse_references (message, notmuch_message_get_message_id (message),
938                       parents, refs);
939
940     in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
941     parse_references (message, notmuch_message_get_message_id (message),
942                       parents, in_reply_to);
943
944     /* Carefully avoid adding any self-referential in-reply-to term. */
945     in_reply_to_message_id = _parse_message_id (message, in_reply_to, NULL);
946     if (in_reply_to_message_id &&
947         strcmp (in_reply_to_message_id,
948                 notmuch_message_get_message_id (message)))
949     {
950         _notmuch_message_add_term (message, "replyto",
951                              _parse_message_id (message, in_reply_to, NULL));
952     }
953
954     keys = g_hash_table_get_keys (parents);
955     for (l = keys; l; l = l->next) {
956         char *parent_message_id;
957         const char *parent_thread_id;
958
959         parent_message_id = (char *) l->data;
960         parent_thread_id = _resolve_message_id_to_thread_id (notmuch,
961                                                              message,
962                                                              parent_message_id);
963
964         if (parent_thread_id == NULL) {
965             _notmuch_message_add_term (message, "reference",
966                                        parent_message_id);
967         } else {
968             if (*thread_id == NULL) {
969                 *thread_id = talloc_strdup (message, parent_thread_id);
970                 _notmuch_message_add_term (message, "thread", *thread_id);
971             } else if (strcmp (*thread_id, parent_thread_id)) {
972                 ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
973                 if (ret)
974                     goto DONE;
975             }
976         }
977     }
978
979   DONE:
980     if (keys)
981         g_list_free (keys);
982     if (parents)
983         g_hash_table_unref (parents);
984
985     return ret;
986 }
987
988 static notmuch_status_t
989 _notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
990                                             notmuch_message_t *message,
991                                             const char **thread_id)
992 {
993     const char *message_id = notmuch_message_get_message_id (message);
994     Xapian::PostingIterator child, children_end;
995     notmuch_message_t *child_message = NULL;
996     const char *child_thread_id;
997     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
998     notmuch_private_status_t private_status;
999
1000     find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
1001
1002     for ( ; child != children_end; child++) {
1003
1004         child_message = _notmuch_message_create (message, notmuch,
1005                                                  *child, &private_status);
1006         if (child_message == NULL) {
1007             ret = COERCE_STATUS (private_status,
1008                                  "Cannot find document for doc_id from query");
1009             goto DONE;
1010         }
1011
1012         child_thread_id = notmuch_message_get_thread_id (child_message);
1013         if (*thread_id == NULL) {
1014             *thread_id = talloc_strdup (message, child_thread_id);
1015             _notmuch_message_add_term (message, "thread", *thread_id);
1016         } else if (strcmp (*thread_id, child_thread_id)) {
1017             _notmuch_message_remove_term (child_message, "reference",
1018                                           message_id);
1019             _notmuch_message_sync (child_message);
1020             ret = _merge_threads (notmuch, *thread_id, child_thread_id);
1021             if (ret)
1022                 goto DONE;
1023         }
1024
1025         notmuch_message_destroy (child_message);
1026         child_message = NULL;
1027     }
1028
1029   DONE:
1030     if (child_message)
1031         notmuch_message_destroy (child_message);
1032
1033     return ret;
1034 }
1035
1036 /* Given a (mostly empty) 'message' and its corresponding
1037  * 'message_file' link it to existing threads in the database.
1038  *
1039  * We first look at 'message_file' and its link-relevant headers
1040  * (References and In-Reply-To) for message IDs. We also look in the
1041  * database for existing message that reference 'message'.
1042  *
1043  * The end result is to call _notmuch_message_ensure_thread_id which
1044  * generates a new thread ID if the message doesn't connect to any
1045  * existing threads.
1046  */
1047 static notmuch_status_t
1048 _notmuch_database_link_message (notmuch_database_t *notmuch,
1049                                 notmuch_message_t *message,
1050                                 notmuch_message_file_t *message_file)
1051 {
1052     notmuch_status_t status;
1053     const char *thread_id = NULL;
1054
1055     status = _notmuch_database_link_message_to_parents (notmuch, message,
1056                                                         message_file,
1057                                                         &thread_id);
1058     if (status)
1059         return status;
1060
1061     status = _notmuch_database_link_message_to_children (notmuch, message,
1062                                                          &thread_id);
1063     if (status)
1064         return status;
1065
1066     if (thread_id == NULL)
1067         _notmuch_message_ensure_thread_id (message);
1068
1069     return NOTMUCH_STATUS_SUCCESS;
1070 }
1071
1072 notmuch_status_t
1073 notmuch_database_add_message (notmuch_database_t *notmuch,
1074                               const char *filename,
1075                               notmuch_message_t **message_ret)
1076 {
1077     notmuch_message_file_t *message_file;
1078     notmuch_message_t *message = NULL;
1079     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1080     notmuch_private_status_t private_status;
1081
1082     const char *date, *header;
1083     const char *from, *to, *subject;
1084     char *message_id = NULL;
1085
1086     if (message_ret)
1087         *message_ret = NULL;
1088
1089     message_file = notmuch_message_file_open (filename);
1090     if (message_file == NULL) {
1091         ret = NOTMUCH_STATUS_FILE_ERROR;
1092         goto DONE;
1093     }
1094
1095     notmuch_message_file_restrict_headers (message_file,
1096                                            "date",
1097                                            "from",
1098                                            "in-reply-to",
1099                                            "message-id",
1100                                            "references",
1101                                            "subject",
1102                                            "to",
1103                                            (char *) NULL);
1104
1105     try {
1106         /* Before we do any real work, (especially before doing a
1107          * potential SHA-1 computation on the entire file's contents),
1108          * let's make sure that what we're looking at looks like an
1109          * actual email message.
1110          */
1111         from = notmuch_message_file_get_header (message_file, "from");
1112         subject = notmuch_message_file_get_header (message_file, "subject");
1113         to = notmuch_message_file_get_header (message_file, "to");
1114
1115         if ((from == NULL || *from == '\0') &&
1116             (subject == NULL || *subject == '\0') &&
1117             (to == NULL || *to == '\0'))
1118         {
1119             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
1120             goto DONE;
1121         }
1122
1123         /* Now that we're sure it's mail, the first order of business
1124          * is to find a message ID (or else create one ourselves). */
1125
1126         header = notmuch_message_file_get_header (message_file, "message-id");
1127         if (header && *header != '\0') {
1128             message_id = _parse_message_id (message_file, header, NULL);
1129
1130             /* So the header value isn't RFC-compliant, but it's
1131              * better than no message-id at all. */
1132             if (message_id == NULL)
1133                 message_id = talloc_strdup (message_file, header);
1134
1135             /* Reject a Message ID that's too long. */
1136             if (message_id && strlen (message_id) + 1 > NOTMUCH_TERM_MAX) {
1137                 talloc_free (message_id);
1138                 message_id = NULL;
1139             }
1140         }
1141
1142         if (message_id == NULL ) {
1143             /* No message-id at all, let's generate one by taking a
1144              * hash over the file's contents. */
1145             char *sha1 = notmuch_sha1_of_file (filename);
1146
1147             /* If that failed too, something is really wrong. Give up. */
1148             if (sha1 == NULL) {
1149                 ret = NOTMUCH_STATUS_FILE_ERROR;
1150                 goto DONE;
1151             }
1152
1153             message_id = talloc_asprintf (message_file,
1154                                           "notmuch-sha1-%s", sha1);
1155             free (sha1);
1156         }
1157
1158         /* Now that we have a message ID, we get a message object,
1159          * (which may or may not reference an existing document in the
1160          * database). */
1161
1162         message = _notmuch_message_create_for_message_id (notmuch,
1163                                                           message_id,
1164                                                           &private_status);
1165
1166         talloc_free (message_id);
1167
1168         if (message == NULL) {
1169             ret = COERCE_STATUS (private_status,
1170                                  "Unexpected status value from _notmuch_message_create_for_message_id");
1171             goto DONE;
1172         }
1173
1174         /* Is this a newly created message object? */
1175         if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1176             _notmuch_message_set_filename (message, filename);
1177             _notmuch_message_add_term (message, "type", "mail");
1178         } else {
1179             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
1180             goto DONE;
1181         }
1182
1183         ret = _notmuch_database_link_message (notmuch, message, message_file);
1184         if (ret)
1185             goto DONE;
1186
1187         date = notmuch_message_file_get_header (message_file, "date");
1188         _notmuch_message_set_date (message, date);
1189
1190         _notmuch_message_index_file (message, filename);
1191
1192         _notmuch_message_sync (message);
1193     } catch (const Xapian::Error &error) {
1194         fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
1195                  error.get_description().c_str());
1196         notmuch->exception_reported = TRUE;
1197         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1198         goto DONE;
1199     }
1200
1201   DONE:
1202     if (message) {
1203         if (ret == NOTMUCH_STATUS_SUCCESS && message_ret)
1204             *message_ret = message;
1205         else
1206             notmuch_message_destroy (message);
1207     }
1208
1209     if (message_file)
1210         notmuch_message_file_close (message_file);
1211
1212     return ret;
1213 }
1214
1215 notmuch_tags_t *
1216 _notmuch_convert_tags (void *ctx, Xapian::TermIterator &i,
1217                        Xapian::TermIterator &end)
1218 {
1219     const char *prefix = _find_prefix ("tag");
1220     notmuch_tags_t *tags;
1221     std::string tag;
1222
1223     /* Currently this iteration is written with the assumption that
1224      * "tag" has a single-character prefix. */
1225     assert (strlen (prefix) == 1);
1226
1227     tags = _notmuch_tags_create (ctx);
1228     if (unlikely (tags == NULL))
1229         return NULL;
1230
1231     i.skip_to (prefix);
1232
1233     while (i != end) {
1234         tag = *i;
1235
1236         if (tag.empty () || tag[0] != *prefix)
1237             break;
1238
1239         _notmuch_tags_add_tag (tags, tag.c_str () + 1);
1240
1241         i++;
1242     }
1243
1244     _notmuch_tags_prepare_iterator (tags);
1245
1246     return tags;
1247 }
1248
1249 notmuch_tags_t *
1250 notmuch_database_get_all_tags (notmuch_database_t *db)
1251 {
1252     Xapian::TermIterator i, end;
1253     i = db->xapian_db->allterms_begin();
1254     end = db->xapian_db->allterms_end();
1255     return _notmuch_convert_tags(db, i, end);
1256 }