]> git.notmuchmail.org Git - notmuch/blob - database.cc
Shuffle the value numbers around in the database.
[notmuch] / database.cc
1 /* database.cc - The database interfaces of the notmuch mail library
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "database-private.h"
22
23 #include <iostream>
24
25 #include <xapian.h>
26
27 #include <glib.h> /* g_strdup_printf, g_free, GPtrArray, GHashTable */
28
29 using namespace std;
30
31 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
32
33 typedef struct {
34     const char *name;
35     const char *prefix;
36 } prefix_t;
37
38 /* With these prefix values we follow the conventions published here:
39  *
40  * http://xapian.org/docs/omega/termprefixes.html
41  *
42  * as much as makes sense. Note that I took some liberty in matching
43  * the reserved prefix values to notmuch concepts, (for example, 'G'
44  * is documented as "newsGroup (or similar entity - e.g. a web forum
45  * name)", for which I think the thread is the closest analogue in
46  * notmuch. This in spite of the fact that we will eventually be
47  * storing mailing-list messages where 'G' for "mailing list name"
48  * might be even a closer analogue. I'm treating the single-character
49  * prefixes preferentially for core notmuch concepts (which will be
50  * nearly universal to all mail messages).
51  */
52
53 prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
54     { "type", "T" },
55     { "thread", "G" },
56     { "ref", "XREFERENCE" },
57     { "timestamp", "XTIMESTAMP" },
58 };
59
60 prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
61     { "tag", "K" },
62     { "id", "Q" }
63 };
64
65 const char *
66 _find_prefix (const char *name)
67 {
68     unsigned int i;
69
70     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++)
71         if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
72             return BOOLEAN_PREFIX_INTERNAL[i].prefix;
73
74     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++)
75         if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
76             return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
77
78     fprintf (stderr, "Internal error: No prefix exists for '%s'\n", name);
79     exit (1);
80
81     return "";
82 }
83
84 const char *
85 notmuch_status_to_string (notmuch_status_t status)
86 {
87     switch (status) {
88     case NOTMUCH_STATUS_SUCCESS:
89         return "No error occurred";
90     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
91         return "A Xapian exception occurred";
92     case NOTMUCH_STATUS_FILE_ERROR:
93         return "Something went wrong trying to read or write a file";
94     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
95         return "File is not an email";
96     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
97         return "Message ID is identical to a message in database";
98     case NOTMUCH_STATUS_NULL_POINTER:
99         return "Erroneous NULL pointer";
100     case NOTMUCH_STATUS_TAG_TOO_LONG:
101         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
102     default:
103     case NOTMUCH_STATUS_LAST_STATUS:
104         return "Unknown error status value";
105     }
106 }
107
108 /* XXX: We should drop this function and convert all callers to call
109  * _notmuch_message_add_term instead. */
110 static void
111 add_term (Xapian::Document doc,
112           const char *prefix_name,
113           const char *value)
114 {
115     const char *prefix;
116     char *term;
117
118     if (value == NULL)
119         return;
120
121     prefix = _find_prefix (prefix_name);
122
123     term = g_strdup_printf ("%s%s", prefix, value);
124
125     if (strlen (term) <= NOTMUCH_TERM_MAX)
126         doc.add_term (term);
127
128     g_free (term);
129 }
130
131 static void
132 find_doc_ids (notmuch_database_t *notmuch,
133               const char *prefix_name,
134               const char *value,
135               Xapian::PostingIterator *begin,
136               Xapian::PostingIterator *end)
137 {
138     Xapian::PostingIterator i;
139     char *term;
140
141     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
142
143     *begin = notmuch->xapian_db->postlist_begin (term);
144
145     *end = notmuch->xapian_db->postlist_end (term);
146
147     free (term);
148 }
149
150 static notmuch_private_status_t
151 find_unique_doc_id (notmuch_database_t *notmuch,
152                     const char *prefix_name,
153                     const char *value,
154                     unsigned int *doc_id)
155 {
156     Xapian::PostingIterator i, end;
157
158     find_doc_ids (notmuch, prefix_name, value, &i, &end);
159
160     if (i == end) {
161         *doc_id = 0;
162         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
163     } else {
164         *doc_id = *i;
165         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
166     }
167 }
168
169 static Xapian::Document
170 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
171 {
172     return notmuch->xapian_db->get_document (doc_id);
173 }
174
175 static notmuch_private_status_t
176 find_unique_document (notmuch_database_t *notmuch,
177                       const char *prefix_name,
178                       const char *value,
179                       Xapian::Document *document,
180                       unsigned int *doc_id)
181 {
182     notmuch_private_status_t status;
183
184     status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
185
186     if (status) {
187         *document = Xapian::Document ();
188         return status;
189     }
190
191     *document = find_document_for_doc_id (notmuch, *doc_id);
192     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
193 }
194
195 static void
196 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
197 {
198     string value_string;
199     const char *value, *id, *comma;
200
201     value_string = doc.get_value (NOTMUCH_VALUE_THREAD);
202     value = value_string.c_str();
203     if (strlen (value)) {
204         id = value;
205         while (*id) {
206             comma = strchr (id, ',');
207             if (comma == NULL)
208                 comma = id + strlen (id);
209             g_hash_table_insert (thread_ids,
210                                  strndup (id, comma - id), NULL);
211             id = comma;
212             if (*id)
213                 id++;
214         }
215     }
216 }
217
218 notmuch_message_t *
219 notmuch_database_find_message (notmuch_database_t *notmuch,
220                                const char *message_id)
221 {
222     notmuch_private_status_t status;
223     unsigned int doc_id;
224
225     status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
226
227     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
228         return NULL;
229
230     return _notmuch_message_create (notmuch, notmuch, doc_id);
231 }
232
233 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
234  * given message based on looking into the database for any messages
235  * referenced in parents, and also for any messages in the database
236  * referencing message_id.
237  *
238  * Caller should free all strings in the array and the array itself,
239  * (g_ptr_array_free) when done. */
240 static GPtrArray *
241 find_thread_ids (notmuch_database_t *notmuch,
242                  GPtrArray *parents,
243                  const char *message_id)
244 {
245     Xapian::PostingIterator child, children_end;
246     Xapian::Document doc;
247     GHashTable *thread_ids;
248     GList *keys, *l;
249     unsigned int i;
250     const char *parent_message_id;
251     GPtrArray *result;
252
253     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
254                                         free, NULL);
255
256     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
257     for ( ; child != children_end; child++) {
258         doc = find_document_for_doc_id (notmuch, *child);
259         insert_thread_id (thread_ids, doc);
260     }
261
262     for (i = 0; i < parents->len; i++) {
263         notmuch_message_t *parent;
264         notmuch_thread_ids_t *ids;
265
266         parent_message_id = (char *) g_ptr_array_index (parents, i);
267         parent = notmuch_database_find_message (notmuch, parent_message_id);
268         if (parent == NULL)
269             continue;
270
271         for (ids = notmuch_message_get_thread_ids (parent);
272              notmuch_thread_ids_has_more (ids);
273              notmuch_thread_ids_advance (ids))
274         {
275             const char *id;
276
277             id = notmuch_thread_ids_get (ids);
278             g_hash_table_insert (thread_ids, strdup (id), NULL);
279         }
280
281         notmuch_message_destroy (parent);
282     }
283
284     result = g_ptr_array_new ();
285
286     keys = g_hash_table_get_keys (thread_ids);
287     for (l = keys; l; l = l->next) {
288         char *id = (char *) l->data;
289         g_ptr_array_add (result, id);
290     }
291     g_list_free (keys);
292
293     /* We're done with the hash table, but we've taken the pointers to
294      * the allocated strings and put them into our result array, so
295      * tell the hash not to free them on its way out. */
296     g_hash_table_steal_all (thread_ids);
297     g_hash_table_unref (thread_ids);
298
299     return result;
300 }
301
302 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
303  * a (potentially nested) parenthesized sequence with '\' used to
304  * escape any character (including parentheses).
305  *
306  * If the sequence to be skipped continues to the end of the string,
307  * then 'str' will be left pointing at the final terminating '\0'
308  * character.
309  */
310 static void
311 skip_space_and_comments (const char **str)
312 {
313     const char *s;
314
315     s = *str;
316     while (*s && (isspace (*s) || *s == '(')) {
317         while (*s && isspace (*s))
318             s++;
319         if (*s == '(') {
320             int nesting = 1;
321             s++;
322             while (*s && nesting) {
323                 if (*s == '(')
324                     nesting++;
325                 else if (*s == ')')
326                     nesting--;
327                 else if (*s == '\\')
328                     if (*(s+1))
329                         s++;
330                 s++;
331             }
332         }
333     }
334
335     *str = s;
336 }
337
338 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
339  * comments, and the '<' and '>' delimeters.
340  *
341  * If not NULL, then *next will be made to point to the first character
342  * not parsed, (possibly pointing to the final '\0' terminator.
343  *
344  * Returns a newly allocated string which the caller should free()
345  * when done with it.
346  *
347  * Returns NULL if there is any error parsing the message-id. */
348 static char *
349 parse_message_id (const char *message_id, const char **next)
350 {
351     const char *s, *end;
352     char *result;
353
354     if (message_id == NULL)
355         return NULL;
356
357     s = message_id;
358
359     skip_space_and_comments (&s);
360
361     /* Skip any unstructured text as well. */
362     while (*s && *s != '<')
363         s++;
364
365     if (*s == '<') {
366         s++;
367     } else {
368         if (next)
369             *next = s;
370         return NULL;
371     }
372
373     skip_space_and_comments (&s);
374
375     end = s;
376     while (*end && *end != '>')
377         end++;
378     if (next) {
379         if (*end)
380             *next = end + 1;
381         else
382             *next = end;
383     }
384
385     if (end > s && *end == '>')
386         end--;
387     if (end <= s)
388         return NULL;
389
390     result = strndup (s, end - s + 1);
391
392     /* Finally, collapse any whitespace that is within the message-id
393      * itself. */
394     {
395         char *r;
396         int len;
397
398         for (r = result, len = strlen (r); *r; r++, len--)
399             if (*r == ' ' || *r == '\t')
400                 memmove (r, r+1, len);
401     }
402
403     return result;
404 }
405
406 /* Parse a References header value, putting a copy of each referenced
407  * message-id into 'array'. */
408 static void
409 parse_references (GPtrArray *array,
410                   const char *refs)
411 {
412     char *ref;
413
414     if (refs == NULL)
415         return;
416
417     while (*refs) {
418         ref = parse_message_id (refs, &refs);
419
420         if (ref)
421             g_ptr_array_add (array, ref);
422     }
423 }
424
425 char *
426 notmuch_database_default_path (void)
427 {
428     if (getenv ("NOTMUCH_BASE"))
429         return strdup (getenv ("NOTMUCH_BASE"));
430
431     return g_strdup_printf ("%s/mail", getenv ("HOME"));
432 }
433
434 notmuch_database_t *
435 notmuch_database_create (const char *path)
436 {
437     notmuch_database_t *notmuch = NULL;
438     char *notmuch_path = NULL;
439     struct stat st;
440     int err;
441     char *local_path = NULL;
442
443     if (path == NULL)
444         path = local_path = notmuch_database_default_path ();
445
446     err = stat (path, &st);
447     if (err) {
448         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
449                  path, strerror (errno));
450         goto DONE;
451     }
452
453     if (! S_ISDIR (st.st_mode)) {
454         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
455                  path);
456         goto DONE;
457     }
458
459     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
460
461     err = mkdir (notmuch_path, 0755);
462
463     if (err) {
464         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
465                  notmuch_path, strerror (errno));
466         goto DONE;
467     }
468
469     notmuch = notmuch_database_open (path);
470
471   DONE:
472     if (notmuch_path)
473         free (notmuch_path);
474     if (local_path)
475         free (local_path);
476
477     return notmuch;
478 }
479
480 notmuch_database_t *
481 notmuch_database_open (const char *path)
482 {
483     notmuch_database_t *notmuch = NULL;
484     char *notmuch_path = NULL, *xapian_path = NULL;
485     struct stat st;
486     int err;
487     char *local_path = NULL;
488     unsigned int i;
489
490     if (path == NULL)
491         path = local_path = notmuch_database_default_path ();
492
493     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
494
495     err = stat (notmuch_path, &st);
496     if (err) {
497         fprintf (stderr, "Error opening database at %s: %s\n",
498                  notmuch_path, strerror (errno));
499         goto DONE;
500     }
501
502     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
503
504     notmuch = talloc (NULL, notmuch_database_t);
505     notmuch->path = talloc_strdup (notmuch, path);
506
507     try {
508         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
509                                                            Xapian::DB_CREATE_OR_OPEN);
510         notmuch->query_parser = new Xapian::QueryParser;
511         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
512         notmuch->query_parser->set_database (*notmuch->xapian_db);
513
514         for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
515             prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
516             notmuch->query_parser->add_boolean_prefix (prefix->name,
517                                                        prefix->prefix);
518         }
519     } catch (const Xapian::Error &error) {
520         fprintf (stderr, "A Xapian exception occurred: %s\n",
521                  error.get_msg().c_str());
522     }
523     
524   DONE:
525     if (local_path)
526         free (local_path);
527     if (notmuch_path)
528         free (notmuch_path);
529     if (xapian_path)
530         free (xapian_path);
531
532     return notmuch;
533 }
534
535 void
536 notmuch_database_close (notmuch_database_t *notmuch)
537 {
538     delete notmuch->query_parser;
539     delete notmuch->xapian_db;
540     talloc_free (notmuch);
541 }
542
543 const char *
544 notmuch_database_get_path (notmuch_database_t *notmuch)
545 {
546     return notmuch->path;
547 }
548
549 notmuch_private_status_t
550 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
551                          Xapian::Document *doc, unsigned int *doc_id)
552 {
553     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
554 }
555
556 /* We allow the user to use arbitrarily long keys for timestamps,
557  * (they're for filesystem paths after all, which have no limit we
558  * know about). But we have a term-length limit. So if we exceed that,
559  * we'll use the SHA-1 of the user's key as the actual key for
560  * constructing a database term.
561  *
562  * Caution: This function returns a newly allocated string which the
563  * caller should free() when finished.
564  */
565 static char *
566 timestamp_db_key (const char *key)
567 {
568     int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
569
570     if (term_len > NOTMUCH_TERM_MAX)
571         return notmuch_sha1_of_string (key);
572     else
573         return strdup (key);
574 }
575
576 notmuch_status_t
577 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
578                                 const char *key, time_t timestamp)
579 {
580     Xapian::Document doc;
581     unsigned int doc_id;
582     notmuch_private_status_t status;
583     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
584     char *db_key = NULL;
585
586     db_key = timestamp_db_key (key);
587
588     try {
589         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
590
591         doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
592                        Xapian::sortable_serialise (timestamp));
593
594         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
595             char *term = talloc_asprintf (NULL, "%s%s",
596                                           _find_prefix ("timestamp"), db_key);
597             doc.add_term (term);
598             talloc_free (term);
599
600             notmuch->xapian_db->add_document (doc);
601         } else {
602             notmuch->xapian_db->replace_document (doc_id, doc);
603         }
604
605     } catch (Xapian::Error &error) {
606         fprintf (stderr, "A Xapian exception occurred: %s.\n",
607                  error.get_msg().c_str());
608         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
609     }
610
611     if (db_key)
612         free (db_key);
613
614     return ret;
615 }
616
617 time_t
618 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
619 {
620     Xapian::Document doc;
621     unsigned int doc_id;
622     notmuch_private_status_t status;
623     char *db_key = NULL;
624     time_t ret = 0;
625
626     db_key = timestamp_db_key (key);
627
628     try {
629         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
630
631         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
632             goto DONE;
633
634         ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
635     } catch (Xapian::Error &error) {
636         goto DONE;
637     }
638
639   DONE:
640     if (db_key)
641         free (db_key);
642
643     return ret;
644 }
645
646 notmuch_status_t
647 notmuch_database_add_message (notmuch_database_t *notmuch,
648                               const char *filename)
649 {
650     notmuch_message_file_t *message_file;
651     notmuch_message_t *message;
652     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
653
654     GPtrArray *parents, *thread_ids;
655
656     const char *refs, *in_reply_to, *date, *header;
657     const char *from, *to, *subject, *old_filename;
658     char *message_id;
659
660     unsigned int i;
661
662     message_file = notmuch_message_file_open (filename);
663     if (message_file == NULL) {
664         ret = NOTMUCH_STATUS_FILE_ERROR;
665         goto DONE;
666     }
667
668     notmuch_message_file_restrict_headers (message_file,
669                                            "date",
670                                            "from",
671                                            "in-reply-to",
672                                            "message-id",
673                                            "references",
674                                            "subject",
675                                            "to",
676                                            (char *) NULL);
677
678     try {
679         /* The first order of business is to find/create a message ID. */
680
681         header = notmuch_message_file_get_header (message_file, "message-id");
682         if (header) {
683             message_id = parse_message_id (header, NULL);
684             /* So the header value isn't RFC-compliant, but it's
685              * better than no message-id at all. */
686             if (message_id == NULL)
687                 message_id = xstrdup (header);
688         } else {
689             /* No message-id at all, let's generate one by taking a
690              * hash over the file's contents. */
691             char *sha1 = notmuch_sha1_of_file (filename);
692
693             /* If that failed too, something is really wrong. Give up. */
694             if (sha1 == NULL) {
695                 ret = NOTMUCH_STATUS_FILE_ERROR;
696                 goto DONE;
697             }
698
699             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
700             free (sha1);
701         }
702
703         /* Now that we have a message ID, we get a message object,
704          * (which may or may not reference an existing document in the
705          * database). */
706
707         /* Use NULL for owner since we want to free this locally. */
708
709         /* XXX: This call can fail by either out-of-memory or an
710          * "impossible" Xapian exception. We should rewrite it to
711          * allow us to propagate the error status. */
712         message = _notmuch_message_create_for_message_id (NULL, notmuch,
713                                                           message_id);
714         if (message == NULL) {
715             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
716             fprintf (stderr, "I mean, it's possible you ran out of memory, but then this code path is still an internal error since it should have detected that and propagated the status value up the stack.\n");
717             exit (1);
718         }
719
720         /* Has a message previously been added with the same ID? */
721         old_filename = notmuch_message_get_filename (message);
722         if (old_filename && strlen (old_filename)) {
723             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
724             goto DONE;
725         } else {
726             _notmuch_message_set_filename (message, filename);
727             _notmuch_message_add_term (message, "type", "mail");
728         }
729
730         /* Next, find the thread(s) to which this message belongs. */
731         parents = g_ptr_array_new ();
732
733         refs = notmuch_message_file_get_header (message_file, "references");
734         parse_references (parents, refs);
735
736         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
737         parse_references (parents, in_reply_to);
738
739         for (i = 0; i < parents->len; i++)
740             _notmuch_message_add_term (message, "ref",
741                                        (char *) g_ptr_array_index (parents, i));
742
743         thread_ids = find_thread_ids (notmuch, parents, message_id);
744
745         free (message_id);
746
747         for (i = 0; i < parents->len; i++)
748             g_free (g_ptr_array_index (parents, i));
749         g_ptr_array_free (parents, TRUE);
750
751         if (thread_ids->len) {
752             unsigned int i;
753             GString *thread_id;
754             char *id;
755
756             for (i = 0; i < thread_ids->len; i++) {
757                 id = (char *) thread_ids->pdata[i];
758                 _notmuch_message_add_thread_id (message, id);
759                 if (i == 0)
760                     thread_id = g_string_new (id);
761                 else
762                     g_string_append_printf (thread_id, ",%s", id);
763
764                 free (id);
765             }
766             g_string_free (thread_id, TRUE);
767         } else {
768             _notmuch_message_ensure_thread_id (message);
769         }
770
771         g_ptr_array_free (thread_ids, TRUE);
772
773         date = notmuch_message_file_get_header (message_file, "date");
774         _notmuch_message_set_date (message, date);
775
776         from = notmuch_message_file_get_header (message_file, "from");
777         subject = notmuch_message_file_get_header (message_file, "subject");
778         to = notmuch_message_file_get_header (message_file, "to");
779
780         if (from == NULL &&
781             subject == NULL &&
782             to == NULL)
783         {
784             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
785             goto DONE;
786         } else {
787             _notmuch_message_sync (message);
788         }
789     } catch (const Xapian::Error &error) {
790         fprintf (stderr, "A Xapian exception occurred: %s.\n",
791                  error.get_msg().c_str());
792         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
793         goto DONE;
794     }
795
796   DONE:
797     if (message)
798         notmuch_message_destroy (message);
799     if (message_file)
800         notmuch_message_file_close (message_file);
801
802     return ret;
803 }