]> git.notmuchmail.org Git - notmuch/blob - lib/database.cc
1efb14d4a0bdc2327a366870069867ba392ad1aa
[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 void
778 notmuch_database_close (notmuch_database_t *notmuch)
779 {
780     try {
781         if (notmuch->xapian_db != NULL &&
782             notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE)
783             (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->flush ();
784     } catch (const Xapian::Error &error) {
785         if (! notmuch->exception_reported) {
786             fprintf (stderr, "Error: A Xapian exception occurred flushing database: %s\n",
787                      error.get_msg().c_str());
788         }
789     }
790
791     /* Many Xapian objects (and thus notmuch objects) hold references to
792      * the database, so merely deleting the database may not suffice to
793      * close it.  Thus, we explicitly close it here. */
794     if (notmuch->xapian_db != NULL) {
795         try {
796             notmuch->xapian_db->close();
797         } catch (const Xapian::Error &error) {
798             /* do nothing */
799         }
800     }
801
802     delete notmuch->term_gen;
803     notmuch->term_gen = NULL;
804     delete notmuch->query_parser;
805     notmuch->query_parser = NULL;
806     delete notmuch->xapian_db;
807     notmuch->xapian_db = NULL;
808     delete notmuch->value_range_processor;
809     notmuch->value_range_processor = NULL;
810     delete notmuch->date_range_processor;
811     notmuch->date_range_processor = NULL;
812 }
813
814 #if HAVE_XAPIAN_COMPACT
815 static int
816 unlink_cb (const char *path,
817            unused (const struct stat *sb),
818            unused (int type),
819            unused (struct FTW *ftw))
820 {
821     return remove (path);
822 }
823
824 static int
825 rmtree (const char *path)
826 {
827     return nftw (path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
828 }
829
830 class NotmuchCompactor : public Xapian::Compactor
831 {
832     notmuch_compact_status_cb_t status_cb;
833     void *status_closure;
834
835 public:
836     NotmuchCompactor(notmuch_compact_status_cb_t cb, void *closure) :
837         status_cb (cb), status_closure (closure) { }
838
839     virtual void
840     set_status (const std::string &table, const std::string &status)
841     {
842         char *msg;
843
844         if (status_cb == NULL)
845             return;
846
847         if (status.length () == 0)
848             msg = talloc_asprintf (NULL, "compacting table %s", table.c_str());
849         else
850             msg = talloc_asprintf (NULL, "     %s", status.c_str());
851
852         if (msg == NULL) {
853             return;
854         }
855
856         status_cb (msg, status_closure);
857         talloc_free (msg);
858     }
859 };
860
861 /* Compacts the given database, optionally saving the original database
862  * in backup_path. Additionally, a callback function can be provided to
863  * give the user feedback on the progress of the (likely long-lived)
864  * compaction process.
865  *
866  * The backup path must point to a directory on the same volume as the
867  * original database. Passing a NULL backup_path will result in the
868  * uncompacted database being deleted after compaction has finished.
869  * Note that the database write lock will be held during the
870  * compaction process to protect data integrity.
871  */
872 notmuch_status_t
873 notmuch_database_compact (const char *path,
874                           const char *backup_path,
875                           notmuch_compact_status_cb_t status_cb,
876                           void *closure)
877 {
878     void *local;
879     char *notmuch_path, *xapian_path, *compact_xapian_path;
880     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
881     notmuch_database_t *notmuch = NULL;
882     struct stat statbuf;
883     notmuch_bool_t keep_backup;
884
885     local = talloc_new (NULL);
886     if (! local)
887         return NOTMUCH_STATUS_OUT_OF_MEMORY;
888
889     ret = notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch);
890     if (ret) {
891         goto DONE;
892     }
893
894     if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
895         ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
896         goto DONE;
897     }
898
899     if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
900         ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
901         goto DONE;
902     }
903
904     if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) {
905         ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
906         goto DONE;
907     }
908
909     if (backup_path == NULL) {
910         if (! (backup_path = talloc_asprintf (local, "%s.old", xapian_path))) {
911             ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
912             goto DONE;
913         }
914         keep_backup = FALSE;
915     }
916     else {
917         keep_backup = TRUE;
918     }
919
920     if (stat (backup_path, &statbuf) != -1) {
921         fprintf (stderr, "Path already exists: %s\n", backup_path);
922         ret = NOTMUCH_STATUS_FILE_ERROR;
923         goto DONE;
924     }
925     if (errno != ENOENT) {
926         fprintf (stderr, "Unknown error while stat()ing path: %s\n",
927                  strerror (errno));
928         ret = NOTMUCH_STATUS_FILE_ERROR;
929         goto DONE;
930     }
931
932     /* Unconditionally attempt to remove old work-in-progress database (if
933      * any). This is "protected" by database lock. If this fails due to write
934      * errors (etc), the following code will fail and provide error message.
935      */
936     (void) rmtree (compact_xapian_path);
937
938     try {
939         NotmuchCompactor compactor (status_cb, closure);
940
941         compactor.set_renumber (false);
942         compactor.add_source (xapian_path);
943         compactor.set_destdir (compact_xapian_path);
944         compactor.compact ();
945     } catch (const Xapian::Error &error) {
946         fprintf (stderr, "Error while compacting: %s\n", error.get_msg().c_str());
947         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
948         goto DONE;
949     }
950
951     if (rename (xapian_path, backup_path)) {
952         fprintf (stderr, "Error moving %s to %s: %s\n",
953                  xapian_path, backup_path, strerror (errno));
954         ret = NOTMUCH_STATUS_FILE_ERROR;
955         goto DONE;
956     }
957
958     if (rename (compact_xapian_path, xapian_path)) {
959         fprintf (stderr, "Error moving %s to %s: %s\n",
960                  compact_xapian_path, xapian_path, strerror (errno));
961         ret = NOTMUCH_STATUS_FILE_ERROR;
962         goto DONE;
963     }
964
965     if (! keep_backup) {
966         if (rmtree (backup_path)) {
967             fprintf (stderr, "Error removing old database %s: %s\n",
968                      backup_path, strerror (errno));
969             ret = NOTMUCH_STATUS_FILE_ERROR;
970             goto DONE;
971         }
972     }
973
974   DONE:
975     if (notmuch)
976         notmuch_database_destroy (notmuch);
977
978     talloc_free (local);
979
980     return ret;
981 }
982 #else
983 notmuch_status_t
984 notmuch_database_compact (unused (const char *path),
985                           unused (const char *backup_path),
986                           unused (notmuch_compact_status_cb_t status_cb),
987                           unused (void *closure))
988 {
989     fprintf (stderr, "notmuch was compiled against a xapian version lacking compaction support.\n");
990     return NOTMUCH_STATUS_UNSUPPORTED_OPERATION;
991 }
992 #endif
993
994 void
995 notmuch_database_destroy (notmuch_database_t *notmuch)
996 {
997     notmuch_database_close (notmuch);
998     talloc_free (notmuch);
999 }
1000
1001 const char *
1002 notmuch_database_get_path (notmuch_database_t *notmuch)
1003 {
1004     return notmuch->path;
1005 }
1006
1007 unsigned int
1008 notmuch_database_get_version (notmuch_database_t *notmuch)
1009 {
1010     unsigned int version;
1011     string version_string;
1012     const char *str;
1013     char *end;
1014
1015     version_string = notmuch->xapian_db->get_metadata ("version");
1016     if (version_string.empty ())
1017         return 0;
1018
1019     str = version_string.c_str ();
1020     if (str == NULL || *str == '\0')
1021         return 0;
1022
1023     version = strtoul (str, &end, 10);
1024     if (*end != '\0')
1025         INTERNAL_ERROR ("Malformed database version: %s", str);
1026
1027     return version;
1028 }
1029
1030 notmuch_bool_t
1031 notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
1032 {
1033     return notmuch->needs_upgrade;
1034 }
1035
1036 static volatile sig_atomic_t do_progress_notify = 0;
1037
1038 static void
1039 handle_sigalrm (unused (int signal))
1040 {
1041     do_progress_notify = 1;
1042 }
1043
1044 /* Upgrade the current database.
1045  *
1046  * After opening a database in read-write mode, the client should
1047  * check if an upgrade is needed (notmuch_database_needs_upgrade) and
1048  * if so, upgrade with this function before making any modifications.
1049  *
1050  * The optional progress_notify callback can be used by the caller to
1051  * provide progress indication to the user. If non-NULL it will be
1052  * called periodically with 'count' as the number of messages upgraded
1053  * so far and 'total' the overall number of messages that will be
1054  * converted.
1055  */
1056 notmuch_status_t
1057 notmuch_database_upgrade (notmuch_database_t *notmuch,
1058                           void (*progress_notify) (void *closure,
1059                                                    double progress),
1060                           void *closure)
1061 {
1062     Xapian::WritableDatabase *db;
1063     struct sigaction action;
1064     struct itimerval timerval;
1065     notmuch_bool_t timer_is_active = FALSE;
1066     unsigned int version;
1067     notmuch_status_t status;
1068     unsigned int count = 0, total = 0;
1069
1070     status = _notmuch_database_ensure_writable (notmuch);
1071     if (status)
1072         return status;
1073
1074     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1075
1076     version = notmuch_database_get_version (notmuch);
1077
1078     if (version >= NOTMUCH_DATABASE_VERSION)
1079         return NOTMUCH_STATUS_SUCCESS;
1080
1081     if (progress_notify) {
1082         /* Setup our handler for SIGALRM */
1083         memset (&action, 0, sizeof (struct sigaction));
1084         action.sa_handler = handle_sigalrm;
1085         sigemptyset (&action.sa_mask);
1086         action.sa_flags = SA_RESTART;
1087         sigaction (SIGALRM, &action, NULL);
1088
1089         /* Then start a timer to send SIGALRM once per second. */
1090         timerval.it_interval.tv_sec = 1;
1091         timerval.it_interval.tv_usec = 0;
1092         timerval.it_value.tv_sec = 1;
1093         timerval.it_value.tv_usec = 0;
1094         setitimer (ITIMER_REAL, &timerval, NULL);
1095
1096         timer_is_active = TRUE;
1097     }
1098
1099     /* Before version 1, each message document had its filename in the
1100      * data field. Copy that into the new format by calling
1101      * notmuch_message_add_filename.
1102      */
1103     if (version < 1) {
1104         notmuch_query_t *query = notmuch_query_create (notmuch, "");
1105         notmuch_messages_t *messages;
1106         notmuch_message_t *message;
1107         char *filename;
1108         Xapian::TermIterator t, t_end;
1109
1110         total = notmuch_query_count_messages (query);
1111
1112         for (messages = notmuch_query_search_messages (query);
1113              notmuch_messages_valid (messages);
1114              notmuch_messages_move_to_next (messages))
1115         {
1116             if (do_progress_notify) {
1117                 progress_notify (closure, (double) count / total);
1118                 do_progress_notify = 0;
1119             }
1120
1121             message = notmuch_messages_get (messages);
1122
1123             filename = _notmuch_message_talloc_copy_data (message);
1124             if (filename && *filename != '\0') {
1125                 _notmuch_message_add_filename (message, filename);
1126                 _notmuch_message_sync (message);
1127             }
1128             talloc_free (filename);
1129
1130             notmuch_message_destroy (message);
1131
1132             count++;
1133         }
1134
1135         notmuch_query_destroy (query);
1136
1137         /* Also, before version 1 we stored directory timestamps in
1138          * XTIMESTAMP documents instead of the current XDIRECTORY
1139          * documents. So copy those as well. */
1140
1141         t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
1142
1143         for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
1144              t != t_end;
1145              t++)
1146         {
1147             Xapian::PostingIterator p, p_end;
1148             std::string term = *t;
1149
1150             p_end = notmuch->xapian_db->postlist_end (term);
1151
1152             for (p = notmuch->xapian_db->postlist_begin (term);
1153                  p != p_end;
1154                  p++)
1155             {
1156                 Xapian::Document document;
1157                 time_t mtime;
1158                 notmuch_directory_t *directory;
1159
1160                 if (do_progress_notify) {
1161                     progress_notify (closure, (double) count / total);
1162                     do_progress_notify = 0;
1163                 }
1164
1165                 document = find_document_for_doc_id (notmuch, *p);
1166                 mtime = Xapian::sortable_unserialise (
1167                     document.get_value (NOTMUCH_VALUE_TIMESTAMP));
1168
1169                 directory = _notmuch_directory_create (notmuch, term.c_str() + 10,
1170                                                        NOTMUCH_FIND_CREATE, &status);
1171                 notmuch_directory_set_mtime (directory, mtime);
1172                 notmuch_directory_destroy (directory);
1173             }
1174         }
1175     }
1176
1177     /*
1178      * Prior to version 2, the "folder:" prefix was probabilistic and
1179      * stemmed. Change it to the current boolean prefix. Add "path:"
1180      * prefixes while at it.
1181      */
1182     if (version < 2) {
1183         notmuch_query_t *query = notmuch_query_create (notmuch, "");
1184         notmuch_messages_t *messages;
1185         notmuch_message_t *message;
1186
1187         count = 0;
1188         total = notmuch_query_count_messages (query);
1189
1190         for (messages = notmuch_query_search_messages (query);
1191              notmuch_messages_valid (messages);
1192              notmuch_messages_move_to_next (messages)) {
1193             if (do_progress_notify) {
1194                 progress_notify (closure, (double) count / total);
1195                 do_progress_notify = 0;
1196             }
1197
1198             message = notmuch_messages_get (messages);
1199
1200             _notmuch_message_upgrade_folder (message);
1201             _notmuch_message_sync (message);
1202
1203             notmuch_message_destroy (message);
1204
1205             count++;
1206         }
1207
1208         notmuch_query_destroy (query);
1209     }
1210
1211     db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
1212     db->flush ();
1213
1214     /* Now that the upgrade is complete we can remove the old data
1215      * and documents that are no longer needed. */
1216     if (version < 1) {
1217         notmuch_query_t *query = notmuch_query_create (notmuch, "");
1218         notmuch_messages_t *messages;
1219         notmuch_message_t *message;
1220         char *filename;
1221
1222         for (messages = notmuch_query_search_messages (query);
1223              notmuch_messages_valid (messages);
1224              notmuch_messages_move_to_next (messages))
1225         {
1226             if (do_progress_notify) {
1227                 progress_notify (closure, (double) count / total);
1228                 do_progress_notify = 0;
1229             }
1230
1231             message = notmuch_messages_get (messages);
1232
1233             filename = _notmuch_message_talloc_copy_data (message);
1234             if (filename && *filename != '\0') {
1235                 _notmuch_message_clear_data (message);
1236                 _notmuch_message_sync (message);
1237             }
1238             talloc_free (filename);
1239
1240             notmuch_message_destroy (message);
1241         }
1242
1243         notmuch_query_destroy (query);
1244     }
1245
1246     if (version < 1) {
1247         Xapian::TermIterator t, t_end;
1248
1249         t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
1250
1251         for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
1252              t != t_end;
1253              t++)
1254         {
1255             Xapian::PostingIterator p, p_end;
1256             std::string term = *t;
1257
1258             p_end = notmuch->xapian_db->postlist_end (term);
1259
1260             for (p = notmuch->xapian_db->postlist_begin (term);
1261                  p != p_end;
1262                  p++)
1263             {
1264                 if (do_progress_notify) {
1265                     progress_notify (closure, (double) count / total);
1266                     do_progress_notify = 0;
1267                 }
1268
1269                 db->delete_document (*p);
1270             }
1271         }
1272     }
1273
1274     if (timer_is_active) {
1275         /* Now stop the timer. */
1276         timerval.it_interval.tv_sec = 0;
1277         timerval.it_interval.tv_usec = 0;
1278         timerval.it_value.tv_sec = 0;
1279         timerval.it_value.tv_usec = 0;
1280         setitimer (ITIMER_REAL, &timerval, NULL);
1281
1282         /* And disable the signal handler. */
1283         action.sa_handler = SIG_IGN;
1284         sigaction (SIGALRM, &action, NULL);
1285     }
1286
1287     return NOTMUCH_STATUS_SUCCESS;
1288 }
1289
1290 notmuch_status_t
1291 notmuch_database_begin_atomic (notmuch_database_t *notmuch)
1292 {
1293     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
1294         notmuch->atomic_nesting > 0)
1295         goto DONE;
1296
1297     try {
1298         (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
1299     } catch (const Xapian::Error &error) {
1300         fprintf (stderr, "A Xapian exception occurred beginning transaction: %s.\n",
1301                  error.get_msg().c_str());
1302         notmuch->exception_reported = TRUE;
1303         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1304     }
1305
1306 DONE:
1307     notmuch->atomic_nesting++;
1308     return NOTMUCH_STATUS_SUCCESS;
1309 }
1310
1311 notmuch_status_t
1312 notmuch_database_end_atomic (notmuch_database_t *notmuch)
1313 {
1314     Xapian::WritableDatabase *db;
1315
1316     if (notmuch->atomic_nesting == 0)
1317         return NOTMUCH_STATUS_UNBALANCED_ATOMIC;
1318
1319     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY ||
1320         notmuch->atomic_nesting > 1)
1321         goto DONE;
1322
1323     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1324     try {
1325         db->commit_transaction ();
1326
1327         /* This is a hack for testing.  Xapian never flushes on a
1328          * non-flushed commit, even if the flush threshold is 1.
1329          * However, we rely on flushing to test atomicity. */
1330         const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
1331         if (thresh && atoi (thresh) == 1)
1332             db->flush ();
1333     } catch (const Xapian::Error &error) {
1334         fprintf (stderr, "A Xapian exception occurred committing transaction: %s.\n",
1335                  error.get_msg().c_str());
1336         notmuch->exception_reported = TRUE;
1337         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1338     }
1339
1340 DONE:
1341     notmuch->atomic_nesting--;
1342     return NOTMUCH_STATUS_SUCCESS;
1343 }
1344
1345 /* We allow the user to use arbitrarily long paths for directories. But
1346  * we have a term-length limit. So if we exceed that, we'll use the
1347  * SHA-1 of the path for the database term.
1348  *
1349  * Note: This function may return the original value of 'path'. If it
1350  * does not, then the caller is responsible to free() the returned
1351  * value.
1352  */
1353 const char *
1354 _notmuch_database_get_directory_db_path (const char *path)
1355 {
1356     int term_len = strlen (_find_prefix ("directory")) + strlen (path);
1357
1358     if (term_len > NOTMUCH_TERM_MAX)
1359         return notmuch_sha1_of_string (path);
1360     else
1361         return path;
1362 }
1363
1364 /* Given a path, split it into two parts: the directory part is all
1365  * components except for the last, and the basename is that last
1366  * component. Getting the return-value for either part is optional
1367  * (the caller can pass NULL).
1368  *
1369  * The original 'path' can represent either a regular file or a
1370  * directory---the splitting will be carried out in the same way in
1371  * either case. Trailing slashes on 'path' will be ignored, and any
1372  * cases of multiple '/' characters appearing in series will be
1373  * treated as a single '/'.
1374  *
1375  * Allocation (if any) will have 'ctx' as the talloc owner. But
1376  * pointers will be returned within the original path string whenever
1377  * possible.
1378  *
1379  * Note: If 'path' is non-empty and contains no non-trailing slash,
1380  * (that is, consists of a filename with no parent directory), then
1381  * the directory returned will be an empty string. However, if 'path'
1382  * is an empty string, then both directory and basename will be
1383  * returned as NULL.
1384  */
1385 notmuch_status_t
1386 _notmuch_database_split_path (void *ctx,
1387                               const char *path,
1388                               const char **directory,
1389                               const char **basename)
1390 {
1391     const char *slash;
1392
1393     if (path == NULL || *path == '\0') {
1394         if (directory)
1395             *directory = NULL;
1396         if (basename)
1397             *basename = NULL;
1398         return NOTMUCH_STATUS_SUCCESS;
1399     }
1400
1401     /* Find the last slash (not counting a trailing slash), if any. */
1402
1403     slash = path + strlen (path) - 1;
1404
1405     /* First, skip trailing slashes. */
1406     while (slash != path) {
1407         if (*slash != '/')
1408             break;
1409
1410         --slash;
1411     }
1412
1413     /* Then, find a slash. */
1414     while (slash != path) {
1415         if (*slash == '/')
1416             break;
1417
1418         if (basename)
1419             *basename = slash;
1420
1421         --slash;
1422     }
1423
1424     /* Finally, skip multiple slashes. */
1425     while (slash != path) {
1426         if (*slash != '/')
1427             break;
1428
1429         --slash;
1430     }
1431
1432     if (slash == path) {
1433         if (directory)
1434             *directory = talloc_strdup (ctx, "");
1435         if (basename)
1436             *basename = path;
1437     } else {
1438         if (directory)
1439             *directory = talloc_strndup (ctx, path, slash - path + 1);
1440     }
1441
1442     return NOTMUCH_STATUS_SUCCESS;
1443 }
1444
1445 /* Find the document ID of the specified directory.
1446  *
1447  * If (flags & NOTMUCH_FIND_CREATE), a new directory document will be
1448  * created if one does not exist for 'path'.  Otherwise, if the
1449  * directory document does not exist, this sets *directory_id to
1450  * ((unsigned int)-1) and returns NOTMUCH_STATUS_SUCCESS.
1451  */
1452 notmuch_status_t
1453 _notmuch_database_find_directory_id (notmuch_database_t *notmuch,
1454                                      const char *path,
1455                                      notmuch_find_flags_t flags,
1456                                      unsigned int *directory_id)
1457 {
1458     notmuch_directory_t *directory;
1459     notmuch_status_t status;
1460
1461     if (path == NULL) {
1462         *directory_id = 0;
1463         return NOTMUCH_STATUS_SUCCESS;
1464     }
1465
1466     directory = _notmuch_directory_create (notmuch, path, flags, &status);
1467     if (status || !directory) {
1468         *directory_id = -1;
1469         return status;
1470     }
1471
1472     *directory_id = _notmuch_directory_get_document_id (directory);
1473
1474     notmuch_directory_destroy (directory);
1475
1476     return NOTMUCH_STATUS_SUCCESS;
1477 }
1478
1479 const char *
1480 _notmuch_database_get_directory_path (void *ctx,
1481                                       notmuch_database_t *notmuch,
1482                                       unsigned int doc_id)
1483 {
1484     Xapian::Document document;
1485
1486     document = find_document_for_doc_id (notmuch, doc_id);
1487
1488     return talloc_strdup (ctx, document.get_data ().c_str ());
1489 }
1490
1491 /* Given a legal 'filename' for the database, (either relative to
1492  * database path or absolute with initial components identical to
1493  * database path), return a new string (with 'ctx' as the talloc
1494  * owner) suitable for use as a direntry term value.
1495  *
1496  * If (flags & NOTMUCH_FIND_CREATE), the necessary directory documents
1497  * will be created in the database as needed.  Otherwise, if the
1498  * necessary directory documents do not exist, this sets
1499  * *direntry to NULL and returns NOTMUCH_STATUS_SUCCESS.
1500  */
1501 notmuch_status_t
1502 _notmuch_database_filename_to_direntry (void *ctx,
1503                                         notmuch_database_t *notmuch,
1504                                         const char *filename,
1505                                         notmuch_find_flags_t flags,
1506                                         char **direntry)
1507 {
1508     const char *relative, *directory, *basename;
1509     Xapian::docid directory_id;
1510     notmuch_status_t status;
1511
1512     relative = _notmuch_database_relative_path (notmuch, filename);
1513
1514     status = _notmuch_database_split_path (ctx, relative,
1515                                            &directory, &basename);
1516     if (status)
1517         return status;
1518
1519     status = _notmuch_database_find_directory_id (notmuch, directory, flags,
1520                                                   &directory_id);
1521     if (status || directory_id == (unsigned int)-1) {
1522         *direntry = NULL;
1523         return status;
1524     }
1525
1526     *direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
1527
1528     return NOTMUCH_STATUS_SUCCESS;
1529 }
1530
1531 /* Given a legal 'path' for the database, return the relative path.
1532  *
1533  * The return value will be a pointer to the original path contents,
1534  * and will be either the original string (if 'path' was relative) or
1535  * a portion of the string (if path was absolute and begins with the
1536  * database path).
1537  */
1538 const char *
1539 _notmuch_database_relative_path (notmuch_database_t *notmuch,
1540                                  const char *path)
1541 {
1542     const char *db_path, *relative;
1543     unsigned int db_path_len;
1544
1545     db_path = notmuch_database_get_path (notmuch);
1546     db_path_len = strlen (db_path);
1547
1548     relative = path;
1549
1550     if (*relative == '/') {
1551         while (*relative == '/' && *(relative+1) == '/')
1552             relative++;
1553
1554         if (strncmp (relative, db_path, db_path_len) == 0)
1555         {
1556             relative += db_path_len;
1557             while (*relative == '/')
1558                 relative++;
1559         }
1560     }
1561
1562     return relative;
1563 }
1564
1565 notmuch_status_t
1566 notmuch_database_get_directory (notmuch_database_t *notmuch,
1567                                 const char *path,
1568                                 notmuch_directory_t **directory)
1569 {
1570     notmuch_status_t status;
1571
1572     if (directory == NULL)
1573         return NOTMUCH_STATUS_NULL_POINTER;
1574     *directory = NULL;
1575
1576     try {
1577         *directory = _notmuch_directory_create (notmuch, path,
1578                                                 NOTMUCH_FIND_LOOKUP, &status);
1579     } catch (const Xapian::Error &error) {
1580         fprintf (stderr, "A Xapian exception occurred getting directory: %s.\n",
1581                  error.get_msg().c_str());
1582         notmuch->exception_reported = TRUE;
1583         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
1584     }
1585     return status;
1586 }
1587
1588 /* Allocate a document ID that satisfies the following criteria:
1589  *
1590  * 1. The ID does not exist for any document in the Xapian database
1591  *
1592  * 2. The ID was not previously returned from this function
1593  *
1594  * 3. The ID is the smallest integer satisfying (1) and (2)
1595  *
1596  * This function will trigger an internal error if these constraints
1597  * cannot all be satisfied, (that is, the pool of available document
1598  * IDs has been exhausted).
1599  */
1600 unsigned int
1601 _notmuch_database_generate_doc_id (notmuch_database_t *notmuch)
1602 {
1603     assert (notmuch->last_doc_id >= notmuch->xapian_db->get_lastdocid ());
1604
1605     notmuch->last_doc_id++;
1606
1607     if (notmuch->last_doc_id == 0)
1608         INTERNAL_ERROR ("Xapian document IDs are exhausted.\n");
1609
1610     return notmuch->last_doc_id;
1611 }
1612
1613 static const char *
1614 _notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
1615 {
1616     /* 16 bytes (+ terminator) for hexadecimal representation of
1617      * a 64-bit integer. */
1618     static char thread_id[17];
1619     Xapian::WritableDatabase *db;
1620
1621     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1622
1623     notmuch->last_thread_id++;
1624
1625     sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
1626
1627     db->set_metadata ("last_thread_id", thread_id);
1628
1629     return thread_id;
1630 }
1631
1632 static char *
1633 _get_metadata_thread_id_key (void *ctx, const char *message_id)
1634 {
1635     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
1636         message_id = _message_id_compressed (ctx, message_id);
1637
1638     return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
1639                             message_id);
1640 }
1641
1642 /* Find the thread ID to which the message with 'message_id' belongs.
1643  *
1644  * Note: 'thread_id_ret' must not be NULL!
1645  * On success '*thread_id_ret' is set to a newly talloced string belonging to
1646  * 'ctx'.
1647  *
1648  * Note: If there is no message in the database with the given
1649  * 'message_id' then a new thread_id will be allocated for this
1650  * message and stored in the database metadata, (where this same
1651  * thread ID can be looked up if the message is added to the database
1652  * later).
1653  */
1654 static notmuch_status_t
1655 _resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
1656                                   void *ctx,
1657                                   const char *message_id,
1658                                   const char **thread_id_ret)
1659 {
1660     notmuch_status_t status;
1661     notmuch_message_t *message;
1662     string thread_id_string;
1663     char *metadata_key;
1664     Xapian::WritableDatabase *db;
1665
1666     status = notmuch_database_find_message (notmuch, message_id, &message);
1667
1668     if (status)
1669         return status;
1670
1671     if (message) {
1672         *thread_id_ret = talloc_steal (ctx,
1673                                        notmuch_message_get_thread_id (message));
1674
1675         notmuch_message_destroy (message);
1676
1677         return NOTMUCH_STATUS_SUCCESS;
1678     }
1679
1680     /* Message has not been seen yet.
1681      *
1682      * We may have seen a reference to it already, in which case, we
1683      * can return the thread ID stored in the metadata. Otherwise, we
1684      * generate a new thread ID and store it there.
1685      */
1686     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1687     metadata_key = _get_metadata_thread_id_key (ctx, message_id);
1688     thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
1689
1690     if (thread_id_string.empty()) {
1691         *thread_id_ret = talloc_strdup (ctx,
1692                                         _notmuch_database_generate_thread_id (notmuch));
1693         db->set_metadata (metadata_key, *thread_id_ret);
1694     } else {
1695         *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str());
1696     }
1697
1698     talloc_free (metadata_key);
1699
1700     return NOTMUCH_STATUS_SUCCESS;
1701 }
1702
1703 static notmuch_status_t
1704 _merge_threads (notmuch_database_t *notmuch,
1705                 const char *winner_thread_id,
1706                 const char *loser_thread_id)
1707 {
1708     Xapian::PostingIterator loser, loser_end;
1709     notmuch_message_t *message = NULL;
1710     notmuch_private_status_t private_status;
1711     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1712
1713     find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
1714
1715     for ( ; loser != loser_end; loser++) {
1716         message = _notmuch_message_create (notmuch, notmuch,
1717                                            *loser, &private_status);
1718         if (message == NULL) {
1719             ret = COERCE_STATUS (private_status,
1720                                  "Cannot find document for doc_id from query");
1721             goto DONE;
1722         }
1723
1724         _notmuch_message_remove_term (message, "thread", loser_thread_id);
1725         _notmuch_message_add_term (message, "thread", winner_thread_id);
1726         _notmuch_message_sync (message);
1727
1728         notmuch_message_destroy (message);
1729         message = NULL;
1730     }
1731
1732   DONE:
1733     if (message)
1734         notmuch_message_destroy (message);
1735
1736     return ret;
1737 }
1738
1739 static void
1740 _my_talloc_free_for_g_hash (void *ptr)
1741 {
1742     talloc_free (ptr);
1743 }
1744
1745 static notmuch_status_t
1746 _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
1747                                            notmuch_message_t *message,
1748                                            notmuch_message_file_t *message_file,
1749                                            const char **thread_id)
1750 {
1751     GHashTable *parents = NULL;
1752     const char *refs, *in_reply_to, *in_reply_to_message_id;
1753     const char *last_ref_message_id, *this_message_id;
1754     GList *l, *keys = NULL;
1755     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1756
1757     parents = g_hash_table_new_full (g_str_hash, g_str_equal,
1758                                      _my_talloc_free_for_g_hash, NULL);
1759     this_message_id = notmuch_message_get_message_id (message);
1760
1761     refs = notmuch_message_file_get_header (message_file, "references");
1762     last_ref_message_id = parse_references (message,
1763                                             this_message_id,
1764                                             parents, refs);
1765
1766     in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
1767     in_reply_to_message_id = parse_references (message,
1768                                                this_message_id,
1769                                                parents, in_reply_to);
1770
1771     /* For the parent of this message, use the last message ID of the
1772      * References header, if available.  If not, fall back to the
1773      * first message ID in the In-Reply-To header. */
1774     if (last_ref_message_id) {
1775         _notmuch_message_add_term (message, "replyto",
1776                                    last_ref_message_id);
1777     } else if (in_reply_to_message_id) {
1778         _notmuch_message_add_term (message, "replyto",
1779                              in_reply_to_message_id);
1780     }
1781
1782     keys = g_hash_table_get_keys (parents);
1783     for (l = keys; l; l = l->next) {
1784         char *parent_message_id;
1785         const char *parent_thread_id = NULL;
1786
1787         parent_message_id = (char *) l->data;
1788
1789         _notmuch_message_add_term (message, "reference",
1790                                    parent_message_id);
1791
1792         ret = _resolve_message_id_to_thread_id (notmuch,
1793                                                 message,
1794                                                 parent_message_id,
1795                                                 &parent_thread_id);
1796         if (ret)
1797             goto DONE;
1798
1799         if (*thread_id == NULL) {
1800             *thread_id = talloc_strdup (message, parent_thread_id);
1801             _notmuch_message_add_term (message, "thread", *thread_id);
1802         } else if (strcmp (*thread_id, parent_thread_id)) {
1803             ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
1804             if (ret)
1805                 goto DONE;
1806         }
1807     }
1808
1809   DONE:
1810     if (keys)
1811         g_list_free (keys);
1812     if (parents)
1813         g_hash_table_unref (parents);
1814
1815     return ret;
1816 }
1817
1818 static notmuch_status_t
1819 _notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
1820                                             notmuch_message_t *message,
1821                                             const char **thread_id)
1822 {
1823     const char *message_id = notmuch_message_get_message_id (message);
1824     Xapian::PostingIterator child, children_end;
1825     notmuch_message_t *child_message = NULL;
1826     const char *child_thread_id;
1827     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1828     notmuch_private_status_t private_status;
1829
1830     find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
1831
1832     for ( ; child != children_end; child++) {
1833
1834         child_message = _notmuch_message_create (message, notmuch,
1835                                                  *child, &private_status);
1836         if (child_message == NULL) {
1837             ret = COERCE_STATUS (private_status,
1838                                  "Cannot find document for doc_id from query");
1839             goto DONE;
1840         }
1841
1842         child_thread_id = notmuch_message_get_thread_id (child_message);
1843         if (*thread_id == NULL) {
1844             *thread_id = talloc_strdup (message, child_thread_id);
1845             _notmuch_message_add_term (message, "thread", *thread_id);
1846         } else if (strcmp (*thread_id, child_thread_id)) {
1847             _notmuch_message_remove_term (child_message, "reference",
1848                                           message_id);
1849             _notmuch_message_sync (child_message);
1850             ret = _merge_threads (notmuch, *thread_id, child_thread_id);
1851             if (ret)
1852                 goto DONE;
1853         }
1854
1855         notmuch_message_destroy (child_message);
1856         child_message = NULL;
1857     }
1858
1859   DONE:
1860     if (child_message)
1861         notmuch_message_destroy (child_message);
1862
1863     return ret;
1864 }
1865
1866 /* Given a (mostly empty) 'message' and its corresponding
1867  * 'message_file' link it to existing threads in the database.
1868  *
1869  * The first check is in the metadata of the database to see if we
1870  * have pre-allocated a thread_id in advance for this message, (which
1871  * would have happened if a message was previously added that
1872  * referenced this one).
1873  *
1874  * Second, we look at 'message_file' and its link-relevant headers
1875  * (References and In-Reply-To) for message IDs.
1876  *
1877  * Finally, we look in the database for existing message that
1878  * reference 'message'.
1879  *
1880  * In all cases, we assign to the current message the first thread_id
1881  * found (through either parent or child). We will also merge any
1882  * existing, distinct threads where this message belongs to both,
1883  * (which is not uncommon when messages are processed out of order).
1884  *
1885  * Finally, if no thread ID has been found through parent or child, we
1886  * call _notmuch_message_generate_thread_id to generate a new thread
1887  * ID. This should only happen for new, top-level messages, (no
1888  * References or In-Reply-To header in this message, and no previously
1889  * added message refers to this message).
1890  */
1891 static notmuch_status_t
1892 _notmuch_database_link_message (notmuch_database_t *notmuch,
1893                                 notmuch_message_t *message,
1894                                 notmuch_message_file_t *message_file)
1895 {
1896     notmuch_status_t status;
1897     const char *message_id, *thread_id = NULL;
1898     char *metadata_key;
1899     string stored_id;
1900
1901     message_id = notmuch_message_get_message_id (message);
1902     metadata_key = _get_metadata_thread_id_key (message, message_id);
1903
1904     /* Check if we have already seen related messages to this one.
1905      * If we have then use the thread_id that we stored at that time.
1906      */
1907     stored_id = notmuch->xapian_db->get_metadata (metadata_key);
1908     if (! stored_id.empty()) {
1909         Xapian::WritableDatabase *db;
1910
1911         db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1912
1913         /* Clear the metadata for this message ID. We don't need it
1914          * anymore. */
1915         db->set_metadata (metadata_key, "");
1916         thread_id = stored_id.c_str();
1917
1918         _notmuch_message_add_term (message, "thread", thread_id);
1919     }
1920     talloc_free (metadata_key);
1921
1922     status = _notmuch_database_link_message_to_parents (notmuch, message,
1923                                                         message_file,
1924                                                         &thread_id);
1925     if (status)
1926         return status;
1927
1928     status = _notmuch_database_link_message_to_children (notmuch, message,
1929                                                          &thread_id);
1930     if (status)
1931         return status;
1932
1933     /* If not part of any existing thread, generate a new thread ID. */
1934     if (thread_id == NULL) {
1935         thread_id = _notmuch_database_generate_thread_id (notmuch);
1936
1937         _notmuch_message_add_term (message, "thread", thread_id);
1938     }
1939
1940     return NOTMUCH_STATUS_SUCCESS;
1941 }
1942
1943 notmuch_status_t
1944 notmuch_database_add_message (notmuch_database_t *notmuch,
1945                               const char *filename,
1946                               notmuch_message_t **message_ret)
1947 {
1948     notmuch_message_file_t *message_file;
1949     notmuch_message_t *message = NULL;
1950     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
1951     notmuch_private_status_t private_status;
1952
1953     const char *date, *header;
1954     const char *from, *to, *subject;
1955     char *message_id = NULL;
1956
1957     if (message_ret)
1958         *message_ret = NULL;
1959
1960     ret = _notmuch_database_ensure_writable (notmuch);
1961     if (ret)
1962         return ret;
1963
1964     message_file = notmuch_message_file_open (filename);
1965     if (message_file == NULL)
1966         return NOTMUCH_STATUS_FILE_ERROR;
1967
1968     /* Adding a message may change many documents.  Do this all
1969      * atomically. */
1970     ret = notmuch_database_begin_atomic (notmuch);
1971     if (ret)
1972         goto DONE;
1973
1974     /* Parse message up front to get better error status. */
1975     ret = _notmuch_message_file_parse (message_file);
1976     if (ret)
1977         goto DONE;
1978
1979     try {
1980         /* Before we do any real work, (especially before doing a
1981          * potential SHA-1 computation on the entire file's contents),
1982          * let's make sure that what we're looking at looks like an
1983          * actual email message.
1984          */
1985         from = notmuch_message_file_get_header (message_file, "from");
1986         subject = notmuch_message_file_get_header (message_file, "subject");
1987         to = notmuch_message_file_get_header (message_file, "to");
1988
1989         if ((from == NULL || *from == '\0') &&
1990             (subject == NULL || *subject == '\0') &&
1991             (to == NULL || *to == '\0'))
1992         {
1993             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
1994             goto DONE;
1995         }
1996
1997         /* Now that we're sure it's mail, the first order of business
1998          * is to find a message ID (or else create one ourselves). */
1999
2000         header = notmuch_message_file_get_header (message_file, "message-id");
2001         if (header && *header != '\0') {
2002             message_id = _parse_message_id (message_file, header, NULL);
2003
2004             /* So the header value isn't RFC-compliant, but it's
2005              * better than no message-id at all. */
2006             if (message_id == NULL)
2007                 message_id = talloc_strdup (message_file, header);
2008
2009             /* If a message ID is too long, substitute its sha1 instead. */
2010             if (message_id && strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) {
2011                 char *compressed = _message_id_compressed (message_file,
2012                                                            message_id);
2013                 talloc_free (message_id);
2014                 message_id = compressed;
2015             }
2016         }
2017
2018         if (message_id == NULL ) {
2019             /* No message-id at all, let's generate one by taking a
2020              * hash over the file's contents. */
2021             char *sha1 = notmuch_sha1_of_file (filename);
2022
2023             /* If that failed too, something is really wrong. Give up. */
2024             if (sha1 == NULL) {
2025                 ret = NOTMUCH_STATUS_FILE_ERROR;
2026                 goto DONE;
2027             }
2028
2029             message_id = talloc_asprintf (message_file,
2030                                           "notmuch-sha1-%s", sha1);
2031             free (sha1);
2032         }
2033
2034         /* Now that we have a message ID, we get a message object,
2035          * (which may or may not reference an existing document in the
2036          * database). */
2037
2038         message = _notmuch_message_create_for_message_id (notmuch,
2039                                                           message_id,
2040                                                           &private_status);
2041
2042         talloc_free (message_id);
2043
2044         if (message == NULL) {
2045             ret = COERCE_STATUS (private_status,
2046                                  "Unexpected status value from _notmuch_message_create_for_message_id");
2047             goto DONE;
2048         }
2049
2050         _notmuch_message_add_filename (message, filename);
2051
2052         /* Is this a newly created message object? */
2053         if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
2054             _notmuch_message_add_term (message, "type", "mail");
2055
2056             ret = _notmuch_database_link_message (notmuch, message,
2057                                                   message_file);
2058             if (ret)
2059                 goto DONE;
2060
2061             date = notmuch_message_file_get_header (message_file, "date");
2062             _notmuch_message_set_header_values (message, date, from, subject);
2063
2064             ret = _notmuch_message_index_file (message, message_file);
2065             if (ret)
2066                 goto DONE;
2067         } else {
2068             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
2069         }
2070
2071         _notmuch_message_sync (message);
2072     } catch (const Xapian::Error &error) {
2073         fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
2074                  error.get_msg().c_str());
2075         notmuch->exception_reported = TRUE;
2076         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
2077         goto DONE;
2078     }
2079
2080   DONE:
2081     if (message) {
2082         if ((ret == NOTMUCH_STATUS_SUCCESS ||
2083              ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
2084             *message_ret = message;
2085         else
2086             notmuch_message_destroy (message);
2087     }
2088
2089     if (message_file)
2090         notmuch_message_file_close (message_file);
2091
2092     ret2 = notmuch_database_end_atomic (notmuch);
2093     if ((ret == NOTMUCH_STATUS_SUCCESS ||
2094          ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
2095         ret2 != NOTMUCH_STATUS_SUCCESS)
2096         ret = ret2;
2097
2098     return ret;
2099 }
2100
2101 notmuch_status_t
2102 notmuch_database_remove_message (notmuch_database_t *notmuch,
2103                                  const char *filename)
2104 {
2105     notmuch_status_t status;
2106     notmuch_message_t *message;
2107
2108     status = notmuch_database_find_message_by_filename (notmuch, filename,
2109                                                         &message);
2110
2111     if (status == NOTMUCH_STATUS_SUCCESS && message) {
2112             status = _notmuch_message_remove_filename (message, filename);
2113             if (status == NOTMUCH_STATUS_SUCCESS)
2114                 _notmuch_message_delete (message);
2115             else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
2116                 _notmuch_message_sync (message);
2117
2118             notmuch_message_destroy (message);
2119     }
2120
2121     return status;
2122 }
2123
2124 notmuch_status_t
2125 notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
2126                                            const char *filename,
2127                                            notmuch_message_t **message_ret)
2128 {
2129     void *local;
2130     const char *prefix = _find_prefix ("file-direntry");
2131     char *direntry, *term;
2132     Xapian::PostingIterator i, end;
2133     notmuch_status_t status;
2134
2135     if (message_ret == NULL)
2136         return NOTMUCH_STATUS_NULL_POINTER;
2137
2138     /* return NULL on any failure */
2139     *message_ret = NULL;
2140
2141     local = talloc_new (notmuch);
2142
2143     try {
2144         status = _notmuch_database_filename_to_direntry (
2145             local, notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
2146         if (status || !direntry)
2147             goto DONE;
2148
2149         term = talloc_asprintf (local, "%s%s", prefix, direntry);
2150
2151         find_doc_ids_for_term (notmuch, term, &i, &end);
2152
2153         if (i != end) {
2154             notmuch_private_status_t private_status;
2155
2156             *message_ret = _notmuch_message_create (notmuch, notmuch, *i,
2157                                                     &private_status);
2158             if (*message_ret == NULL)
2159                 status = NOTMUCH_STATUS_OUT_OF_MEMORY;
2160         }
2161     } catch (const Xapian::Error &error) {
2162         fprintf (stderr, "Error: A Xapian exception occurred finding message by filename: %s\n",
2163                  error.get_msg().c_str());
2164         notmuch->exception_reported = TRUE;
2165         status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
2166     }
2167
2168   DONE:
2169     talloc_free (local);
2170
2171     if (status && *message_ret) {
2172         notmuch_message_destroy (*message_ret);
2173         *message_ret = NULL;
2174     }
2175     return status;
2176 }
2177
2178 notmuch_string_list_t *
2179 _notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i,
2180                                          Xapian::TermIterator &end,
2181                                          const char *prefix)
2182 {
2183     int prefix_len = strlen (prefix);
2184     notmuch_string_list_t *list;
2185
2186     list = _notmuch_string_list_create (ctx);
2187     if (unlikely (list == NULL))
2188         return NULL;
2189
2190     for (i.skip_to (prefix); i != end; i++) {
2191         /* Terminate loop at first term without desired prefix. */
2192         if (strncmp ((*i).c_str (), prefix, prefix_len))
2193             break;
2194
2195         _notmuch_string_list_append (list, (*i).c_str () + prefix_len);
2196     }
2197
2198     return list;
2199 }
2200
2201 notmuch_tags_t *
2202 notmuch_database_get_all_tags (notmuch_database_t *db)
2203 {
2204     Xapian::TermIterator i, end;
2205     notmuch_string_list_t *tags;
2206
2207     try {
2208         i = db->xapian_db->allterms_begin();
2209         end = db->xapian_db->allterms_end();
2210         tags = _notmuch_database_get_terms_with_prefix (db, i, end,
2211                                                         _find_prefix ("tag"));
2212         _notmuch_string_list_sort (tags);
2213         return _notmuch_tags_create (db, tags);
2214     } catch (const Xapian::Error &error) {
2215         fprintf (stderr, "A Xapian exception occurred getting tags: %s.\n",
2216                  error.get_msg().c_str());
2217         db->exception_reported = TRUE;
2218         return NULL;
2219     }
2220 }