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