]> git.notmuchmail.org Git - notmuch/blob - lib/database.cc
Merge branch 'release'
[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 #include "parse-time-vrp.h"
23
24 #include <iostream>
25
26 #include <sys/time.h>
27 #include <signal.h>
28
29 #include <glib.h> /* g_free, GPtrArray, GHashTable */
30 #include <glib-object.h> /* g_type_init */
31
32 #include <gmime/gmime.h> /* g_mime_init */
33
34 using namespace std;
35
36 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
37
38 typedef struct {
39     const char *name;
40     const char *prefix;
41 } prefix_t;
42
43 #define NOTMUCH_DATABASE_VERSION 1
44
45 #define STRINGIFY(s) _SUB_STRINGIFY(s)
46 #define _SUB_STRINGIFY(s) #s
47
48 /* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
49  *
50  * We currently have two different types of documents (mail and
51  * directory) and also some metadata.
52  *
53  * Mail document
54  * -------------
55  * A mail document is associated with a particular email message file
56  * on disk. It is indexed with the following prefixed terms which the
57  * database uses to construct threads, etc.:
58  *
59  *    Single terms of given prefix:
60  *
61  *      type:   mail
62  *
63  *      id:     Unique ID of mail. This is from the Message-ID header
64  *              if present and not too long (see NOTMUCH_MESSAGE_ID_MAX).
65  *              If it's present and too long, then we use
66  *              "notmuch-sha1-<sha1_sum_of_message_id>".
67  *              If this header is not present, we use
68  *              "notmuch-sha1-<sha1_sum_of_entire_file>".
69  *
70  *      thread: The ID of the thread to which the mail belongs
71  *
72  *      replyto: The ID from the In-Reply-To header of the mail (if any).
73  *
74  *    Multiple terms of given prefix:
75  *
76  *      reference: All message IDs from In-Reply-To and References
77  *                 headers in the message.
78  *
79  *      tag:       Any tags associated with this message by the user.
80  *
81  *      file-direntry:  A colon-separated pair of values
82  *                      (INTEGER:STRING), where INTEGER is the
83  *                      document ID of a directory document, and
84  *                      STRING is the name of a file within that
85  *                      directory for this mail message.
86  *
87  *    A mail document also has four values:
88  *
89  *      TIMESTAMP:      The time_t value corresponding to the message's
90  *                      Date header.
91  *
92  *      MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
93  *
94  *      FROM:           The value of the "From" header
95  *
96  *      SUBJECT:        The value of the "Subject" header
97  *
98  * In addition, terms from the content of the message are added with
99  * "from", "to", "attachment", and "subject" prefixes for use by the
100  * user in searching. Similarly, terms from the path of the mail
101  * message are added with a "folder" prefix. But the database doesn't
102  * really care itself about any of these.
103  *
104  * The data portion of a mail document is empty.
105  *
106  * Directory document
107  * ------------------
108  * A directory document is used by a client of the notmuch library to
109  * maintain data necessary to allow for efficient polling of mail
110  * directories.
111  *
112  * All directory documents contain one term:
113  *
114  *      directory:      The directory path (relative to the database path)
115  *                      Or the SHA1 sum of the directory path (if the
116  *                      path itself is too long to fit in a Xapian
117  *                      term).
118  *
119  * And all directory documents for directories other than top-level
120  * directories also contain the following term:
121  *
122  *      directory-direntry: A colon-separated pair of values
123  *                          (INTEGER:STRING), where INTEGER is the
124  *                          document ID of the parent directory
125  *                          document, and STRING is the name of this
126  *                          directory within that parent.
127  *
128  * All directory documents have a single value:
129  *
130  *      TIMESTAMP:      The mtime of the directory (at last scan)
131  *
132  * The data portion of a directory document contains the path of the
133  * directory (relative to the database path).
134  *
135  * Database metadata
136  * -----------------
137  * Xapian allows us to store arbitrary name-value pairs as
138  * "metadata". We currently use the following metadata names with the
139  * given meanings:
140  *
141  *      version         The database schema version, (which is distinct
142  *                      from both the notmuch package version (see
143  *                      notmuch --version) and the libnotmuch library
144  *                      version. The version is stored as an base-10
145  *                      ASCII integer. The initial database version
146  *                      was 1, (though a schema existed before that
147  *                      were no "version" database value existed at
148  *                      all). Successive versions are allocated as
149  *                      changes are made to the database (such as by
150  *                      indexing new fields).
151  *
152  *      last_thread_id  The last thread ID generated. This is stored
153  *                      as a 16-byte hexadecimal ASCII representation
154  *                      of a 64-bit unsigned integer. The first ID
155  *                      generated is 1 and the value will be
156  *                      incremented for each thread ID.
157  *
158  *      thread_id_*     A pre-allocated thread ID for a particular
159  *                      message. This is actually an arbitrarily large
160  *                      family of metadata name. Any particular name is
161  *                      formed by concatenating "thread_id_" with a message
162  *                      ID (or the SHA1 sum of a message ID if it is very
163  *                      long---see description of 'id' in the mail
164  *                      document). The value stored is a thread ID.
165  *
166  *                      These thread ID metadata values are stored
167  *                      whenever a message references a parent message
168  *                      that does not yet exist in the database. A
169  *                      thread ID will be allocated and stored, and if
170  *                      the message is later added, the stored thread
171  *                      ID will be used (and the metadata value will
172  *                      be cleared).
173  *
174  *                      Even before a message is added, it's
175  *                      pre-allocated thread ID is useful so that all
176  *                      descendant messages that reference this common
177  *                      parent can be recognized as belonging to the
178  *                      same thread.
179  */
180
181 /* With these prefix values we follow the conventions published here:
182  *
183  * http://xapian.org/docs/omega/termprefixes.html
184  *
185  * as much as makes sense. Note that I took some liberty in matching
186  * the reserved prefix values to notmuch concepts, (for example, 'G'
187  * is documented as "newsGroup (or similar entity - e.g. a web forum
188  * name)", for which I think the thread is the closest analogue in
189  * notmuch. This in spite of the fact that we will eventually be
190  * storing mailing-list messages where 'G' for "mailing list name"
191  * might be even a closer analogue. I'm treating the single-character
192  * prefixes preferentially for core notmuch concepts (which will be
193  * nearly universal to all mail messages).
194  */
195
196 static prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
197     { "type",                   "T" },
198     { "reference",              "XREFERENCE" },
199     { "replyto",                "XREPLYTO" },
200     { "directory",              "XDIRECTORY" },
201     { "file-direntry",          "XFDIRENTRY" },
202     { "directory-direntry",     "XDDIRENTRY" },
203 };
204
205 static prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
206     { "thread",                 "G" },
207     { "tag",                    "K" },
208     { "is",                     "K" },
209     { "id",                     "Q" }
210 };
211
212 static prefix_t PROBABILISTIC_PREFIX[]= {
213     { "from",                   "XFROM" },
214     { "to",                     "XTO" },
215     { "attachment",             "XATTACHMENT" },
216     { "subject",                "XSUBJECT"},
217     { "folder",                 "XFOLDER"}
218 };
219
220 const char *
221 _find_prefix (const char *name)
222 {
223     unsigned int i;
224
225     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++) {
226         if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
227             return BOOLEAN_PREFIX_INTERNAL[i].prefix;
228     }
229
230     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
231         if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
232             return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
233     }
234
235     for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
236         if (strcmp (name, PROBABILISTIC_PREFIX[i].name) == 0)
237             return PROBABILISTIC_PREFIX[i].prefix;
238     }
239
240     INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
241
242     return "";
243 }
244
245 const char *
246 notmuch_status_to_string (notmuch_status_t status)
247 {
248     switch (status) {
249     case NOTMUCH_STATUS_SUCCESS:
250         return "No error occurred";
251     case NOTMUCH_STATUS_OUT_OF_MEMORY:
252         return "Out of memory";
253     case NOTMUCH_STATUS_READ_ONLY_DATABASE:
254         return "Attempt to write to a read-only database";
255     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
256         return "A Xapian exception occurred";
257     case NOTMUCH_STATUS_FILE_ERROR:
258         return "Something went wrong trying to read or write a file";
259     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
260         return "File is not an email";
261     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
262         return "Message ID is identical to a message in database";
263     case NOTMUCH_STATUS_NULL_POINTER:
264         return "Erroneous NULL pointer";
265     case NOTMUCH_STATUS_TAG_TOO_LONG:
266         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
267     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
268         return "Unbalanced number of calls to notmuch_message_freeze/thaw";
269     case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
270         return "Unbalanced number of calls to notmuch_database_begin_atomic/end_atomic";
271     default:
272     case NOTMUCH_STATUS_LAST_STATUS:
273         return "Unknown error status value";
274     }
275 }
276
277 static void
278 find_doc_ids_for_term (notmuch_database_t *notmuch,
279                        const char *term,
280                        Xapian::PostingIterator *begin,
281                        Xapian::PostingIterator *end)
282 {
283     *begin = notmuch->xapian_db->postlist_begin (term);
284
285     *end = notmuch->xapian_db->postlist_end (term);
286 }
287
288 static void
289 find_doc_ids (notmuch_database_t *notmuch,
290               const char *prefix_name,
291               const char *value,
292               Xapian::PostingIterator *begin,
293               Xapian::PostingIterator *end)
294 {
295     char *term;
296
297     term = talloc_asprintf (notmuch, "%s%s",
298                             _find_prefix (prefix_name), value);
299
300     find_doc_ids_for_term (notmuch, term, begin, end);
301
302     talloc_free (term);
303 }
304
305 notmuch_private_status_t
306 _notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
307                                       const char *prefix_name,
308                                       const char *value,
309                                       unsigned int *doc_id)
310 {
311     Xapian::PostingIterator i, end;
312
313     find_doc_ids (notmuch, prefix_name, value, &i, &end);
314
315     if (i == end) {
316         *doc_id = 0;
317         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
318     }
319
320     *doc_id = *i;
321
322 #if DEBUG_DATABASE_SANITY
323     i++;
324
325     if (i != end)
326         INTERNAL_ERROR ("Term %s:%s is not unique as expected.\n",
327                         prefix_name, value);
328 #endif
329
330     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
331 }
332
333 static Xapian::Document
334 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
335 {
336     return notmuch->xapian_db->get_document (doc_id);
337 }
338
339 /* Generate a compressed version of 'message_id' of the form:
340  *
341  *      notmuch-sha1-<sha1_sum_of_message_id>
342  */
343 static char *
344 _message_id_compressed (void *ctx, const char *message_id)
345 {
346     char *sha1, *compressed;
347
348     sha1 = notmuch_sha1_of_string (message_id);
349
350     compressed = talloc_asprintf (ctx, "notmuch-sha1-%s", sha1);
351     free (sha1);
352
353     return compressed;
354 }
355
356 notmuch_status_t
357 notmuch_database_find_message (notmuch_database_t *notmuch,
358                                const char *message_id,
359                                notmuch_message_t **message_ret)
360 {
361     notmuch_private_status_t status;
362     unsigned int doc_id;
363
364     if (message_ret == NULL)
365         return NOTMUCH_STATUS_NULL_POINTER;
366
367     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
368         message_id = _message_id_compressed (notmuch, message_id);
369
370     try {
371         status = _notmuch_database_find_unique_doc_id (notmuch, "id",
372                                                        message_id, &doc_id);
373
374         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
375             *message_ret = NULL;
376         else {
377             *message_ret = _notmuch_message_create (notmuch, notmuch, doc_id,
378                                                     NULL);
379             if (*message_ret == NULL)
380                 return NOTMUCH_STATUS_OUT_OF_MEMORY;
381         }
382
383         return NOTMUCH_STATUS_SUCCESS;
384     } catch (const Xapian::Error &error) {
385         fprintf (stderr, "A Xapian exception occurred finding message: %s.\n",
386                  error.get_msg().c_str());
387         notmuch->exception_reported = TRUE;
388         *message_ret = NULL;
389         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
390     }
391 }
392
393 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
394  * a (potentially nested) parenthesized sequence with '\' used to
395  * escape any character (including parentheses).
396  *
397  * If the sequence to be skipped continues to the end of the string,
398  * then 'str' will be left pointing at the final terminating '\0'
399  * character.
400  */
401 static void
402 skip_space_and_comments (const char **str)
403 {
404     const char *s;
405
406     s = *str;
407     while (*s && (isspace (*s) || *s == '(')) {
408         while (*s && isspace (*s))
409             s++;
410         if (*s == '(') {
411             int nesting = 1;
412             s++;
413             while (*s && nesting) {
414                 if (*s == '(') {
415                     nesting++;
416                 } else if (*s == ')') {
417                     nesting--;
418                 } else if (*s == '\\') {
419                     if (*(s+1))
420                         s++;
421                 }
422                 s++;
423             }
424         }
425     }
426
427     *str = s;
428 }
429
430 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
431  * comments, and the '<' and '>' delimiters.
432  *
433  * If not NULL, then *next will be made to point to the first character
434  * not parsed, (possibly pointing to the final '\0' terminator.
435  *
436  * Returns a newly talloc'ed string belonging to 'ctx'.
437  *
438  * Returns NULL if there is any error parsing the message-id. */
439 static char *
440 _parse_message_id (void *ctx, const char *message_id, const char **next)
441 {
442     const char *s, *end;
443     char *result;
444
445     if (message_id == NULL || *message_id == '\0')
446         return NULL;
447
448     s = message_id;
449
450     skip_space_and_comments (&s);
451
452     /* Skip any unstructured text as well. */
453     while (*s && *s != '<')
454         s++;
455
456     if (*s == '<') {
457         s++;
458     } else {
459         if (next)
460             *next = s;
461         return NULL;
462     }
463
464     skip_space_and_comments (&s);
465
466     end = s;
467     while (*end && *end != '>')
468         end++;
469     if (next) {
470         if (*end)
471             *next = end + 1;
472         else
473             *next = end;
474     }
475
476     if (end > s && *end == '>')
477         end--;
478     if (end <= s)
479         return NULL;
480
481     result = talloc_strndup (ctx, s, end - s + 1);
482
483     /* Finally, collapse any whitespace that is within the message-id
484      * itself. */
485     {
486         char *r;
487         int len;
488
489         for (r = result, len = strlen (r); *r; r++, len--)
490             if (*r == ' ' || *r == '\t')
491                 memmove (r, r+1, len);
492     }
493
494     return result;
495 }
496
497 /* Parse a References header value, putting a (talloc'ed under 'ctx')
498  * copy of each referenced message-id into 'hash'.
499  *
500  * We explicitly avoid including any reference identical to
501  * 'message_id' in the result (to avoid mass confusion when a single
502  * message references itself cyclically---and yes, mail messages are
503  * not infrequent in the wild that do this---don't ask me why).
504 */
505 static void
506 parse_references (void *ctx,
507                   const char *message_id,
508                   GHashTable *hash,
509                   const char *refs)
510 {
511     char *ref;
512
513     if (refs == NULL || *refs == '\0')
514         return;
515
516     while (*refs) {
517         ref = _parse_message_id (ctx, refs, &refs);
518
519         if (ref && strcmp (ref, message_id))
520             g_hash_table_insert (hash, ref, NULL);
521     }
522 }
523
524 notmuch_status_t
525 notmuch_database_create (const char *path, notmuch_database_t **database)
526 {
527     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
528     notmuch_database_t *notmuch = NULL;
529     char *notmuch_path = NULL;
530     struct stat st;
531     int err;
532
533     if (path == NULL) {
534         fprintf (stderr, "Error: Cannot create a database for a NULL path.\n");
535         status = NOTMUCH_STATUS_NULL_POINTER;
536         goto DONE;
537     }
538
539     err = stat (path, &st);
540     if (err) {
541         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
542                  path, strerror (errno));
543         status = NOTMUCH_STATUS_FILE_ERROR;
544         goto DONE;
545     }
546
547     if (! S_ISDIR (st.st_mode)) {
548         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
549                  path);
550         status = NOTMUCH_STATUS_FILE_ERROR;
551         goto DONE;
552     }
553
554     notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch");
555
556     err = mkdir (notmuch_path, 0755);
557
558     if (err) {
559         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
560                  notmuch_path, strerror (errno));
561         status = NOTMUCH_STATUS_FILE_ERROR;
562         goto DONE;
563     }
564
565     status = notmuch_database_open (path,
566                                     NOTMUCH_DATABASE_MODE_READ_WRITE,
567                                     &notmuch);
568     if (status)
569         goto DONE;
570     status = notmuch_database_upgrade (notmuch, NULL, NULL);
571     if (status) {
572         notmuch_database_close(notmuch);
573         notmuch = NULL;
574     }
575
576   DONE:
577     if (notmuch_path)
578         talloc_free (notmuch_path);
579
580     if (database)
581         *database = notmuch;
582     else
583         talloc_free (notmuch);
584     return status;
585 }
586
587 notmuch_status_t
588 _notmuch_database_ensure_writable (notmuch_database_t *notmuch)
589 {
590     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
591         fprintf (stderr, "Cannot write to a read-only database.\n");
592         return NOTMUCH_STATUS_READ_ONLY_DATABASE;
593     }
594
595     return NOTMUCH_STATUS_SUCCESS;
596 }
597
598 notmuch_status_t
599 notmuch_database_open (const char *path,
600                        notmuch_database_mode_t mode,
601                        notmuch_database_t **database)
602 {
603     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
604     void *local = talloc_new (NULL);
605     notmuch_database_t *notmuch = NULL;
606     char *notmuch_path, *xapian_path;
607     struct stat st;
608     int err;
609     unsigned int i, version;
610     static int initialized = 0;
611
612     if (path == NULL) {
613         fprintf (stderr, "Error: Cannot open a database for a NULL path.\n");
614         status = NOTMUCH_STATUS_NULL_POINTER;
615         goto DONE;
616     }
617
618     if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
619         fprintf (stderr, "Out of memory\n");
620         status = NOTMUCH_STATUS_OUT_OF_MEMORY;
621         goto DONE;
622     }
623
624     err = stat (notmuch_path, &st);
625     if (err) {
626         fprintf (stderr, "Error opening database at %s: %s\n",
627                  notmuch_path, strerror (errno));
628         status = NOTMUCH_STATUS_FILE_ERROR;
629         goto DONE;
630     }
631
632     if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
633         fprintf (stderr, "Out of memory\n");
634         status = NOTMUCH_STATUS_OUT_OF_MEMORY;
635         goto DONE;
636     }
637
638     /* Initialize the GLib type system and threads */
639     g_type_init ();
640
641     /* Initialize gmime */
642     if (! initialized) {
643         g_mime_init (0);
644         initialized = 1;
645     }
646
647     notmuch = talloc_zero (NULL, notmuch_database_t);
648     notmuch->exception_reported = FALSE;
649     notmuch->path = talloc_strdup (notmuch, path);
650
651     if (notmuch->path[strlen (notmuch->path) - 1] == '/')
652         notmuch->path[strlen (notmuch->path) - 1] = '\0';
653
654     notmuch->needs_upgrade = FALSE;
655     notmuch->mode = mode;
656     notmuch->atomic_nesting = 0;
657     try {
658         string last_thread_id;
659
660         if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
661             notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
662                                                                Xapian::DB_CREATE_OR_OPEN);
663             version = notmuch_database_get_version (notmuch);
664
665             if (version > NOTMUCH_DATABASE_VERSION) {
666                 fprintf (stderr,
667                          "Error: Notmuch database at %s\n"
668                          "       has a newer database format version (%u) than supported by this\n"
669                          "       version of notmuch (%u). Refusing to open this database in\n"
670                          "       read-write mode.\n",
671                          notmuch_path, version, NOTMUCH_DATABASE_VERSION);
672                 notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
673                 notmuch_database_destroy (notmuch);
674                 notmuch = NULL;
675                 status = NOTMUCH_STATUS_FILE_ERROR;
676                 goto DONE;
677             }
678
679             if (version < NOTMUCH_DATABASE_VERSION)
680                 notmuch->needs_upgrade = TRUE;
681         } else {
682             notmuch->xapian_db = new Xapian::Database (xapian_path);
683             version = notmuch_database_get_version (notmuch);
684             if (version > NOTMUCH_DATABASE_VERSION)
685             {
686                 fprintf (stderr,
687                          "Warning: Notmuch database at %s\n"
688                          "         has a newer database format version (%u) than supported by this\n"
689                          "         version of notmuch (%u). Some operations may behave incorrectly,\n"
690                          "         (but the database will not be harmed since it is being opened\n"
691                          "         in read-only mode).\n",
692                          notmuch_path, version, NOTMUCH_DATABASE_VERSION);
693             }
694         }
695
696         notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
697         last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
698         if (last_thread_id.empty ()) {
699             notmuch->last_thread_id = 0;
700         } else {
701             const char *str;
702             char *end;
703
704             str = last_thread_id.c_str ();
705             notmuch->last_thread_id = strtoull (str, &end, 16);
706             if (*end != '\0')
707                 INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
708         }
709
710         notmuch->query_parser = new Xapian::QueryParser;
711         notmuch->term_gen = new Xapian::TermGenerator;
712         notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
713         notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
714         notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
715
716         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
717         notmuch->query_parser->set_database (*notmuch->xapian_db);
718         notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
719         notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
720         notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
721         notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor);
722
723         for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
724             prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
725             notmuch->query_parser->add_boolean_prefix (prefix->name,
726                                                        prefix->prefix);
727         }
728
729         for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
730             prefix_t *prefix = &PROBABILISTIC_PREFIX[i];
731             notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
732         }
733     } catch (const Xapian::Error &error) {
734         fprintf (stderr, "A Xapian exception occurred opening database: %s\n",
735                  error.get_msg().c_str());
736         notmuch_database_destroy (notmuch);
737         notmuch = NULL;
738         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
739     }
740
741   DONE:
742     talloc_free (local);
743
744     if (database)
745         *database = notmuch;
746     else
747         talloc_free (notmuch);
748     return status;
749 }
750
751 void
752 notmuch_database_close (notmuch_database_t *notmuch)
753 {
754     try {
755         if (notmuch->xapian_db != NULL &&
756             notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE)
757             (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->flush ();
758     } catch (const Xapian::Error &error) {
759         if (! notmuch->exception_reported) {
760             fprintf (stderr, "Error: A Xapian exception occurred flushing database: %s\n",
761                      error.get_msg().c_str());
762         }
763     }
764
765     /* Many Xapian objects (and thus notmuch objects) hold references to
766      * the database, so merely deleting the database may not suffice to
767      * close it.  Thus, we explicitly close it here. */
768     if (notmuch->xapian_db != NULL) {
769         try {
770             notmuch->xapian_db->close();
771         } catch (const Xapian::Error &error) {
772             /* do nothing */
773         }
774     }
775
776     delete notmuch->term_gen;
777     notmuch->term_gen = NULL;
778     delete notmuch->query_parser;
779     notmuch->query_parser = NULL;
780     delete notmuch->xapian_db;
781     notmuch->xapian_db = NULL;
782     delete notmuch->value_range_processor;
783     notmuch->value_range_processor = NULL;
784     delete notmuch->date_range_processor;
785     notmuch->date_range_processor = NULL;
786 }
787
788 void
789 notmuch_database_destroy (notmuch_database_t *notmuch)
790 {
791     notmuch_database_close (notmuch);
792     talloc_free (notmuch);
793 }
794
795 const char *
796 notmuch_database_get_path (notmuch_database_t *notmuch)
797 {
798     return notmuch->path;
799 }
800
801 unsigned int
802 notmuch_database_get_version (notmuch_database_t *notmuch)
803 {
804     unsigned int version;
805     string version_string;
806     const char *str;
807     char *end;
808
809     version_string = notmuch->xapian_db->get_metadata ("version");
810     if (version_string.empty ())
811         return 0;
812
813     str = version_string.c_str ();
814     if (str == NULL || *str == '\0')
815         return 0;
816
817     version = strtoul (str, &end, 10);
818     if (*end != '\0')
819         INTERNAL_ERROR ("Malformed database version: %s", str);
820
821     return version;
822 }
823
824 notmuch_bool_t
825 notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
826 {
827     return notmuch->needs_upgrade;
828 }
829
830 static volatile sig_atomic_t do_progress_notify = 0;
831
832 static void
833 handle_sigalrm (unused (int signal))
834 {
835     do_progress_notify = 1;
836 }
837
838 /* Upgrade the current database.
839  *
840  * After opening a database in read-write mode, the client should
841  * check if an upgrade is needed (notmuch_database_needs_upgrade) and
842  * if so, upgrade with this function before making any modifications.
843  *
844  * The optional progress_notify callback can be used by the caller to
845  * provide progress indication to the user. If non-NULL it will be
846  * called periodically with 'count' as the number of messages upgraded
847  * so far and 'total' the overall number of messages that will be
848  * converted.
849  */
850 notmuch_status_t
851 notmuch_database_upgrade (notmuch_database_t *notmuch,
852                           void (*progress_notify) (void *closure,
853                                                    double progress),
854                           void *closure)
855 {
856     Xapian::WritableDatabase *db;
857     struct sigaction action;
858     struct itimerval timerval;
859     notmuch_bool_t timer_is_active = FALSE;
860     unsigned int version;
861     notmuch_status_t status;
862     unsigned int count = 0, total = 0;
863
864     status = _notmuch_database_ensure_writable (notmuch);
865     if (status)
866         return status;
867
868     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
869
870     version = notmuch_database_get_version (notmuch);
871
872     if (version >= NOTMUCH_DATABASE_VERSION)
873         return NOTMUCH_STATUS_SUCCESS;
874
875     if (progress_notify) {
876         /* Setup our handler for SIGALRM */
877         memset (&action, 0, sizeof (struct sigaction));
878         action.sa_handler = handle_sigalrm;
879         sigemptyset (&action.sa_mask);
880         action.sa_flags = SA_RESTART;
881         sigaction (SIGALRM, &action, NULL);
882
883         /* Then start a timer to send SIGALRM once per second. */
884         timerval.it_interval.tv_sec = 1;
885         timerval.it_interval.tv_usec = 0;
886         timerval.it_value.tv_sec = 1;
887         timerval.it_value.tv_usec = 0;
888         setitimer (ITIMER_REAL, &timerval, NULL);
889
890         timer_is_active = TRUE;
891     }
892
893     /* Before version 1, each message document had its filename in the
894      * data field. Copy that into the new format by calling
895      * notmuch_message_add_filename.
896      */
897     if (version < 1) {
898         notmuch_query_t *query = notmuch_query_create (notmuch, "");
899         notmuch_messages_t *messages;
900         notmuch_message_t *message;
901         char *filename;
902         Xapian::TermIterator t, t_end;
903
904         total = notmuch_query_count_messages (query);
905
906         for (messages = notmuch_query_search_messages (query);
907              notmuch_messages_valid (messages);
908              notmuch_messages_move_to_next (messages))
909         {
910             if (do_progress_notify) {
911                 progress_notify (closure, (double) count / total);
912                 do_progress_notify = 0;
913             }
914
915             message = notmuch_messages_get (messages);
916
917             filename = _notmuch_message_talloc_copy_data (message);
918             if (filename && *filename != '\0') {
919                 _notmuch_message_add_filename (message, filename);
920                 _notmuch_message_sync (message);
921             }
922             talloc_free (filename);
923
924             notmuch_message_destroy (message);
925
926             count++;
927         }
928
929         notmuch_query_destroy (query);
930
931         /* Also, before version 1 we stored directory timestamps in
932          * XTIMESTAMP documents instead of the current XDIRECTORY
933          * documents. So copy those as well. */
934
935         t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
936
937         for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
938              t != t_end;
939              t++)
940         {
941             Xapian::PostingIterator p, p_end;
942             std::string term = *t;
943
944             p_end = notmuch->xapian_db->postlist_end (term);
945
946             for (p = notmuch->xapian_db->postlist_begin (term);
947                  p != p_end;
948                  p++)
949             {
950                 Xapian::Document document;
951                 time_t mtime;
952                 notmuch_directory_t *directory;
953
954                 if (do_progress_notify) {
955                     progress_notify (closure, (double) count / total);
956                     do_progress_notify = 0;
957                 }
958
959                 document = find_document_for_doc_id (notmuch, *p);
960                 mtime = Xapian::sortable_unserialise (
961                     document.get_value (NOTMUCH_VALUE_TIMESTAMP));
962
963                 directory = _notmuch_directory_create (notmuch, term.c_str() + 10,
964                                                        NOTMUCH_FIND_CREATE, &status);
965                 notmuch_directory_set_mtime (directory, mtime);
966                 notmuch_directory_destroy (directory);
967             }
968         }
969     }
970
971     db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
972     db->flush ();
973
974     /* Now that the upgrade is complete we can remove the old data
975      * and documents that are no longer needed. */
976     if (version < 1) {
977         notmuch_query_t *query = notmuch_query_create (notmuch, "");
978         notmuch_messages_t *messages;
979         notmuch_message_t *message;
980         char *filename;
981
982         for (messages = notmuch_query_search_messages (query);
983              notmuch_messages_valid (messages);
984              notmuch_messages_move_to_next (messages))
985         {
986             if (do_progress_notify) {
987                 progress_notify (closure, (double) count / total);
988                 do_progress_notify = 0;
989             }
990
991             message = notmuch_messages_get (messages);
992
993             filename = _notmuch_message_talloc_copy_data (message);
994             if (filename && *filename != '\0') {
995                 _notmuch_message_clear_data (message);
996                 _notmuch_message_sync (message);
997             }
998             talloc_free (filename);
999
1000             notmuch_message_destroy (message);
1001         }
1002
1003         notmuch_query_destroy (query);
1004     }
1005
1006     if (version < 1) {
1007         Xapian::TermIterator t, t_end;
1008
1009         t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
1010
1011         for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
1012              t != t_end;
1013              t++)
1014         {
1015             Xapian::PostingIterator p, p_end;
1016             std::string term = *t;
1017
1018             p_end = notmuch->xapian_db->postlist_end (term);
1019
1020             for (p = notmuch->xapian_db->postlist_begin (term);
1021                  p != p_end;
1022                  p++)
1023             {
1024                 if (do_progress_notify) {
1025                     progress_notify (closure, (double) count / total);
1026                     do_progress_notify = 0;
1027                 }
1028
1029                 db->delete_document (*p);
1030             }
1031         }
1032     }
1033
1034     if (timer_is_active) {
1035         /* Now stop the timer. */
1036         timerval.it_interval.tv_sec = 0;
1037         timerval.it_interval.tv_usec = 0;
1038         timerval.it_value.tv_sec = 0;
1039         timerval.it_value.tv_usec = 0;
1040         setitimer (ITIMER_REAL, &timerval, NULL);
1041
1042         /* And disable the signal handler. */
1043         action.sa_handler = SIG_IGN;
1044         sigaction (SIGALRM, &action, NULL);
1045     }
1046
1047     return NOTMUCH_STATUS_SUCCESS;
1048 }
1049
1050 notmuch_status_t
1051 notmuch_database_begin_atomic (notmuch_database_t *notmuch)
1052 {
1053     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
1054         notmuch->atomic_nesting > 0)
1055         goto DONE;
1056
1057     try {
1058         (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
1059     } catch (const Xapian::Error &error) {
1060         fprintf (stderr, "A Xapian exception occurred beginning transaction: %s.\n",
1061                  error.get_msg().c_str());
1062         notmuch->exception_reported = TRUE;
1063         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1064     }
1065
1066 DONE:
1067     notmuch->atomic_nesting++;
1068     return NOTMUCH_STATUS_SUCCESS;
1069 }
1070
1071 notmuch_status_t
1072 notmuch_database_end_atomic (notmuch_database_t *notmuch)
1073 {
1074     Xapian::WritableDatabase *db;
1075
1076     if (notmuch->atomic_nesting == 0)
1077         return NOTMUCH_STATUS_UNBALANCED_ATOMIC;
1078
1079     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
1080         notmuch->atomic_nesting > 1)
1081         goto DONE;
1082
1083     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1084     try {
1085         db->commit_transaction ();
1086
1087         /* This is a hack for testing.  Xapian never flushes on a
1088          * non-flushed commit, even if the flush threshold is 1.
1089          * However, we rely on flushing to test atomicity. */
1090         const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
1091         if (thresh && atoi (thresh) == 1)
1092             db->flush ();
1093     } catch (const Xapian::Error &error) {
1094         fprintf (stderr, "A Xapian exception occurred committing transaction: %s.\n",
1095                  error.get_msg().c_str());
1096         notmuch->exception_reported = TRUE;
1097         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1098     }
1099
1100 DONE:
1101     notmuch->atomic_nesting--;
1102     return NOTMUCH_STATUS_SUCCESS;
1103 }
1104
1105 /* We allow the user to use arbitrarily long paths for directories. But
1106  * we have a term-length limit. So if we exceed that, we'll use the
1107  * SHA-1 of the path for the database term.
1108  *
1109  * Note: This function may return the original value of 'path'. If it
1110  * does not, then the caller is responsible to free() the returned
1111  * value.
1112  */
1113 const char *
1114 _notmuch_database_get_directory_db_path (const char *path)
1115 {
1116     int term_len = strlen (_find_prefix ("directory")) + strlen (path);
1117
1118     if (term_len > NOTMUCH_TERM_MAX)
1119         return notmuch_sha1_of_string (path);
1120     else
1121         return path;
1122 }
1123
1124 /* Given a path, split it into two parts: the directory part is all
1125  * components except for the last, and the basename is that last
1126  * component. Getting the return-value for either part is optional
1127  * (the caller can pass NULL).
1128  *
1129  * The original 'path' can represent either a regular file or a
1130  * directory---the splitting will be carried out in the same way in
1131  * either case. Trailing slashes on 'path' will be ignored, and any
1132  * cases of multiple '/' characters appearing in series will be
1133  * treated as a single '/'.
1134  *
1135  * Allocation (if any) will have 'ctx' as the talloc owner. But
1136  * pointers will be returned within the original path string whenever
1137  * possible.
1138  *
1139  * Note: If 'path' is non-empty and contains no non-trailing slash,
1140  * (that is, consists of a filename with no parent directory), then
1141  * the directory returned will be an empty string. However, if 'path'
1142  * is an empty string, then both directory and basename will be
1143  * returned as NULL.
1144  */
1145 notmuch_status_t
1146 _notmuch_database_split_path (void *ctx,
1147                               const char *path,
1148                               const char **directory,
1149                               const char **basename)
1150 {
1151     const char *slash;
1152
1153     if (path == NULL || *path == '\0') {
1154         if (directory)
1155             *directory = NULL;
1156         if (basename)
1157             *basename = NULL;
1158         return NOTMUCH_STATUS_SUCCESS;
1159     }
1160
1161     /* Find the last slash (not counting a trailing slash), if any. */
1162
1163     slash = path + strlen (path) - 1;
1164
1165     /* First, skip trailing slashes. */
1166     while (slash != path) {
1167         if (*slash != '/')
1168             break;
1169
1170         --slash;
1171     }
1172
1173     /* Then, find a slash. */
1174     while (slash != path) {
1175         if (*slash == '/')
1176             break;
1177
1178         if (basename)
1179             *basename = slash;
1180
1181         --slash;
1182     }
1183
1184     /* Finally, skip multiple slashes. */
1185     while (slash != path) {
1186         if (*slash != '/')
1187             break;
1188
1189         --slash;
1190     }
1191
1192     if (slash == path) {
1193         if (directory)
1194             *directory = talloc_strdup (ctx, "");
1195         if (basename)
1196             *basename = path;
1197     } else {
1198         if (directory)
1199             *directory = talloc_strndup (ctx, path, slash - path + 1);
1200     }
1201
1202     return NOTMUCH_STATUS_SUCCESS;
1203 }
1204
1205 /* Find the document ID of the specified directory.
1206  *
1207  * If (flags & NOTMUCH_FIND_CREATE), a new directory document will be
1208  * created if one does not exist for 'path'.  Otherwise, if the
1209  * directory document does not exist, this sets *directory_id to
1210  * ((unsigned int)-1) and returns NOTMUCH_STATUS_SUCCESS.
1211  */
1212 notmuch_status_t
1213 _notmuch_database_find_directory_id (notmuch_database_t *notmuch,
1214                                      const char *path,
1215                                      notmuch_find_flags_t flags,
1216                                      unsigned int *directory_id)
1217 {
1218     notmuch_directory_t *directory;
1219     notmuch_status_t status;
1220
1221     if (path == NULL) {
1222         *directory_id = 0;
1223         return NOTMUCH_STATUS_SUCCESS;
1224     }
1225
1226     directory = _notmuch_directory_create (notmuch, path, flags, &status);
1227     if (status || !directory) {
1228         *directory_id = -1;
1229         return status;
1230     }
1231
1232     *directory_id = _notmuch_directory_get_document_id (directory);
1233
1234     notmuch_directory_destroy (directory);
1235
1236     return NOTMUCH_STATUS_SUCCESS;
1237 }
1238
1239 const char *
1240 _notmuch_database_get_directory_path (void *ctx,
1241                                       notmuch_database_t *notmuch,
1242                                       unsigned int doc_id)
1243 {
1244     Xapian::Document document;
1245
1246     document = find_document_for_doc_id (notmuch, doc_id);
1247
1248     return talloc_strdup (ctx, document.get_data ().c_str ());
1249 }
1250
1251 /* Given a legal 'filename' for the database, (either relative to
1252  * database path or absolute with initial components identical to
1253  * database path), return a new string (with 'ctx' as the talloc
1254  * owner) suitable for use as a direntry term value.
1255  *
1256  * If (flags & NOTMUCH_FIND_CREATE), the necessary directory documents
1257  * will be created in the database as needed.  Otherwise, if the
1258  * necessary directory documents do not exist, this sets
1259  * *direntry to NULL and returns NOTMUCH_STATUS_SUCCESS.
1260  */
1261 notmuch_status_t
1262 _notmuch_database_filename_to_direntry (void *ctx,
1263                                         notmuch_database_t *notmuch,
1264                                         const char *filename,
1265                                         notmuch_find_flags_t flags,
1266                                         char **direntry)
1267 {
1268     const char *relative, *directory, *basename;
1269     Xapian::docid directory_id;
1270     notmuch_status_t status;
1271
1272     relative = _notmuch_database_relative_path (notmuch, filename);
1273
1274     status = _notmuch_database_split_path (ctx, relative,
1275                                            &directory, &basename);
1276     if (status)
1277         return status;
1278
1279     status = _notmuch_database_find_directory_id (notmuch, directory, flags,
1280                                                   &directory_id);
1281     if (status || directory_id == (unsigned int)-1) {
1282         *direntry = NULL;
1283         return status;
1284     }
1285
1286     *direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
1287
1288     return NOTMUCH_STATUS_SUCCESS;
1289 }
1290
1291 /* Given a legal 'path' for the database, return the relative path.
1292  *
1293  * The return value will be a pointer to the original path contents,
1294  * and will be either the original string (if 'path' was relative) or
1295  * a portion of the string (if path was absolute and begins with the
1296  * database path).
1297  */
1298 const char *
1299 _notmuch_database_relative_path (notmuch_database_t *notmuch,
1300                                  const char *path)
1301 {
1302     const char *db_path, *relative;
1303     unsigned int db_path_len;
1304
1305     db_path = notmuch_database_get_path (notmuch);
1306     db_path_len = strlen (db_path);
1307
1308     relative = path;
1309
1310     if (*relative == '/') {
1311         while (*relative == '/' && *(relative+1) == '/')
1312             relative++;
1313
1314         if (strncmp (relative, db_path, db_path_len) == 0)
1315         {
1316             relative += db_path_len;
1317             while (*relative == '/')
1318                 relative++;
1319         }
1320     }
1321
1322     return relative;
1323 }
1324
1325 notmuch_status_t
1326 notmuch_database_get_directory (notmuch_database_t *notmuch,
1327                                 const char *path,
1328                                 notmuch_directory_t **directory)
1329 {
1330     notmuch_status_t status;
1331
1332     if (directory == NULL)
1333         return NOTMUCH_STATUS_NULL_POINTER;
1334     *directory = NULL;
1335
1336     try {
1337         *directory = _notmuch_directory_create (notmuch, path,
1338                                                 NOTMUCH_FIND_LOOKUP, &status);
1339     } catch (const Xapian::Error &error) {
1340         fprintf (stderr, "A Xapian exception occurred getting directory: %s.\n",
1341                  error.get_msg().c_str());
1342         notmuch->exception_reported = TRUE;
1343         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1344     }
1345     return status;
1346 }
1347
1348 /* Allocate a document ID that satisfies the following criteria:
1349  *
1350  * 1. The ID does not exist for any document in the Xapian database
1351  *
1352  * 2. The ID was not previously returned from this function
1353  *
1354  * 3. The ID is the smallest integer satisfying (1) and (2)
1355  *
1356  * This function will trigger an internal error if these constraints
1357  * cannot all be satisfied, (that is, the pool of available document
1358  * IDs has been exhausted).
1359  */
1360 unsigned int
1361 _notmuch_database_generate_doc_id (notmuch_database_t *notmuch)
1362 {
1363     assert (notmuch->last_doc_id >= notmuch->xapian_db->get_lastdocid ());
1364
1365     notmuch->last_doc_id++;
1366
1367     if (notmuch->last_doc_id == 0)
1368         INTERNAL_ERROR ("Xapian document IDs are exhausted.\n");        
1369
1370     return notmuch->last_doc_id;
1371 }
1372
1373 static const char *
1374 _notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
1375 {
1376     /* 16 bytes (+ terminator) for hexadecimal representation of
1377      * a 64-bit integer. */
1378     static char thread_id[17];
1379     Xapian::WritableDatabase *db;
1380
1381     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1382
1383     notmuch->last_thread_id++;
1384
1385     sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
1386
1387     db->set_metadata ("last_thread_id", thread_id);
1388
1389     return thread_id;
1390 }
1391
1392 static char *
1393 _get_metadata_thread_id_key (void *ctx, const char *message_id)
1394 {
1395     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
1396         message_id = _message_id_compressed (ctx, message_id);
1397
1398     return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
1399                             message_id);
1400 }
1401
1402 /* Find the thread ID to which the message with 'message_id' belongs.
1403  *
1404  * Note: 'thread_id_ret' must not be NULL!
1405  * On success '*thread_id_ret' is set to a newly talloced string belonging to
1406  * 'ctx'.
1407  *
1408  * Note: If there is no message in the database with the given
1409  * 'message_id' then a new thread_id will be allocated for this
1410  * message and stored in the database metadata, (where this same
1411  * thread ID can be looked up if the message is added to the database
1412  * later).
1413  */
1414 static notmuch_status_t
1415 _resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
1416                                   void *ctx,
1417                                   const char *message_id,
1418                                   const char **thread_id_ret)
1419 {
1420     notmuch_status_t status;
1421     notmuch_message_t *message;
1422     string thread_id_string;
1423     char *metadata_key;
1424     Xapian::WritableDatabase *db;
1425
1426     status = notmuch_database_find_message (notmuch, message_id, &message);
1427
1428     if (status)
1429         return status;
1430
1431     if (message) {
1432         *thread_id_ret = talloc_steal (ctx,
1433                                        notmuch_message_get_thread_id (message));
1434
1435         notmuch_message_destroy (message);
1436
1437         return NOTMUCH_STATUS_SUCCESS;
1438     }
1439
1440     /* Message has not been seen yet.
1441      *
1442      * We may have seen a reference to it already, in which case, we
1443      * can return the thread ID stored in the metadata. Otherwise, we
1444      * generate a new thread ID and store it there.
1445      */
1446     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1447     metadata_key = _get_metadata_thread_id_key (ctx, message_id);
1448     thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
1449
1450     if (thread_id_string.empty()) {
1451         *thread_id_ret = talloc_strdup (ctx,
1452                                         _notmuch_database_generate_thread_id (notmuch));
1453         db->set_metadata (metadata_key, *thread_id_ret);
1454     } else {
1455         *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str());
1456     }
1457
1458     talloc_free (metadata_key);
1459
1460     return NOTMUCH_STATUS_SUCCESS;
1461 }
1462
1463 static notmuch_status_t
1464 _merge_threads (notmuch_database_t *notmuch,
1465                 const char *winner_thread_id,
1466                 const char *loser_thread_id)
1467 {
1468     Xapian::PostingIterator loser, loser_end;
1469     notmuch_message_t *message = NULL;
1470     notmuch_private_status_t private_status;
1471     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1472
1473     find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
1474
1475     for ( ; loser != loser_end; loser++) {
1476         message = _notmuch_message_create (notmuch, notmuch,
1477                                            *loser, &private_status);
1478         if (message == NULL) {
1479             ret = COERCE_STATUS (private_status,
1480                                  "Cannot find document for doc_id from query");
1481             goto DONE;
1482         }
1483
1484         _notmuch_message_remove_term (message, "thread", loser_thread_id);
1485         _notmuch_message_add_term (message, "thread", winner_thread_id);
1486         _notmuch_message_sync (message);
1487
1488         notmuch_message_destroy (message);
1489         message = NULL;
1490     }
1491
1492   DONE:
1493     if (message)
1494         notmuch_message_destroy (message);
1495
1496     return ret;
1497 }
1498
1499 static void
1500 _my_talloc_free_for_g_hash (void *ptr)
1501 {
1502     talloc_free (ptr);
1503 }
1504
1505 static notmuch_status_t
1506 _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
1507                                            notmuch_message_t *message,
1508                                            notmuch_message_file_t *message_file,
1509                                            const char **thread_id)
1510 {
1511     GHashTable *parents = NULL;
1512     const char *refs, *in_reply_to, *in_reply_to_message_id;
1513     GList *l, *keys = NULL;
1514     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1515
1516     parents = g_hash_table_new_full (g_str_hash, g_str_equal,
1517                                      _my_talloc_free_for_g_hash, NULL);
1518
1519     refs = notmuch_message_file_get_header (message_file, "references");
1520     parse_references (message, notmuch_message_get_message_id (message),
1521                       parents, refs);
1522
1523     in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
1524     parse_references (message, notmuch_message_get_message_id (message),
1525                       parents, in_reply_to);
1526
1527     /* Carefully avoid adding any self-referential in-reply-to term. */
1528     in_reply_to_message_id = _parse_message_id (message, in_reply_to, NULL);
1529     if (in_reply_to_message_id &&
1530         strcmp (in_reply_to_message_id,
1531                 notmuch_message_get_message_id (message)))
1532     {
1533         _notmuch_message_add_term (message, "replyto",
1534                              _parse_message_id (message, in_reply_to, NULL));
1535     }
1536
1537     keys = g_hash_table_get_keys (parents);
1538     for (l = keys; l; l = l->next) {
1539         char *parent_message_id;
1540         const char *parent_thread_id = NULL;
1541
1542         parent_message_id = (char *) l->data;
1543
1544         _notmuch_message_add_term (message, "reference",
1545                                    parent_message_id);
1546
1547         ret = _resolve_message_id_to_thread_id (notmuch,
1548                                                 message,
1549                                                 parent_message_id,
1550                                                 &parent_thread_id);
1551         if (ret)
1552             goto DONE;
1553
1554         if (*thread_id == NULL) {
1555             *thread_id = talloc_strdup (message, parent_thread_id);
1556             _notmuch_message_add_term (message, "thread", *thread_id);
1557         } else if (strcmp (*thread_id, parent_thread_id)) {
1558             ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
1559             if (ret)
1560                 goto DONE;
1561         }
1562     }
1563
1564   DONE:
1565     if (keys)
1566         g_list_free (keys);
1567     if (parents)
1568         g_hash_table_unref (parents);
1569
1570     return ret;
1571 }
1572
1573 static notmuch_status_t
1574 _notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
1575                                             notmuch_message_t *message,
1576                                             const char **thread_id)
1577 {
1578     const char *message_id = notmuch_message_get_message_id (message);
1579     Xapian::PostingIterator child, children_end;
1580     notmuch_message_t *child_message = NULL;
1581     const char *child_thread_id;
1582     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1583     notmuch_private_status_t private_status;
1584
1585     find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
1586
1587     for ( ; child != children_end; child++) {
1588
1589         child_message = _notmuch_message_create (message, notmuch,
1590                                                  *child, &private_status);
1591         if (child_message == NULL) {
1592             ret = COERCE_STATUS (private_status,
1593                                  "Cannot find document for doc_id from query");
1594             goto DONE;
1595         }
1596
1597         child_thread_id = notmuch_message_get_thread_id (child_message);
1598         if (*thread_id == NULL) {
1599             *thread_id = talloc_strdup (message, child_thread_id);
1600             _notmuch_message_add_term (message, "thread", *thread_id);
1601         } else if (strcmp (*thread_id, child_thread_id)) {
1602             _notmuch_message_remove_term (child_message, "reference",
1603                                           message_id);
1604             _notmuch_message_sync (child_message);
1605             ret = _merge_threads (notmuch, *thread_id, child_thread_id);
1606             if (ret)
1607                 goto DONE;
1608         }
1609
1610         notmuch_message_destroy (child_message);
1611         child_message = NULL;
1612     }
1613
1614   DONE:
1615     if (child_message)
1616         notmuch_message_destroy (child_message);
1617
1618     return ret;
1619 }
1620
1621 /* Given a (mostly empty) 'message' and its corresponding
1622  * 'message_file' link it to existing threads in the database.
1623  *
1624  * The first check is in the metadata of the database to see if we
1625  * have pre-allocated a thread_id in advance for this message, (which
1626  * would have happened if a message was previously added that
1627  * referenced this one).
1628  *
1629  * Second, we look at 'message_file' and its link-relevant headers
1630  * (References and In-Reply-To) for message IDs.
1631  *
1632  * Finally, we look in the database for existing message that
1633  * reference 'message'.
1634  *
1635  * In all cases, we assign to the current message the first thread_id
1636  * found (through either parent or child). We will also merge any
1637  * existing, distinct threads where this message belongs to both,
1638  * (which is not uncommon when messages are processed out of order).
1639  *
1640  * Finally, if no thread ID has been found through parent or child, we
1641  * call _notmuch_message_generate_thread_id to generate a new thread
1642  * ID. This should only happen for new, top-level messages, (no
1643  * References or In-Reply-To header in this message, and no previously
1644  * added message refers to this message).
1645  */
1646 static notmuch_status_t
1647 _notmuch_database_link_message (notmuch_database_t *notmuch,
1648                                 notmuch_message_t *message,
1649                                 notmuch_message_file_t *message_file)
1650 {
1651     notmuch_status_t status;
1652     const char *message_id, *thread_id = NULL;
1653     char *metadata_key;
1654     string stored_id;
1655
1656     message_id = notmuch_message_get_message_id (message);
1657     metadata_key = _get_metadata_thread_id_key (message, message_id);
1658
1659     /* Check if we have already seen related messages to this one.
1660      * If we have then use the thread_id that we stored at that time.
1661      */
1662     stored_id = notmuch->xapian_db->get_metadata (metadata_key);
1663     if (! stored_id.empty()) {
1664         Xapian::WritableDatabase *db;
1665
1666         db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1667
1668         /* Clear the metadata for this message ID. We don't need it
1669          * anymore. */
1670         db->set_metadata (metadata_key, "");
1671         thread_id = stored_id.c_str();
1672
1673         _notmuch_message_add_term (message, "thread", thread_id);
1674     }
1675     talloc_free (metadata_key);
1676
1677     status = _notmuch_database_link_message_to_parents (notmuch, message,
1678                                                         message_file,
1679                                                         &thread_id);
1680     if (status)
1681         return status;
1682
1683     status = _notmuch_database_link_message_to_children (notmuch, message,
1684                                                          &thread_id);
1685     if (status)
1686         return status;
1687
1688     /* If not part of any existing thread, generate a new thread ID. */
1689     if (thread_id == NULL) {
1690         thread_id = _notmuch_database_generate_thread_id (notmuch);
1691
1692         _notmuch_message_add_term (message, "thread", thread_id);
1693     }
1694
1695     return NOTMUCH_STATUS_SUCCESS;
1696 }
1697
1698 notmuch_status_t
1699 notmuch_database_add_message (notmuch_database_t *notmuch,
1700                               const char *filename,
1701                               notmuch_message_t **message_ret)
1702 {
1703     notmuch_message_file_t *message_file;
1704     notmuch_message_t *message = NULL;
1705     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
1706     notmuch_private_status_t private_status;
1707
1708     const char *date, *header;
1709     const char *from, *to, *subject;
1710     char *message_id = NULL;
1711
1712     if (message_ret)
1713         *message_ret = NULL;
1714
1715     ret = _notmuch_database_ensure_writable (notmuch);
1716     if (ret)
1717         return ret;
1718
1719     message_file = notmuch_message_file_open (filename);
1720     if (message_file == NULL)
1721         return NOTMUCH_STATUS_FILE_ERROR;
1722
1723     /* Adding a message may change many documents.  Do this all
1724      * atomically. */
1725     ret = notmuch_database_begin_atomic (notmuch);
1726     if (ret)
1727         goto DONE;
1728
1729     notmuch_message_file_restrict_headers (message_file,
1730                                            "date",
1731                                            "from",
1732                                            "in-reply-to",
1733                                            "message-id",
1734                                            "references",
1735                                            "subject",
1736                                            "to",
1737                                            (char *) NULL);
1738
1739     try {
1740         /* Before we do any real work, (especially before doing a
1741          * potential SHA-1 computation on the entire file's contents),
1742          * let's make sure that what we're looking at looks like an
1743          * actual email message.
1744          */
1745         from = notmuch_message_file_get_header (message_file, "from");
1746         subject = notmuch_message_file_get_header (message_file, "subject");
1747         to = notmuch_message_file_get_header (message_file, "to");
1748
1749         if ((from == NULL || *from == '\0') &&
1750             (subject == NULL || *subject == '\0') &&
1751             (to == NULL || *to == '\0'))
1752         {
1753             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
1754             goto DONE;
1755         }
1756
1757         /* Now that we're sure it's mail, the first order of business
1758          * is to find a message ID (or else create one ourselves). */
1759
1760         header = notmuch_message_file_get_header (message_file, "message-id");
1761         if (header && *header != '\0') {
1762             message_id = _parse_message_id (message_file, header, NULL);
1763
1764             /* So the header value isn't RFC-compliant, but it's
1765              * better than no message-id at all. */
1766             if (message_id == NULL)
1767                 message_id = talloc_strdup (message_file, header);
1768
1769             /* If a message ID is too long, substitute its sha1 instead. */
1770             if (message_id && strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) {
1771                 char *compressed = _message_id_compressed (message_file,
1772                                                            message_id);
1773                 talloc_free (message_id);
1774                 message_id = compressed;
1775             }
1776         }
1777
1778         if (message_id == NULL ) {
1779             /* No message-id at all, let's generate one by taking a
1780              * hash over the file's contents. */
1781             char *sha1 = notmuch_sha1_of_file (filename);
1782
1783             /* If that failed too, something is really wrong. Give up. */
1784             if (sha1 == NULL) {
1785                 ret = NOTMUCH_STATUS_FILE_ERROR;
1786                 goto DONE;
1787             }
1788
1789             message_id = talloc_asprintf (message_file,
1790                                           "notmuch-sha1-%s", sha1);
1791             free (sha1);
1792         }
1793
1794         /* Now that we have a message ID, we get a message object,
1795          * (which may or may not reference an existing document in the
1796          * database). */
1797
1798         message = _notmuch_message_create_for_message_id (notmuch,
1799                                                           message_id,
1800                                                           &private_status);
1801
1802         talloc_free (message_id);
1803
1804         if (message == NULL) {
1805             ret = COERCE_STATUS (private_status,
1806                                  "Unexpected status value from _notmuch_message_create_for_message_id");
1807             goto DONE;
1808         }
1809
1810         _notmuch_message_add_filename (message, filename);
1811
1812         /* Is this a newly created message object? */
1813         if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1814             _notmuch_message_add_term (message, "type", "mail");
1815
1816             ret = _notmuch_database_link_message (notmuch, message,
1817                                                   message_file);
1818             if (ret)
1819                 goto DONE;
1820
1821             date = notmuch_message_file_get_header (message_file, "date");
1822             _notmuch_message_set_header_values (message, date, from, subject);
1823
1824             ret = _notmuch_message_index_file (message, filename);
1825             if (ret)
1826                 goto DONE;
1827         } else {
1828             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
1829         }
1830
1831         _notmuch_message_sync (message);
1832     } catch (const Xapian::Error &error) {
1833         fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
1834                  error.get_msg().c_str());
1835         notmuch->exception_reported = TRUE;
1836         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1837         goto DONE;
1838     }
1839
1840   DONE:
1841     if (message) {
1842         if ((ret == NOTMUCH_STATUS_SUCCESS ||
1843              ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
1844             *message_ret = message;
1845         else
1846             notmuch_message_destroy (message);
1847     }
1848
1849     if (message_file)
1850         notmuch_message_file_close (message_file);
1851
1852     ret2 = notmuch_database_end_atomic (notmuch);
1853     if ((ret == NOTMUCH_STATUS_SUCCESS ||
1854          ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
1855         ret2 != NOTMUCH_STATUS_SUCCESS)
1856         ret = ret2;
1857
1858     return ret;
1859 }
1860
1861 notmuch_status_t
1862 notmuch_database_remove_message (notmuch_database_t *notmuch,
1863                                  const char *filename)
1864 {
1865     notmuch_status_t status;
1866     notmuch_message_t *message;
1867
1868     status = notmuch_database_find_message_by_filename (notmuch, filename,
1869                                                         &message);
1870
1871     if (status == NOTMUCH_STATUS_SUCCESS && message) {
1872             status = _notmuch_message_remove_filename (message, filename);
1873             if (status == NOTMUCH_STATUS_SUCCESS)
1874                 _notmuch_message_delete (message);
1875             else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
1876                 _notmuch_message_sync (message);
1877
1878             notmuch_message_destroy (message);
1879     }
1880
1881     return status;
1882 }
1883
1884 notmuch_status_t
1885 notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
1886                                            const char *filename,
1887                                            notmuch_message_t **message_ret)
1888 {
1889     void *local;
1890     const char *prefix = _find_prefix ("file-direntry");
1891     char *direntry, *term;
1892     Xapian::PostingIterator i, end;
1893     notmuch_status_t status;
1894
1895     if (message_ret == NULL)
1896         return NOTMUCH_STATUS_NULL_POINTER;
1897
1898     /* return NULL on any failure */
1899     *message_ret = NULL;
1900
1901     local = talloc_new (notmuch);
1902
1903     try {
1904         status = _notmuch_database_filename_to_direntry (
1905             local, notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
1906         if (status || !direntry)
1907             goto DONE;
1908
1909         term = talloc_asprintf (local, "%s%s", prefix, direntry);
1910
1911         find_doc_ids_for_term (notmuch, term, &i, &end);
1912
1913         if (i != end) {
1914             notmuch_private_status_t private_status;
1915
1916             *message_ret = _notmuch_message_create (notmuch, notmuch, *i,
1917                                                     &private_status);
1918             if (*message_ret == NULL)
1919                 status = NOTMUCH_STATUS_OUT_OF_MEMORY;
1920         }
1921     } catch (const Xapian::Error &error) {
1922         fprintf (stderr, "Error: A Xapian exception occurred finding message by filename: %s\n",
1923                  error.get_msg().c_str());
1924         notmuch->exception_reported = TRUE;
1925         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1926     }
1927
1928   DONE:
1929     talloc_free (local);
1930
1931     if (status && *message_ret) {
1932         notmuch_message_destroy (*message_ret);
1933         *message_ret = NULL;
1934     }
1935     return status;
1936 }
1937
1938 notmuch_string_list_t *
1939 _notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i,
1940                                          Xapian::TermIterator &end,
1941                                          const char *prefix)
1942 {
1943     int prefix_len = strlen (prefix);
1944     notmuch_string_list_t *list;
1945
1946     list = _notmuch_string_list_create (ctx);
1947     if (unlikely (list == NULL))
1948         return NULL;
1949
1950     for (i.skip_to (prefix); i != end; i++) {
1951         /* Terminate loop at first term without desired prefix. */
1952         if (strncmp ((*i).c_str (), prefix, prefix_len))
1953             break;
1954
1955         _notmuch_string_list_append (list, (*i).c_str () + prefix_len);
1956     }
1957
1958     return list;
1959 }
1960
1961 notmuch_tags_t *
1962 notmuch_database_get_all_tags (notmuch_database_t *db)
1963 {
1964     Xapian::TermIterator i, end;
1965     notmuch_string_list_t *tags;
1966
1967     try {
1968         i = db->xapian_db->allterms_begin();
1969         end = db->xapian_db->allterms_end();
1970         tags = _notmuch_database_get_terms_with_prefix (db, i, end,
1971                                                         _find_prefix ("tag"));
1972         _notmuch_string_list_sort (tags);
1973         return _notmuch_tags_create (db, tags);
1974     } catch (const Xapian::Error &error) {
1975         fprintf (stderr, "A Xapian exception occurred getting tags: %s.\n",
1976                  error.get_msg().c_str());
1977         db->exception_reported = TRUE;
1978         return NULL;
1979     }
1980 }