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