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